From 6c40ebcb1c55f2db2eef6ccd5c86f59b63aa7328 Mon Sep 17 00:00:00 2001 From: Charlotte Yun Date: Tue, 25 Aug 2026 13:47:39 -0700 Subject: [PATCH 1/4] feat: Add TelemetryConfiguration and Tracer/Logger options --- Core/src/Telemetry/TelemetryConfiguration.php | 74 +++++++++++ .../Telemetry/TelemetryConfigurationTest.php | 118 ++++++++++++++++++ Gax/src/Options/ClientOptions.php | 55 ++++++++ 3 files changed, 247 insertions(+) create mode 100644 Core/src/Telemetry/TelemetryConfiguration.php create mode 100644 Core/tests/Unit/Telemetry/TelemetryConfigurationTest.php diff --git a/Core/src/Telemetry/TelemetryConfiguration.php b/Core/src/Telemetry/TelemetryConfiguration.php new file mode 100644 index 000000000000..d2ac6f66446e --- /dev/null +++ b/Core/src/Telemetry/TelemetryConfiguration.php @@ -0,0 +1,74 @@ +originalTracingEnabled = getenv('GOOGLE_SDK_PHP_TRACING_ENABLED'); + $this->originalLoggingEnabled = getenv('GOOGLE_SDK_PHP_LOGGING_ENABLED'); + $this->originalMetricsEnabled = getenv('GOOGLE_SDK_PHP_METRICS_ENABLED'); + $this->originalLegacyTelemetry = getenv('GOOGLE_API_ENABLE_TELEMETRY'); + putenv('GOOGLE_SDK_PHP_TRACING_ENABLED'); + putenv('GOOGLE_SDK_PHP_LOGGING_ENABLED'); + putenv('GOOGLE_SDK_PHP_METRICS_ENABLED'); + putenv('GOOGLE_API_ENABLE_TELEMETRY'); + } + + public function tearDown(): void + { + if ($this->originalTracingEnabled !== false) { + putenv("GOOGLE_SDK_PHP_TRACING_ENABLED={$this->originalTracingEnabled}"); + } + if ($this->originalLoggingEnabled !== false) { + putenv("GOOGLE_SDK_PHP_LOGGING_ENABLED={$this->originalLoggingEnabled}"); + } + if ($this->originalMetricsEnabled !== false) { + putenv("GOOGLE_SDK_PHP_METRICS_ENABLED={$this->originalMetricsEnabled}"); + } + if ($this->originalLegacyTelemetry !== false) { + putenv("GOOGLE_API_ENABLE_TELEMETRY={$this->originalLegacyTelemetry}"); + } + } + + public function testIsTracingEnabledDefaultFalse() + { + $this->assertFalse(TelemetryConfiguration::isTracingEnabled()); + } + + public function testIsTracingEnabledWithSpecificEnv() + { + putenv('GOOGLE_SDK_PHP_TRACING_ENABLED=true'); + $this->assertTrue(TelemetryConfiguration::isTracingEnabled()); + + putenv('GOOGLE_SDK_PHP_TRACING_ENABLED=false'); + $this->assertFalse(TelemetryConfiguration::isTracingEnabled()); + } + + public function testIsTracingEnabledWithLegacyEnv() + { + putenv('GOOGLE_API_ENABLE_TELEMETRY=true'); + $this->assertTrue(TelemetryConfiguration::isTracingEnabled()); + + putenv('GOOGLE_API_ENABLE_TELEMETRY=false'); + $this->assertFalse(TelemetryConfiguration::isTracingEnabled()); + } + + public function testIsTracingEnabledPrecedence() + { + // Specific flag takes precedence over legacy flag + putenv('GOOGLE_SDK_PHP_TRACING_ENABLED=false'); + putenv('GOOGLE_API_ENABLE_TELEMETRY=true'); + $this->assertFalse(TelemetryConfiguration::isTracingEnabled()); + + putenv('GOOGLE_SDK_PHP_TRACING_ENABLED=true'); + putenv('GOOGLE_API_ENABLE_TELEMETRY=false'); + $this->assertTrue(TelemetryConfiguration::isTracingEnabled()); + } + + public function testIsLoggingEnabledPrecedence() + { + putenv('GOOGLE_SDK_PHP_LOGGING_ENABLED=false'); + putenv('GOOGLE_API_ENABLE_TELEMETRY=true'); + $this->assertFalse(TelemetryConfiguration::isLoggingEnabled()); + + putenv('GOOGLE_SDK_PHP_LOGGING_ENABLED=true'); + putenv('GOOGLE_API_ENABLE_TELEMETRY=false'); + $this->assertTrue(TelemetryConfiguration::isLoggingEnabled()); + } + + public function testIsMetricsEnabledPrecedence() + { + putenv('GOOGLE_SDK_PHP_METRICS_ENABLED=false'); + putenv('GOOGLE_API_ENABLE_TELEMETRY=true'); + $this->assertFalse(TelemetryConfiguration::isMetricsEnabled()); + + putenv('GOOGLE_SDK_PHP_METRICS_ENABLED=true'); + putenv('GOOGLE_API_ENABLE_TELEMETRY=false'); + $this->assertTrue(TelemetryConfiguration::isMetricsEnabled()); + } +} diff --git a/Gax/src/Options/ClientOptions.php b/Gax/src/Options/ClientOptions.php index b9677fe20399..bbaabd135d84 100644 --- a/Gax/src/Options/ClientOptions.php +++ b/Gax/src/Options/ClientOptions.php @@ -1,4 +1,5 @@ setUniverseDomain($arr['universeDomain'] ?? null); $this->setApiKey($arr['apiKey'] ?? null); $this->setLogger($arr['logger'] ?? null); + $this->setTracerProvider($arr['tracerProvider'] ?? null); + $this->setLoggerProvider($arr['loggerProvider'] ?? null); } /** @@ -418,4 +433,44 @@ public function setLogger(null|false|LoggerInterface $logger): self return $this; } + + /** + * @param TracerProviderInterface|null $tracerProvider + * + * @return $this + */ + public function setTracerProvider($tracerProvider): self + { + $this->tracerProvider = $tracerProvider; + + return $this; + } + + /** + * @param LoggerProviderInterface|null $loggerProvider + * + * @return $this + */ + public function setLoggerProvider($loggerProvider): self + { + $this->loggerProvider = $loggerProvider; + + return $this; + } + + /** + * @return TracerProviderInterface|null + */ + public function getTracerProvider() + { + return $this->tracerProvider; + } + + /** + * @return LoggerProviderInterface|null + */ + public function getLoggerProvider() + { + return $this->loggerProvider; + } } From 3167b4d24fedf9be55bd0c520e45c6cbcf37f242 Mon Sep 17 00:00:00 2001 From: Charlotte Yun Date: Tue, 25 Aug 2026 10:33:43 -0700 Subject: [PATCH 2/4] build: Add open-telemetry/api dependency to Core and Gax --- Core/composer.json | 3 ++- Gax/composer.json | 3 ++- composer.json | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Core/composer.json b/Core/composer.json index edae4f42383a..7ad4bed10912 100644 --- a/Core/composer.json +++ b/Core/composer.json @@ -12,7 +12,8 @@ "guzzlehttp/psr7": "^2.6.3||^3.0", "monolog/monolog": "^2.9||^3.0", "psr/http-message": "^1.0||^2.0", - "google/gax": "^1.38.0" + "google/gax": "^1.38.0", + "open-telemetry/api": "^1.0" }, "require-dev": { "phpunit/phpunit": "^9.0", diff --git a/Gax/composer.json b/Gax/composer.json index d2b521c8139e..de5ce31f11d6 100644 --- a/Gax/composer.json +++ b/Gax/composer.json @@ -17,7 +17,8 @@ "guzzlehttp/psr7": "^2.6.3||^3.0", "google/common-protos": "^4.9", "google/longrunning": "~0.4", - "ramsey/uuid": "^4.0" + "ramsey/uuid": "^4.0", + "open-telemetry/api": "^1.0" }, "require-dev": { "phpunit/phpunit": "^9.6", diff --git a/composer.json b/composer.json index 783100ad9f94..4f93074da549 100644 --- a/composer.json +++ b/composer.json @@ -69,7 +69,8 @@ "google/protobuf": "^4.31||^5.34", "google/grpc-gcp": "^0.4", "ramsey/uuid": "^4.0", - "open-telemetry/sdk": "^1.13" + "open-telemetry/sdk": "^1.13", + "open-telemetry/api": "^1.0" }, "require-dev": { "phpunit/phpunit": "^9.6", From 9e675edbdb43bef67cdadd245f473b81f3b9cab4 Mon Sep 17 00:00:00 2001 From: Charlotte Yun Date: Tue, 25 Aug 2026 10:40:24 -0700 Subject: [PATCH 3/4] feat: implement L4 network error logs in GAX --- Gax/src/GapicClientTrait.php | 14 ++++++++++-- Gax/src/Transport/GrpcTransport.php | 32 ++++++++++++++++++++++++-- Gax/src/Transport/RestTransport.php | 35 +++++++++++++++++++++++++---- 3 files changed, 73 insertions(+), 8 deletions(-) diff --git a/Gax/src/GapicClientTrait.php b/Gax/src/GapicClientTrait.php index 0d8877a9b40b..12f5c677f822 100644 --- a/Gax/src/GapicClientTrait.php +++ b/Gax/src/GapicClientTrait.php @@ -370,6 +370,13 @@ private function setClientOptions(array $options) ); } + $telemetryOptions = [ + 'tracerProvider' => $options['tracerProvider'] ?? null, + 'loggerProvider' => $options['loggerProvider'] ?? null, + 'gcp.client.service' => $this->serviceName, + 'gcp.client.version' => $options['libVersion'] ?? null, + ]; + $transport = $options['transport'] ?: self::defaultTransport(); $this->transport = $transport instanceof TransportInterface ? $transport @@ -378,7 +385,8 @@ private function setClientOptions(array $options) $transport, $options['transportConfig'], $options['clientCertSource'], - $hasEmulator + $hasEmulator, + $telemetryOptions ); } @@ -396,7 +404,8 @@ private function createTransport( $transport, $transportConfig, ?callable $clientCertSource = null, - bool $hasEmulator = false + bool $hasEmulator = false, + array $telemetryOptions = [] ) { if (!is_string($transport)) { throw new ValidationException( @@ -419,6 +428,7 @@ private function createTransport( $configForSpecifiedTransport->setClientCertSource($clientCertSource); $configForSpecifiedTransport = $configForSpecifiedTransport->toArray(); } + $configForSpecifiedTransport += $telemetryOptions; switch ($transport) { case 'grpc': // Setting the user agent for gRPC requires special handling diff --git a/Gax/src/Transport/GrpcTransport.php b/Gax/src/Transport/GrpcTransport.php index 50bdc3d7de1a..a367f8492e3c 100644 --- a/Gax/src/Transport/GrpcTransport.php +++ b/Gax/src/Transport/GrpcTransport.php @@ -1,4 +1,5 @@ logger = $logger; + $this->loggerProvider = $telemetryOptions['loggerProvider'] ?? null; + $this->clientService = $telemetryOptions['gcp.client.service'] ?? ''; + $this->clientVersion = $telemetryOptions['gcp.client.version'] ?? ''; } /** @@ -136,6 +145,9 @@ public static function build(string $apiEndpoint, array $config = []) 'interceptors' => [], 'clientCertSource' => null, 'logger' => null, + 'loggerProvider' => null, + 'gcp.client.service' => '', + 'gcp.client.version' => '', ]; list($addr, $port) = self::normalizeServiceAddress($apiEndpoint); $host = "$addr:$port"; @@ -161,7 +173,14 @@ public static function build(string $apiEndpoint, array $config = []) if ($config['logger'] === false) { $config['logger'] = null; } - return new GrpcTransport($host, $stubOpts, $channel, $config['interceptors'], $config['logger']); + return new GrpcTransport( + $host, + $stubOpts, + $channel, + $config['interceptors'], + $config['logger'], + $config + ); } catch (Exception $ex) { throw new ValidationException( 'Failed to build GrpcTransport: ' . $ex->getMessage(), @@ -328,6 +347,15 @@ function () use ($unaryCall, $options, &$promise, $requestEvent) { } $promise->resolve($response); } else { + if ($this->loggerProvider) { + $this->loggerProvider->getLogger('google-cloud-php', $this->clientVersion) + ->logRecordBuilder() + ->setSeverityNumber(17) // ERROR + ->setBody($status->details) + ->setAttribute('rpc.grpc.status_code', $status->code) + ->setAttribute('gcp.client.service', $this->clientService) + ->emit(); + } throw ApiException::createFromStdClass($status); } }, diff --git a/Gax/src/Transport/RestTransport.php b/Gax/src/Transport/RestTransport.php index ae0245130dd9..cae5c97e3aab 100644 --- a/Gax/src/Transport/RestTransport.php +++ b/Gax/src/Transport/RestTransport.php @@ -1,4 +1,5 @@ requestBuilder = $requestBuilder; $this->httpHandler = $httpHandler; $this->transportName = 'REST'; + $this->loggerProvider = $telemetryOptions['loggerProvider'] ?? null; + $this->clientService = $telemetryOptions['gcp.client.service'] ?? ''; + $this->clientVersion = $telemetryOptions['gcp.client.version'] ?? ''; } /** @@ -97,13 +106,16 @@ public static function build(string $apiEndpoint, string $restConfigPath, array 'clientCertSource' => null, 'hasEmulator' => false, 'logger' => null, + 'loggerProvider' => null, + 'gcp.client.service' => '', + 'gcp.client.version' => '', ]; list($baseUri, $port) = self::normalizeServiceAddress($apiEndpoint); $requestBuilder = $config['hasEmulator'] ? new InsecureRequestBuilder("$baseUri:$port", $restConfigPath) : new RequestBuilder("$baseUri:$port", $restConfigPath); $httpHandler = $config['httpHandler'] ?: self::buildHttpHandlerAsync($config['logger']); - $transport = new RestTransport($requestBuilder, $httpHandler); + $transport = new RestTransport($requestBuilder, $httpHandler, $config); if ($config['clientCertSource']) { $transport->configureMtlsChannel($config['clientCertSource']); } @@ -122,14 +134,16 @@ public function startUnaryCall(Call $call, array $options) // call the HTTP handler $httpHandler = $this->httpHandler; - return $httpHandler( + $promise = $httpHandler( $this->requestBuilder->build( $call->getMethod(), $call->getMessage(), $headers ), $this->getCallOptions($options) - )->then( + ); + + return $promise->then( function (ResponseInterface $response) use ($call, $options) { $decodeType = $call->getDecodeType(); /** @var Message $return */ @@ -170,6 +184,19 @@ function (ResponseInterface $response) use ($call, $options) { return $return; }, function (\Throwable $ex) { + if ($this->loggerProvider) { + $statusCode = $ex->getCode(); + if ($ex instanceof RequestException && method_exists($ex, 'getResponse') && $ex->getResponse()) { + $statusCode = $ex->getResponse()->getStatusCode(); + } + $this->loggerProvider->getLogger('google-cloud-php', $this->clientVersion) + ->logRecordBuilder() + ->setSeverityNumber(17) // ERROR + ->setBody($ex->getMessage()) + ->setAttribute('http.status_code', $statusCode) + ->setAttribute('gcp.client.service', $this->clientService) + ->emit(); + } // Guzzle 7 carries the response on RequestException, Guzzle 8 // only on its ResponseException subclass, hence the // method_exists() check. From c6877eb1e8337d82954d498336c8c9397a20ff37 Mon Sep 17 00:00:00 2001 From: Charlotte Yun Date: Tue, 25 Aug 2026 11:04:31 -0700 Subject: [PATCH 4/4] feat: Add PSR-18 middleware for tracing auth requests (Task 5) --- Core/src/GrpcRequestWrapper.php | 6 ++ Core/src/RequestWrapper.php | 8 ++ Core/src/Telemetry/AuthTracingMiddleware.php | 82 ++++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 Core/src/Telemetry/AuthTracingMiddleware.php diff --git a/Core/src/GrpcRequestWrapper.php b/Core/src/GrpcRequestWrapper.php index 7bcfa7601eb1..3164aabd6b60 100644 --- a/Core/src/GrpcRequestWrapper.php +++ b/Core/src/GrpcRequestWrapper.php @@ -81,6 +81,12 @@ public function __construct(array $config = []) ]; $this->authHttpHandler = $config['authHttpHandler'] ?: HttpHandlerFactory::build(); + if (isset($config['tracerProvider'])) { + $this->authHttpHandler = new \Google\Cloud\Core\Telemetry\AuthTracingMiddleware( + $this->authHttpHandler, + $config['tracerProvider'] + ); + } $this->serializer = $config['serializer']; $this->grpcOptions = $config['grpcOptions']; } diff --git a/Core/src/RequestWrapper.php b/Core/src/RequestWrapper.php index e0d4d729d864..03f204d884e1 100644 --- a/Core/src/RequestWrapper.php +++ b/Core/src/RequestWrapper.php @@ -176,6 +176,14 @@ public function __construct(array $config = []) $this->calcDelayFunction = $config['restCalcDelayFunction']; $this->httpHandler = $config['httpHandler'] ?: HttpHandlerFactory::build(); $this->authHttpHandler = $config['authHttpHandler'] ?: $this->httpHandler; + + if (isset($config['tracerProvider'])) { + $this->authHttpHandler = new \Google\Cloud\Core\Telemetry\AuthTracingMiddleware( + $this->authHttpHandler, + $config['tracerProvider'] + ); + } + $this->asyncHttpHandler = $config['asyncHttpHandler'] ?: $this->buildDefaultAsyncHandler(); $this->universeDomain = $config['universeDomain']; diff --git a/Core/src/Telemetry/AuthTracingMiddleware.php b/Core/src/Telemetry/AuthTracingMiddleware.php new file mode 100644 index 000000000000..b6b080b4c2f9 --- /dev/null +++ b/Core/src/Telemetry/AuthTracingMiddleware.php @@ -0,0 +1,82 @@ +httpHandler = $httpHandler; + $this->tracerProvider = $tracerProvider; + } + + /** + * Can be used as a callable for google-auth-library-php + * + * @param RequestInterface $request + * @param array $options + * @return ResponseInterface|PromiseInterface + */ + public function __invoke(RequestInterface $request, array $options = []) + { + $span = $this->tracerProvider->getTracer('google-cloud-php', '') + ->spanBuilder('AuthRequest') + ->setSpanKind(SpanKind::KIND_CLIENT) + ->setAttribute('rpc.system', 'http') + ->setAttribute('rpc.service', 'auth') + ->startSpan(); + + $scope = $span->activate(); + + try { + $handler = $this->httpHandler; + $response = $handler($request, $options); + + if ($response instanceof PromiseInterface) { + return $response->then( + function (ResponseInterface $res) use ($span) { + $span->setStatus(StatusCode::STATUS_OK); + $span->end(); + return $res; + }, + function (\Throwable $e) use ($span) { + $span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage()); + $span->end(); + throw $e; + } + ); + } + + $span->setStatus(StatusCode::STATUS_OK); + return $response; + } catch (\Throwable $e) { + $span->setStatus(StatusCode::STATUS_ERROR, $e->getMessage()); + throw $e; + } finally { + $scope->detach(); + if (!isset($response) || !($response instanceof PromiseInterface)) { + $span->end(); + } + } + } +}