@mastra/otel-bridge 1.4.3 → 1.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,271 +1,268 @@
1
- import { TracingEventType } from '@mastra/core/observability';
2
- import { BaseExporter, getExternalParentId } from '@mastra/observability';
3
- import { convertLog, SpanConverter, getSpanKind } from '@mastra/otel-exporter';
4
- import { trace, isSpanContextValid, TraceFlags, context } from '@opentelemetry/api';
5
- import { logs } from '@opentelemetry/api-logs';
6
-
7
- // src/bridge.ts
1
+ import { TracingEventType } from "@mastra/core/observability";
2
+ import { BaseExporter, getExternalParentId } from "@mastra/observability";
3
+ import { SpanConverter, convertLog, getSpanKind } from "@mastra/otel-exporter";
4
+ import { TraceFlags, context, isSpanContextValid, trace } from "@opentelemetry/api";
5
+ import { logs } from "@opentelemetry/api-logs";
6
+ //#region src/bridge.ts
7
+ /**
8
+ * OpenTelemetry Bridge implementation
9
+ *
10
+ * Creates real OTEL spans when Mastra spans are created, maintaining proper
11
+ * context propagation for nested instrumentation.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * import { OtelBridge } from '@mastra/otel-bridge';
16
+ * import { Mastra } from '@mastra/core';
17
+ *
18
+ * const mastra = new Mastra({
19
+ * agents: { myAgent },
20
+ * observability: {
21
+ * configs: {
22
+ * default: {
23
+ * serviceName: 'my-service',
24
+ * bridge: new OtelBridge(),
25
+ * }
26
+ * }
27
+ * }
28
+ * });
29
+ * ```
30
+ */
8
31
  var OtelBridge = class extends BaseExporter {
9
- name = "otel";
10
- tracerProvider;
11
- loggerProvider;
12
- otelTracer;
13
- otelLogger;
14
- otelSpanMap = /* @__PURE__ */ new Map();
15
- spanConverter;
16
- constructor(config = {}) {
17
- super(config);
18
- this.tracerProvider = config.tracerProvider ?? trace.getTracerProvider();
19
- this.otelTracer = this.tracerProvider.getTracer("@mastra/otel-bridge", "1.0.0");
20
- this.loggerProvider = config.loggerProvider ?? logs.getLoggerProvider();
21
- this.otelLogger = this.loggerProvider.getLogger("@mastra/otel-bridge", "1.0.0");
22
- }
23
- /**
24
- * Handle Mastra tracing events
25
- *
26
- * Ships OTEL spans when Mastra spans end.
27
- * This maintains proper span hierarchy and allows OTEL-instrumented code within
28
- * Mastra spans to have correct parent-child relationships.
29
- * Note: OTEL spans are created when registerSpan is called when the span is first created.
30
- */
31
- async _exportTracingEvent(event) {
32
- if (event.type === TracingEventType.SPAN_ENDED) {
33
- await this.handleSpanEnded(event);
34
- }
35
- }
36
- /**
37
- * Forward Mastra log events into the globally-registered OTEL LoggerProvider.
38
- *
39
- * If the user has not registered a LoggerProvider (e.g. via @opentelemetry/sdk-logs
40
- * or NodeSDK's logRecordProcessor option), the API returns a no-op logger and
41
- * emit() is a silent no-op — the bridge degrades gracefully.
42
- *
43
- * Trace correlation:
44
- * - If the log carries a spanId we have an OTEL span for, emit under that span's
45
- * stored context so the log nests beneath it in the trace.
46
- * - Else if the log carries traceId+spanId, attach a SpanContext built from those
47
- * IDs so backends still correlate by ID.
48
- * - Else emit under whatever context is currently active.
49
- */
50
- async onLogEvent(event) {
51
- if (this.isDisabled) return;
52
- try {
53
- const params = convertLog(event.log);
54
- const attributes = { ...params.attributes };
55
- if (params.traceId) attributes["mastra.traceId"] = params.traceId;
56
- if (params.spanId) attributes["mastra.spanId"] = params.spanId;
57
- const logContext = this.resolveLogContext(params.traceId, params.spanId);
58
- this.otelLogger.emit({
59
- timestamp: params.timestamp,
60
- severityNumber: params.severityNumber,
61
- severityText: params.severityText,
62
- body: params.body,
63
- attributes,
64
- context: logContext
65
- });
66
- } catch (error) {
67
- this.logger.error("[OtelBridge] Failed to emit log:", error);
68
- }
69
- }
70
- /**
71
- * Pick the OTEL Context to emit a log under so trace correlation is correct.
72
- */
73
- resolveLogContext(traceId, spanId) {
74
- if (spanId) {
75
- const entry = this.otelSpanMap.get(spanId);
76
- if (entry) return entry.otelContext;
77
- }
78
- if (traceId && spanId) {
79
- const candidate = {
80
- traceId,
81
- spanId,
82
- traceFlags: TraceFlags.SAMPLED,
83
- isRemote: false
84
- };
85
- if (isSpanContextValid(candidate)) {
86
- return trace.setSpanContext(context.active(), candidate);
87
- }
88
- }
89
- return context.active();
90
- }
91
- /**
92
- * Initialize with tracing configuration
93
- */
94
- init(options) {
95
- this.spanConverter = new SpanConverter({
96
- packageName: "@mastra/otel-bridge",
97
- serviceName: options.config?.serviceName,
98
- format: "GenAI_v1_38_0"
99
- });
100
- }
101
- /**
102
- * Create a span in the bridge's tracing system.
103
- * Called during Mastra span construction to get bridge-generated identifiers.
104
- *
105
- * @param options - Span creation options from Mastra
106
- * @returns Span identifiers (spanId, traceId, parentSpanId) from bridge, or undefined if creation fails
107
- */
108
- createSpan(options) {
109
- try {
110
- let parentOtelContext = context.active();
111
- const externalParentId = getExternalParentId(options);
112
- if (externalParentId) {
113
- const parentEntry = this.otelSpanMap.get(externalParentId);
114
- if (parentEntry) {
115
- parentOtelContext = parentEntry.otelContext;
116
- }
117
- }
118
- const otelSpan = this.otelTracer.startSpan(
119
- options.name,
120
- {
121
- kind: getSpanKind(options.type)
122
- },
123
- parentOtelContext
124
- );
125
- const spanContext = trace.setSpan(parentOtelContext, otelSpan);
126
- const otelSpanContext = otelSpan.spanContext();
127
- if (!isSpanContextValid(otelSpanContext)) {
128
- otelSpan.end();
129
- return void 0;
130
- }
131
- const spanId = otelSpanContext.spanId;
132
- const traceId = otelSpanContext.traceId;
133
- this.otelSpanMap.set(spanId, { otelSpan, otelContext: spanContext });
134
- const parentSpan = trace.getSpan(parentOtelContext);
135
- const parentSpanContext = parentSpan?.spanContext();
136
- const parentSpanId = parentSpanContext && isSpanContextValid(parentSpanContext) ? parentSpanContext.spanId : void 0;
137
- this.logger.debug(
138
- `[OtelBridge.createSpan] Created span [spanId=${spanId}] [traceId=${traceId}] [parentSpanId=${parentSpanId}] [type=${options.type}] [mapSize=${this.otelSpanMap.size}]`
139
- );
140
- return { spanId, traceId, parentSpanId };
141
- } catch (error) {
142
- this.logger.error("[OtelBridge] Failed to create span:", error);
143
- return void 0;
144
- }
145
- }
146
- /**
147
- * Handle SPAN_ENDED event
148
- *
149
- * Retrieves the OTEL span created at SPAN_STARTED, sets all final attributes,
150
- * events, and status, then ends the span. Cleans up the span map entry.
151
- */
152
- async handleSpanEnded(event) {
153
- try {
154
- const mastraSpan = event.exportedSpan;
155
- const entry = this.otelSpanMap.get(mastraSpan.id);
156
- if (!entry) {
157
- this.logger.warn(`[OtelBridge] No OTEL span found for Mastra span [id=${mastraSpan.id}].`);
158
- return;
159
- }
160
- this.otelSpanMap.delete(mastraSpan.id);
161
- if (!this.spanConverter) {
162
- return;
163
- }
164
- const { otelSpan } = entry;
165
- this.logger.debug(`[OtelBridge] Ending OTEL span [mastraId=${mastraSpan.id}] [name=${mastraSpan.name}]`);
166
- const readableSpan = await this.spanConverter.convertSpan(mastraSpan);
167
- otelSpan.updateName(readableSpan.name);
168
- for (const [key, value] of Object.entries(readableSpan.attributes)) {
169
- if (value !== void 0 && value !== null && typeof value !== "object") {
170
- otelSpan.setAttribute(key, value);
171
- }
172
- }
173
- otelSpan.setStatus(readableSpan.status);
174
- for (const event2 of readableSpan.events) {
175
- if (event2.name === "exception" && event2.attributes) {
176
- const error = new Error(event2.attributes["exception.message"]);
177
- otelSpan.recordException(error);
178
- }
179
- }
180
- otelSpan.end(mastraSpan.endTime);
181
- this.logger.debug(
182
- `[OtelBridge] Completed OTEL span [mastraId=${mastraSpan.id}] [traceId=${otelSpan.spanContext().traceId}]`
183
- );
184
- } catch (error) {
185
- this.logger.error("[OtelBridge] Failed to handle SPAN_ENDED:", error);
186
- }
187
- }
188
- /**
189
- * Execute a function (sync or async) within the OTEL context of a Mastra span.
190
- * Retrieves the stored OTEL context for the span and executes the function within it.
191
- *
192
- * This is the core implementation used by both executeInContext and executeInContextSync.
193
- *
194
- * @param spanId - The ID of the Mastra span to use as context
195
- * @param fn - The function to execute within the span context
196
- * @returns The result of the function execution
197
- */
198
- executeWithSpanContext(spanId, fn) {
199
- const entry = this.otelSpanMap.get(spanId);
200
- this.logger.debug(
201
- `[OtelBridge.executeWithSpanContext] spanId=${spanId}, inMap=${!!entry}, storedOtelSpan=${entry?.otelSpan.spanContext().spanId || "none"}`
202
- );
203
- const spanContext = entry?.otelContext;
204
- if (spanContext) {
205
- return context.with(spanContext, fn);
206
- }
207
- return fn();
208
- }
209
- /**
210
- * Execute an async function within the OTEL context of a Mastra span.
211
- *
212
- * @param spanId - The ID of the Mastra span to use as context
213
- * @param fn - The async function to execute within the span context
214
- * @returns The result of the function execution
215
- */
216
- executeInContext(spanId, fn) {
217
- return this.executeWithSpanContext(spanId, fn);
218
- }
219
- /**
220
- * Execute a synchronous function within the OTEL context of a Mastra span.
221
- *
222
- * @param spanId - The ID of the Mastra span to use as context
223
- * @param fn - The synchronous function to execute within the span context
224
- * @returns The result of the function execution
225
- */
226
- executeInContextSync(spanId, fn) {
227
- return this.executeWithSpanContext(spanId, fn);
228
- }
229
- /**
230
- * Force flush any buffered spans without shutting down the bridge.
231
- *
232
- * Attempts to flush the underlying OTEL tracer provider if it supports
233
- * the forceFlush operation. This is useful in serverless environments
234
- * where you need to ensure all spans are exported before the runtime
235
- * instance is terminated.
236
- */
237
- async flush() {
238
- await this.flushProvider(this.tracerProvider, "tracer");
239
- await this.flushProvider(this.loggerProvider, "logger");
240
- }
241
- async flushProvider(provider, label) {
242
- try {
243
- if (provider && typeof provider === "object" && "forceFlush" in provider && typeof provider.forceFlush === "function") {
244
- await provider.forceFlush();
245
- this.logger.debug(`[OtelBridge] Flushed ${label} provider`);
246
- } else {
247
- this.logger.debug(
248
- `[OtelBridge] ${label === "tracer" ? "Tracer" : "Logger"} provider does not support forceFlush`
249
- );
250
- }
251
- } catch (error) {
252
- this.logger.error(`[OtelBridge] Failed to flush ${label} provider:`, error);
253
- }
254
- }
255
- /**
256
- * Shutdown the bridge and clean up resources
257
- */
258
- async shutdown() {
259
- await this.flush();
260
- for (const [spanId, { otelSpan }] of this.otelSpanMap.entries()) {
261
- this.logger.warn(`[OtelBridge] Force-ending span that was not properly closed [id=${spanId}]`);
262
- otelSpan.end();
263
- }
264
- this.otelSpanMap.clear();
265
- this.logger.info("[OtelBridge] Shutdown complete");
266
- }
32
+ name = "otel";
33
+ tracerProvider;
34
+ loggerProvider;
35
+ otelTracer;
36
+ otelLogger;
37
+ otelSpanMap = /* @__PURE__ */ new Map();
38
+ spanConverter;
39
+ constructor(config = {}) {
40
+ super(config);
41
+ this.tracerProvider = config.tracerProvider ?? trace.getTracerProvider();
42
+ this.otelTracer = this.tracerProvider.getTracer("@mastra/otel-bridge", "1.0.0");
43
+ this.loggerProvider = config.loggerProvider ?? logs.getLoggerProvider();
44
+ this.otelLogger = this.loggerProvider.getLogger("@mastra/otel-bridge", "1.0.0");
45
+ }
46
+ /**
47
+ * Handle Mastra tracing events
48
+ *
49
+ * Ships OTEL spans when Mastra spans end.
50
+ * This maintains proper span hierarchy and allows OTEL-instrumented code within
51
+ * Mastra spans to have correct parent-child relationships.
52
+ * Note: OTEL spans are created when registerSpan is called when the span is first created.
53
+ */
54
+ async _exportTracingEvent(event) {
55
+ if (event.type === TracingEventType.SPAN_ENDED) await this.handleSpanEnded(event);
56
+ }
57
+ /**
58
+ * Forward Mastra log events into the globally-registered OTEL LoggerProvider.
59
+ *
60
+ * If the user has not registered a LoggerProvider (e.g. via @opentelemetry/sdk-logs
61
+ * or NodeSDK's logRecordProcessor option), the API returns a no-op logger and
62
+ * emit() is a silent no-op the bridge degrades gracefully.
63
+ *
64
+ * Trace correlation:
65
+ * - If the log carries a spanId we have an OTEL span for, emit under that span's
66
+ * stored context so the log nests beneath it in the trace.
67
+ * - Else if the log carries traceId+spanId, attach a SpanContext built from those
68
+ * IDs so backends still correlate by ID.
69
+ * - Else emit under whatever context is currently active.
70
+ */
71
+ async onLogEvent(event) {
72
+ if (this.isDisabled) return;
73
+ try {
74
+ const params = convertLog(event.log);
75
+ const attributes = { ...params.attributes };
76
+ if (params.traceId) attributes["mastra.traceId"] = params.traceId;
77
+ if (params.spanId) attributes["mastra.spanId"] = params.spanId;
78
+ const logContext = this.resolveLogContext(params.traceId, params.spanId);
79
+ this.otelLogger.emit({
80
+ timestamp: params.timestamp,
81
+ severityNumber: params.severityNumber,
82
+ severityText: params.severityText,
83
+ body: params.body,
84
+ attributes,
85
+ context: logContext
86
+ });
87
+ } catch (error) {
88
+ this.logger.error("[OtelBridge] Failed to emit log:", error);
89
+ }
90
+ }
91
+ /**
92
+ * Pick the OTEL Context to emit a log under so trace correlation is correct.
93
+ */
94
+ resolveLogContext(traceId, spanId) {
95
+ if (spanId) {
96
+ const entry = this.otelSpanMap.get(spanId);
97
+ if (entry) return entry.otelContext;
98
+ }
99
+ if (traceId && spanId) {
100
+ const candidate = {
101
+ traceId,
102
+ spanId,
103
+ traceFlags: TraceFlags.SAMPLED,
104
+ isRemote: false
105
+ };
106
+ if (isSpanContextValid(candidate)) return trace.setSpanContext(context.active(), candidate);
107
+ }
108
+ return context.active();
109
+ }
110
+ /**
111
+ * Initialize with tracing configuration
112
+ */
113
+ init(options) {
114
+ this.spanConverter = new SpanConverter({
115
+ packageName: "@mastra/otel-bridge",
116
+ serviceName: options.config?.serviceName,
117
+ format: "GenAI_v1_38_0"
118
+ });
119
+ }
120
+ /**
121
+ * Create a span in the bridge's tracing system.
122
+ * Called during Mastra span construction to get bridge-generated identifiers.
123
+ *
124
+ * @param options - Span creation options from Mastra
125
+ * @returns Span identifiers (spanId, traceId, parentSpanId) from bridge, or undefined if creation fails
126
+ */
127
+ createSpan(options) {
128
+ try {
129
+ let parentOtelContext = context.active();
130
+ const externalParentId = getExternalParentId(options);
131
+ if (externalParentId) {
132
+ const parentEntry = this.otelSpanMap.get(externalParentId);
133
+ if (parentEntry) parentOtelContext = parentEntry.otelContext;
134
+ }
135
+ const otelSpan = this.otelTracer.startSpan(options.name, { kind: getSpanKind(options.type) }, parentOtelContext);
136
+ const spanContext = trace.setSpan(parentOtelContext, otelSpan);
137
+ const otelSpanContext = otelSpan.spanContext();
138
+ if (!isSpanContextValid(otelSpanContext)) {
139
+ otelSpan.end();
140
+ return;
141
+ }
142
+ const spanId = otelSpanContext.spanId;
143
+ const traceId = otelSpanContext.traceId;
144
+ this.otelSpanMap.set(spanId, {
145
+ otelSpan,
146
+ otelContext: spanContext
147
+ });
148
+ const parentSpanContext = trace.getSpan(parentOtelContext)?.spanContext();
149
+ const parentSpanId = parentSpanContext && isSpanContextValid(parentSpanContext) ? parentSpanContext.spanId : void 0;
150
+ this.logger.debug(`[OtelBridge.createSpan] Created span [spanId=${spanId}] [traceId=${traceId}] [parentSpanId=${parentSpanId}] [type=${options.type}] [mapSize=${this.otelSpanMap.size}]`);
151
+ return {
152
+ spanId,
153
+ traceId,
154
+ parentSpanId
155
+ };
156
+ } catch (error) {
157
+ this.logger.error("[OtelBridge] Failed to create span:", error);
158
+ return;
159
+ }
160
+ }
161
+ /**
162
+ * Handle SPAN_ENDED event
163
+ *
164
+ * Retrieves the OTEL span created at SPAN_STARTED, sets all final attributes,
165
+ * events, and status, then ends the span. Cleans up the span map entry.
166
+ */
167
+ async handleSpanEnded(event) {
168
+ try {
169
+ const mastraSpan = event.exportedSpan;
170
+ const entry = this.otelSpanMap.get(mastraSpan.id);
171
+ if (!entry) {
172
+ this.logger.warn(`[OtelBridge] No OTEL span found for Mastra span [id=${mastraSpan.id}].`);
173
+ return;
174
+ }
175
+ this.otelSpanMap.delete(mastraSpan.id);
176
+ if (!this.spanConverter) return;
177
+ const { otelSpan } = entry;
178
+ this.logger.debug(`[OtelBridge] Ending OTEL span [mastraId=${mastraSpan.id}] [name=${mastraSpan.name}]`);
179
+ const readableSpan = await this.spanConverter.convertSpan(mastraSpan);
180
+ otelSpan.updateName(readableSpan.name);
181
+ for (const [key, value] of Object.entries(readableSpan.attributes)) if (value !== void 0 && value !== null && typeof value !== "object") otelSpan.setAttribute(key, value);
182
+ otelSpan.setStatus(readableSpan.status);
183
+ for (const event of readableSpan.events) if (event.name === "exception" && event.attributes) {
184
+ const error = new Error(event.attributes["exception.message"]);
185
+ otelSpan.recordException(error);
186
+ }
187
+ otelSpan.end(mastraSpan.endTime);
188
+ this.logger.debug(`[OtelBridge] Completed OTEL span [mastraId=${mastraSpan.id}] [traceId=${otelSpan.spanContext().traceId}]`);
189
+ } catch (error) {
190
+ this.logger.error("[OtelBridge] Failed to handle SPAN_ENDED:", error);
191
+ }
192
+ }
193
+ /**
194
+ * Execute a function (sync or async) within the OTEL context of a Mastra span.
195
+ * Retrieves the stored OTEL context for the span and executes the function within it.
196
+ *
197
+ * This is the core implementation used by both executeInContext and executeInContextSync.
198
+ *
199
+ * @param spanId - The ID of the Mastra span to use as context
200
+ * @param fn - The function to execute within the span context
201
+ * @returns The result of the function execution
202
+ */
203
+ executeWithSpanContext(spanId, fn) {
204
+ const entry = this.otelSpanMap.get(spanId);
205
+ this.logger.debug(`[OtelBridge.executeWithSpanContext] spanId=${spanId}, inMap=${!!entry}, storedOtelSpan=${entry?.otelSpan.spanContext().spanId || "none"}`);
206
+ const spanContext = entry?.otelContext;
207
+ if (spanContext) return context.with(spanContext, fn);
208
+ return fn();
209
+ }
210
+ /**
211
+ * Execute an async function within the OTEL context of a Mastra span.
212
+ *
213
+ * @param spanId - The ID of the Mastra span to use as context
214
+ * @param fn - The async function to execute within the span context
215
+ * @returns The result of the function execution
216
+ */
217
+ executeInContext(spanId, fn) {
218
+ return this.executeWithSpanContext(spanId, fn);
219
+ }
220
+ /**
221
+ * Execute a synchronous function within the OTEL context of a Mastra span.
222
+ *
223
+ * @param spanId - The ID of the Mastra span to use as context
224
+ * @param fn - The synchronous function to execute within the span context
225
+ * @returns The result of the function execution
226
+ */
227
+ executeInContextSync(spanId, fn) {
228
+ return this.executeWithSpanContext(spanId, fn);
229
+ }
230
+ /**
231
+ * Force flush any buffered spans without shutting down the bridge.
232
+ *
233
+ * Attempts to flush the underlying OTEL tracer provider if it supports
234
+ * the forceFlush operation. This is useful in serverless environments
235
+ * where you need to ensure all spans are exported before the runtime
236
+ * instance is terminated.
237
+ */
238
+ async flush() {
239
+ await this.flushProvider(this.tracerProvider, "tracer");
240
+ await this.flushProvider(this.loggerProvider, "logger");
241
+ }
242
+ async flushProvider(provider, label) {
243
+ try {
244
+ if (provider && typeof provider === "object" && "forceFlush" in provider && typeof provider.forceFlush === "function") {
245
+ await provider.forceFlush();
246
+ this.logger.debug(`[OtelBridge] Flushed ${label} provider`);
247
+ } else this.logger.debug(`[OtelBridge] ${label === "tracer" ? "Tracer" : "Logger"} provider does not support forceFlush`);
248
+ } catch (error) {
249
+ this.logger.error(`[OtelBridge] Failed to flush ${label} provider:`, error);
250
+ }
251
+ }
252
+ /**
253
+ * Shutdown the bridge and clean up resources
254
+ */
255
+ async shutdown() {
256
+ await this.flush();
257
+ for (const [spanId, { otelSpan }] of this.otelSpanMap.entries()) {
258
+ this.logger.warn(`[OtelBridge] Force-ending span that was not properly closed [id=${spanId}]`);
259
+ otelSpan.end();
260
+ }
261
+ this.otelSpanMap.clear();
262
+ this.logger.info("[OtelBridge] Shutdown complete");
263
+ }
267
264
  };
268
-
265
+ //#endregion
269
266
  export { OtelBridge };
270
- //# sourceMappingURL=index.js.map
267
+
271
268
  //# sourceMappingURL=index.js.map