1414 * limitations under the License.
1515 */
1616
17+ import { Context } from '@opentelemetry/api' ;
1718import { NodeSDK } from '@opentelemetry/sdk-node' ;
1819import {
1920 BatchSpanProcessor ,
2021 SimpleSpanProcessor ,
22+ type ReadableSpan ,
23+ type Span ,
2124 type SpanProcessor ,
2225} from '@opentelemetry/sdk-trace-base' ;
2326import { logger } from '../logging.js' ;
@@ -29,6 +32,7 @@ import { RealtimeSpanProcessor } from './realtime-span-processor.js';
2932
3033let telemetrySDK : NodeSDK | null = null ;
3134let nodeOtelConfig : TelemetryConfig | null = null ;
35+ let isSigtermHandlerRegistered = false ;
3236
3337export function initNodeTelemetryProvider ( ) {
3438 setTelemetryProvider ( {
@@ -37,6 +41,64 @@ export function initNodeTelemetryProvider() {
3741 } ) ;
3842}
3943
44+ /**
45+ * MultiSpanProcessor is a multiplexer that allows Genkit to register multiple
46+ * span processors reliably.
47+ *
48+ * We provide it as the sole entry in the `spanProcessors` array to the
49+ * OpenTelemetry NodeSDK because the SDK's internal logic for merging
50+ * `traceExporter`, `spanProcessor` (singular), and `spanProcessors` (plural)
51+ * varies across versions and can lead to exporters being silently overwritten
52+ * or ignored.
53+ *
54+ * By wrapping all processors into this single delegate, we guarantee that
55+ * Genkit's internal telemetry and any user-provided exporters both receive
56+ * every start and end event.
57+ */
58+ export class MultiSpanProcessor implements SpanProcessor {
59+ constructor ( private processors : SpanProcessor [ ] ) { }
60+ onStart ( span : Span , parentContext : Context ) {
61+ this . processors . forEach ( ( p ) => {
62+ try {
63+ p . onStart ?.( span , parentContext ) ;
64+ } catch ( e ) {
65+ logger . error ( `Error in span processor onStart: ${ e } ` ) ;
66+ }
67+ } ) ;
68+ }
69+ onEnd ( span : ReadableSpan ) {
70+ this . processors . forEach ( ( p ) => {
71+ try {
72+ p . onEnd ( span ) ;
73+ } catch ( e ) {
74+ logger . error ( `Error in span processor onEnd: ${ e } ` ) ;
75+ }
76+ } ) ;
77+ }
78+ async forceFlush ( ) {
79+ await Promise . all (
80+ this . processors . map ( async ( p ) => {
81+ try {
82+ await p . forceFlush ( ) ;
83+ } catch ( e ) {
84+ logger . error ( `Error in span processor forceFlush: ${ e } ` ) ;
85+ }
86+ } )
87+ ) ;
88+ }
89+ async shutdown ( ) {
90+ await Promise . all (
91+ this . processors . map ( async ( p ) => {
92+ try {
93+ await p . shutdown ( ) ;
94+ } catch ( e ) {
95+ logger . error ( `Error in span processor shutdown: ${ e } ` ) ;
96+ }
97+ } )
98+ ) ;
99+ }
100+ }
101+
40102/**
41103 * Enables tracing and metrics open telemetry configuration.
42104 */
@@ -52,23 +114,48 @@ async function enableTelemetry(
52114 ? await telemetryConfig
53115 : telemetryConfig ;
54116
55- nodeOtelConfig = telemetryConfig || { } ;
117+ // If already initialized and new config is empty,
118+ // skip to avoid unnecessary restarts
119+ if (
120+ telemetrySDK &&
121+ ( ! telemetryConfig ||
122+ ( ! telemetryConfig . spanProcessors &&
123+ ! telemetryConfig . spanProcessor &&
124+ ! telemetryConfig . traceExporter &&
125+ ! telemetryConfig . metricReader ) )
126+ ) {
127+ return ;
128+ }
129+
130+ nodeOtelConfig = { ...( telemetryConfig || { } ) } ;
56131
57132 const processors : SpanProcessor [ ] = [ createTelemetryServerProcessor ( ) ] ;
58- if ( nodeOtelConfig . traceExporter ) {
59- throw new Error ( 'Please specify spanProcessors instead.' ) ;
60- }
61133 if ( nodeOtelConfig . spanProcessors ) {
62134 processors . push ( ...nodeOtelConfig . spanProcessors ) ;
63135 }
64136 if ( nodeOtelConfig . spanProcessor ) {
65137 processors . push ( nodeOtelConfig . spanProcessor ) ;
66138 delete nodeOtelConfig . spanProcessor ;
67139 }
68- nodeOtelConfig . spanProcessors = processors ;
140+
141+ if ( processors . length > 1 ) {
142+ nodeOtelConfig . spanProcessors = [ new MultiSpanProcessor ( processors ) ] ;
143+ } else {
144+ nodeOtelConfig . spanProcessors = processors ;
145+ }
146+ delete nodeOtelConfig . spanProcessor ;
147+
148+ if ( telemetrySDK ) {
149+ await cleanUpTracing ( ) ;
150+ }
151+
69152 telemetrySDK = new NodeSDK ( nodeOtelConfig ) ;
70153 telemetrySDK . start ( ) ;
71- process . on ( 'SIGTERM' , async ( ) => await cleanUpTracing ( ) ) ;
154+
155+ if ( ! isSigtermHandlerRegistered ) {
156+ process . on ( 'SIGTERM' , async ( ) => await cleanUpTracing ( ) ) ;
157+ isSigtermHandlerRegistered = true ;
158+ }
72159}
73160
74161async function cleanUpTracing ( ) : Promise < void > {
0 commit comments