Skip to content

Commit 9d18bcb

Browse files
committed
fix(tracing): upgrade to otel 2.0
1 parent 9950eb8 commit 9d18bcb

14 files changed

Lines changed: 1910 additions & 606 deletions

File tree

js/core/package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@
2626
"author": "genkit",
2727
"license": "Apache-2.0",
2828
"dependencies": {
29-
"@opentelemetry/api": "^1.9.0",
30-
"@opentelemetry/context-async-hooks": "~1.25.0",
31-
"@opentelemetry/core": "~1.25.0",
32-
"@opentelemetry/sdk-metrics": "~1.25.0",
33-
"@opentelemetry/sdk-node": "^0.52.0",
34-
"@opentelemetry/sdk-trace-base": "~1.25.0",
29+
"@opentelemetry/api": "^1.9.1",
30+
"@opentelemetry/context-async-hooks": "^2.6.1",
31+
"@opentelemetry/core": "^2.6.1",
32+
"@opentelemetry/sdk-metrics": "^2.6.1",
33+
"@opentelemetry/sdk-node": "^0.214.0",
34+
"@opentelemetry/sdk-trace-base": "^2.6.1",
3535
"@types/json-schema": "^7.0.15",
3636
"ajv": "^8.12.0",
3737
"ajv-formats": "^3.0.1",

js/core/src/tracing.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,10 @@ export async function enableTelemetry(
100100
if (isOTelInitializationDisabled()) {
101101
return;
102102
}
103-
global[instrumentationKey] =
104-
telemetryConfig instanceof Promise ? telemetryConfig : Promise.resolve();
105-
return getTelemetryProvider().enableTelemetry(telemetryConfig);
103+
const instrumentationPromise =
104+
getTelemetryProvider().enableTelemetry(telemetryConfig);
105+
global[instrumentationKey] = instrumentationPromise;
106+
return instrumentationPromise;
106107
}
107108

108109
/**

js/core/src/tracing/exporter.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export class TraceServerExporter implements SpanExporter {
7373
displayName: span.name,
7474
links: span.links,
7575
spanKind: SpanKind[span.kind],
76-
parentSpanId: span.parentSpanId,
76+
parentSpanId: span.parentSpanContext?.spanId,
7777
sameProcessAsParentSpan: { value: !span.spanContext().isRemote },
7878
status: span.status,
7979
timeEvents: {
@@ -86,17 +86,17 @@ export class TraceServerExporter implements SpanExporter {
8686
})),
8787
},
8888
};
89-
if (span.instrumentationLibrary !== undefined) {
89+
if (span.instrumentationScope !== undefined) {
9090
spanData.instrumentationLibrary = {
91-
name: span.instrumentationLibrary.name,
91+
name: span.instrumentationScope.name,
9292
};
93-
if (span.instrumentationLibrary.schemaUrl !== undefined) {
93+
if (span.instrumentationScope.schemaUrl !== undefined) {
9494
spanData.instrumentationLibrary.schemaUrl =
95-
span.instrumentationLibrary.schemaUrl;
95+
span.instrumentationScope.schemaUrl;
9696
}
97-
if (span.instrumentationLibrary.version !== undefined) {
97+
if (span.instrumentationScope.version !== undefined) {
9898
spanData.instrumentationLibrary.version =
99-
span.instrumentationLibrary.version;
99+
span.instrumentationScope.version;
100100
}
101101
}
102102
deleteUndefinedProps(spanData);

js/core/src/tracing/node-telemetry-provider.ts

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@
1414
* limitations under the License.
1515
*/
1616

17+
import { Context } from '@opentelemetry/api';
1718
import { NodeSDK } from '@opentelemetry/sdk-node';
1819
import {
1920
BatchSpanProcessor,
2021
SimpleSpanProcessor,
22+
type ReadableSpan,
2123
type SpanProcessor,
2224
} from '@opentelemetry/sdk-trace-base';
2325
import { logger } from '../logging.js';
@@ -29,6 +31,7 @@ import { RealtimeSpanProcessor } from './realtime-span-processor.js';
2931

3032
let telemetrySDK: NodeSDK | null = null;
3133
let nodeOtelConfig: TelemetryConfig | null = null;
34+
let isSigtermHandlerRegistered = false;
3235

3336
export function initNodeTelemetryProvider() {
3437
setTelemetryProvider({
@@ -37,6 +40,35 @@ export function initNodeTelemetryProvider() {
3740
});
3841
}
3942

43+
/**
44+
* MultiSpanProcessor is a multiplexer that allows Genkit to register multiple
45+
* span processors reliably.
46+
*
47+
* It is used instead of providing a `spanProcessors` array to the OpenTelemetry NodeSDK
48+
* because the SDK's internal logic for merging `traceExporter`, `spanProcessor` (singular),
49+
* and `spanProcessors` (plural) varies across versions and can lead to exporters being
50+
* silently overwritten or ignored.
51+
*
52+
* By wrapping all processors into this single delegate, we guarantee that Genkit's
53+
* internal telemetry and any user-provided exporters both receive every start and
54+
* end event, providing a robust compatibility layer during the OTel 2.0 upgrade.
55+
*/
56+
class MultiSpanProcessor implements SpanProcessor {
57+
constructor(private processors: SpanProcessor[]) {}
58+
onStart(span: any, parentContext: Context) {
59+
this.processors.forEach((p) => p.onStart?.(span, parentContext));
60+
}
61+
onEnd(span: ReadableSpan) {
62+
this.processors.forEach((p) => p.onEnd(span));
63+
}
64+
async forceFlush() {
65+
await Promise.all(this.processors.map((p) => p.forceFlush()));
66+
}
67+
async shutdown() {
68+
await Promise.all(this.processors.map((p) => p.shutdown()));
69+
}
70+
}
71+
4072
/**
4173
* Enables tracing and metrics open telemetry configuration.
4274
*/
@@ -52,23 +84,48 @@ async function enableTelemetry(
5284
? await telemetryConfig
5385
: telemetryConfig;
5486

87+
// If already initialized and new config is empty, skip to avoid unnecessary restarts
88+
if (
89+
telemetrySDK &&
90+
(!telemetryConfig ||
91+
(!telemetryConfig.spanProcessors &&
92+
!telemetryConfig.spanProcessor &&
93+
!telemetryConfig.traceExporter &&
94+
!telemetryConfig.metricReader))
95+
) {
96+
return;
97+
}
98+
5599
nodeOtelConfig = telemetryConfig || {};
56100

57-
const processors: SpanProcessor[] = [createTelemetryServerProcessor()];
58-
if (nodeOtelConfig.traceExporter) {
59-
throw new Error('Please specify spanProcessors instead.');
60-
}
101+
const processors: SpanProcessor[] = [];
61102
if (nodeOtelConfig.spanProcessors) {
62103
processors.push(...nodeOtelConfig.spanProcessors);
63104
}
64105
if (nodeOtelConfig.spanProcessor) {
65106
processors.push(nodeOtelConfig.spanProcessor);
66107
delete nodeOtelConfig.spanProcessor;
67108
}
68-
nodeOtelConfig.spanProcessors = processors;
109+
processors.push(createTelemetryServerProcessor());
110+
111+
if (processors.length > 1) {
112+
nodeOtelConfig.spanProcessor = new MultiSpanProcessor(processors);
113+
} else {
114+
nodeOtelConfig.spanProcessor = processors[0];
115+
}
116+
delete nodeOtelConfig.spanProcessors;
117+
118+
if (telemetrySDK) {
119+
await cleanUpTracing();
120+
}
121+
69122
telemetrySDK = new NodeSDK(nodeOtelConfig);
70123
telemetrySDK.start();
71-
process.on('SIGTERM', async () => await cleanUpTracing());
124+
125+
if (!isSigtermHandlerRegistered) {
126+
process.on('SIGTERM', async () => await cleanUpTracing());
127+
isSigtermHandlerRegistered = true;
128+
}
72129
}
73130

74131
async function cleanUpTracing(): Promise<void> {
@@ -113,7 +170,7 @@ function maybeFlushMetrics(): Promise<void> {
113170
* Flushes all configured span processors.
114171
*/
115172
async function flushTracing() {
116-
if (nodeOtelConfig?.spanProcessors) {
117-
await Promise.all(nodeOtelConfig.spanProcessors.map((p) => p.forceFlush()));
173+
if (nodeOtelConfig?.spanProcessor) {
174+
await nodeOtelConfig.spanProcessor.forceFlush();
118175
}
119176
}

js/core/tests/action_test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { z } from 'zod';
2121
import { action, defineAction } from '../src/action.js';
2222
import { initNodeFeatures } from '../src/node.js';
2323
import { Registry } from '../src/registry.js';
24-
import { enableTelemetry } from '../src/tracing.js';
24+
import { enableTelemetry, flushTracing } from '../src/tracing.js';
2525
import { TestSpanExporter } from './utils.js';
2626

2727
initNodeFeatures();
@@ -33,8 +33,9 @@ enableTelemetry({
3333

3434
describe('action', () => {
3535
var registry: Registry;
36-
beforeEach(() => {
36+
beforeEach(async () => {
3737
registry = new Registry();
38+
await flushTracing();
3839
spanExporter.exportedSpans = [];
3940
});
4041

@@ -289,6 +290,7 @@ describe('action', () => {
289290
act.__action.key = 'some-custom-key';
290291

291292
await act();
293+
await flushTracing();
292294

293295
assert.strictEqual(spanExporter.exportedSpans.length, 1);
294296
assert.strictEqual(

js/core/tests/flow_test.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { defineFlow, run } from '../src/flow.js';
2121
import { defineAction, getContext, z } from '../src/index.js';
2222
import { initNodeFeatures } from '../src/node.js';
2323
import { Registry } from '../src/registry.js';
24-
import { enableTelemetry } from '../src/tracing.js';
24+
import { enableTelemetry, flushTracing } from '../src/tracing.js';
2525
import { TestSpanExporter } from './utils.js';
2626

2727
initNodeFeatures();
@@ -48,10 +48,12 @@ function createTestFlow(registry: Registry) {
4848
describe('flow', () => {
4949
let registry: Registry;
5050

51-
beforeEach(() => {
51+
beforeEach(async () => {
5252
// Skips starting reflection server.
5353
delete process.env.GENKIT_ENV;
5454
registry = new Registry();
55+
await flushTracing();
56+
spanExporter.exportedSpans = [];
5557
});
5658

5759
describe('runFlow', () => {
@@ -370,6 +372,8 @@ describe('flow', () => {
370372
telemetryLabels: { custom: 'label' },
371373
});
372374

375+
await flushTracing();
376+
373377
assert.equal(result, 'bar foo');
374378
assert.strictEqual(spanExporter.exportedSpans.length, 1);
375379
assert.strictEqual(spanExporter.exportedSpans[0].displayName, 'testFlow');
@@ -394,6 +398,8 @@ describe('flow', () => {
394398
});
395399
const result = await output;
396400

401+
await flushTracing();
402+
397403
assert.equal(result, 'bar foo');
398404
assert.strictEqual(spanExporter.exportedSpans.length, 1);
399405
assert.strictEqual(spanExporter.exportedSpans[0].displayName, 'testFlow');
@@ -445,6 +451,8 @@ describe('flow', () => {
445451
context: { user: 'pavel' },
446452
});
447453

454+
await flushTracing();
455+
448456
assert.equal(result, 'foo bar');
449457
assert.strictEqual(spanExporter.exportedSpans.length, 3);
450458

js/core/tests/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export class TestSpanExporter implements SpanExporter {
4141
displayName: span.name,
4242
links: span.links,
4343
spanKind: SpanKind[span.kind],
44-
parentSpanId: span.parentSpanId,
44+
parentSpanId: span.parentSpanContext?.spanId,
4545
sameProcessAsParentSpan: { value: !span.spanContext().isRemote },
4646
status: span.status,
4747
timeEvents: {

js/plugins/google-cloud/package.json

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,19 @@
3333
"dependencies": {
3434
"@google-cloud/logging-winston": "^6.0.0",
3535
"@google-cloud/modelarmor": "^0.4.1",
36-
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.19.0",
37-
"@google-cloud/opentelemetry-cloud-trace-exporter": "^2.4.1",
38-
"@google-cloud/opentelemetry-resource-util": "^2.4.0",
39-
"@opentelemetry/api": "^1.9.0",
40-
"@opentelemetry/auto-instrumentations-node": "^0.49.1",
41-
"@opentelemetry/core": "~1.25.0",
42-
"@opentelemetry/instrumentation": "^0.52.0",
43-
"@opentelemetry/instrumentation-pino": "^0.41.0",
44-
"@opentelemetry/instrumentation-winston": "^0.39.0",
45-
"@opentelemetry/resources": "~1.25.0",
46-
"@opentelemetry/sdk-metrics": "~1.25.0",
47-
"@opentelemetry/sdk-node": "^0.52.0",
48-
"@opentelemetry/sdk-trace-base": "~1.25.0",
36+
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.21.0",
37+
"@google-cloud/opentelemetry-cloud-trace-exporter": "^3.0.0",
38+
"@google-cloud/opentelemetry-resource-util": "^3.0.0",
39+
"@opentelemetry/api": "^1.9.1",
40+
"@opentelemetry/auto-instrumentations-node": "^0.72.0",
41+
"@opentelemetry/core": "^2.6.1",
42+
"@opentelemetry/instrumentation": "^0.214.0",
43+
"@opentelemetry/instrumentation-pino": "^0.60.0",
44+
"@opentelemetry/instrumentation-winston": "^0.58.0",
45+
"@opentelemetry/resources": "^2.6.1",
46+
"@opentelemetry/sdk-metrics": "^2.6.1",
47+
"@opentelemetry/sdk-node": "^0.214.0",
48+
"@opentelemetry/sdk-trace-base": "^2.6.1",
4949
"google-auth-library": "^9.6.3",
5050
"node-fetch": "^3.3.2",
5151
"winston": "^3.12.0"

js/plugins/google-cloud/src/gcpOpenTelemetry.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ import { type ExportResult } from '@opentelemetry/core';
2626
import type { Instrumentation } from '@opentelemetry/instrumentation';
2727
import { PinoInstrumentation } from '@opentelemetry/instrumentation-pino';
2828
import { WinstonInstrumentation } from '@opentelemetry/instrumentation-winston';
29-
import { Resource } from '@opentelemetry/resources';
29+
import { Resource, resourceFromAttributes } from '@opentelemetry/resources';
3030
import {
31+
AggregationOption,
3132
AggregationTemporality,
32-
DefaultAggregation,
33-
ExponentialHistogramAggregation,
33+
AggregationType,
3434
InMemoryMetricExporter,
3535
InstrumentType,
3636
PeriodicExportingMetricReader,
@@ -73,8 +73,10 @@ export class GcpOpenTelemetry {
7373

7474
constructor(config: GcpTelemetryConfig) {
7575
this.config = config;
76-
this.resource = new Resource({ type: 'global' }).merge(
77-
new GcpDetectorSync().detect()
76+
77+
const detected = new GcpDetectorSync().detect();
78+
this.resource = resourceFromAttributes({ type: 'global' }).merge(
79+
resourceFromAttributes(detected.attributes ?? {})
7880
);
7981
}
8082

@@ -237,11 +239,11 @@ class MetricExporterWrapper extends MetricExporter {
237239
});
238240
}
239241

240-
selectAggregation(instrumentType: InstrumentType) {
242+
selectAggregation(instrumentType: InstrumentType): AggregationOption {
241243
if (instrumentType === InstrumentType.HISTOGRAM) {
242-
return new ExponentialHistogramAggregation();
244+
return { type: AggregationType.EXPONENTIAL_HISTOGRAM };
243245
}
244-
return new DefaultAggregation();
246+
return { type: AggregationType.DEFAULT };
245247
}
246248

247249
selectAggregationTemporality(instrumentType: InstrumentType) {

js/plugins/google-cloud/tests/metrics_test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import type {
2929
ScopeMetrics,
3030
SumMetricData,
3131
} from '@opentelemetry/sdk-metrics';
32+
import { DataPointType } from '@opentelemetry/sdk-metrics';
3233
import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
3334
import * as assert from 'assert';
3435
import { genkit, z, type GenerateResponseData, type Genkit } from 'genkit';
@@ -652,7 +653,8 @@ describe('GoogleCloudMetrics', () => {
652653
if (genkitMetrics) {
653654
const counterMetric: SumMetricData = genkitMetrics.metrics.find(
654655
(e) =>
655-
e.descriptor.name === metricName && e.descriptor.type === 'COUNTER'
656+
e.descriptor.name === metricName &&
657+
e.dataPointType === DataPointType.SUM
656658
) as SumMetricData;
657659
if (counterMetric) {
658660
return counterMetric.dataPoints;
@@ -692,7 +694,8 @@ describe('GoogleCloudMetrics', () => {
692694
} else {
693695
const histogramMetric = genkitMetrics.metrics.find(
694696
(e) =>
695-
e.descriptor.name === metricName && e.descriptor.type === 'HISTOGRAM'
697+
e.descriptor.name === metricName &&
698+
e.dataPointType === DataPointType.HISTOGRAM
696699
) as HistogramMetricData;
697700
if (histogramMetric === undefined) {
698701
assert.fail(

0 commit comments

Comments
 (0)