Skip to content

Conversation

@lucas-angeli-gimenes
Copy link
Collaborator

Description

This PR adds a native gRPC transport implementation using Swoole's HTTP/2 coroutine client, providing non-blocking async telemetry export.

The standard open-telemetry/transport-grpc package uses ext-grpc which is blocking and has no Swoole hook support. This means gRPC calls block the entire worker, defeating the purpose of using Swoole coroutines.

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Performance/Refactor
  • CI/CD/Chore

Checklist

  • Title follows Conventional Commits (e.g., feat: add SQS aspect)
  • Tests added/updated
  • composer test passes locally
  • Documentation updated (README/Docs)
  • No breaking changes (or documented if any)

Copilot AI review requested due to automatic review settings February 11, 2026 18:37
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a Swoole coroutine-based OTLP/gRPC transport and wires new OTLP gRPC exporters for traces/metrics/logs into the package configuration, aiming to avoid blocking ext-grpc behavior in Swoole workers.

Changes:

  • Introduces SwooleGrpcTransport + SwooleGrpcTransportFactory using Swoole\Coroutine\Http2\Client.
  • Adds OTLP gRPC exporter factories for traces, metrics, and logs using the new transport.
  • Updates default published config to expose new otlp_grpc exporters and updates Composer dependencies.

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
src/Transport/SwooleGrpcTransport.php Implements gRPC framing + HTTP/2 request/response handling for OTLP export.
src/Transport/SwooleGrpcTransportFactory.php Parses endpoints/headers and constructs SwooleGrpcTransport.
src/Factory/Trace/Exporter/OtlpGrpcTraceExporterFactory.php Adds trace exporter factory using the new gRPC transport.
src/Factory/Metric/Exporter/OtlpGrpcMetricExporterFactory.php Adds metric exporter factory using the new gRPC transport.
src/Factory/Log/Exporter/OtlpGrpcLogExporterFactory.php Adds log exporter factory using the new gRPC transport.
publish/open-telemetry.php Registers new otlp_grpc exporters and changes an instrumentation default (ignore_paths).
tests/Unit/Transport/SwooleGrpcTransportTest.php Unit tests for transport lifecycle + closed-send behavior.
tests/Unit/Transport/SwooleGrpcTransportFactoryTest.php Unit tests for endpoint parsing and validation (currently has failing assertions).
tests/Unit/Factory/*/Exporter/OtlpGrpc*ExporterFactoryTest.php Smoke tests verifying exporter factories produce the correct exporter interface types.
composer.json Adds prefer-stable and requires hyperf/grpc-client.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

private function parseEndpoint(string $endpoint): array
{
$parts = parse_url($endpoint);

Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parse_url() can return false for malformed endpoints; accessing $parts['scheme'] on false will emit warnings. Consider explicitly handling false (and/or non-array) return values and throwing InvalidArgumentException before checking keys.

Suggested change
if (! is_array($parts)) {
throw new InvalidArgumentException('Endpoint is malformed and could not be parsed');
}

Copilot uses AI. Check for mistakes.
Comment on lines +67 to +68
'retry_delay' => (int) env('OTEL_TRACES_RETRY_DELAY_MS', 100),
'max_retries' => (int) env('OTEL_TRACES_RETRY_MAX', 3),
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new gRPC exporter options use flat retry_delay/max_retries keys, while the existing OTLP/HTTP exporters in the same config file use a nested retry array. This inconsistency makes configuration harder to reason about; consider aligning the shape (either flatten or nest consistently) and updating the corresponding factories accordingly.

Suggested change
'retry_delay' => (int) env('OTEL_TRACES_RETRY_DELAY_MS', 100),
'max_retries' => (int) env('OTEL_TRACES_RETRY_MAX', 3),
'retry' => [
'delay_ms' => (int) env('OTEL_TRACES_RETRY_DELAY_MS', 100),
'max_retries' => (int) env('OTEL_TRACES_RETRY_MAX', 3),
],

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +36
return new LogsExporter(
(new SwooleGrpcTransportFactory())->create(
endpoint: $endpoint,
contentType: 'application/x-protobuf',
headers: $options['headers'] ?? [],
compression: $options['compression'] ?? TransportFactoryInterface::COMPRESSION_GZIP,
timeout: $options['timeout'] ?? 10,
retryDelay: $options['retry_delay'] ?? 100,
maxRetries: $options['max_retries'] ?? 3,
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These factories pass a magic string 'application/x-protobuf' to the transport factory. Since SwooleGrpcTransportFactory validates against ContentTypes::PROTOBUF, use the ContentTypes::PROTOBUF constant here to avoid accidental mismatch if the upstream constant changes.

Copilot uses AI. Check for mistakes.
Comment on lines +71 to +77
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('not a valid gRPC method');

$factory->create(
endpoint: 'http://localhost:4317/invalid-path',
contentType: ContentTypes::PROTOBUF,
);
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expectExceptionMessage('not a valid gRPC method') doesn’t match the actual exception text (Endpoint path is not a valid gRPC method: "..."), so this test will fail. Adjust the assertion to match the full message or use a partial/regex match.

Copilot uses AI. Check for mistakes.
Comment on lines +14 to +49
public function create(
string $endpoint,
string $contentType = ContentTypes::PROTOBUF,
array $headers = [],
$compression = null,
float $timeout = 10.,
int $retryDelay = 100,
int $maxRetries = 3,
?string $cacert = null,
?string $cert = null,
?string $key = null,
): TransportInterface {
if ($contentType !== ContentTypes::PROTOBUF) {
throw new InvalidArgumentException(
sprintf('Unsupported content type "%s", gRPC transport supports only %s', $contentType, ContentTypes::PROTOBUF)
);
}

$parsed = $this->parseEndpoint($endpoint);

$grpcHeaders = $this->buildHeaders($headers);

$compressionType = null;
if ($compression !== null) {
$compressionType = is_array($compression) ? ($compression[0] ?? null) : $compression;
}

return new SwooleGrpcTransport(
host: $parsed['host'],
port: $parsed['port'],
method: $parsed['method'],
headers: $grpcHeaders,
timeout: $timeout,
ssl: $parsed['ssl'],
compression: $compressionType,
);
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

retryDelay, maxRetries, and TLS params ($cacert, $cert, $key) are accepted by create() but ignored when constructing SwooleGrpcTransport. Either implement retry/TLS handling (e.g., client set() options) or remove/mark these params as unsupported to avoid misleading configuration.

Copilot uses AI. Check for mistakes.
"hyperf/database": "~3.1.0",
"hyperf/di": "~3.1.0",
"hyperf/event": "~3.1.0",
"hyperf/grpc-client": "~3.1.0",
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hyperf/grpc-client is added to require, but the new implementation uses Swoole\Coroutine\Http2\Client directly and there are no references to the Hyperf gRPC client in the codebase. If it’s not needed, remove this dependency to avoid extra install surface and transitive constraints.

Suggested change
"hyperf/grpc-client": "~3.1.0",

Copilot uses AI. Check for mistakes.
Comment on lines 205 to 207
'ignore_paths' => [
// '/^\/health$/',
'/^\/health$/',
],
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change enables '/^\/health$/' in ignore_paths, which alters default instrumentation behavior and is not described in the PR summary (which focuses on gRPC exporter/transport). Either revert this unrelated config change or call it out explicitly in the PR description/release notes.

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +34
return new SpanExporter(
(new SwooleGrpcTransportFactory())->create(
endpoint: $endpoint,
contentType: 'application/x-protobuf',
headers: $options['headers'] ?? [],
compression: $options['compression'] ?? TransportFactoryInterface::COMPRESSION_GZIP,
timeout: $options['timeout'] ?? 10,
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These factories pass a magic string 'application/x-protobuf' to the transport factory. Since SwooleGrpcTransportFactory validates against ContentTypes::PROTOBUF, use the ContentTypes::PROTOBUF constant here to avoid accidental mismatch if the upstream constant changes.

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +37
return new MetricExporter(
transport: (new SwooleGrpcTransportFactory())->create(
endpoint: $endpoint,
contentType: 'application/x-protobuf',
headers: $options['headers'] ?? [],
compression: $options['compression'] ?? TransportFactoryInterface::COMPRESSION_GZIP,
timeout: $options['timeout'] ?? 10,
retryDelay: $options['retry_delay'] ?? 100,
maxRetries: $options['max_retries'] ?? 3,
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These factories pass a magic string 'application/x-protobuf' to the transport factory. Since SwooleGrpcTransportFactory validates against ContentTypes::PROTOBUF, use the ContentTypes::PROTOBUF constant here to avoid accidental mismatch if the upstream constant changes.

Copilot uses AI. Check for mistakes.
Comment on lines +45 to +51
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Unsupported content type');

$factory->create(
endpoint: 'http://localhost:4317/opentelemetry.proto.collector.trace.v1.TraceService/Export',
contentType: 'application/json',
);
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expectExceptionMessage('Unsupported content type') will fail because the factory throws Unsupported content type "%s", gRPC transport supports only %s (full message must match). Update the test to assert the full message or switch to expectExceptionMessageMatches()/assertStringContainsString()-style matching.

Copilot uses AI. Check for mistakes.
@lucas-angeli-gimenes lucas-angeli-gimenes merged commit 740521f into main Feb 12, 2026
6 checks passed
@lucas-angeli-gimenes lucas-angeli-gimenes deleted the feature/grpc branch February 12, 2026 11:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants