Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Spanner/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
28 changes: 7 additions & 21 deletions Spanner/src/Middleware/MetricsAttemptMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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'
];
Expand Down
20 changes: 3 additions & 17 deletions Spanner/src/Middleware/MetricsOperationMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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'
];
Expand Down
2 changes: 2 additions & 0 deletions Spanner/src/OpenTelemetry/MetricsExporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
/**
* MetricsExporter exports Spanner client metrics to Google Cloud Monitoring
* using the internal service endpoint.
*
* @deprecated
*/
class MetricsExporter implements PushMetricExporterInterface, AggregationTemporalitySelectorInterface
{
Expand Down
162 changes: 162 additions & 0 deletions Spanner/src/OpenTelemetry/OtlpMetricsExporter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
<?php
/*
* Copyright 2026 Google LLC
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

namespace Google\Cloud\Spanner\OpenTelemetry;

use Google\ApiCore\CredentialsWrapper;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\HandlerStack;
use OpenTelemetry\Contrib\Otlp\MetricExporter as OtlpMetricExporter;
use OpenTelemetry\SDK\Common\Export\Http\PsrTransportFactory;
use OpenTelemetry\SDK\Metrics\AggregationTemporalitySelectorInterface;
use OpenTelemetry\SDK\Metrics\Data\Metric as OTelMetric;
use OpenTelemetry\SDK\Metrics\Data\Temporality;
use OpenTelemetry\SDK\Metrics\MetricMetadataInterface;
use OpenTelemetry\SDK\Metrics\PushMetricExporterInterface;
use Psr\Http\Message\RequestInterface;
use Throwable;

/**
* OtlpMetricsExporter encapsulates the standard OpenTelemetry OTLP MetricExporter
* targeting Google Cloud Telemetry endpoint.
*
* @internal
*/
class OtlpMetricsExporter implements PushMetricExporterInterface, AggregationTemporalitySelectorInterface
{
private const DEFAULT_ENDPOINT = 'https://telemetry.googleapis.com/v1/metrics';
public const MONITORING_WRITE_SCOPE = 'https://www.googleapis.com/auth/monitoring.write';
public const CLOUD_PLATFORM_SCOPE = 'https://www.googleapis.com/auth/cloud-platform';

private PushMetricExporterInterface $otlpExporter;

/**
* @param CredentialsWrapper $metricsCredentials The credentials wrapper for metric export.
* @param int $timeoutMillis The timeout defined for the metrics client during export.
* @param array $options Optional configuration parameters.
* @param PushMetricExporterInterface|null $otlpExporter Optional inner exporter for testing.
*/
public function __construct(
CredentialsWrapper $metricsCredentials,
int $timeoutMillis = 5000,
array $options = [],
?PushMetricExporterInterface $otlpExporter = null
) {
if ($otlpExporter !== null) {
$this->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<OTelMetric> $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;
}
}
Loading
Loading