From 97583db5cc67bfe8a8700e43a31310c23e8cbead Mon Sep 17 00:00:00 2001 From: Alka Trivedi Date: Tue, 15 Sep 2026 01:19:57 +0530 Subject: [PATCH 1/5] chore(Spanner)!: upgrade OpenTelemetry to v2 --- .../src/cloud-util.ts | 4 +- handwritten/spanner/package.json | 10 ++--- handwritten/spanner/src/metrics/constants.ts | 42 ++++++++----------- .../spanner/src/metrics/external-types.ts | 13 ++++++ .../src/metrics/metrics-tracer-factory.ts | 13 +++--- handwritten/spanner/src/metrics/transform.ts | 3 +- .../test/metrics/metrics-tracer-factory.ts | 4 +- handwritten/spanner/test/metrics/metrics.ts | 4 +- .../test/metrics/spanner-metrics-exporter.ts | 4 +- handwritten/spanner/test/metrics/transform.ts | 4 +- 10 files changed, 54 insertions(+), 47 deletions(-) diff --git a/handwritten/spanner/google-cloud-spanner-executor/src/cloud-util.ts b/handwritten/spanner/google-cloud-spanner-executor/src/cloud-util.ts index d6e3e1d042ca..4feb267f4ca5 100644 --- a/handwritten/spanner/google-cloud-spanner-executor/src/cloud-util.ts +++ b/handwritten/spanner/google-cloud-spanner-executor/src/cloud-util.ts @@ -21,7 +21,7 @@ import {SpannerOptions} from '../../src'; import {trace} from '@opentelemetry/api'; import {NodeTracerProvider} from '@opentelemetry/sdk-trace-node'; import {OTLPTraceExporter} from '@opentelemetry/exporter-trace-otlp-grpc'; -import {Resource} from '@opentelemetry/resources'; +import {resourceFromAttributes} from '@opentelemetry/resources'; import {ATTR_SERVICE_NAME} from '@opentelemetry/semantic-conventions'; import { BatchSpanProcessor, @@ -50,7 +50,7 @@ export class CloudUtil { }); const provider = new NodeTracerProvider({ - resource: new Resource({ + resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: 'spanner-node-worker-proxy', 'gcp.project_id': WorkerProxy.PROJECT_ID, }) as any, diff --git a/handwritten/spanner/package.json b/handwritten/spanner/package.json index 0bd522a07d7a..bd710b3351fd 100644 --- a/handwritten/spanner/package.json +++ b/handwritten/spanner/package.json @@ -56,7 +56,6 @@ "@babel/traverse": "7.27.7", "@google-cloud/common": "^6.0.0", "@google-cloud/monitoring": "^5.0.0", - "@google-cloud/opentelemetry-resource-util": "^2.4.0", "@google-cloud/precise-date": "^5.0.0", "@google-cloud/promisify": "^5.0.0", "@google-cloud/spanner-api": "^0.2.0", @@ -65,8 +64,9 @@ "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^2.0.0", "@opentelemetry/core": "^2.0.0", - "@opentelemetry/resources": "^1.8.0", - "@opentelemetry/sdk-metrics": "^1.30.1", + "@opentelemetry/resource-detector-gcp": "^0.57.0", + "@opentelemetry/resources": "^2.11.0", + "@opentelemetry/sdk-metrics": "^2.11.0", "@opentelemetry/semantic-conventions": "^1.30.0", "@types/big.js": "^6.2.2", "@types/stack-trace": "^0.0.33", @@ -87,9 +87,9 @@ }, "devDependencies": { "@grpc/reflection": "^1.0.4", - "@opentelemetry/sdk-trace-base": "^2.0.0", + "@opentelemetry/sdk-trace-base": "^2.11.0", "@opentelemetry/sdk-trace-node": "^2.0.0", - "@opentelemetry/exporter-trace-otlp-grpc": "^0.57.0", + "@opentelemetry/exporter-trace-otlp-grpc": "^0.222.0", "@types/concat-stream": "^2.0.3", "@types/extend": "^3.0.4", "@types/is": "^0.0.25", diff --git a/handwritten/spanner/src/metrics/constants.ts b/handwritten/spanner/src/metrics/constants.ts index 959eeb3d817b..215270bc8e1a 100644 --- a/handwritten/spanner/src/metrics/constants.ts +++ b/handwritten/spanner/src/metrics/constants.ts @@ -11,10 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -import { - View, - ExplicitBucketHistogramAggregation, -} from '@opentelemetry/sdk-metrics'; +import {AggregationType, ViewOptions} from '@opentelemetry/sdk-metrics'; export const SPANNER_METER_NAME = 'spanner-nodejs'; export const CLIENT_METRICS_PREFIX = 'spanner.googleapis.com/internal/client'; @@ -91,33 +88,30 @@ export const HISTOGRAM_BUCKET_BOUNDARIES = [ ]; // Defined Views for metric aggregation -export const OPERATION_LATENCY_VIEW = new View({ +const HISTOGRAM_AGGREGATION = { + type: AggregationType.EXPLICIT_BUCKET_HISTOGRAM as const, + options: {boundaries: HISTOGRAM_BUCKET_BOUNDARIES}, +}; + +export const OPERATION_LATENCY_VIEW: ViewOptions = { instrumentName: METRIC_NAME_OPERATION_LATENCIES, - aggregation: new ExplicitBucketHistogramAggregation( - HISTOGRAM_BUCKET_BOUNDARIES, - ), -}); + aggregation: HISTOGRAM_AGGREGATION, +}; -export const ATTEMPT_LATENCY_VIEW = new View({ +export const ATTEMPT_LATENCY_VIEW: ViewOptions = { instrumentName: METRIC_NAME_ATTEMPT_LATENCIES, - aggregation: new ExplicitBucketHistogramAggregation( - HISTOGRAM_BUCKET_BOUNDARIES, - ), -}); + aggregation: HISTOGRAM_AGGREGATION, +}; -export const GFE_LATENCY_VIEW = new View({ +export const GFE_LATENCY_VIEW: ViewOptions = { instrumentName: METRIC_NAME_GFE_LATENCIES, - aggregation: new ExplicitBucketHistogramAggregation( - HISTOGRAM_BUCKET_BOUNDARIES, - ), -}); + aggregation: HISTOGRAM_AGGREGATION, +}; -export const AFE_LATENCY_VIEW = new View({ +export const AFE_LATENCY_VIEW: ViewOptions = { instrumentName: METRIC_NAME_AFE_LATENCIES, - aggregation: new ExplicitBucketHistogramAggregation( - HISTOGRAM_BUCKET_BOUNDARIES, - ), -}); + aggregation: HISTOGRAM_AGGREGATION, +}; export const METRIC_VIEWS = [ OPERATION_LATENCY_VIEW, diff --git a/handwritten/spanner/src/metrics/external-types.ts b/handwritten/spanner/src/metrics/external-types.ts index 07c08f9b08ba..ac8f994cea79 100644 --- a/handwritten/spanner/src/metrics/external-types.ts +++ b/handwritten/spanner/src/metrics/external-types.ts @@ -35,3 +35,16 @@ export enum ValueType { DOUBLE = 'DOUBLE', DISTRIBUTION = 'DISTRIBUTION', } + +/** + * A Google Cloud Monitoring monitored resource. + * + * Previously imported from `@google-cloud/opentelemetry-resource-util`, which is + * deprecated and scheduled for archival. The interface is a plain data shape, so + * it is declared locally instead. + * See https://cloud.google.com/monitoring/api/ref_v3/rest/v3/MonitoredResource + */ +export interface MonitoredResource { + type: string; + labels: {[key: string]: string}; +} diff --git a/handwritten/spanner/src/metrics/metrics-tracer-factory.ts b/handwritten/spanner/src/metrics/metrics-tracer-factory.ts index d4de5f4c234b..3fac91ed4351 100644 --- a/handwritten/spanner/src/metrics/metrics-tracer-factory.ts +++ b/handwritten/spanner/src/metrics/metrics-tracer-factory.ts @@ -17,8 +17,11 @@ import * as os from 'os'; import * as process from 'process'; import {MeterProvider, MetricReader} from '@opentelemetry/sdk-metrics'; import {Counter, Histogram, context, ROOT_CONTEXT} from '@opentelemetry/api'; -import {detectResources, Resource} from '@opentelemetry/resources'; -import {GcpDetectorSync} from '@google-cloud/opentelemetry-resource-util'; +import { + detectResources, + resourceFromAttributes, +} from '@opentelemetry/resources'; +import {gcpDetector} from '@opentelemetry/resource-detector-gcp'; import * as Constants from './constants'; import {MetricsTracer} from './metrics-tracer'; const version = require('../../../package.json').version; @@ -122,7 +125,7 @@ export class MetricsTracerFactory { */ public getMeterProvider(readers: MetricReader[] = []): MeterProvider { if (this._meterProvider === null) { - const resource = new Resource({ + const resource = resourceFromAttributes({ [Constants.MONITORED_RES_LABEL_KEY_PROJECT]: this._projectId, [Constants.MONITORED_RES_LABEL_KEY_CLIENT_HASH]: this._clientHash, [Constants.MONITORED_RES_LABEL_KEY_LOCATION]: this._location, @@ -454,14 +457,14 @@ export class MetricsTracerFactory { /** * Gets the location (region) of the client, otherwise returns to the "global" region. - * Uses GcpDetectorSync to detect the region from the environment. + * Uses the GCP resource detector to detect the region from the environment. * @returns The detected region string, or "global" if not found. */ private static async _detectClientLocation(): Promise { const defaultRegion = 'global'; try { const resource = await detectResources({ - detectors: [new GcpDetectorSync()], + detectors: [gcpDetector], }); await resource?.waitForAsyncAttributes?.(); diff --git a/handwritten/spanner/src/metrics/transform.ts b/handwritten/spanner/src/metrics/transform.ts index 71a0052c1eee..b1620a95ab60 100644 --- a/handwritten/spanner/src/metrics/transform.ts +++ b/handwritten/spanner/src/metrics/transform.ts @@ -21,9 +21,8 @@ import { ResourceMetrics, } from '@opentelemetry/sdk-metrics'; import {Resource} from '@opentelemetry/resources'; -import {MonitoredResource} from '@google-cloud/opentelemetry-resource-util'; import * as path from 'path'; -import {MetricKind, ValueType} from './external-types'; +import {MetricKind, MonitoredResource, ValueType} from './external-types'; import { SPANNER_METER_NAME, CLIENT_METRICS_PREFIX, diff --git a/handwritten/spanner/test/metrics/metrics-tracer-factory.ts b/handwritten/spanner/test/metrics/metrics-tracer-factory.ts index 78dc1d27a132..241d14706b61 100644 --- a/handwritten/spanner/test/metrics/metrics-tracer-factory.ts +++ b/handwritten/spanner/test/metrics/metrics-tracer-factory.ts @@ -79,13 +79,11 @@ describe('MetricsTracerFactory', () => { MetricsTracerFactory.enabled = true; sandbox.resetHistory(); await MetricsTracerFactory.resetInstance(); - const provider = - MetricsTracerFactory.getInstance('project-id')!.getMeterProvider(); const reader = new PeriodicExportingMetricReader({ exporter: mockExporter, exportIntervalMillis: 60000, }); - provider.addMetricReader(reader); + MetricsTracerFactory.getInstance('project-id')!.getMeterProvider([reader]); }); afterEach(async () => { diff --git a/handwritten/spanner/test/metrics/metrics.ts b/handwritten/spanner/test/metrics/metrics.ts index 70dbfa4e8cd4..66dceafd049d 100644 --- a/handwritten/spanner/test/metrics/metrics.ts +++ b/handwritten/spanner/test/metrics/metrics.ts @@ -294,7 +294,7 @@ describe('Test metrics with mock server', () => { attributes, ); // Since we only have one attempt, the attempt latency should be fairly close to the operation latency - assertApprox(operationLatency, attemptLatency, 30); + assertApprox(operationLatency, attemptLatency, 100); const gfeLatency = getAggregatedValue(gfeLatenciesData, attributes); assert.strictEqual(gfeLatency, 123); @@ -719,7 +719,7 @@ describe('Test metrics with mock server', () => { attributes, ); // Since we only have one attempt, the attempt latency should be fairly close to the operation latency - assertApprox(operationLatency, attemptLatency, 30); + assertApprox(operationLatency, attemptLatency, 100); const gfeLatency = getAggregatedValue(gfeLatenciesData, attributes); assert.strictEqual(gfeLatency, 123); diff --git a/handwritten/spanner/test/metrics/spanner-metrics-exporter.ts b/handwritten/spanner/test/metrics/spanner-metrics-exporter.ts index cd1bb1787052..5c7d6e009021 100644 --- a/handwritten/spanner/test/metrics/spanner-metrics-exporter.ts +++ b/handwritten/spanner/test/metrics/spanner-metrics-exporter.ts @@ -31,7 +31,7 @@ import { } from '../../src/metrics/constants'; import {Counter, Meter, Histogram} from '@opentelemetry/api'; import {ExportResult, ExportResultCode} from '@opentelemetry/core'; -import {Resource} from '@opentelemetry/resources'; +import {resourceFromAttributes} from '@opentelemetry/resources'; const PROJECT_ID = 'test-project'; const INSTANCE_ID = 'test-instance'; @@ -95,7 +95,7 @@ describe('Export', () => { beforeEach(() => { exporter = new CloudMonitoringMetricsExporter({auth}, PROJECT_ID); reader = new InMemoryMetricReader(); - const resource = new Resource({ + const resource = resourceFromAttributes({ ['project_id']: PROJECT_ID, ['client_hash']: CLIENT_HASH, ['location']: LOCATION, diff --git a/handwritten/spanner/test/metrics/transform.ts b/handwritten/spanner/test/metrics/transform.ts index c9cadd05044a..03c7f006f13e 100644 --- a/handwritten/spanner/test/metrics/transform.ts +++ b/handwritten/spanner/test/metrics/transform.ts @@ -28,7 +28,7 @@ import { MeterProvider, MetricReader, } from '@opentelemetry/sdk-metrics'; -import {Resource} from '@opentelemetry/resources'; +import {Resource, resourceFromAttributes} from '@opentelemetry/resources'; import { Attributes, Counter, @@ -84,7 +84,7 @@ describe('transform', () => { sandbox.stub(MetricsTracerFactory, 'getInstance').returns(mockFactory); reader = new InMemoryMetricReader(); - resource = new Resource({ + resource = resourceFromAttributes({ ['project_id']: 'project_id', ['client_hash']: 'test_hash', ['location']: 'test_location', From bc4ce0b66a9610281cfa0e982c254c2113b7bef7 Mon Sep 17 00:00:00 2001 From: Alka Trivedi Date: Wed, 16 Sep 2026 16:04:59 +0530 Subject: [PATCH 2/5] fix presubmits by modifying the pnpm lock file --- pnpm-lock.yaml | 258 +++++++++++++++++++++++++++++++------------------ 1 file changed, 166 insertions(+), 92 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index af53db88ae4d..215773a3bf98 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1701,7 +1701,7 @@ importers: version: 6.0.0 '@google-cloud/opentelemetry-cloud-monitoring-exporter': specifier: ^0.20.0 - version: 0.20.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@1.30.1(@opentelemetry/api@1.9.1))(encoding@0.1.13) + version: 0.20.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@1.30.1(@opentelemetry/api@1.9.1))(encoding@0.1.13) '@google-cloud/opentelemetry-resource-util': specifier: ^2.4.0 version: 2.4.0(@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.1))(encoding@0.1.13) @@ -2222,7 +2222,7 @@ importers: version: 0.4.10(@types/node@24.13.3) '@google-cloud/opentelemetry-cloud-trace-exporter': specifier: ^3.0.0 - version: 3.0.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1))(encoding@0.1.13) + version: 3.0.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(encoding@0.1.13) '@google-cloud/promisify': specifier: ^6.0.1 version: 6.0.1 @@ -2488,7 +2488,7 @@ importers: version: 9.0.2 '@google-cloud/opentelemetry-cloud-trace-exporter': specifier: ^2.1.0 - version: 2.4.1(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1))(encoding@0.1.13) + version: 2.4.1(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1))(encoding@0.1.13) '@google-cloud/pubsub': specifier: ^6.0.0 version: 6.0.1 @@ -2930,9 +2930,6 @@ importers: '@google-cloud/monitoring': specifier: ^5.0.0 version: 5.4.0 - '@google-cloud/opentelemetry-resource-util': - specifier: ^2.4.0 - version: 2.4.0(@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.1))(encoding@0.1.13) '@google-cloud/precise-date': specifier: ^5.0.0 version: 5.1.0 @@ -2957,12 +2954,15 @@ importers: '@opentelemetry/core': specifier: ^2.0.0 version: 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resource-detector-gcp': + specifier: ^0.57.0 + version: 0.57.0(@opentelemetry/api@1.9.1) '@opentelemetry/resources': - specifier: ^1.8.0 - version: 1.30.1(@opentelemetry/api@1.9.1) + specifier: ^2.11.0 + version: 2.11.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-metrics': - specifier: ^1.30.1 - version: 1.30.1(@opentelemetry/api@1.9.1) + specifier: ^2.11.0 + version: 2.11.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': specifier: ^1.30.0 version: 1.39.0 @@ -3019,11 +3019,11 @@ importers: specifier: ^1.0.4 version: 1.0.4(@grpc/grpc-js@1.14.4) '@opentelemetry/exporter-trace-otlp-grpc': - specifier: ^0.57.0 - version: 0.57.2(@opentelemetry/api@1.9.1) + specifier: ^0.222.0 + version: 0.222.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-trace-base': - specifier: ^2.0.0 - version: 2.10.0(@opentelemetry/api@1.9.1) + specifier: ^2.11.0 + version: 2.11.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-trace-node': specifier: ^2.0.0 version: 2.10.0(@opentelemetry/api@1.9.1) @@ -16937,14 +16937,14 @@ packages: '@opencensus/propagation-stackdriver@0.1.0': resolution: {integrity: sha512-YLklu8jnnYKaJ8gUFz3rM0FVdsWXEJAMLzeeU4JRac6LI34raENy4kvRezZtNEFS5KthaJUsYg04sPc/Ag0w4w==} + '@opentelemetry/api-logs@0.222.0': + resolution: {integrity: sha512-9mb1If+IF6u0ZVXkHQ6ogEae5HwA6ajIVUgpSDQyRASxft6BSXHvBvPooRle3yFN/fKnCdSOnuu0OC3PLcF6+g==} + engines: {node: '>=8.0.0'} + '@opentelemetry/api-logs@0.52.1': resolution: {integrity: sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A==} engines: {node: '>=14'} - '@opentelemetry/api-logs@0.57.2': - resolution: {integrity: sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==} - engines: {node: '>=14'} - '@opentelemetry/api@1.9.1': resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} engines: {node: '>=8.0.0'} @@ -16985,18 +16985,24 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/core@2.11.0': + resolution: {integrity: sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/exporter-trace-otlp-grpc@0.222.0': + resolution: {integrity: sha512-uisilePEdOMa3hIfXW82XXU6FofF8gw3iv+PoboIMWEUBC7XbJB3XnIHSdrSDsqCw3njR/If08qD0yzOY1U9Sg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-trace-otlp-grpc@0.52.1': resolution: {integrity: sha512-pVkSH20crBwMTqB3nIN4jpQKUEoB0Z94drIHpYyEqs7UBr+I0cpYyOR3bqjA/UasQUMROb3GX8ZX4/9cVRqGBQ==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.0.0 - '@opentelemetry/exporter-trace-otlp-grpc@0.57.2': - resolution: {integrity: sha512-gHU1vA3JnHbNxEXg5iysqCWxN9j83d7/epTYBZflqQnTyCC4N7yZXn/dMM+bEmyhQPGjhCkNZLx4vZuChH1PYw==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-http@0.52.1': resolution: {integrity: sha512-05HcNizx0BxcFKKnS5rwOV+2GevLTVIRA0tRgWYyw4yCgR53Ic/xk83toYKts7kbzcI+dswInUg/4s8oyA+tqg==} engines: {node: '>=14'} @@ -17021,15 +17027,21 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/otlp-exporter-base@0.222.0': + resolution: {integrity: sha512-YbywG3veEm2Fb6TbdxRkuquWob6eVWXuA8/Ba1tXz9jHfUqpdE3keilOHEtPboC4CvS1bjeeVfNkWGOOrLj+lw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/otlp-exporter-base@0.52.1': resolution: {integrity: sha512-z175NXOtX5ihdlshtYBe5RpGeBoTXVCKPPLiQlD6FHvpM4Ch+p2B0yWKYSrBfLH24H9zjJiBdTrtD+hLlfnXEQ==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': ^1.0.0 - '@opentelemetry/otlp-exporter-base@0.57.2': - resolution: {integrity: sha512-XdxEzL23Urhidyebg5E6jZoaiW5ygP/mRjxLHixogbqwDy2Faduzb5N0o/Oi+XTIJu+iyxXdVORjXax+Qgfxag==} - engines: {node: '>=14'} + '@opentelemetry/otlp-grpc-exporter-base@0.222.0': + resolution: {integrity: sha512-Kw5WqDBYYpCgY+edHWZS/Jp3bdguqNkCyEWOsqFhf8RmtcEuWYzPFA9i+L6UsNoJXTs8ngeQB6dwELH1czNYjA==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 @@ -17039,9 +17051,9 @@ packages: peerDependencies: '@opentelemetry/api': ^1.0.0 - '@opentelemetry/otlp-grpc-exporter-base@0.57.2': - resolution: {integrity: sha512-USn173KTWy0saqqRB5yU9xUZ2xdgb1Rdu5IosJnm9aV4hMTuFFRTUsQxbgc24QxpCHeoKzzCSnS/JzdV0oM2iQ==} - engines: {node: '>=14'} + '@opentelemetry/otlp-transformer@0.222.0': + resolution: {integrity: sha512-/F3BZ89+CJQnZkMh2tCrtcdB+XT2Dxhj4FFE+WPQ//413hmFL0/RfEX6vgOIWGhiSzrkHWTK3+6SiT7K5/g/jQ==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 @@ -17051,12 +17063,6 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/otlp-transformer@0.57.2': - resolution: {integrity: sha512-48IIRj49gbQVK52jYsw70+Jv+JbahT8BqT2Th7C4H7RCM9d0gZ5sgNPoMpWldmfjvIsSgiGJtjfk9MeZvjhoig==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - '@opentelemetry/propagator-b3@1.25.1': resolution: {integrity: sha512-p6HFscpjrv7//kE+7L+3Vn00VEDUJB0n6ZrjkTYHrJ58QZ8B3ajSJhRbCcY6guQ3PDjTbxWklyvIN2ojVbIb1A==} engines: {node: '>=14'} @@ -17081,6 +17087,12 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/resource-detector-gcp@0.57.0': + resolution: {integrity: sha512-wp+hk7jD7mxA6swAqa5wQ3jws2egh1DCPZ9FfEgntmFw/mc8MaGZjwuttOoMeRjIlcBCCaNUHuN8B+MZaujAXQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + '@opentelemetry/resources@1.25.1': resolution: {integrity: sha512-pkZT+iFYIZsVn6+GzM0kSX+u3MSLCY9md+lIJOoKl/P+gJFfxJte/60Usdp8Ce4rOs8GduUpSPNe1ddGyDT1sQ==} engines: {node: '>=14'} @@ -17099,14 +17111,20 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-logs@0.52.1': - resolution: {integrity: sha512-MBYh+WcPPsN8YpRHRmK1Hsca9pVlyyKd4BxOC4SsgHACnl/bPp4Cri9hWhVm5+2tiQ9Zf4qSc1Jshw9tOLGWQA==} - engines: {node: '>=14'} + '@opentelemetry/resources@2.11.0': + resolution: {integrity: sha512-Ie7+8q8MDF4FAEQCKVMTx3ReUvxiIAgIiiW3c9JdmP8+HMcDy20puT+AHjexnExgnbvBxjQ9fjkFDWrikJ2jQA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.222.0': + resolution: {integrity: sha512-+19YHODIjaUCArxleaJtuufFZVpz/xvvK+VllQqE+W8hHolxdoRwHfK/s667zezwh1hkx6FFF+oYzetYgqK+Bg==} + engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.4.0 <1.10.0' - '@opentelemetry/sdk-logs@0.57.2': - resolution: {integrity: sha512-TXFHJ5c+BKggWbdEQ/inpgIzEmS2BGQowLE9UhsMd7YYlUfBQJ4uax0VF/B5NYigdM/75OoJGhAV3upEhK+3gg==} + '@opentelemetry/sdk-logs@0.52.1': + resolution: {integrity: sha512-MBYh+WcPPsN8YpRHRmK1Hsca9pVlyyKd4BxOC4SsgHACnl/bPp4Cri9hWhVm5+2tiQ9Zf4qSc1Jshw9tOLGWQA==} engines: {node: '>=14'} peerDependencies: '@opentelemetry/api': '>=1.4.0 <1.10.0' @@ -17123,6 +17141,12 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/sdk-metrics@2.11.0': + resolution: {integrity: sha512-7GXXcObyHyDUUSG+L+kJoquty01bzm7ivE7+SSgXXJcHuPzGviptxwARmI2c+bnnxjexGQbJnyNlN8HxBP/Y7A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + '@opentelemetry/sdk-node@0.52.1': resolution: {integrity: sha512-uEG+gtEr6eKd8CVWeKMhH2olcCHM9dEK68pe0qE0be32BcCRsvYURhHaD1Srngh1SQcnQzZ4TP324euxqtBOJA==} engines: {node: '>=14'} @@ -17147,6 +17171,12 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/sdk-trace-base@2.11.0': + resolution: {integrity: sha512-H19x/TX/LZdqiYOjM7fqtSxwlplC5pgelavqbQdHbhdq0q/AI/TGkM2dfGuuynTXmJPeF2HoZVoPDu+TGoW78A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/sdk-trace-node@1.25.1': resolution: {integrity: sha512-nMcjFIKxnFqoez4gUmihdBrbpsEnAX/Xj16sGvZm+guceYE0NE00vLhpDVK6f3q8Q4VFI5xG8JjlXKMB/SkTTQ==} engines: {node: '>=14'} @@ -17171,6 +17201,12 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/sdk-trace@2.11.0': + resolution: {integrity: sha512-fFnTqGm8/G73GQVnxYi7LXa1ZVYEUvgL6XI1LpvV0bPC7WQ/ZGgKxCSl8FnlZBKto9JHHEFTO6s6CUpvvtwFrA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/semantic-conventions@1.25.1': resolution: {integrity: sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==} engines: {node: '>=14'} @@ -25916,12 +25952,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@google-cloud/opentelemetry-cloud-monitoring-exporter@0.20.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@1.30.1(@opentelemetry/api@1.9.1))(encoding@0.1.13)': + '@google-cloud/opentelemetry-cloud-monitoring-exporter@0.20.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-metrics@1.30.1(@opentelemetry/api@1.9.1))(encoding@0.1.13)': dependencies: '@google-cloud/opentelemetry-resource-util': 2.4.0(@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.1))(encoding@0.1.13) '@google-cloud/precise-date': 4.0.0 '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.1) google-auth-library: 9.15.1(encoding@0.1.13) @@ -25930,13 +25966,13 @@ snapshots: - encoding - supports-color - '@google-cloud/opentelemetry-cloud-trace-exporter@2.4.1(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1))(encoding@0.1.13)': + '@google-cloud/opentelemetry-cloud-trace-exporter@2.4.1(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1))(encoding@0.1.13)': dependencies: '@google-cloud/opentelemetry-resource-util': 2.4.0(@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.1))(encoding@0.1.13) '@grpc/grpc-js': 1.14.4 '@grpc/proto-loader': 0.7.15 '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.1) google-auth-library: 9.15.1(encoding@0.1.13) @@ -25944,15 +25980,15 @@ snapshots: - encoding - supports-color - '@google-cloud/opentelemetry-cloud-trace-exporter@3.0.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1))(encoding@0.1.13)': + '@google-cloud/opentelemetry-cloud-trace-exporter@3.0.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1))(encoding@0.1.13)': dependencies: - '@google-cloud/opentelemetry-resource-util': 3.0.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1))(encoding@0.1.13) + '@google-cloud/opentelemetry-resource-util': 3.0.0(@opentelemetry/core@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(encoding@0.1.13) '@grpc/grpc-js': 1.14.4 '@grpc/proto-loader': 0.8.1 '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.11.0(@opentelemetry/api@1.9.1) google-auth-library: 9.15.1(encoding@0.1.13) transitivePeerDependencies: - encoding @@ -25967,10 +26003,10 @@ snapshots: - encoding - supports-color - '@google-cloud/opentelemetry-resource-util@3.0.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1))(encoding@0.1.13)': + '@google-cloud/opentelemetry-resource-util@3.0.0(@opentelemetry/core@2.11.0(@opentelemetry/api@1.9.1))(@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1))(encoding@0.1.13)': dependencies: - '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.11.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.39.0 gcp-metadata: 6.1.1(encoding@0.1.13) transitivePeerDependencies: @@ -26910,11 +26946,11 @@ snapshots: hex2dec: 1.1.2 uuid: 8.0.0 - '@opentelemetry/api-logs@0.52.1': + '@opentelemetry/api-logs@0.222.0': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/api-logs@0.57.2': + '@opentelemetry/api-logs@0.52.1': dependencies: '@opentelemetry/api': 1.9.1 @@ -26947,6 +26983,19 @@ snapshots: '@opentelemetry/api': 1.9.1 '@opentelemetry/semantic-conventions': 1.39.0 + '@opentelemetry/core@2.11.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.39.0 + + '@opentelemetry/exporter-trace-otlp-grpc@0.222.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/otlp-exporter-base': 0.222.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-grpc-exporter-base': 0.222.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.222.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-trace-otlp-grpc@0.52.1(@opentelemetry/api@1.9.1)': dependencies: '@grpc/grpc-js': 1.14.4 @@ -26957,17 +27006,6 @@ snapshots: '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-trace-otlp-grpc@0.57.2(@opentelemetry/api@1.9.1)': - dependencies: - '@grpc/grpc-js': 1.14.4 - '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-grpc-exporter-base': 0.57.2(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-trace-otlp-http@0.52.1(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -27006,17 +27044,25 @@ snapshots: transitivePeerDependencies: - supports-color + '@opentelemetry/otlp-exporter-base@0.222.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.222.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base@0.52.1(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) '@opentelemetry/otlp-transformer': 0.52.1(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base@0.57.2(@opentelemetry/api@1.9.1)': + '@opentelemetry/otlp-grpc-exporter-base@0.222.0(@opentelemetry/api@1.9.1)': dependencies: + '@grpc/grpc-js': 1.14.4 '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.222.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.222.0(@opentelemetry/api@1.9.1) '@opentelemetry/otlp-grpc-exporter-base@0.52.1(@opentelemetry/api@1.9.1)': dependencies: @@ -27026,13 +27072,15 @@ snapshots: '@opentelemetry/otlp-exporter-base': 0.52.1(@opentelemetry/api@1.9.1) '@opentelemetry/otlp-transformer': 0.52.1(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-grpc-exporter-base@0.57.2(@opentelemetry/api@1.9.1)': + '@opentelemetry/otlp-transformer@0.222.0(@opentelemetry/api@1.9.1)': dependencies: - '@grpc/grpc-js': 1.14.4 '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/api-logs': 0.222.0 + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.222.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace': 2.11.0(@opentelemetry/api@1.9.1) '@opentelemetry/otlp-transformer@0.52.1(@opentelemetry/api@1.9.1)': dependencies: @@ -27045,17 +27093,6 @@ snapshots: '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.1) protobufjs: 7.5.9 - '@opentelemetry/otlp-transformer@0.57.2(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/api-logs': 0.57.2 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.1) - protobufjs: 7.5.9 - '@opentelemetry/propagator-b3@1.25.1(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -27076,6 +27113,15 @@ snapshots: '@opentelemetry/api': 1.9.1 '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resource-detector-gcp@0.57.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.11.0(@opentelemetry/api@1.9.1) + gcp-metadata: 8.1.2 + transitivePeerDependencies: + - supports-color + '@opentelemetry/resources@1.25.1(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -27094,6 +27140,20 @@ snapshots: '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.39.0 + '@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.39.0 + + '@opentelemetry/sdk-logs@0.222.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.222.0 + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.39.0 + '@opentelemetry/sdk-logs@0.52.1(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -27101,13 +27161,6 @@ snapshots: '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.1) '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-logs@0.57.2(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/api-logs': 0.57.2 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-metrics@1.25.1(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -27121,6 +27174,12 @@ snapshots: '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-node@0.52.1(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -27162,6 +27221,14 @@ snapshots: '@opentelemetry/sdk-trace': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.39.0 + '@opentelemetry/sdk-trace-base@2.11.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.39.0 + '@opentelemetry/sdk-trace-node@1.25.1(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 @@ -27196,6 +27263,13 @@ snapshots: '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.39.0 + '@opentelemetry/sdk-trace@2.11.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.11.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.39.0 + '@opentelemetry/semantic-conventions@1.25.1': {} '@opentelemetry/semantic-conventions@1.28.0': {} From 744da7bf681be6073684d72637a54daf915d5735 Mon Sep 17 00:00:00 2001 From: Alka Trivedi Date: Thu, 17 Sep 2026 10:14:17 +0530 Subject: [PATCH 3/5] add docs --- handwritten/spanner/OBSERVABILITY.md | 27 +++++++++++++++++---- handwritten/spanner/test/metrics/metrics.ts | 4 +-- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/handwritten/spanner/OBSERVABILITY.md b/handwritten/spanner/OBSERVABILITY.md index ce82cab99bb5..6cacfe792b53 100644 --- a/handwritten/spanner/OBSERVABILITY.md +++ b/handwritten/spanner/OBSERVABILITY.md @@ -20,17 +20,24 @@ enable OpenTelemetry with appropriate exporters at the startup of your applicati #### OpenTelemetry Dependencies Add the following dependencies in your `package.json` or install them directly. + +> **Note:** `@google-cloud/spanner` uses the OpenTelemetry **v2** SDK. The versions +> below are the v2-compatible releases. If you are upgrading from an older version +> of this client, see the [OpenTelemetry JS 2.x migration guide](https://github.com/open-telemetry/opentelemetry-js/blob/main/doc/upgrade-to-2.x.md). + ```javascript // Required packages for OpenTelemetry SDKs -"@opentelemetry/sdk-trace-base": "^1.26.0", -"@opentelemetry/sdk-trace-node": "^1.26.0", +"@opentelemetry/sdk-trace-base": "^2.11.0", +"@opentelemetry/sdk-trace-node": "^2.0.0", +"@opentelemetry/resources": "^2.11.0", +"@opentelemetry/semantic-conventions": "^1.30.0", // Package to use Google Cloud Trace exporter -"@google-cloud/opentelemetry-cloud-trace-exporter": "^2.4.1", +"@google-cloud/opentelemetry-cloud-trace-exporter": "^3.0.0", // Packages to enable gRPC instrumentation -"@opentelemetry/instrumentation": "^0.53.0", -"@opentelemetry/instrumentation-grpc": "^0.53.0", +"@opentelemetry/instrumentation": "^0.222.0", +"@opentelemetry/instrumentation-grpc": "^0.222.0", ``` #### OpenTelemetry Configuration @@ -46,11 +53,21 @@ const { const { TraceExporter, } = require('@google-cloud/opentelemetry-cloud-trace-exporter'); +const {resourceFromAttributes} = require('@opentelemetry/resources'); +const {ATTR_SERVICE_NAME} = require('@opentelemetry/semantic-conventions'); const exporter = new TraceExporter(); +// Describe the service that is emitting the traces. +// Note: in OpenTelemetry v2 the `Resource` class was replaced by the +// `resourceFromAttributes` factory function. +const resource = resourceFromAttributes({ + [ATTR_SERVICE_NAME]: 'my-service-name', +}); + // Create the tracerProvider that the exporter shall be attached to. const provider = new NodeTracerProvider({ resource: resource, + sampler: new TraceIdRatioBasedSampler(0.1), // sample 10% spanProcessors: [new BatchSpanProcessor(exporter)] }); diff --git a/handwritten/spanner/test/metrics/metrics.ts b/handwritten/spanner/test/metrics/metrics.ts index 66dceafd049d..70dbfa4e8cd4 100644 --- a/handwritten/spanner/test/metrics/metrics.ts +++ b/handwritten/spanner/test/metrics/metrics.ts @@ -294,7 +294,7 @@ describe('Test metrics with mock server', () => { attributes, ); // Since we only have one attempt, the attempt latency should be fairly close to the operation latency - assertApprox(operationLatency, attemptLatency, 100); + assertApprox(operationLatency, attemptLatency, 30); const gfeLatency = getAggregatedValue(gfeLatenciesData, attributes); assert.strictEqual(gfeLatency, 123); @@ -719,7 +719,7 @@ describe('Test metrics with mock server', () => { attributes, ); // Since we only have one attempt, the attempt latency should be fairly close to the operation latency - assertApprox(operationLatency, attemptLatency, 100); + assertApprox(operationLatency, attemptLatency, 30); const gfeLatency = getAggregatedValue(gfeLatenciesData, attributes); assert.strictEqual(gfeLatency, 123); From 01478f5a0e7bee19d196b6f9dbb0ef482b9d5625 Mon Sep 17 00:00:00 2001 From: Alka Trivedi Date: Thu, 17 Sep 2026 21:54:14 +0530 Subject: [PATCH 4/5] review comments --- handwritten/spanner/src/metrics/constants.ts | 1 + .../src/metrics/metrics-tracer-factory.ts | 60 +++- .../test/metrics/metrics-tracer-factory.ts | 295 ++++++++++++++++++ 3 files changed, 343 insertions(+), 13 deletions(-) diff --git a/handwritten/spanner/src/metrics/constants.ts b/handwritten/spanner/src/metrics/constants.ts index 215270bc8e1a..bfeb0831785f 100644 --- a/handwritten/spanner/src/metrics/constants.ts +++ b/handwritten/spanner/src/metrics/constants.ts @@ -22,6 +22,7 @@ export const TRACER_CLEANUP_INTERVAL_MS = 30 * 60 * 1000; // 30 Minutes // OTel semantic conventions // See https://github.com/open-telemetry/opentelemetry-js/blob/main/semantic-conventions/README.md#unstable-semconv export const ATTR_CLOUD_REGION = 'cloud.region'; +export const ATTR_CLOUD_AVAILABILITY_ZONE = 'cloud.availability_zone'; // Minimum period that must past between metric exports export const MIN_EXPORT_FREQUENCY_MS = 30 * 1000; diff --git a/handwritten/spanner/src/metrics/metrics-tracer-factory.ts b/handwritten/spanner/src/metrics/metrics-tracer-factory.ts index 3fac91ed4351..105c01a52f3f 100644 --- a/handwritten/spanner/src/metrics/metrics-tracer-factory.ts +++ b/handwritten/spanner/src/metrics/metrics-tracer-factory.ts @@ -54,6 +54,8 @@ export class MetricsTracerFactory { private _clientName: string; private _clientUid: string; private _location = 'global'; + private _locationPromise: Promise | null = null; + private _metricReaders: MetricReader[] = []; private _projectId: string; private _currentOperationTracers = new Map(); private _currentOperationLastUpdatedMs = new Map(); @@ -73,12 +75,15 @@ export class MetricsTracerFactory { // Only perform async call to retrieve location is metrics are enabled. if (MetricsTracerFactory.enabled) { - (async () => { - const location = await MetricsTracerFactory._detectClientLocation(); - this._location = location.length > 0 ? location : 'global'; - })().catch(error => { - throw error; - }); + this._locationPromise = MetricsTracerFactory._detectClientLocation() + .then(location => { + this._location = location.length > 0 ? location : 'global'; + return this._location; + }) + .catch(error => { + console.warn('Unable to detect location.', error); + return this._location; + }); } this._clientHash = MetricsTracerFactory._generateClientHash( @@ -120,15 +125,38 @@ export class MetricsTracerFactory { /** * Returns the MeterProvider, creating it and metric instruments if not already initialized. * Client-wide attributes that are known at this time are cached to be provided to all MetricsTracers. + * + * If called again with readers when the existing MeterProvider has none, the provider is + * recreated with those readers. If readers are already attached, additional readers are ignored. + * * @param readers Optional array of MetricReader instances to attach to the MeterProvider. * @returns The OTEL MeterProvider instance. */ public getMeterProvider(readers: MetricReader[] = []): MeterProvider { + if (this._meterProvider !== null && readers.length > 0) { + if (this._metricReaders.length === 0) { + const staleMeterProvider = this._meterProvider; + this._meterProvider = null; + staleMeterProvider.shutdown().catch(error => { + console.warn( + 'Unable to shut down the previous MeterProvider.', + error, + ); + }); + } else { + console.warn( + 'MeterProvider is already initialized with metric readers. The ' + + 'additional readers passed to getMeterProvider() are ignored.', + ); + } + } + if (this._meterProvider === null) { const resource = resourceFromAttributes({ [Constants.MONITORED_RES_LABEL_KEY_PROJECT]: this._projectId, [Constants.MONITORED_RES_LABEL_KEY_CLIENT_HASH]: this._clientHash, - [Constants.MONITORED_RES_LABEL_KEY_LOCATION]: this._location, + [Constants.MONITORED_RES_LABEL_KEY_LOCATION]: + this._locationPromise ?? this._location, [Constants.MONITORED_RES_LABEL_KEY_INSTANCE]: 'unknown', [Constants.MONITORED_RES_LABEL_KEY_INSTANCE_CONFIG]: 'unknown', }); @@ -137,6 +165,7 @@ export class MetricsTracerFactory { readers: readers, views: Constants.METRIC_VIEWS, }); + this._metricReaders = readers; this._createMetricInstruments(); } return this._meterProvider; @@ -159,6 +188,7 @@ export class MetricsTracerFactory { await this._meterProvider!.shutdown(); } this._meterProvider = null; + this._metricReaders = []; this._currentOperationTracers = new Map(); this._currentOperationLastUpdatedMs = new Map(); } @@ -456,9 +486,11 @@ export class MetricsTracerFactory { } /** - * Gets the location (region) of the client, otherwise returns to the "global" region. - * Uses the GCP resource detector to detect the region from the environment. - * @returns The detected region string, or "global" if not found. + * Gets the location (region or zone) of the client, or defaults to "global". + * Uses the GCP resource detector to detect the location from the environment. + * Checks `cloud.region` first and falls back to `cloud.availability_zone` + * for zonal GKE clusters where `cloud.region` is not set. + * @returns The detected location string, or "global" if not found. */ private static async _detectClientLocation(): Promise { const defaultRegion = 'global'; @@ -469,9 +501,11 @@ export class MetricsTracerFactory { await resource?.waitForAsyncAttributes?.(); - const region = resource.attributes[Constants.ATTR_CLOUD_REGION]; - if (typeof region === 'string' && region) { - return region; + const location = + resource.attributes[Constants.ATTR_CLOUD_REGION] ?? + resource.attributes[Constants.ATTR_CLOUD_AVAILABILITY_ZONE]; + if (typeof location === 'string' && location) { + return location; } } catch (err) { console.warn('Unable to detect location.', err); diff --git a/handwritten/spanner/test/metrics/metrics-tracer-factory.ts b/handwritten/spanner/test/metrics/metrics-tracer-factory.ts index 241d14706b61..18e17795a2ff 100644 --- a/handwritten/spanner/test/metrics/metrics-tracer-factory.ts +++ b/handwritten/spanner/test/metrics/metrics-tracer-factory.ts @@ -14,12 +14,16 @@ import { MeterProvider, + MetricReader, PeriodicExportingMetricReader, + ResourceMetrics, } from '@opentelemetry/sdk-metrics'; import * as assert from 'assert'; import * as sinon from 'sinon'; +import {gcpDetector} from '@opentelemetry/resource-detector-gcp'; import * as Constants from '../../src/metrics/constants'; import {MetricsTracerFactory} from '../../src/metrics/metrics-tracer-factory'; +import {transformResourceMetricToTimeSeriesArray} from '../../src/metrics/transform'; import {CloudMonitoringMetricsExporter} from '../../src/metrics/spanner-metrics-exporter'; describe('MetricsTracerFactory', () => { @@ -65,6 +69,9 @@ describe('MetricsTracerFactory', () => { .returns({add: addGfeConnectivityErrorCountStub}); sandbox.stub(MeterProvider.prototype, 'getMeter').returns(meterStub as any); + sandbox + .stub(MetricsTracerFactory as any, '_detectClientLocation') + .resolves('global'); // metrics provider and related objects mockExporter = sandbox.createStubInstance(CloudMonitoringMetricsExporter); @@ -196,13 +203,20 @@ describe('MetricsTracerFactory', () => { describe('getInstanceAttributes', () => { let factory: MetricsTracerFactory; + let sandbox: sinon.SinonSandbox; + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox + .stub(MetricsTracerFactory as any, '_detectClientLocation') + .resolves('global'); factory = new (MetricsTracerFactory as any)(); }); afterEach(async () => { await factory.resetMeterProvider(); clearInterval(factory['_intervalTracerCleanup']); + sandbox.restore(); }); it('should extract project, instance, and database from full resource path', () => { @@ -246,9 +260,14 @@ describe('getInstanceAttributes', () => { describe('MetricsTracerFactory with set clock', () => { let clock: sinon.SinonFakeTimers; + let sandbox: sinon.SinonSandbox; beforeEach(async () => { MetricsTracerFactory.enabled = true; + sandbox = sinon.createSandbox(); + sandbox + .stub(MetricsTracerFactory as any, '_detectClientLocation') + .resolves('global'); await MetricsTracerFactory.resetInstance(); // Use fake timers to control the clock clock = sinon.useFakeTimers(); @@ -257,6 +276,7 @@ describe('MetricsTracerFactory with set clock', () => { afterEach(() => { // Restore the real timers clock.restore(); + sandbox.restore(); }); describe('_cleanMetricTracers', () => { @@ -311,3 +331,278 @@ describe('MetricsTracerFactory with set clock', () => { }); }); }); + +describe('MetricsTracerFactory location detection failures', () => { + let sandbox: sinon.SinonSandbox; + let warnStub: sinon.SinonStub; + + beforeEach(async () => { + sandbox = sinon.createSandbox(); + warnStub = sandbox.stub(console, 'warn'); + MetricsTracerFactory.enabled = true; + await MetricsTracerFactory.resetInstance(); + }); + + afterEach(async () => { + sandbox.restore(); + await MetricsTracerFactory.resetInstance(); + }); + + it('should not trigger an UnhandledPromiseRejection if _detectClientLocation rejects', async () => { + let rejectLocation!: (err: Error) => void; + const locationPromise = new Promise((_resolve, reject) => { + rejectLocation = reject; + }); + + let unhandledError: Error | null = null; + const onUnhandledRejection = (err: Error) => { + unhandledError = err; + }; + process.once('unhandledRejection', onUnhandledRejection); + + try { + sandbox + .stub(MetricsTracerFactory as any, '_detectClientLocation') + .returns(locationPromise); + + const factory = MetricsTracerFactory.getInstance('test-project'); + + rejectLocation(new Error('Unexpected detector failure')); + await locationPromise.catch(() => {}); + await new Promise(resolve => setImmediate(resolve)); + + assert.strictEqual(unhandledError, null); + assert.ok(warnStub.calledWith('Unable to detect location.')); + assert.strictEqual(factory!['_location'], 'global'); + } finally { + process.removeListener('unhandledRejection', onUnhandledRejection); + } + }); +}); + +describe('MetricsTracerFactory._detectClientLocation', () => { + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + const stubDetectedAttributes = (attributes: Record) => { + sandbox.stub(gcpDetector, 'detect').returns({ + attributes: Object.fromEntries( + Object.entries(attributes).map(([key, value]) => [ + key, + Promise.resolve(value), + ]), + ), + } as any); + }; + + it('should detect client location on a zonal GKE cluster where cloud.availability_zone is set', async () => { + stubDetectedAttributes({ + 'cloud.platform': 'gcp_kubernetes_engine', + 'cloud.availability_zone': 'us-central1-a', + }); + + const detectedLocation = await ( + MetricsTracerFactory as any + )._detectClientLocation(); + + assert.strictEqual(detectedLocation, 'us-central1-a'); + }); + + it('should detect client location on a regional GKE cluster where cloud.region is set', async () => { + stubDetectedAttributes({ + 'cloud.platform': 'gcp_kubernetes_engine', + 'cloud.region': 'us-central1', + }); + + const detectedLocation = await ( + MetricsTracerFactory as any + )._detectClientLocation(); + + assert.strictEqual(detectedLocation, 'us-central1'); + }); + + it('should prefer cloud.region over cloud.availability_zone when both are set', async () => { + stubDetectedAttributes({ + 'cloud.platform': 'gcp_compute_engine', + 'cloud.availability_zone': 'us-central1-a', + 'cloud.region': 'us-central1', + }); + + const detectedLocation = await ( + MetricsTracerFactory as any + )._detectClientLocation(); + + assert.strictEqual(detectedLocation, 'us-central1'); + }); + + it('should fall back to global when neither attribute is set', async () => { + stubDetectedAttributes({}); + + const detectedLocation = await ( + MetricsTracerFactory as any + )._detectClientLocation(); + + assert.strictEqual(detectedLocation, 'global'); + }); +}); + +describe('MetricsTracerFactory exported location', () => { + let sandbox: sinon.SinonSandbox; + + beforeEach(async () => { + sandbox = sinon.createSandbox(); + MetricsTracerFactory.enabled = true; + await MetricsTracerFactory.resetInstance(); + }); + + afterEach(async () => { + sandbox.restore(); + await MetricsTracerFactory.resetInstance(); + }); + + it('should export the detected GCP location rather than global when _detectClientLocation resolves asynchronously', async () => { + let resolveLocation!: (location: string) => void; + const locationPromise = new Promise(resolve => { + resolveLocation = resolve; + }); + sandbox + .stub(MetricsTracerFactory as any, '_detectClientLocation') + .returns(locationPromise); + + const exported: ResourceMetrics[] = []; + const capturingExporter = { + export: ( + resourceMetrics: ResourceMetrics, + resultCallback: (result: {code: number}) => void, + ) => { + exported.push(resourceMetrics); + resultCallback({code: 0}); + }, + forceFlush: async () => {}, + shutdown: async () => {}, + }; + const reader = new PeriodicExportingMetricReader({ + exporter: capturingExporter as any, + exportIntervalMillis: 60000, + }); + + const factory = MetricsTracerFactory.getInstance('test-project')!; + factory.getMeterProvider([reader]); + assert.strictEqual(factory['_location'], 'global'); + + const tracer = factory.createMetricsTracer( + 'some-method', + 'projects/test-project/instances/inst/databases/db', + '1.1a2bc3d4.1.1.1.1', + ); + tracer!.recordOperationStart(); + tracer!.recordOperationCompletion(); + + resolveLocation('us-central1'); + await locationPromise; + + await reader.forceFlush(); + + assert.strictEqual(exported.length, 1); + const timeSeries = transformResourceMetricToTimeSeriesArray( + exported[0], + 'test-project', + ); + assert.ok(timeSeries.length > 0); + assert.strictEqual(timeSeries[0].resource!.labels!.location, 'us-central1'); + + await reader.shutdown(); + }); +}); + +describe('MetricsTracerFactory getMeterProvider readers', () => { + let sandbox: sinon.SinonSandbox; + let warnStub: sinon.SinonStub; + + class InMemoryMetricReader extends MetricReader { + protected async onForceFlush(): Promise {} + protected async onShutdown(): Promise {} + } + + const recordOperation = (factory: MetricsTracerFactory) => { + const tracer = factory.createMetricsTracer( + 'some-method', + 'projects/test-project/instances/inst/databases/db', + '1.1a2bc3d4.1.1.1.1', + ); + tracer!.recordOperationStart(); + tracer!.recordOperationCompletion(); + }; + + beforeEach(async () => { + sandbox = sinon.createSandbox(); + warnStub = sandbox.stub(console, 'warn'); + MetricsTracerFactory.enabled = true; + await MetricsTracerFactory.resetInstance(); + sandbox + .stub(MetricsTracerFactory as any, '_detectClientLocation') + .resolves('us-central1'); + }); + + afterEach(async () => { + sandbox.restore(); + await MetricsTracerFactory.resetInstance(); + }); + + it('should bind MetricReader passed to getMeterProvider([reader]) even if getMeterProvider() was called earlier', async () => { + const factory = MetricsTracerFactory.getInstance('test-project')!; + + const initialMeterProvider = factory.getMeterProvider(); + + const reader = new InMemoryMetricReader(); + const rebuiltMeterProvider = factory.getMeterProvider([reader]); + + assert.notStrictEqual(rebuiltMeterProvider, initialMeterProvider); + + recordOperation(factory); + + const {resourceMetrics} = await reader.collect(); + assert.ok(resourceMetrics.scopeMetrics.length > 0); + + await reader.shutdown(); + }); + + it('should keep returning the same MeterProvider for calls without readers', () => { + const factory = MetricsTracerFactory.getInstance('test-project')!; + const reader = new InMemoryMetricReader(); + + const meterProvider = factory.getMeterProvider([reader]); + + assert.strictEqual(factory.getMeterProvider(), meterProvider); + assert.ok(warnStub.notCalled); + }); + + it('should warn instead of silently ignoring readers once readers are bound', async () => { + const factory = MetricsTracerFactory.getInstance('test-project')!; + const boundReader = new InMemoryMetricReader(); + const meterProvider = factory.getMeterProvider([boundReader]); + + const ignoredReader = new InMemoryMetricReader(); + const sameMeterProvider = factory.getMeterProvider([ignoredReader]); + + assert.strictEqual(sameMeterProvider, meterProvider); + assert.ok( + warnStub.calledWithMatch( + 'MeterProvider is already initialized with metric readers.', + ), + ); + + recordOperation(factory); + const {resourceMetrics} = await boundReader.collect(); + assert.ok(resourceMetrics.scopeMetrics.length > 0); + + await boundReader.shutdown(); + }); +}); From 1a68f1cc43f13a9bf3545ba903ea9bf6e6faacf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Thu, 17 Sep 2026 22:04:56 +0200 Subject: [PATCH 5/5] fix(spanner): address OpenTelemetry v2 metrics integration review comments Fixes metric exporter hang on shutdown when throttled, prevents context leak in periodic reader interval, avoids duplicate MetricReader warnings with multiple clients, and guards MetricsTracerFactory against ADC placeholder project IDs. Adds corresponding zero-sleep unit tests. --- handwritten/spanner/OBSERVABILITY.md | 1 + handwritten/spanner/src/index.ts | 90 +++++++++++-------- .../src/metrics/metrics-tracer-factory.ts | 14 +++ .../src/metrics/spanner-metrics-exporter.ts | 4 + handwritten/spanner/src/metrics/transform.ts | 28 ++++-- .../test/metrics/metrics-tracer-factory.ts | 77 ++++++++++++++++ .../test/metrics/spanner-metrics-exporter.ts | 16 ++++ handwritten/spanner/test/metrics/transform.ts | 2 +- 8 files changed, 185 insertions(+), 47 deletions(-) diff --git a/handwritten/spanner/OBSERVABILITY.md b/handwritten/spanner/OBSERVABILITY.md index 6cacfe792b53..a2ea9ac7a228 100644 --- a/handwritten/spanner/OBSERVABILITY.md +++ b/handwritten/spanner/OBSERVABILITY.md @@ -49,6 +49,7 @@ const { } = require('@opentelemetry/sdk-trace-node'); const { BatchSpanProcessor, + TraceIdRatioBasedSampler, } = require('@opentelemetry/sdk-trace-base'); const { TraceExporter, diff --git a/handwritten/spanner/src/index.ts b/handwritten/spanner/src/index.ts index 17287fba98bf..8d56857d51e0 100644 --- a/handwritten/spanner/src/index.ts +++ b/handwritten/spanner/src/index.ts @@ -41,7 +41,7 @@ import { IProtoMessageParams, IProtoEnumParams, } from './codec'; -import {context, propagation} from '@opentelemetry/api'; +import {context, propagation, ROOT_CONTEXT} from '@opentelemetry/api'; import {Backup} from './backup'; import {Database} from './database'; import { @@ -534,7 +534,7 @@ class Spanner extends GrpcService { if (!this.clients_.has(clientName)) { this.clients_.set( clientName, - new v1[clientName](this.options as ClientOptions), + new v1.InstanceAdminClient(this.options as ClientOptions), ); } return this.clients_.get(clientName)! as v1.InstanceAdminClient; @@ -558,7 +558,7 @@ class Spanner extends GrpcService { if (!this.clients_.has(clientName)) { this.clients_.set( clientName, - new v1[clientName](this.options as ClientOptions), + new v1.DatabaseAdminClient(this.options as ClientOptions), ); } return this.clients_.get(clientName)! as v1.DatabaseAdminClient; @@ -615,10 +615,9 @@ class Spanner extends GrpcService { if (callback) { // process.nextTick prevents Unhandled Promise Rejections if callback throws - res.then( - () => process.nextTick(() => callback(null)), - err => process.nextTick(() => callback(err)), - ); + res + .then(() => process.nextTick(() => callback(null))) + .catch(err => process.nextTick(() => callback(err))); } else { return res; } @@ -1676,35 +1675,46 @@ class Spanner extends GrpcService { !metricsExplicitlyDisabled && !this._isInSecureCredentials; MetricsTracerFactory.enabled = this._metricsEnabled; if (this._metricsEnabled) { - try { - this.auth.getProjectId((err, projectId) => { - if (err || !projectId) { - console.error( - 'Unable to get Project Id for client side metrics, will skip exporting client' + - ' side metrics' + - err, - ); - return; - } + const initializeMetrics = (projectId: string) => { + this.projectId_ = projectId; + const factory = MetricsTracerFactory.getInstance(projectId); + if (factory && !factory.hasMetricReaders()) { + context.with(ROOT_CONTEXT, () => { + const periodicReader = new PeriodicExportingMetricReader({ + exporter: new CloudMonitoringMetricsExporter( + {auth: this.auth}, + projectId, + ), + exportIntervalMillis: 60000, + }); + factory.getMeterProvider([periodicReader]); + }); + } + }; - this.projectId_ = projectId; - const factory = MetricsTracerFactory.getInstance(projectId); - const periodicReader = new PeriodicExportingMetricReader({ - exporter: new CloudMonitoringMetricsExporter( - {auth: this.auth}, - projectId, - ), - exportIntervalMillis: 60000, + if (this.projectId_ && this.projectId_ !== '{{projectId}}') { + initializeMetrics(this.projectId_); + } else { + try { + this.auth.getProjectId((err, projectId) => { + if (err || !projectId) { + console.error( + 'Unable to get Project Id for client side metrics, will skip exporting client' + + ' side metrics' + + err, + ); + return; + } + + initializeMetrics(projectId); }); - // Retrieve the MeterProvider to trigger construction - factory!.getMeterProvider([periodicReader]); - }); - } catch (err) { - console.error( - 'Unable to configure client side metrics, will skip exporting client' + - ' side metrics' + - err, - ); + } catch (err) { + console.error( + 'Unable to configure client side metrics, will skip exporting client' + + ' side metrics' + + err, + ); + } } } } @@ -1727,7 +1737,10 @@ class Spanner extends GrpcService { const clientName = config.client; try { if (!this.clients_.has(clientName)) { - this.clients_.set(clientName, new v1[clientName](this.options)); + this.clients_.set( + clientName, + new (v1 as Record)[clientName](this.options), + ); } } catch (err) { callback(err, null); @@ -1860,7 +1873,8 @@ class Spanner extends GrpcService { if ( this._metricsEnabled && config.client === 'SpannerClient' && - this.projectId_ + this.projectId_ && + this.projectId_ !== '{{projectId}}' ) { metricsTracer = MetricsTracerFactory?.getInstance(this.projectId_)?.createMetricsTracer( @@ -1896,6 +1910,7 @@ class Spanner extends GrpcService { .then(val => { metricsTracer?.recordOperationCompletion(); resolve(val); + return val; }) .catch(error => { metricsTracer?.recordOperationCompletion(); @@ -1928,7 +1943,8 @@ class Spanner extends GrpcService { if ( this._metricsEnabled && config.client === 'SpannerClient' && - this.projectId_ + this.projectId_ && + this.projectId_ !== '{{projectId}}' ) { metricsTracer = MetricsTracerFactory?.getInstance(this.projectId_)?.createMetricsTracer( diff --git a/handwritten/spanner/src/metrics/metrics-tracer-factory.ts b/handwritten/spanner/src/metrics/metrics-tracer-factory.ts index 105c01a52f3f..17b4973c5f93 100644 --- a/handwritten/spanner/src/metrics/metrics-tracer-factory.ts +++ b/handwritten/spanner/src/metrics/metrics-tracer-factory.ts @@ -249,6 +249,20 @@ export class MetricsTracerFactory { return this._clientName; } + /** + * Returns the detected client location. + */ + get location(): string { + return this._location; + } + + /** + * Returns true if the MeterProvider is initialized with at least one MetricReader. + */ + public hasMetricReaders(): boolean { + return this._metricReaders.length > 0; + } + /** * Creates a new MetricsTracer for a given resource name and method, and stores it for later retrieval. * Returns null if metrics are disabled. diff --git a/handwritten/spanner/src/metrics/spanner-metrics-exporter.ts b/handwritten/spanner/src/metrics/spanner-metrics-exporter.ts index 9796ccd348b2..9cafb9caefc2 100644 --- a/handwritten/spanner/src/metrics/spanner-metrics-exporter.ts +++ b/handwritten/spanner/src/metrics/spanner-metrics-exporter.ts @@ -57,6 +57,7 @@ export class CloudMonitoringMetricsExporter implements PushMetricExporter { now.getTime() - this._lastExported.getTime() <= MIN_EXPORT_FREQUENCY_MS ) { + resultCallback({code: ExportResultCode.SUCCESS}); return; } @@ -81,6 +82,9 @@ export class CloudMonitoringMetricsExporter implements PushMetricExporter { private async _exportAsync( resourceMetrics: ResourceMetrics, ): Promise { + if (resourceMetrics?.resource?.asyncAttributesPending) { + await resourceMetrics.resource.waitForAsyncAttributes?.(); + } const timeSeriesList = transformResourceMetricToTimeSeriesArray( resourceMetrics, this._projectId, diff --git a/handwritten/spanner/src/metrics/transform.ts b/handwritten/spanner/src/metrics/transform.ts index b1620a95ab60..da40867c7baa 100644 --- a/handwritten/spanner/src/metrics/transform.ts +++ b/handwritten/spanner/src/metrics/transform.ts @@ -32,6 +32,7 @@ import { METRIC_NAMES, METRIC_LABEL_KEY_CLIENT_UID, METRIC_LABEL_KEY_CLIENT_NAME, + MONITORED_RES_LABEL_KEY_LOCATION, UNKNOWN_ATTRIBUTE, } from './constants'; import {MetricsTracerFactory} from './metrics-tracer-factory'; @@ -124,10 +125,10 @@ function _createTimeSeries( ) { const type = path.posix.join(CLIENT_METRICS_PREFIX, metric.descriptor.name); const resourceLabels = resource - ? _extractLabels(resource, projectId) + ? _extractLabels(resource, projectId, true) : {metricLabels: {}, monitoredResourceLabels: {}}; - const dataLabels = _extractLabels(dataPoint, projectId); + const dataLabels = _extractLabels(dataPoint, projectId, false); const labels = { ...resourceLabels.metricLabels, @@ -205,18 +206,27 @@ function _transformPoint(metric: MetricData, dataPoint: DataPoint) { } } -/** Extracts metric and monitored resource labels from data point */ +/** Extracts metric and monitored resource labels from data point or resource */ function _extractLabels( {attributes = {}}: DataPoint | Resource, projectId: string, + isResource = false, ) { const factory = MetricsTracerFactory.getInstance(projectId); - // Add Client name and Client UID metric labels - attributes[METRIC_LABEL_KEY_CLIENT_UID] = - factory?.clientUid ?? UNKNOWN_ATTRIBUTE; - attributes[METRIC_LABEL_KEY_CLIENT_NAME] = - factory?.clientName ?? UNKNOWN_ATTRIBUTE; - return Object.entries(attributes).reduce( + const combinedAttributes: Record = { + ...attributes, + [METRIC_LABEL_KEY_CLIENT_UID]: factory?.clientUid ?? UNKNOWN_ATTRIBUTE, + [METRIC_LABEL_KEY_CLIENT_NAME]: factory?.clientName ?? UNKNOWN_ATTRIBUTE, + }; + if ( + isResource && + factory?.location && + (!combinedAttributes[MONITORED_RES_LABEL_KEY_LOCATION] || + combinedAttributes[MONITORED_RES_LABEL_KEY_LOCATION] === 'global') + ) { + combinedAttributes[MONITORED_RES_LABEL_KEY_LOCATION] = factory.location; + } + return Object.entries(combinedAttributes).reduce( (result, [key, value]) => { const normalizedKey = _normalizeLabelKey(key); const val = value?.toString(); diff --git a/handwritten/spanner/test/metrics/metrics-tracer-factory.ts b/handwritten/spanner/test/metrics/metrics-tracer-factory.ts index 18e17795a2ff..737728ce016d 100644 --- a/handwritten/spanner/test/metrics/metrics-tracer-factory.ts +++ b/handwritten/spanner/test/metrics/metrics-tracer-factory.ts @@ -25,6 +25,7 @@ import * as Constants from '../../src/metrics/constants'; import {MetricsTracerFactory} from '../../src/metrics/metrics-tracer-factory'; import {transformResourceMetricToTimeSeriesArray} from '../../src/metrics/transform'; import {CloudMonitoringMetricsExporter} from '../../src/metrics/spanner-metrics-exporter'; +import {Spanner} from '../../src'; describe('MetricsTracerFactory', () => { let sandbox: sinon.SinonSandbox; @@ -520,6 +521,50 @@ describe('MetricsTracerFactory exported location', () => { await reader.shutdown(); }); + + it('should export the detected GCP location via reader.collect() without requiring waitForAsyncAttributes', async () => { + let resolveLocation!: (location: string) => void; + const locationPromise = new Promise(resolve => { + resolveLocation = resolve; + }); + sandbox + .stub(MetricsTracerFactory as any, '_detectClientLocation') + .returns(locationPromise); + + class DirectCollectReader extends MetricReader { + protected async onForceFlush(): Promise {} + protected async onShutdown(): Promise {} + } + + const reader = new DirectCollectReader(); + const factory = MetricsTracerFactory.getInstance('test-project')!; + factory.getMeterProvider([reader]); + assert.strictEqual(factory.location, 'global'); + + const tracer = factory.createMetricsTracer( + 'some-method', + 'projects/test-project/instances/inst/databases/db', + '1.1a2bc3d4.1.1.1.1', + ); + tracer!.recordOperationStart(); + tracer!.recordOperationCompletion(); + + resolveLocation('us-central1'); + await locationPromise; + await Promise.resolve(); + assert.strictEqual(factory.location, 'us-central1'); + + // Collect directly via MetricReader.collect() (which does NOT invoke waitForAsyncAttributes) + const {resourceMetrics} = await reader.collect(); + const timeSeries = transformResourceMetricToTimeSeriesArray( + resourceMetrics, + 'test-project', + ); + assert.ok(timeSeries.length > 0); + assert.strictEqual(timeSeries[0].resource!.labels!.location, 'us-central1'); + + await reader.shutdown(); + }); }); describe('MetricsTracerFactory getMeterProvider readers', () => { @@ -545,6 +590,16 @@ describe('MetricsTracerFactory getMeterProvider readers', () => { sandbox = sinon.createSandbox(); warnStub = sandbox.stub(console, 'warn'); MetricsTracerFactory.enabled = true; + if ( + Object.prototype.hasOwnProperty.call( + process.env, + 'SPANNER_DISABLE_BUILTIN_METRICS', + ) + ) { + sandbox.replace(process.env, 'SPANNER_DISABLE_BUILTIN_METRICS', 'false'); + } else { + sandbox.define(process.env, 'SPANNER_DISABLE_BUILTIN_METRICS', 'false'); + } await MetricsTracerFactory.resetInstance(); sandbox .stub(MetricsTracerFactory as any, '_detectClientLocation') @@ -559,10 +614,13 @@ describe('MetricsTracerFactory getMeterProvider readers', () => { it('should bind MetricReader passed to getMeterProvider([reader]) even if getMeterProvider() was called earlier', async () => { const factory = MetricsTracerFactory.getInstance('test-project')!; + assert.strictEqual(factory.hasMetricReaders(), false); const initialMeterProvider = factory.getMeterProvider(); + assert.strictEqual(factory.hasMetricReaders(), false); const reader = new InMemoryMetricReader(); const rebuiltMeterProvider = factory.getMeterProvider([reader]); + assert.strictEqual(factory.hasMetricReaders(), true); assert.notStrictEqual(rebuiltMeterProvider, initialMeterProvider); @@ -605,4 +663,23 @@ describe('MetricsTracerFactory getMeterProvider readers', () => { await boundReader.shutdown(); }); + + it('should initialize metrics synchronously when projectId is known and avoid duplicate warnings for multiple instances', () => { + const spanner1 = new Spanner({ + projectId: 'test-project', + }); + assert.ok(spanner1); + + const factory = MetricsTracerFactory.getInstance('test-project')!; + assert.ok(factory); + assert.strictEqual(factory.hasMetricReaders(), true); + + const spanner2 = new Spanner({ + projectId: 'test-project', + }); + assert.ok(spanner2); + + // Must not log a warning because hasMetricReaders() prevented re-registering + assert.ok(warnStub.notCalled); + }); }); diff --git a/handwritten/spanner/test/metrics/spanner-metrics-exporter.ts b/handwritten/spanner/test/metrics/spanner-metrics-exporter.ts index 5c7d6e009021..798f18b0e084 100644 --- a/handwritten/spanner/test/metrics/spanner-metrics-exporter.ts +++ b/handwritten/spanner/test/metrics/spanner-metrics-exporter.ts @@ -266,4 +266,20 @@ describe('Export', () => { const callbackResult = resultCallbackSpy.getCall(0).args[0]; assert.strictEqual(callbackResult.code, ExportResultCode.SUCCESS); }); + + it('should invoke resultCallback with SUCCESS when throttled by MIN_EXPORT_FREQUENCY_MS', async () => { + const {resourceMetrics} = await reader.collect(); + // Simulate an export that just occurred 1 second ago + (exporter as any)._lastExported = new Date(); + + const resultCallbackSpy = sinon.spy(); + exporter.export(resourceMetrics, resultCallbackSpy); + + // Must be called synchronously / immediately rather than hanging + assert.strictEqual(resultCallbackSpy.calledOnce, true); + assert.strictEqual( + resultCallbackSpy.getCall(0).args[0].code, + ExportResultCode.SUCCESS, + ); + }); }); diff --git a/handwritten/spanner/test/metrics/transform.ts b/handwritten/spanner/test/metrics/transform.ts index 03c7f006f13e..8d0f2f70136f 100644 --- a/handwritten/spanner/test/metrics/transform.ts +++ b/handwritten/spanner/test/metrics/transform.ts @@ -244,7 +244,7 @@ describe('transform', () => { it('should extract metric and resource labels', () => { const dataLabels = _extractLabels(sumDataPoint, 'project_id'); - const resourceLabels = _extractLabels(resource, 'project_id'); + const resourceLabels = _extractLabels(resource, 'project_id', true); // Metric Labels assert.strictEqual(dataLabels.metricLabels['client_uid'], 'test_uid');