diff --git a/Spanner/composer.json b/Spanner/composer.json index f1829221006a..4f9db614ec40 100644 --- a/Spanner/composer.json +++ b/Spanner/composer.json @@ -8,8 +8,8 @@ "ext-grpc": "*", "google/cloud-core": "^1.73.0", "google/gax": "^1.41.0", - "google/cloud-monitoring": "^2.2", - "open-telemetry/sdk": "^1.13" + "open-telemetry/sdk": "^1.13", + "open-telemetry/exporter-otlp": "^1.4" }, "require-dev": { "phpunit/phpunit": "^9.6", diff --git a/Spanner/src/Middleware/MetricsAttemptMiddleware.php b/Spanner/src/Middleware/MetricsAttemptMiddleware.php index 14625cc152ac..7105f74cb38a 100644 --- a/Spanner/src/Middleware/MetricsAttemptMiddleware.php +++ b/Spanner/src/Middleware/MetricsAttemptMiddleware.php @@ -63,14 +63,10 @@ class MetricsAttemptMiddleware implements MiddlewareInterface /** @var callable */ private $nextHandler; - private string $projectId; private string $clientId; private string $clientName; - private string $location; private bool $directPathEnabled; - private const INSTANCE_CONFIG = 'unknown'; - private const BUCKET_BOUNDS = [ 0.0, 0.5, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, @@ -86,57 +82,51 @@ class MetricsAttemptMiddleware implements MiddlewareInterface * @param callable $nextHandler * @param MeterInterface $meter * @param string $clientId - * @param string $projectId * @param string $clientName - * @param string $location */ public function __construct( callable $nextHandler, MeterInterface $meter, string $clientId, - string $projectId, - string $clientName, - string $location + string $clientName ) { $this->nextHandler = $nextHandler; $advisory = ['ExplicitBucketBoundaries' => self::BUCKET_BOUNDS]; $this->attemptLatencyHistogram = $meter->createHistogram( - 'attempt_latencies', + 'spanner.googleapis.com/internal/client/attempt_latencies', 'ms', 'The latency of an RPC attempt', $advisory ); $this->attemptCountCounter = $meter->createCounter( - 'attempt_count', + 'spanner.googleapis.com/internal/client/attempt_count', '1', 'The number of RPC attempts' ); $this->attemptGfeHistogram = $meter->createHistogram( - 'gfe_latencies', + 'spanner.googleapis.com/internal/client/gfe_latencies', 'ms', 'Latency between Google\'s network receiving an RPC and reading back the first byte of the response', $advisory ); $this->gfeConnectivityErrorCounter = $meter->createCounter( - 'gfe_connectivity_error_count', + 'spanner.googleapis.com/internal/client/gfe_connectivity_error_count', '1', 'Number of RPC attempts that failed to reach the GFE or returned no GFE headers' ); $this->attemptAfeHistogram = $meter->createHistogram( - 'afe_latencies', + 'spanner.googleapis.com/internal/client/afe_latencies', 'ms', 'Latency between Spanner Spanner AFE receiving and returning a response.', $advisory ); $this->afeConnectivityErrorCounter = $meter->createCounter( - 'afe_connectivity_error_count', + 'spanner.googleapis.com/internal/client/afe_connectivity_error_count', '1', 'Number of connectivity errors for Spanner AFE' ); $this->clientId = $clientId; - $this->projectId = $projectId; $this->clientName = 'spanner-php/' . $clientName; - $this->location = $location; $this->directPathEnabled = filter_var( getenv('GOOGLE_SPANNER_ENABLE_DIRECT_ACCESS'), FILTER_VALIDATE_BOOLEAN @@ -323,13 +313,9 @@ private function getMetricLabels(string $method, array $options, int $code, bool return [ 'method' => $methodName, 'status' => $codeName, - 'instance_id' => $instanceId, 'database' => $databaseId, - 'project_id' => $this->projectId, 'client_uid' => $this->clientId, 'client_name' => $this->clientName, - 'instance_config' => self::INSTANCE_CONFIG, - 'location' => $this->location, 'directpath_enabled' => $this->directPathEnabled ? 'true' : 'false', 'directpath_used' => $directPathUsed ? 'true' : 'false' ]; diff --git a/Spanner/src/Middleware/MetricsOperationMiddleware.php b/Spanner/src/Middleware/MetricsOperationMiddleware.php index 825862fca928..dd200b7cb0a3 100644 --- a/Spanner/src/Middleware/MetricsOperationMiddleware.php +++ b/Spanner/src/Middleware/MetricsOperationMiddleware.php @@ -56,14 +56,10 @@ class MetricsOperationMiddleware implements MiddlewareInterface /** @var callable */ private $nextHandler; - private string $projectId; private string $clientId; private string $clientName; - private string $location; private bool $directPathEnabled; - private const INSTANCE_CONFIG = 'unknown'; - private const BUCKET_BOUNDS = [ 0.0, 0.5, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, @@ -79,34 +75,28 @@ class MetricsOperationMiddleware implements MiddlewareInterface * @param callable $nextHandler * @param MeterInterface $meter * @param string $clientId - * @param string $projectId - * @param string $location */ public function __construct( callable $nextHandler, MeterInterface $meter, string $clientId, - string $projectId, - string $clientName, - string $location + string $clientName ) { $this->nextHandler = $nextHandler; $advisory = ['ExplicitBucketBoundaries' => self::BUCKET_BOUNDS]; $this->operationLatencyHistogram = $meter->createHistogram( - 'operation_latencies', + 'spanner.googleapis.com/internal/client/operation_latencies', 'ms', 'The latency of an RPC operations', $advisory ); $this->operationCountCounter = $meter->createCounter( - 'operation_count', + 'spanner.googleapis.com/internal/client/operation_count', '1', 'The number of RPC operations' ); $this->clientId = $clientId; - $this->projectId = $projectId; $this->clientName = 'spanner-php/' . $clientName; - $this->location = $location; $this->directPathEnabled = filter_var( getenv('GOOGLE_SPANNER_ENABLE_DIRECT_ACCESS'), FILTER_VALIDATE_BOOLEAN @@ -222,13 +212,9 @@ private function recordOperation( $labels = [ 'method' => $methodName, 'status' => $codeName, - 'instance_id' => $instanceId, 'database' => $databaseId, - 'project_id' => $this->projectId, 'client_uid' => $this->clientId, 'client_name' => $this->clientName, - 'instance_config' => self::INSTANCE_CONFIG, - 'location' => $this->location, 'directpath_enabled' => $this->directPathEnabled ? 'true' : 'false', 'directpath_used' => $directPathUsed ? 'true' : 'false' ]; diff --git a/Spanner/src/OpenTelemetry/MetricsExporter.php b/Spanner/src/OpenTelemetry/MetricsExporter.php index 249adcb10e47..64e200599a24 100644 --- a/Spanner/src/OpenTelemetry/MetricsExporter.php +++ b/Spanner/src/OpenTelemetry/MetricsExporter.php @@ -60,6 +60,8 @@ /** * MetricsExporter exports Spanner client metrics to Google Cloud Monitoring * using the internal service endpoint. + * + * @deprecated */ class MetricsExporter implements PushMetricExporterInterface, AggregationTemporalitySelectorInterface { diff --git a/Spanner/src/OpenTelemetry/OtlpMetricsExporter.php b/Spanner/src/OpenTelemetry/OtlpMetricsExporter.php new file mode 100644 index 000000000000..88d6f7796111 --- /dev/null +++ b/Spanner/src/OpenTelemetry/OtlpMetricsExporter.php @@ -0,0 +1,162 @@ +otlpExporter = $otlpExporter; + return; + } + + $authCallback = $metricsCredentials->getAuthorizationHeaderCallback(); + $quotaProject = $metricsCredentials->getQuotaProject(); + $handlerStack = $options['handlerStack'] ?? HandlerStack::create(); + $handlerStack->push(function (callable $handler) use ($authCallback, $quotaProject) { + return function (RequestInterface $request, array $options) use ($authCallback, $handler, $quotaProject) { + $headers = $authCallback ? $authCallback() : []; + foreach ($headers as $name => $values) { + $request = $request->withHeader($name, $values); + } + if ($quotaProject) { + $request = $request->withHeader('x-goog-user-project', $quotaProject); + } + return $handler($request, $options); + }; + }); + + $guzzleClient = new GuzzleClient([ + 'handler' => $handlerStack, + 'auth' => 'google_auth', + 'timeout' => $timeoutMillis / 1000, + ]); + + $transport = (new PsrTransportFactory($guzzleClient))->create( + self::DEFAULT_ENDPOINT, + 'application/x-protobuf' + ); + + $this->otlpExporter = new OtlpMetricExporter($transport, Temporality::CUMULATIVE); + } + + /** + * Exports a batch of OTel metrics using the inner OTLP exporter. + * + * @param iterable $batch + * @return bool + */ + public function export(iterable $batch): bool + { + try { + return $this->otlpExporter->export($batch); + } catch (Throwable $e) { + return false; + } + } + + /** + * Implementation of forceFlush method for PushMetricExporterInterface. + * + * @return bool + */ + public function forceFlush(): bool + { + try { + return $this->otlpExporter->forceFlush(); + } catch (Throwable $e) { + return false; + } + } + + /** + * Implementation of shutdown method for PushMetricExporterInterface. + * + * @return bool + */ + public function shutdown(): bool + { + try { + return $this->otlpExporter->shutdown(); + } catch (Throwable $e) { + return false; + } + } + + /** + * Returns the aggregation temporality for the given metric. + * + * @param MetricMetadataInterface $metric + * @return Temporality|string|null + */ + public function temporality(MetricMetadataInterface $metric): Temporality|string|null + { + return Temporality::CUMULATIVE; + } +} diff --git a/Spanner/src/SpannerClient.php b/Spanner/src/SpannerClient.php index ed289bed9976..dfdfb9a9da47 100644 --- a/Spanner/src/SpannerClient.php +++ b/Spanner/src/SpannerClient.php @@ -19,10 +19,12 @@ use Exception; use Google\ApiCore\ClientOptionsTrait; +use Google\ApiCore\CredentialsWrapper; use Google\ApiCore\Middleware\MiddlewareInterface; use Google\ApiCore\Options\CallOptions; use Google\ApiCore\ValidationException; use Google\Auth\Credentials\GCECredentials; +use Google\Auth\GetUniverseDomainInterface; use Google\Cloud\Core\ApiHelperTrait; use Google\Cloud\Core\Compute\Metadata; use Google\Cloud\Core\DetectProjectIdTrait; @@ -33,7 +35,6 @@ use Google\Cloud\Core\LongRunning\LongRunningClientConnection; use Google\Cloud\Core\LongRunning\LongRunningOperation; use Google\Cloud\Core\OptionsValidator; -use Google\Cloud\Monitoring\V3\Client\MetricServiceClient; use Google\Cloud\Spanner\Admin\Database\V1\Client\DatabaseAdminClient; use Google\Cloud\Spanner\Admin\Instance\V1\Client\InstanceAdminClient; use Google\Cloud\Spanner\Admin\Instance\V1\InstanceConfig; @@ -46,7 +47,7 @@ use Google\Cloud\Spanner\Middleware\MetricsOperationMiddleware; use Google\Cloud\Spanner\Middleware\RequestIdHeaderMiddleware; use Google\Cloud\Spanner\Middleware\SpannerMiddleware; -use Google\Cloud\Spanner\OpenTelemetry\MetricsExporter; +use Google\Cloud\Spanner\OpenTelemetry\OtlpMetricsExporter; use Google\Cloud\Spanner\V1\Client\SpannerClient as GapicSpannerClient; use Google\Cloud\Spanner\V1\TransactionOptions\IsolationLevel; use Google\Cloud\Spanner\V1\TransactionOptions\ReadWrite\ReadLockMode; @@ -54,9 +55,11 @@ use Google\Protobuf\Duration; use OpenTelemetry\API\Metrics\MeterInterface; use OpenTelemetry\API\Metrics\MeterProviderInterface; +use OpenTelemetry\SDK\Common\Attribute\Attributes; use OpenTelemetry\SDK\Common\Util\ShutdownHandler; use OpenTelemetry\SDK\Metrics\MeterProvider; use OpenTelemetry\SDK\Metrics\MetricReader\ExportingReader; +use OpenTelemetry\SDK\Resource\ResourceInfo; use Psr\Cache\CacheItemPoolInterface; use Psr\Http\Message\StreamInterface; use Ramsey\Uuid\Uuid as RUUID; @@ -204,10 +207,10 @@ class SpannerClient * @type CacheItemPoolInterface $cacheItemPool * @type bool $enableBuiltInMetrics If true, built-in metrics collection will be enabled. * **Defaults to** false. - * @type int $metricsTimeoutMillis The timeout in milliseconds for the internal - * `MetricServiceClient` used to export metrics. **Defaults to** 100. - * @type MetricServiceClient $metricServiceClient An explicit instance of - * `MetricServiceClient` to use for exporting metrics. + * @type int $metricsTimeoutMillis The timeout in milliseconds for exporting metrics. + * **Defaults to** 5000. + * @type string|array|FetchAuthTokenInterface|CredentialsWrapper $metricsCredentials + * Optional dedicated credentials to use for exporting built-in metrics. * } * @throws GoogleException If the gRPC extension is not enabled. */ @@ -252,6 +255,10 @@ public function __construct(array $options = []) $options['credentialsConfig']['scopes'] = $scopes; } + $metricsCredentials = $options['metricsCredentials'] + ?? $options['credentials'] + ?? null; + if ($emulatorHost) { $emulatorConfig = $this->emulatorGapicConfig($emulatorHost); $options = array_merge( @@ -299,6 +306,7 @@ public function __construct(array $options = []) $this->instanceAdminClient->addMiddleware($middleware); $this->databaseAdminClient->addMiddleware($middleware); + $options['metricsCredentials'] = $metricsCredentials; $this->configureMetrics($options); $this->projectName = InstanceAdminClient::projectName($this->projectId); @@ -1055,67 +1063,51 @@ private function configureKeepAlive(array $config): array private function configureMetrics(array $options): void { - $metricsClient = $this->pluck('metricServiceClient', $options, false); - $timeoutMillis = $this->pluck('metricsTimeoutMillis', $options, false) ?? 100; + $timeoutMillis = $this->pluck('metricsTimeoutMillis', $options, false) ?? 5000; if (!$this->pluck('enableBuiltInMetrics', $options, false)) { return; } - if (!$metricsClient) { - $metricsOptions = [ - 'projectId' => $this->projectId, - 'keyFile' => $options['keyFile'] ?? null, - 'keyFilePath' => $options['keyFilePath'] ?? null, - 'credentials' => $options['credentials'] ?? null, - 'credentialsConfig' => $options['credentialsConfig'] ?? null, - 'universeDomain' => $options['universeDomain'] ?? null, - 'transport' => $options['transport'] ?? null, - 'transportConfig' => $options['transportConfig'] ?? null - ]; - - try { - $metricsClient = new MetricServiceClient(array_filter($metricsOptions)); - } catch (ValidationException $e) { - // If we cannot instantiate the metrics client, we should not stop the execution - return; - } - } - - if (!$metricsClient instanceof MetricServiceClient) { - throw new ValidationException('The "metricServiceClient" option must be a MetricServiceClient instance.'); - } - $location = $this->getLocation(); $metricsClientId = RUUID::uuid4()->toString() . '-' . getmypid(); - $exporter = new MetricsExporter($metricsClient, $this->projectId, $metricsClientId, $timeoutMillis); + $clientHash = $this->generateClientHash($metricsClientId); + + $resource = ResourceInfo::create(Attributes::create([ + 'gcp.resource_type' => 'spanner_instance_client', + 'gcp.project_id' => $this->projectId, + 'project_id' => $this->projectId, + 'client_hash' => $clientHash, + 'location' => $location !== 'global' ? $location : 'us-central1', + ])); + + $metricsCredentials = $this->buildMetricsCredentials($options); + + $exporter = new OtlpMetricsExporter($metricsCredentials, $timeoutMillis, $options); $reader = new ExportingReader($exporter); $this->meterProvider = MeterProvider::builder() + ->setResource($resource) ->addReader($reader) ->build(); $this->meter = $this->meterProvider->getMeter('google-cloud-spanner'); ShutdownHandler::register([$this->meterProvider, 'shutdown']); - $attemptMetricsMiddleware = function (MiddlewareInterface $handler) use ($metricsClientId, $location) { + $attemptMetricsMiddleware = function (MiddlewareInterface $handler) use ($metricsClientId) { return new MetricsAttemptMiddleware( $handler, $this->meter, $metricsClientId, - $this->projectId, - $this->clientVersion(), - $location + $this->clientVersion() ); }; - $operationMetricsMiddleware = function (MiddlewareInterface $handler) use ($metricsClientId, $location) { + $operationMetricsMiddleware = function (MiddlewareInterface $handler) use ($metricsClientId) { return new MetricsOperationMiddleware( $handler, $this->meter, $metricsClientId, - $this->projectId, - $this->clientVersion(), - $location + $this->clientVersion() ); }; @@ -1172,4 +1164,43 @@ private function getLocation(): string return $location; } + + /** + * Returns a hash of the client UUID for the metrics. + * + * @param string $clientUid + * @return string + */ + private function generateClientHash(string $clientUid): string + { + if ($clientUid === '') { + return '000000'; + } + + $hashHex = hash('fnv1a64', $clientUid); + $firstFour = substr($hashHex, 0, 4); + $intVal = hexdec($firstFour); + $tenBits = $intVal >> 6; + return sprintf('%06x', $tenBits); + } + + private function buildMetricsCredentials(array $options): CredentialsWrapper + { + $metricsCredentials = $options['metricsCredentials'] + ?? $options['credentials'] + ?? null; + + $credentialsConfig = [ + 'scopes' => [ + OtlpMetricsExporter::MONITORING_WRITE_SCOPE, + OtlpMetricsExporter::CLOUD_PLATFORM_SCOPE + ] + ]; + + $universeDomain = $options['universeDomain'] ?? GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN; + + $credentialsWrapper = $this->createCredentialsWrapper($metricsCredentials, $credentialsConfig, $universeDomain); + + return $credentialsWrapper; + } } diff --git a/Spanner/tests/Unit/Middleware/BuiltInMetricsAttemptMiddlewareTest.php b/Spanner/tests/Unit/Middleware/BuiltInMetricsAttemptMiddlewareTest.php index 8211ceb50b5c..738753313c99 100644 --- a/Spanner/tests/Unit/Middleware/BuiltInMetricsAttemptMiddlewareTest.php +++ b/Spanner/tests/Unit/Middleware/BuiltInMetricsAttemptMiddlewareTest.php @@ -58,40 +58,40 @@ public function setUp(): void $this->meter = $this->prophesize(MeterInterface::class); $this->meter->createHistogram( - 'attempt_latencies', + 'spanner.googleapis.com/internal/client/attempt_latencies', 'ms', Argument::any(), Argument::any() )->willReturn($this->attemptHistogram->reveal()); $this->meter->createCounter( - 'attempt_count', + 'spanner.googleapis.com/internal/client/attempt_count', '1', Argument::any() )->willReturn($this->attemptCounter->reveal()); $this->meter->createHistogram( - 'gfe_latencies', + 'spanner.googleapis.com/internal/client/gfe_latencies', 'ms', Argument::any(), Argument::any() )->willReturn($this->gfeHistogram->reveal()); $this->meter->createCounter( - 'gfe_connectivity_error_count', + 'spanner.googleapis.com/internal/client/gfe_connectivity_error_count', '1', Argument::any() )->willReturn($this->gfeErrorCounter->reveal()); $this->meter->createHistogram( - 'afe_latencies', + 'spanner.googleapis.com/internal/client/afe_latencies', 'ms', Argument::any(), Argument::any() )->willReturn($this->afeHistogram->reveal()); $this->meter->createCounter( - 'afe_connectivity_error_count', + 'spanner.googleapis.com/internal/client/afe_connectivity_error_count', '1', Argument::any() )->willReturn($this->afeErrorCounter->reveal()); @@ -118,9 +118,7 @@ public function testRecordsAttemptMetrics() $this->nextHandler, $this->meter->reveal(), $clientId, - $projectId, - $version, - $location + $version ); $call = $this->prophesize(Call::class); @@ -137,13 +135,9 @@ public function testRecordsAttemptMetrics() $expectedLabels = [ 'method' => 'Commit', 'status' => 'OK', - 'instance_id' => 'i', 'database' => 'd', - 'project_id' => $projectId, 'client_uid' => $clientId, 'client_name' => $expectedClientName, - 'instance_config' => 'unknown', - 'location' => $location, 'directpath_enabled' => 'false', 'directpath_used' => 'true' ]; @@ -174,9 +168,7 @@ public function testRecordsGfeMetricsOnStreamingResponse() $this->nextHandler, $this->meter->reveal(), 'client', - 'project', - 'name', - 'global' + 'name' ); $call = $this->prophesize(Call::class); @@ -209,9 +201,7 @@ public function testRecordsGfeErrorOnMissingHeader() $this->nextHandler, $this->meter->reveal(), 'client', - 'project', - 'name', - 'global' + 'name' ); $call = $this->prophesize(Call::class); @@ -239,9 +229,7 @@ public function testRecordsMetricsOnError() $this->nextHandler, $this->meter->reveal(), 'client', - 'project', - 'name', - 'global' + 'name' ); $call = $this->prophesize(Call::class); diff --git a/Spanner/tests/Unit/Middleware/BuiltInMetricsOperationMiddlewareTest.php b/Spanner/tests/Unit/Middleware/BuiltInMetricsOperationMiddlewareTest.php index ff480a8d99b3..41888d7e7c14 100644 --- a/Spanner/tests/Unit/Middleware/BuiltInMetricsOperationMiddlewareTest.php +++ b/Spanner/tests/Unit/Middleware/BuiltInMetricsOperationMiddlewareTest.php @@ -46,14 +46,14 @@ public function setUp(): void $this->meter = $this->prophesize(MeterInterface::class); $this->meter->createHistogram( - 'operation_latencies', + 'spanner.googleapis.com/internal/client/operation_latencies', 'ms', Argument::any(), Argument::any() )->willReturn($this->histogram->reveal()); $this->meter->createCounter( - 'operation_count', + 'spanner.googleapis.com/internal/client/operation_count', '1', Argument::any() )->willReturn($this->counter->reveal()); @@ -75,9 +75,7 @@ public function testRecordsOperationMetrics() $this->nextHandler, $this->meter->reveal(), $clientId, - $projectId, - $version, - $location + $version ); $call = $this->prophesize(Call::class); @@ -93,13 +91,9 @@ public function testRecordsOperationMetrics() $expectedLabels = [ 'method' => 'ExecuteSql', 'status' => 'OK', - 'instance_id' => 'i', 'database' => 'd', - 'project_id' => $projectId, 'client_uid' => $clientId, 'client_name' => $expectedClientName, - 'instance_config' => 'unknown', - 'location' => $location, 'directpath_enabled' => 'false', 'directpath_used' => 'false' ]; diff --git a/Spanner/tests/Unit/OpenTelemetry/BuiltInMetricsExporterTest.php b/Spanner/tests/Unit/OpenTelemetry/BuiltInMetricsExporterTest.php index ed333a157564..a6f3c1713e21 100644 --- a/Spanner/tests/Unit/OpenTelemetry/BuiltInMetricsExporterTest.php +++ b/Spanner/tests/Unit/OpenTelemetry/BuiltInMetricsExporterTest.php @@ -1,4 +1,5 @@ markTestSkipped('Google\Cloud\Monitoring\V3\Client\MetricServiceClient class is not available.'); + } + } + /** * @dataProvider hashDataProvider */ @@ -117,7 +126,8 @@ public function testExport() $labels = $timeSeries->getMetric()->getLabels(); if ($labels['method'] !== 'ExecuteSql' || $labels['status'] !== 'OK' || - $labels['database'] !== 'my-db') { + $labels['database'] !== 'my-db' + ) { return false; } diff --git a/Spanner/tests/Unit/OpenTelemetry/OtlpMetricsExporterTest.php b/Spanner/tests/Unit/OpenTelemetry/OtlpMetricsExporterTest.php new file mode 100644 index 000000000000..cd5c47fdbab2 --- /dev/null +++ b/Spanner/tests/Unit/OpenTelemetry/OtlpMetricsExporterTest.php @@ -0,0 +1,167 @@ +prophesize(\Google\ApiCore\CredentialsWrapper::class); + $mockOtlpExporter = $this->prophesize(PushMetricExporterInterface::class); + $exporter = new OtlpMetricsExporter( + $mockCredentials->reveal(), + 100, + [], + $mockOtlpExporter->reveal() + ); + + $scope = new InstrumentationScope('google-cloud-spanner', '1.0.0', null, Attributes::create([])); + $resource = ResourceInfo::create(Attributes::create([ + 'gcp.resource_type' => 'spanner_instance_client', + 'client_hash' => '000212' + ])); + + $attributes = Attributes::create([ + 'method' => 'ExecuteSql', + 'status' => 'OK', + 'instance_id' => 'my-instance', + 'database' => 'my-db' + ]); + + $point = new NumberDataPoint( + 1, + $attributes, + 1711368000000000000, + 1711368060000000000 + ); + + $sum = new Sum([$point], Temporality::CUMULATIVE, true); + $metric = new OTelMetric( + $scope, + $resource, + 'spanner.googleapis.com/internal/client/attempt_count', + '1', + 'desc', + $sum + ); + + $mockOtlpExporter->export(Argument::type('iterable'))->shouldBeCalled()->willReturn(true); + + $this->assertTrue($exporter->export([$metric])); + } + + public function testExportCustomTimeout() + { + $mockCredentials = $this->prophesize(\Google\ApiCore\CredentialsWrapper::class); + $mockOtlpExporter = $this->prophesize(PushMetricExporterInterface::class); + $timeout = 500; + $exporter = new OtlpMetricsExporter($mockCredentials->reveal(), $timeout, [], $mockOtlpExporter->reveal()); + + $scope = new InstrumentationScope('google-cloud-spanner', '1.0.0', null, Attributes::create([])); + $resource = ResourceInfo::create(Attributes::create([])); + + $attributes = Attributes::create([]); + $point = new NumberDataPoint(1, $attributes, 1711368000000000000, 1711368060000000000); + $sum = new Sum([$point], Temporality::CUMULATIVE, true); + $metric = new OTelMetric( + $scope, + $resource, + 'spanner.googleapis.com/internal/client/attempt_count', + '1', + 'desc', + $sum + ); + + $mockOtlpExporter->export(Argument::any())->shouldBeCalled()->willReturn(true); + + $this->assertTrue($exporter->export([$metric])); + } + + public function testConstructWithCustomMetricsCredentials() + { + $mockCredentials = $this->prophesize(\Google\ApiCore\CredentialsWrapper::class); + $exporter = new OtlpMetricsExporter($mockCredentials->reveal(), 5000, []); + + $this->assertInstanceOf(OtlpMetricsExporter::class, $exporter); + } + + public function testGuzzleMiddlewareAttachesAuthorizationAndQuotaProjectHeaders() + { + $mockCredentials = $this->prophesize(\Google\ApiCore\CredentialsWrapper::class); + $mockCredentials->getAuthorizationHeaderCallback()->willReturn(function () { + return ['authorization' => ['Bearer test-metric-token']]; + }); + $mockCredentials->getQuotaProject()->willReturn('test-quota-project-id'); + + $mockHandler = new \GuzzleHttp\Handler\MockHandler([ + new \GuzzleHttp\Psr7\Response(200, [], '') + ]); + $handlerStack = \GuzzleHttp\HandlerStack::create($mockHandler); + + $exporter = new OtlpMetricsExporter( + $mockCredentials->reveal(), + 5000, + ['handlerStack' => $handlerStack] + ); + + $scope = new InstrumentationScope('google-cloud-spanner', '1.0.0', null, Attributes::create([])); + $resource = ResourceInfo::create(Attributes::create([])); + $point = new NumberDataPoint(1, Attributes::create([]), 1711368000000000000, 1711368060000000000); + $sum = new Sum([$point], Temporality::CUMULATIVE, true); + $metric = new OTelMetric( + $scope, + $resource, + 'spanner.googleapis.com/internal/client/attempt_count', + '1', + 'desc', + $sum + ); + + // Access the internal otlpExporter via reflection to trigger HTTP call through transport + $reflection = new ReflectionClass(OtlpMetricsExporter::class); + $property = $reflection->getProperty('otlpExporter'); + $property->setAccessible(true); + $otlpExporter = $property->getValue($exporter); + + $otlpExporter->export([$metric]); + + $lastRequest = $mockHandler->getLastRequest(); + $this->assertNotNull($lastRequest); + $this->assertEquals(['Bearer test-metric-token'], $lastRequest->getHeader('authorization')); + $this->assertEquals(['test-quota-project-id'], $lastRequest->getHeader('x-goog-user-project')); + } +} diff --git a/composer.json b/composer.json index bc6a77c580b8..3da8ef869d11 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/exporter-otlp": "^1.4" }, "require-dev": { "phpunit/phpunit": "^9.6", @@ -211,7 +212,6 @@ "google/cloud-memorystore": "0.13.0", "google/cloud-migrationcenter": "1.3.0", "google/cloud-modelarmor": "0.8.0", - "google/cloud-monitoring": "2.3.0", "google/cloud-netapp": "1.13.0", "google/cloud-network-connectivity": "2.6.0", "google/cloud-network-management": "2.11.0",