@mastra/otel-exporter 1.3.5 → 1.3.6
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/CHANGELOG.md +16 -0
- package/dist/index.cjs +975 -1123
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +973 -1120
- package/dist/index.js.map +1 -1
- package/package.json +9 -8
package/dist/index.js
CHANGED
|
@@ -1,1191 +1,1044 @@
|
|
|
1
|
-
import { SpanType, TracingEventType } from
|
|
2
|
-
import { BaseExporter } from
|
|
3
|
-
import {
|
|
4
|
-
import { ExportResultCode } from
|
|
5
|
-
import { resourceFromAttributes } from
|
|
6
|
-
import {
|
|
7
|
-
import { BatchSpanProcessor } from
|
|
8
|
-
import {
|
|
9
|
-
import { SeverityNumber } from
|
|
10
|
-
import { readFileSync } from
|
|
11
|
-
import { fileURLToPath } from
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
1
|
+
import { SpanType, TracingEventType } from "@mastra/core/observability";
|
|
2
|
+
import { BaseExporter } from "@mastra/observability";
|
|
3
|
+
import { DiagConsoleLogger, DiagLogLevel, SpanKind, SpanStatusCode, TraceFlags, context, diag, trace } from "@opentelemetry/api";
|
|
4
|
+
import { ExportResultCode } from "@opentelemetry/core";
|
|
5
|
+
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
6
|
+
import { BatchLogRecordProcessor, LoggerProvider } from "@opentelemetry/sdk-logs";
|
|
7
|
+
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
8
|
+
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, ATTR_TELEMETRY_SDK_LANGUAGE, ATTR_TELEMETRY_SDK_NAME, ATTR_TELEMETRY_SDK_VERSION } from "@opentelemetry/semantic-conventions";
|
|
9
|
+
import { SeverityNumber } from "@opentelemetry/api-logs";
|
|
10
|
+
import { readFileSync } from "fs";
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
12
|
+
import { ATTR_ERROR_MESSAGE, ATTR_ERROR_TYPE, ATTR_GEN_AI_AGENT_ID, ATTR_GEN_AI_AGENT_NAME, ATTR_GEN_AI_CONVERSATION_ID, ATTR_GEN_AI_INPUT_MESSAGES, ATTR_GEN_AI_OPERATION_NAME, ATTR_GEN_AI_OUTPUT_MESSAGES, ATTR_GEN_AI_PROVIDER_NAME, ATTR_GEN_AI_REQUEST_FREQUENCY_PENALTY, ATTR_GEN_AI_REQUEST_MAX_TOKENS, ATTR_GEN_AI_REQUEST_MODEL, ATTR_GEN_AI_REQUEST_PRESENCE_PENALTY, ATTR_GEN_AI_REQUEST_SEED, ATTR_GEN_AI_REQUEST_STOP_SEQUENCES, ATTR_GEN_AI_REQUEST_TEMPERATURE, ATTR_GEN_AI_REQUEST_TOP_K, ATTR_GEN_AI_REQUEST_TOP_P, ATTR_GEN_AI_RESPONSE_FINISH_REASONS, ATTR_GEN_AI_RESPONSE_ID, ATTR_GEN_AI_RESPONSE_MODEL, ATTR_GEN_AI_SYSTEM_INSTRUCTIONS, ATTR_GEN_AI_TOOL_DESCRIPTION, ATTR_GEN_AI_TOOL_NAME, ATTR_GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, ATTR_GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, ATTR_GEN_AI_USAGE_INPUT_TOKENS, ATTR_GEN_AI_USAGE_OUTPUT_TOKENS, ATTR_SERVER_ADDRESS, ATTR_SERVER_PORT } from "@opentelemetry/semantic-conventions/incubating";
|
|
13
|
+
//#region src/loadExporter.ts
|
|
14
|
+
const EXPORTER_SPECS = {
|
|
15
|
+
traces: {
|
|
16
|
+
"http/json": {
|
|
17
|
+
pkg: "@opentelemetry/exporter-trace-otlp-http",
|
|
18
|
+
exportName: "OTLPTraceExporter"
|
|
19
|
+
},
|
|
20
|
+
"http/protobuf": {
|
|
21
|
+
pkg: "@opentelemetry/exporter-trace-otlp-proto",
|
|
22
|
+
exportName: "OTLPTraceExporter"
|
|
23
|
+
},
|
|
24
|
+
grpc: {
|
|
25
|
+
pkg: "@opentelemetry/exporter-trace-otlp-grpc",
|
|
26
|
+
exportName: "OTLPTraceExporter",
|
|
27
|
+
extras: ["@grpc/grpc-js"]
|
|
28
|
+
},
|
|
29
|
+
zipkin: {
|
|
30
|
+
pkg: "@opentelemetry/exporter-zipkin",
|
|
31
|
+
exportName: "ZipkinExporter"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
logs: {
|
|
35
|
+
"http/json": {
|
|
36
|
+
pkg: "@opentelemetry/exporter-logs-otlp-http",
|
|
37
|
+
exportName: "OTLPLogExporter"
|
|
38
|
+
},
|
|
39
|
+
"http/protobuf": {
|
|
40
|
+
pkg: "@opentelemetry/exporter-logs-otlp-proto",
|
|
41
|
+
exportName: "OTLPLogExporter"
|
|
42
|
+
},
|
|
43
|
+
grpc: {
|
|
44
|
+
pkg: "@opentelemetry/exporter-logs-otlp-grpc",
|
|
45
|
+
exportName: "OTLPLogExporter",
|
|
46
|
+
extras: ["@grpc/grpc-js"]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
38
49
|
};
|
|
39
|
-
|
|
50
|
+
/** Cache keyed by "signal:protocol" */
|
|
51
|
+
const cache = /* @__PURE__ */ new Map();
|
|
52
|
+
/**
|
|
53
|
+
* Load a trace exporter for the given protocol.
|
|
54
|
+
* Backward-compatible with existing usage in tracing.ts.
|
|
55
|
+
*/
|
|
40
56
|
async function loadExporter(protocol, provider) {
|
|
41
|
-
|
|
57
|
+
return loadSignalExporter("traces", protocol, provider);
|
|
42
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Load a signal-specific exporter class for the given protocol.
|
|
61
|
+
* Returns the constructor, or null if the package isn't installed.
|
|
62
|
+
*/
|
|
43
63
|
async function loadSignalExporter(signal, protocol, provider) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
cache.set(cacheKey, ExporterClass);
|
|
67
|
-
return ExporterClass;
|
|
68
|
-
} catch {
|
|
69
|
-
const providerInfo = provider ? ` (required for ${provider})` : "";
|
|
70
|
-
const allPkgs = [spec.pkg, ...spec.extras ?? []].join(" ");
|
|
71
|
-
console.error(
|
|
72
|
-
`[OtelExporter] ${capitalize(signal)} ${protocol} exporter is not installed${providerInfo}.
|
|
73
|
-
Install the required package(s):
|
|
74
|
-
npm install ${allPkgs}`
|
|
75
|
-
);
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
64
|
+
const spec = EXPORTER_SPECS[signal]?.[protocol];
|
|
65
|
+
if (!spec) {
|
|
66
|
+
if (protocol === "zipkin") console.warn(`[OtelExporter] Zipkin does not support OTLP ${signal}. ${capitalize(signal)} export will be disabled.`);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const cacheKey = `${signal}:${protocol}`;
|
|
70
|
+
if (cache.has(cacheKey)) return cache.get(cacheKey);
|
|
71
|
+
try {
|
|
72
|
+
const ExporterClass = (await import(spec.pkg))[spec.exportName];
|
|
73
|
+
if (typeof ExporterClass !== "function") {
|
|
74
|
+
console.error(`[OtelExporter] ${capitalize(signal)} ${protocol} exporter package "${spec.pkg}" did not expose a "${spec.exportName}" export. ${capitalize(signal)} export will be disabled.`);
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
cache.set(cacheKey, ExporterClass);
|
|
78
|
+
return ExporterClass;
|
|
79
|
+
} catch {
|
|
80
|
+
const providerInfo = provider ? ` (required for ${provider})` : "";
|
|
81
|
+
const allPkgs = [spec.pkg, ...spec.extras ?? []].join(" ");
|
|
82
|
+
console.error(`[OtelExporter] ${capitalize(signal)} ${protocol} exporter is not installed${providerInfo}.\n Install the required package(s):\n npm install ${allPkgs}`);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
78
85
|
}
|
|
79
86
|
function capitalize(s) {
|
|
80
|
-
|
|
87
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
81
88
|
}
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/log-converter.ts
|
|
91
|
+
/**
|
|
92
|
+
* Map Mastra LogLevel to OTEL SeverityNumber
|
|
93
|
+
*/
|
|
82
94
|
function mapSeverity(level) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return SeverityNumber.ERROR;
|
|
92
|
-
case "fatal":
|
|
93
|
-
return SeverityNumber.FATAL;
|
|
94
|
-
default:
|
|
95
|
-
return SeverityNumber.UNSPECIFIED;
|
|
96
|
-
}
|
|
95
|
+
switch (level) {
|
|
96
|
+
case "debug": return SeverityNumber.DEBUG;
|
|
97
|
+
case "info": return SeverityNumber.INFO;
|
|
98
|
+
case "warn": return SeverityNumber.WARN;
|
|
99
|
+
case "error": return SeverityNumber.ERROR;
|
|
100
|
+
case "fatal": return SeverityNumber.FATAL;
|
|
101
|
+
default: return SeverityNumber.UNSPECIFIED;
|
|
102
|
+
}
|
|
97
103
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Convert a Date to OTEL HrTime [seconds, nanoseconds]
|
|
106
|
+
*/
|
|
107
|
+
function dateToHrTime$1(date) {
|
|
108
|
+
const ms = date.getTime();
|
|
109
|
+
return [Math.floor(ms / 1e3), ms % 1e3 * 1e6];
|
|
103
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Stringify a value for use as an OTEL log attribute. Falls back to a
|
|
113
|
+
* placeholder rather than throwing if the value contains a circular
|
|
114
|
+
* reference, throws from a custom toJSON, or otherwise cannot be serialized.
|
|
115
|
+
*/
|
|
104
116
|
function safeStringify(value) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
117
|
+
try {
|
|
118
|
+
return JSON.stringify(value);
|
|
119
|
+
} catch {
|
|
120
|
+
return "[unserializable]";
|
|
121
|
+
}
|
|
110
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Build OTEL log attributes from ExportedLog fields.
|
|
125
|
+
* Includes trace correlation and metadata.
|
|
126
|
+
*/
|
|
111
127
|
function buildLogAttributes(log) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
if (log.tags?.length) {
|
|
126
|
-
attributes["mastra.tags"] = safeStringify(log.tags);
|
|
127
|
-
}
|
|
128
|
-
return attributes;
|
|
128
|
+
const attributes = {};
|
|
129
|
+
if (log.data) for (const [key, value] of Object.entries(log.data)) {
|
|
130
|
+
if (value === null || value === void 0) continue;
|
|
131
|
+
attributes[`mastra.log.${key}`] = typeof value === "object" ? safeStringify(value) : value;
|
|
132
|
+
}
|
|
133
|
+
if (log.metadata) for (const [key, value] of Object.entries(log.metadata)) {
|
|
134
|
+
if (value === null || value === void 0) continue;
|
|
135
|
+
attributes[`mastra.metadata.${key}`] = typeof value === "object" ? safeStringify(value) : value;
|
|
136
|
+
}
|
|
137
|
+
if (log.tags?.length) attributes["mastra.tags"] = safeStringify(log.tags);
|
|
138
|
+
return attributes;
|
|
129
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Convert an ExportedLog into parameters suitable for OTEL Logger.emit()
|
|
142
|
+
*/
|
|
130
143
|
function convertLog(log) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
144
|
+
return {
|
|
145
|
+
timestamp: dateToHrTime$1(log.timestamp),
|
|
146
|
+
severityNumber: mapSeverity(log.level),
|
|
147
|
+
severityText: log.level.toUpperCase(),
|
|
148
|
+
body: log.message,
|
|
149
|
+
attributes: buildLogAttributes(log),
|
|
150
|
+
traceId: log.traceId,
|
|
151
|
+
spanId: log.spanId
|
|
152
|
+
};
|
|
140
153
|
}
|
|
141
|
-
|
|
142
|
-
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region src/provider-configs.ts
|
|
143
156
|
function resolveProviderConfig(config, debug) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
if (debug && resolved) {
|
|
166
|
-
const maskedHeaders = {};
|
|
167
|
-
for (const [key, value] of Object.entries(resolved.headers)) {
|
|
168
|
-
maskedHeaders[key] = value ? "[REDACTED]" : "[EMPTY]";
|
|
169
|
-
}
|
|
170
|
-
console.info(`[OtelExporter:debug] Provider "${providerName}" resolved:`, {
|
|
171
|
-
endpoint: resolved.endpoint,
|
|
172
|
-
protocol: resolved.protocol,
|
|
173
|
-
headers: maskedHeaders
|
|
174
|
-
});
|
|
175
|
-
} else if (debug && !resolved) {
|
|
176
|
-
console.info(`[OtelExporter:debug] Provider "${providerName}" resolution returned null (disabled)`);
|
|
177
|
-
}
|
|
178
|
-
return resolved;
|
|
157
|
+
const providerName = Object.keys(config)[0];
|
|
158
|
+
if (debug) console.info(`[OtelExporter:debug] Resolving provider config for: ${providerName}`);
|
|
159
|
+
let resolved;
|
|
160
|
+
if ("dash0" in config) resolved = resolveDash0Config(config.dash0);
|
|
161
|
+
else if ("signoz" in config) resolved = resolveSignozConfig(config.signoz);
|
|
162
|
+
else if ("newrelic" in config) resolved = resolveNewRelicConfig(config.newrelic);
|
|
163
|
+
else if ("traceloop" in config) resolved = resolveTraceloopConfig(config.traceloop);
|
|
164
|
+
else if ("laminar" in config) resolved = resolveLaminarConfig(config.laminar);
|
|
165
|
+
else if ("custom" in config) resolved = resolveCustomConfig(config.custom);
|
|
166
|
+
else return config;
|
|
167
|
+
if (debug && resolved) {
|
|
168
|
+
const maskedHeaders = {};
|
|
169
|
+
for (const [key, value] of Object.entries(resolved.headers)) maskedHeaders[key] = value ? "[REDACTED]" : "[EMPTY]";
|
|
170
|
+
console.info(`[OtelExporter:debug] Provider "${providerName}" resolved:`, {
|
|
171
|
+
endpoint: resolved.endpoint,
|
|
172
|
+
protocol: resolved.protocol,
|
|
173
|
+
headers: maskedHeaders
|
|
174
|
+
});
|
|
175
|
+
} else if (debug && !resolved) console.info(`[OtelExporter:debug] Provider "${providerName}" resolution returned null (disabled)`);
|
|
176
|
+
return resolved;
|
|
179
177
|
}
|
|
180
178
|
function resolveDash0Config(config) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
authorization: `Bearer ${apiKey}`
|
|
202
|
-
// lowercase for gRPC metadata
|
|
203
|
-
};
|
|
204
|
-
if (dataset) {
|
|
205
|
-
headers["dash0-dataset"] = dataset;
|
|
206
|
-
}
|
|
207
|
-
return {
|
|
208
|
-
endpoint,
|
|
209
|
-
headers,
|
|
210
|
-
protocol: "grpc"
|
|
211
|
-
// Use gRPC for Dash0
|
|
212
|
-
};
|
|
179
|
+
const apiKey = config.apiKey ?? process.env.DASH0_API_KEY;
|
|
180
|
+
const configEndpoint = config.endpoint ?? process.env.DASH0_ENDPOINT;
|
|
181
|
+
const dataset = config.dataset ?? process.env.DASH0_DATASET;
|
|
182
|
+
if (!apiKey) {
|
|
183
|
+
console.error("[OtelExporter] Dash0 configuration requires apiKey. Set DASH0_API_KEY environment variable or pass it in config. Tracing will be disabled.");
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
if (!configEndpoint) {
|
|
187
|
+
console.error("[OtelExporter] Dash0 configuration requires endpoint. Set DASH0_ENDPOINT environment variable or pass it in config. Tracing will be disabled.");
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
let endpoint = configEndpoint;
|
|
191
|
+
if (!endpoint.includes("/v1/traces")) endpoint = `${endpoint}/v1/traces`;
|
|
192
|
+
const headers = { authorization: `Bearer ${apiKey}` };
|
|
193
|
+
if (dataset) headers["dash0-dataset"] = dataset;
|
|
194
|
+
return {
|
|
195
|
+
endpoint,
|
|
196
|
+
headers,
|
|
197
|
+
protocol: "grpc"
|
|
198
|
+
};
|
|
213
199
|
}
|
|
214
200
|
function resolveSignozConfig(config) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
headers: {
|
|
228
|
-
"signoz-ingestion-key": apiKey
|
|
229
|
-
},
|
|
230
|
-
protocol: "http/protobuf"
|
|
231
|
-
};
|
|
201
|
+
const apiKey = config.apiKey ?? process.env.SIGNOZ_API_KEY;
|
|
202
|
+
const region = config.region ?? process.env.SIGNOZ_REGION;
|
|
203
|
+
const configEndpoint = config.endpoint ?? process.env.SIGNOZ_ENDPOINT;
|
|
204
|
+
if (!apiKey) {
|
|
205
|
+
console.error("[OtelExporter] SigNoz configuration requires apiKey. Set SIGNOZ_API_KEY environment variable or pass it in config. Tracing will be disabled.");
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
endpoint: configEndpoint || `https://ingest.${region || "us"}.signoz.cloud:443/v1/traces`,
|
|
210
|
+
headers: { "signoz-ingestion-key": apiKey },
|
|
211
|
+
protocol: "http/protobuf"
|
|
212
|
+
};
|
|
232
213
|
}
|
|
233
214
|
function resolveNewRelicConfig(config) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
headers: {
|
|
246
|
-
"api-key": apiKey
|
|
247
|
-
},
|
|
248
|
-
protocol: "http/protobuf"
|
|
249
|
-
};
|
|
215
|
+
const apiKey = config.apiKey ?? process.env.NEW_RELIC_LICENSE_KEY;
|
|
216
|
+
const configEndpoint = config.endpoint ?? process.env.NEW_RELIC_ENDPOINT;
|
|
217
|
+
if (!apiKey) {
|
|
218
|
+
console.error("[OtelExporter] New Relic configuration requires apiKey (license key). Set NEW_RELIC_LICENSE_KEY environment variable or pass it in config. Tracing will be disabled.");
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
endpoint: configEndpoint || "https://otlp.nr-data.net:443/v1/traces",
|
|
223
|
+
headers: { "api-key": apiKey },
|
|
224
|
+
protocol: "http/protobuf"
|
|
225
|
+
};
|
|
250
226
|
}
|
|
251
227
|
function resolveTraceloopConfig(config) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
return {
|
|
269
|
-
endpoint,
|
|
270
|
-
headers,
|
|
271
|
-
protocol: "http/json"
|
|
272
|
-
};
|
|
228
|
+
const apiKey = config.apiKey ?? process.env.TRACELOOP_API_KEY;
|
|
229
|
+
const destinationId = config.destinationId ?? process.env.TRACELOOP_DESTINATION_ID;
|
|
230
|
+
const configEndpoint = config.endpoint ?? process.env.TRACELOOP_ENDPOINT;
|
|
231
|
+
if (!apiKey) {
|
|
232
|
+
console.error("[OtelExporter] Traceloop configuration requires apiKey. Set TRACELOOP_API_KEY environment variable or pass it in config. Tracing will be disabled.");
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
const endpoint = configEndpoint || "https://api.traceloop.com/v1/traces";
|
|
236
|
+
const headers = { Authorization: `Bearer ${apiKey}` };
|
|
237
|
+
if (destinationId) headers["x-traceloop-destination-id"] = destinationId;
|
|
238
|
+
return {
|
|
239
|
+
endpoint,
|
|
240
|
+
headers,
|
|
241
|
+
protocol: "http/json"
|
|
242
|
+
};
|
|
273
243
|
}
|
|
274
244
|
function resolveLaminarConfig(config) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
};
|
|
287
|
-
return {
|
|
288
|
-
endpoint,
|
|
289
|
-
headers,
|
|
290
|
-
protocol: "http/protobuf"
|
|
291
|
-
// Use HTTP/protobuf instead of gRPC for better compatibility
|
|
292
|
-
};
|
|
245
|
+
const apiKey = config.apiKey ?? process.env.LMNR_PROJECT_API_KEY;
|
|
246
|
+
const configEndpoint = config.endpoint ?? process.env.LAMINAR_ENDPOINT;
|
|
247
|
+
if (!apiKey) {
|
|
248
|
+
console.error("[OtelExporter] Laminar configuration requires apiKey. Set LMNR_PROJECT_API_KEY environment variable or pass it in config. Tracing will be disabled.");
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
endpoint: configEndpoint || "https://api.lmnr.ai/v1/traces",
|
|
253
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
254
|
+
protocol: "http/protobuf"
|
|
255
|
+
};
|
|
293
256
|
}
|
|
294
257
|
function resolveCustomConfig(config) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
258
|
+
if (!config.endpoint) {
|
|
259
|
+
console.error("[OtelExporter] Custom configuration requires endpoint. Tracing will be disabled.");
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
endpoint: config.endpoint,
|
|
264
|
+
headers: config.headers || {},
|
|
265
|
+
protocol: config.protocol || "http/json"
|
|
266
|
+
};
|
|
304
267
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
268
|
+
//#endregion
|
|
269
|
+
//#region src/gen-ai-messages.ts
|
|
270
|
+
const isMastraMessagePart = (p) => {
|
|
271
|
+
return typeof p === "object" && p != null && "type" in p && (p.type === "text" || p.type === "tool-call" || p.type === "tool-result") && (p.type === "text" && "text" in p || p.type === "tool-call" && "toolCallId" in p && "toolName" in p && "input" in p || p.type === "tool-result" && "toolCallId" in p && "toolName" in p && "output" in p);
|
|
309
272
|
};
|
|
310
|
-
|
|
311
|
-
|
|
273
|
+
const isMastraMessage = (m) => {
|
|
274
|
+
return typeof m === "object" && m != null && "role" in m && "content" in m && (typeof m.content === "string" || Array.isArray(m.content) && m.content.every(isMastraMessagePart));
|
|
312
275
|
};
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
276
|
+
/**
|
|
277
|
+
* Convert an Input/Output string from a MastraSpan into a jsonified string that adheres to
|
|
278
|
+
* OpenTelemetry gen_ai.input.messages and gen_ai.output.messages schema.
|
|
279
|
+
* If parsing fails at any step, the original inputOutputString is returned unmodified.
|
|
280
|
+
*
|
|
281
|
+
* This conversion is best effort; It assumes a consistent shape for mastra messages, and converts
|
|
282
|
+
* into the gen_ai input and output schemas as of October 20th, 2025.
|
|
283
|
+
*
|
|
284
|
+
* @see https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/#gen-ai-input-messages
|
|
285
|
+
* @see https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-input-messages.json
|
|
286
|
+
* @see https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/#gen-ai-output-messages
|
|
287
|
+
* @see https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-output-messages.json
|
|
288
|
+
*
|
|
289
|
+
* @param inputOutputString a jsonified string that contains messages adhering to what appears to be
|
|
290
|
+
* Mastra's message shape.
|
|
291
|
+
* @returns a jsonified string that contains messages adhering to the OpenTelemetry gen_ai.input.messages and gen_ai.output.messages schema.
|
|
292
|
+
* If parsing fails at any step, the original inputOutputString is returned unmodified.
|
|
293
|
+
*/
|
|
294
|
+
const convertMastraMessagesToGenAIMessages = (inputOutputString) => {
|
|
295
|
+
try {
|
|
296
|
+
const parsedIO = JSON.parse(inputOutputString);
|
|
297
|
+
if (typeof parsedIO !== "object" || parsedIO == null || !("messages" in parsedIO) && !("text" in parsedIO)) return inputOutputString;
|
|
298
|
+
if ("text" in parsedIO) return JSON.stringify([{
|
|
299
|
+
role: "assistant",
|
|
300
|
+
parts: [{
|
|
301
|
+
type: "text",
|
|
302
|
+
content: parsedIO.text
|
|
303
|
+
}]
|
|
304
|
+
}]);
|
|
305
|
+
if (Array.isArray(parsedIO.messages)) return JSON.stringify(parsedIO.messages.map((m) => {
|
|
306
|
+
if (!isMastraMessage(m)) return m;
|
|
307
|
+
const role = m.role;
|
|
308
|
+
let parts = [];
|
|
309
|
+
if (Array.isArray(m.content)) parts = m.content.map((c) => {
|
|
310
|
+
switch (c.type) {
|
|
311
|
+
case "text": return {
|
|
312
|
+
type: "text",
|
|
313
|
+
content: c.text
|
|
314
|
+
};
|
|
315
|
+
case "tool-call": return {
|
|
316
|
+
type: "tool_call",
|
|
317
|
+
id: c.toolCallId,
|
|
318
|
+
name: c.toolName,
|
|
319
|
+
arguments: JSON.stringify(c.input)
|
|
320
|
+
};
|
|
321
|
+
case "tool-result": return {
|
|
322
|
+
type: "tool_call_response",
|
|
323
|
+
id: c.toolCallId,
|
|
324
|
+
name: c.toolName,
|
|
325
|
+
response: JSON.stringify(c.output.value)
|
|
326
|
+
};
|
|
327
|
+
default: return c;
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
else parts = [{
|
|
331
|
+
type: "text",
|
|
332
|
+
content: m.content
|
|
333
|
+
}];
|
|
334
|
+
return {
|
|
335
|
+
role,
|
|
336
|
+
parts
|
|
337
|
+
};
|
|
338
|
+
}));
|
|
339
|
+
return inputOutputString;
|
|
340
|
+
} catch {
|
|
341
|
+
return inputOutputString;
|
|
342
|
+
}
|
|
380
343
|
};
|
|
381
|
-
|
|
382
|
-
|
|
344
|
+
//#endregion
|
|
345
|
+
//#region src/gen-ai-semantics.ts
|
|
346
|
+
/**
|
|
347
|
+
* Utilities for converting Mastra Spans to OTel Spans
|
|
348
|
+
* with Semantic conventions for generative AI systems
|
|
349
|
+
* @see https://github.com/open-telemetry/semantic-conventions/blob/v1.38.0/docs/gen-ai/README.md
|
|
350
|
+
* @see https://github.com/open-telemetry/semantic-conventions/blob/v1.38.0/docs/gen-ai/gen-ai-events.md
|
|
351
|
+
* @see https://github.com/open-telemetry/semantic-conventions/blob/v1.38.0/docs/gen-ai/gen-ai-spans.md
|
|
352
|
+
* @see https://github.com/open-telemetry/semantic-conventions/blob/v1.38.0/docs/gen-ai/gen-ai-agent-spans.md
|
|
353
|
+
* @see https://opentelemetry.io/docs/specs/semconv/gen-ai/non-normative/examples-llm-calls/
|
|
354
|
+
* @see https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/
|
|
355
|
+
*/
|
|
356
|
+
/**
|
|
357
|
+
* Formats UsageStats to OTel GenAI semantic convention attributes.
|
|
358
|
+
*/
|
|
383
359
|
function formatUsageMetrics(usage) {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
}
|
|
395
|
-
if (usage.inputDetails?.cacheRead !== void 0) {
|
|
396
|
-
metrics[ATTR_GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS] = usage.inputDetails.cacheRead;
|
|
397
|
-
}
|
|
398
|
-
if (usage.inputDetails?.cacheWrite !== void 0) {
|
|
399
|
-
metrics[ATTR_GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS] = usage.inputDetails.cacheWrite;
|
|
400
|
-
}
|
|
401
|
-
if (usage.inputDetails?.audio !== void 0) {
|
|
402
|
-
metrics["gen_ai.usage.audio_input_tokens"] = usage.inputDetails.audio;
|
|
403
|
-
}
|
|
404
|
-
if (usage.outputDetails?.audio !== void 0) {
|
|
405
|
-
metrics["gen_ai.usage.audio_output_tokens"] = usage.outputDetails.audio;
|
|
406
|
-
}
|
|
407
|
-
return metrics;
|
|
360
|
+
if (!usage) return {};
|
|
361
|
+
const metrics = {};
|
|
362
|
+
if (usage.inputTokens !== void 0) metrics[ATTR_GEN_AI_USAGE_INPUT_TOKENS] = usage.inputTokens;
|
|
363
|
+
if (usage.outputTokens !== void 0) metrics[ATTR_GEN_AI_USAGE_OUTPUT_TOKENS] = usage.outputTokens;
|
|
364
|
+
if (usage.outputDetails?.reasoning !== void 0) metrics["gen_ai.usage.reasoning_tokens"] = usage.outputDetails.reasoning;
|
|
365
|
+
if (usage.inputDetails?.cacheRead !== void 0) metrics[ATTR_GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS] = usage.inputDetails.cacheRead;
|
|
366
|
+
if (usage.inputDetails?.cacheWrite !== void 0) metrics[ATTR_GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS] = usage.inputDetails.cacheWrite;
|
|
367
|
+
if (usage.inputDetails?.audio !== void 0) metrics["gen_ai.usage.audio_input_tokens"] = usage.inputDetails.audio;
|
|
368
|
+
if (usage.outputDetails?.audio !== void 0) metrics["gen_ai.usage.audio_output_tokens"] = usage.outputDetails.audio;
|
|
369
|
+
return metrics;
|
|
408
370
|
}
|
|
409
371
|
function addModelRequestAttributes(attributes, attrs) {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
if (attrs.provider) {
|
|
414
|
-
attributes[ATTR_GEN_AI_PROVIDER_NAME] = normalizeProvider(attrs.provider);
|
|
415
|
-
}
|
|
416
|
-
Object.assign(attributes, formatUsageMetrics(attrs.usage));
|
|
372
|
+
if (attrs.model) attributes[ATTR_GEN_AI_REQUEST_MODEL] = attrs.model;
|
|
373
|
+
if (attrs.provider) attributes[ATTR_GEN_AI_PROVIDER_NAME] = normalizeProvider(attrs.provider);
|
|
374
|
+
Object.assign(attributes, formatUsageMetrics(attrs.usage));
|
|
417
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* Get the operation name based on span type for gen_ai.operation.name
|
|
378
|
+
*/
|
|
418
379
|
function getOperationName(span) {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
return "invoke_agent";
|
|
430
|
-
case SpanType.WORKFLOW_RUN:
|
|
431
|
-
return "invoke_workflow";
|
|
432
|
-
default:
|
|
433
|
-
return span.type.toLowerCase();
|
|
434
|
-
}
|
|
380
|
+
switch (span.type) {
|
|
381
|
+
case SpanType.MODEL_GENERATION: return "chat";
|
|
382
|
+
case SpanType.RAG_EMBEDDING: return "embeddings";
|
|
383
|
+
case SpanType.TOOL_CALL:
|
|
384
|
+
case SpanType.MCP_TOOL_CALL:
|
|
385
|
+
case SpanType.PROVIDER_TOOL_CALL: return "execute_tool";
|
|
386
|
+
case SpanType.AGENT_RUN: return "invoke_agent";
|
|
387
|
+
case SpanType.WORKFLOW_RUN: return "invoke_workflow";
|
|
388
|
+
default: return span.type.toLowerCase();
|
|
389
|
+
}
|
|
435
390
|
}
|
|
391
|
+
/**
|
|
392
|
+
* Keep only unicode letters, numbers, dot, underscore, space, dash.
|
|
393
|
+
*/
|
|
436
394
|
function sanitizeSpanName(name) {
|
|
437
|
-
|
|
395
|
+
return name.replace(/[^\p{L}\p{N}._ -]/gu, "");
|
|
438
396
|
}
|
|
439
397
|
function getSpanIdentifier(span) {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
case SpanType.RAG_EMBEDDING: {
|
|
446
|
-
const attrs = span.attributes;
|
|
447
|
-
return attrs?.model;
|
|
448
|
-
}
|
|
449
|
-
default:
|
|
450
|
-
return span.entityName ?? span.entityId;
|
|
451
|
-
}
|
|
398
|
+
switch (span.type) {
|
|
399
|
+
case SpanType.MODEL_GENERATION: return span.attributes?.model;
|
|
400
|
+
case SpanType.RAG_EMBEDDING: return span.attributes?.model;
|
|
401
|
+
default: return span.entityName ?? span.entityId;
|
|
402
|
+
}
|
|
452
403
|
}
|
|
404
|
+
/**
|
|
405
|
+
* Get an OTEL-compliant span name based on span type and attributes
|
|
406
|
+
*/
|
|
453
407
|
function getSpanName(span) {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
return `${operation} ${identifier}`;
|
|
458
|
-
}
|
|
459
|
-
return sanitizeSpanName(span.name);
|
|
408
|
+
const identifier = getSpanIdentifier(span);
|
|
409
|
+
if (identifier) return `${getOperationName(span)} ${identifier}`;
|
|
410
|
+
return sanitizeSpanName(span.name);
|
|
460
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* Gets OpenTelemetry attributes from Mastra Span
|
|
414
|
+
* Following OTEL Semantic Conventions for GenAI
|
|
415
|
+
*/
|
|
461
416
|
function getAttributes(span) {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
}
|
|
540
|
-
if (span.type === SpanType.RAG_EMBEDDING && span.attributes) {
|
|
541
|
-
const embeddingAttrs = span.attributes;
|
|
542
|
-
addModelRequestAttributes(attributes, embeddingAttrs);
|
|
543
|
-
if (embeddingAttrs.mode) {
|
|
544
|
-
attributes[`mastra.${spanType}.mode`] = embeddingAttrs.mode;
|
|
545
|
-
}
|
|
546
|
-
if (embeddingAttrs.dimensions !== void 0) {
|
|
547
|
-
attributes["gen_ai.embeddings.dimension.count"] = embeddingAttrs.dimensions;
|
|
548
|
-
attributes[`mastra.${spanType}.dimensions`] = embeddingAttrs.dimensions;
|
|
549
|
-
}
|
|
550
|
-
if (embeddingAttrs.inputCount !== void 0) {
|
|
551
|
-
attributes[`mastra.${spanType}.input_count`] = embeddingAttrs.inputCount;
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
if ((span.type === SpanType.TOOL_CALL || span.type === SpanType.MCP_TOOL_CALL || span.type === SpanType.PROVIDER_TOOL_CALL) && span.attributes) {
|
|
555
|
-
attributes[ATTR_GEN_AI_TOOL_NAME] = span.entityName ?? span.entityId;
|
|
556
|
-
if (span.type === SpanType.MCP_TOOL_CALL) {
|
|
557
|
-
const mcpAttrs = span.attributes;
|
|
558
|
-
if (mcpAttrs.mcpServer) {
|
|
559
|
-
attributes[ATTR_SERVER_ADDRESS] = mcpAttrs.mcpServer;
|
|
560
|
-
}
|
|
561
|
-
} else {
|
|
562
|
-
const toolAttrs = span.attributes;
|
|
563
|
-
if (toolAttrs.toolDescription) {
|
|
564
|
-
attributes[ATTR_GEN_AI_TOOL_DESCRIPTION] = toolAttrs.toolDescription;
|
|
565
|
-
}
|
|
566
|
-
if (toolAttrs.toolType) {
|
|
567
|
-
attributes["gen_ai.tool.type"] = toolAttrs.toolType;
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
if (span.type === SpanType.AGENT_RUN && span.attributes) {
|
|
572
|
-
const agentAttrs = span.attributes;
|
|
573
|
-
if (span.entityId) {
|
|
574
|
-
attributes[ATTR_GEN_AI_AGENT_ID] = span.entityId;
|
|
575
|
-
}
|
|
576
|
-
if (span.entityName) {
|
|
577
|
-
attributes[ATTR_GEN_AI_AGENT_NAME] = span.entityName;
|
|
578
|
-
}
|
|
579
|
-
if (agentAttrs.conversationId) {
|
|
580
|
-
attributes[ATTR_GEN_AI_CONVERSATION_ID] = agentAttrs.conversationId;
|
|
581
|
-
}
|
|
582
|
-
if (agentAttrs.maxSteps) {
|
|
583
|
-
attributes[`mastra.${spanType}.max_steps`] = agentAttrs.maxSteps;
|
|
584
|
-
}
|
|
585
|
-
if (agentAttrs.availableTools) {
|
|
586
|
-
attributes[`gen_ai.tool.definitions`] = JSON.stringify(agentAttrs.availableTools);
|
|
587
|
-
}
|
|
588
|
-
attributes[ATTR_GEN_AI_SYSTEM_INSTRUCTIONS] = agentAttrs.instructions;
|
|
589
|
-
}
|
|
590
|
-
if (span.errorInfo) {
|
|
591
|
-
attributes[ATTR_ERROR_TYPE] = span.errorInfo.id || "unknown";
|
|
592
|
-
attributes[ATTR_ERROR_MESSAGE] = span.errorInfo.message;
|
|
593
|
-
if (span.errorInfo.domain) {
|
|
594
|
-
attributes["error.domain"] = span.errorInfo.domain;
|
|
595
|
-
}
|
|
596
|
-
if (span.errorInfo.category) {
|
|
597
|
-
attributes["error.category"] = span.errorInfo.category;
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
const threadId = span.metadata?.threadId;
|
|
601
|
-
if (typeof threadId === "string" && threadId.length > 0) {
|
|
602
|
-
attributes[ATTR_GEN_AI_CONVERSATION_ID] = threadId;
|
|
603
|
-
}
|
|
604
|
-
return attributes;
|
|
417
|
+
const attributes = {};
|
|
418
|
+
const spanType = span.type.toLowerCase();
|
|
419
|
+
attributes[ATTR_GEN_AI_OPERATION_NAME] = getOperationName(span);
|
|
420
|
+
attributes["mastra.span.type"] = span.type;
|
|
421
|
+
if (span.input !== void 0) {
|
|
422
|
+
const inputStr = typeof span.input === "string" ? span.input : JSON.stringify(span.input);
|
|
423
|
+
if (span.type === SpanType.MODEL_GENERATION) attributes[ATTR_GEN_AI_INPUT_MESSAGES] = convertMastraMessagesToGenAIMessages(inputStr);
|
|
424
|
+
else if (span.type === SpanType.TOOL_CALL || span.type === SpanType.MCP_TOOL_CALL || span.type === SpanType.PROVIDER_TOOL_CALL) attributes["gen_ai.tool.call.arguments"] = inputStr;
|
|
425
|
+
else attributes[`mastra.${spanType}.input`] = inputStr;
|
|
426
|
+
}
|
|
427
|
+
if (span.output !== void 0) {
|
|
428
|
+
const outputStr = typeof span.output === "string" ? span.output : JSON.stringify(span.output);
|
|
429
|
+
if (span.type === SpanType.MODEL_GENERATION) attributes[ATTR_GEN_AI_OUTPUT_MESSAGES] = convertMastraMessagesToGenAIMessages(outputStr);
|
|
430
|
+
else if (span.type === SpanType.TOOL_CALL || span.type === SpanType.MCP_TOOL_CALL || span.type === SpanType.PROVIDER_TOOL_CALL) attributes["gen_ai.tool.call.result"] = outputStr;
|
|
431
|
+
else attributes[`mastra.${spanType}.output`] = outputStr;
|
|
432
|
+
}
|
|
433
|
+
if (span.type === SpanType.MODEL_GENERATION && span.attributes) {
|
|
434
|
+
const modelAttrs = span.attributes;
|
|
435
|
+
addModelRequestAttributes(attributes, modelAttrs);
|
|
436
|
+
if (span.entityId) attributes[ATTR_GEN_AI_AGENT_ID] = span.entityId;
|
|
437
|
+
if (span.entityName) attributes[ATTR_GEN_AI_AGENT_NAME] = span.entityName;
|
|
438
|
+
if (modelAttrs.parameters) {
|
|
439
|
+
if (modelAttrs.parameters.temperature !== void 0) attributes[ATTR_GEN_AI_REQUEST_TEMPERATURE] = modelAttrs.parameters.temperature;
|
|
440
|
+
if (modelAttrs.parameters.maxOutputTokens !== void 0) attributes[ATTR_GEN_AI_REQUEST_MAX_TOKENS] = modelAttrs.parameters.maxOutputTokens;
|
|
441
|
+
if (modelAttrs.parameters.topP !== void 0) attributes[ATTR_GEN_AI_REQUEST_TOP_P] = modelAttrs.parameters.topP;
|
|
442
|
+
if (modelAttrs.parameters.topK !== void 0) attributes[ATTR_GEN_AI_REQUEST_TOP_K] = modelAttrs.parameters.topK;
|
|
443
|
+
if (modelAttrs.parameters.presencePenalty !== void 0) attributes[ATTR_GEN_AI_REQUEST_PRESENCE_PENALTY] = modelAttrs.parameters.presencePenalty;
|
|
444
|
+
if (modelAttrs.parameters.frequencyPenalty !== void 0) attributes[ATTR_GEN_AI_REQUEST_FREQUENCY_PENALTY] = modelAttrs.parameters.frequencyPenalty;
|
|
445
|
+
if (modelAttrs.parameters.stopSequences) attributes[ATTR_GEN_AI_REQUEST_STOP_SEQUENCES] = JSON.stringify(modelAttrs.parameters.stopSequences);
|
|
446
|
+
if (modelAttrs.parameters.seed) attributes[ATTR_GEN_AI_REQUEST_SEED] = modelAttrs.parameters.seed;
|
|
447
|
+
}
|
|
448
|
+
if (modelAttrs.completionStartTime) attributes["mastra.completion_start_time"] = modelAttrs.completionStartTime.toISOString();
|
|
449
|
+
if (modelAttrs.finishReason) attributes[ATTR_GEN_AI_RESPONSE_FINISH_REASONS] = JSON.stringify([modelAttrs.finishReason]);
|
|
450
|
+
if (modelAttrs.responseModel) attributes[ATTR_GEN_AI_RESPONSE_MODEL] = modelAttrs.responseModel;
|
|
451
|
+
if (modelAttrs.responseId) attributes[ATTR_GEN_AI_RESPONSE_ID] = modelAttrs.responseId;
|
|
452
|
+
if (modelAttrs.serverAddress) attributes[ATTR_SERVER_ADDRESS] = modelAttrs.serverAddress;
|
|
453
|
+
if (modelAttrs.serverPort !== void 0) attributes[ATTR_SERVER_PORT] = modelAttrs.serverPort;
|
|
454
|
+
}
|
|
455
|
+
if (span.type === SpanType.RAG_EMBEDDING && span.attributes) {
|
|
456
|
+
const embeddingAttrs = span.attributes;
|
|
457
|
+
addModelRequestAttributes(attributes, embeddingAttrs);
|
|
458
|
+
if (embeddingAttrs.mode) attributes[`mastra.${spanType}.mode`] = embeddingAttrs.mode;
|
|
459
|
+
if (embeddingAttrs.dimensions !== void 0) {
|
|
460
|
+
attributes["gen_ai.embeddings.dimension.count"] = embeddingAttrs.dimensions;
|
|
461
|
+
attributes[`mastra.${spanType}.dimensions`] = embeddingAttrs.dimensions;
|
|
462
|
+
}
|
|
463
|
+
if (embeddingAttrs.inputCount !== void 0) attributes[`mastra.${spanType}.input_count`] = embeddingAttrs.inputCount;
|
|
464
|
+
}
|
|
465
|
+
if ((span.type === SpanType.TOOL_CALL || span.type === SpanType.MCP_TOOL_CALL || span.type === SpanType.PROVIDER_TOOL_CALL) && span.attributes) {
|
|
466
|
+
attributes[ATTR_GEN_AI_TOOL_NAME] = span.entityName ?? span.entityId;
|
|
467
|
+
if (span.type === SpanType.MCP_TOOL_CALL) {
|
|
468
|
+
const mcpAttrs = span.attributes;
|
|
469
|
+
if (mcpAttrs.mcpServer) attributes[ATTR_SERVER_ADDRESS] = mcpAttrs.mcpServer;
|
|
470
|
+
} else {
|
|
471
|
+
const toolAttrs = span.attributes;
|
|
472
|
+
if (toolAttrs.toolDescription) attributes[ATTR_GEN_AI_TOOL_DESCRIPTION] = toolAttrs.toolDescription;
|
|
473
|
+
if (toolAttrs.toolType) attributes["gen_ai.tool.type"] = toolAttrs.toolType;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (span.type === SpanType.AGENT_RUN && span.attributes) {
|
|
477
|
+
const agentAttrs = span.attributes;
|
|
478
|
+
if (span.entityId) attributes[ATTR_GEN_AI_AGENT_ID] = span.entityId;
|
|
479
|
+
if (span.entityName) attributes[ATTR_GEN_AI_AGENT_NAME] = span.entityName;
|
|
480
|
+
if (agentAttrs.conversationId) attributes[ATTR_GEN_AI_CONVERSATION_ID] = agentAttrs.conversationId;
|
|
481
|
+
if (agentAttrs.maxSteps) attributes[`mastra.${spanType}.max_steps`] = agentAttrs.maxSteps;
|
|
482
|
+
if (agentAttrs.availableTools) attributes[`gen_ai.tool.definitions`] = JSON.stringify(agentAttrs.availableTools);
|
|
483
|
+
attributes[ATTR_GEN_AI_SYSTEM_INSTRUCTIONS] = agentAttrs.instructions;
|
|
484
|
+
}
|
|
485
|
+
if (span.errorInfo) {
|
|
486
|
+
attributes[ATTR_ERROR_TYPE] = span.errorInfo.id || "unknown";
|
|
487
|
+
attributes[ATTR_ERROR_MESSAGE] = span.errorInfo.message;
|
|
488
|
+
if (span.errorInfo.domain) attributes["error.domain"] = span.errorInfo.domain;
|
|
489
|
+
if (span.errorInfo.category) attributes["error.category"] = span.errorInfo.category;
|
|
490
|
+
}
|
|
491
|
+
const threadId = span.metadata?.threadId;
|
|
492
|
+
if (typeof threadId === "string" && threadId.length > 0) attributes[ATTR_GEN_AI_CONVERSATION_ID] = threadId;
|
|
493
|
+
return attributes;
|
|
605
494
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
495
|
+
/**
|
|
496
|
+
* Canonical OTel provider keys mapped to a list of possible fuzzy aliases.
|
|
497
|
+
*/
|
|
498
|
+
const PROVIDER_ALIASES = {
|
|
499
|
+
anthropic: ["anthropic", "claude"],
|
|
500
|
+
"aws.bedrock": [
|
|
501
|
+
"awsbedrock",
|
|
502
|
+
"bedrock",
|
|
503
|
+
"amazonbedrock"
|
|
504
|
+
],
|
|
505
|
+
"azure.ai.inference": ["azureaiinference", "azureinference"],
|
|
506
|
+
"azure.ai.openai": [
|
|
507
|
+
"azureaiopenai",
|
|
508
|
+
"azureopenai",
|
|
509
|
+
"msopenai",
|
|
510
|
+
"microsoftopenai"
|
|
511
|
+
],
|
|
512
|
+
cohere: ["cohere"],
|
|
513
|
+
deepseek: ["deepseek"],
|
|
514
|
+
"gcp.gemini": ["gcpgemini", "gemini"],
|
|
515
|
+
"gcp.gen_ai": [
|
|
516
|
+
"gcpgenai",
|
|
517
|
+
"googlegenai",
|
|
518
|
+
"googleai"
|
|
519
|
+
],
|
|
520
|
+
"gcp.vertex_ai": ["gcpvertexai", "vertexai"],
|
|
521
|
+
groq: ["groq"],
|
|
522
|
+
"ibm.watsonx.ai": [
|
|
523
|
+
"ibmwatsonxai",
|
|
524
|
+
"watsonx",
|
|
525
|
+
"watsonxai"
|
|
526
|
+
],
|
|
527
|
+
mistral_ai: ["mistral", "mistralai"],
|
|
528
|
+
openai: ["openai", "oai"],
|
|
529
|
+
perplexity: ["perplexity", "pplx"],
|
|
530
|
+
x_ai: [
|
|
531
|
+
"xai",
|
|
532
|
+
"x-ai",
|
|
533
|
+
"x_ai",
|
|
534
|
+
"x.com ai"
|
|
535
|
+
]
|
|
622
536
|
};
|
|
537
|
+
/**
|
|
538
|
+
* Normalize a provider input string into a matchable token.
|
|
539
|
+
* Keep only alphanumerics and lowercase the result.
|
|
540
|
+
*/
|
|
623
541
|
function normalizeProviderString(input) {
|
|
624
|
-
|
|
542
|
+
return input.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
625
543
|
}
|
|
544
|
+
/**
|
|
545
|
+
* Attempts to map a providerName to one of the canonical OTel provider names.
|
|
546
|
+
* If no match is found, returns the original providerName unchanged.
|
|
547
|
+
*/
|
|
626
548
|
function normalizeProvider(providerName) {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
if (normalized === alias) {
|
|
631
|
-
return canonical;
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
return providerName.toLowerCase();
|
|
549
|
+
const normalized = normalizeProviderString(providerName);
|
|
550
|
+
for (const [canonical, aliases] of Object.entries(PROVIDER_ALIASES)) for (const alias of aliases) if (normalized === alias) return canonical;
|
|
551
|
+
return providerName.toLowerCase();
|
|
636
552
|
}
|
|
637
|
-
|
|
638
|
-
|
|
553
|
+
//#endregion
|
|
554
|
+
//#region src/span-converter.ts
|
|
555
|
+
/**
|
|
556
|
+
* Convert Mastra Spans to OpenTelemetry spans
|
|
557
|
+
*/
|
|
639
558
|
var SpanConverter = class {
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
spanContext: () => spanContext,
|
|
724
|
-
parentSpanContext,
|
|
725
|
-
startTime,
|
|
726
|
-
endTime,
|
|
727
|
-
status,
|
|
728
|
-
attributes,
|
|
729
|
-
links,
|
|
730
|
-
events,
|
|
731
|
-
duration,
|
|
732
|
-
ended: !!span.endTime,
|
|
733
|
-
resource: this.resource,
|
|
734
|
-
instrumentationScope: this.scope,
|
|
735
|
-
droppedAttributesCount: 0,
|
|
736
|
-
droppedEventsCount: 0,
|
|
737
|
-
droppedLinksCount: 0
|
|
738
|
-
};
|
|
739
|
-
return readable;
|
|
740
|
-
}
|
|
559
|
+
params;
|
|
560
|
+
resource;
|
|
561
|
+
scope;
|
|
562
|
+
initPromise;
|
|
563
|
+
format;
|
|
564
|
+
constructor(params) {
|
|
565
|
+
this.params = params;
|
|
566
|
+
this.format = params.format;
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Lazily initialize resource & scope on first use.
|
|
570
|
+
* Subsequent calls reuse the same promise (no races).
|
|
571
|
+
*/
|
|
572
|
+
async initIfNeeded() {
|
|
573
|
+
if (this.initPromise) return this.initPromise;
|
|
574
|
+
this.initPromise = (async () => {
|
|
575
|
+
const packageVersion = await getPackageVersion(this.params.packageName) ?? "unknown";
|
|
576
|
+
const serviceVersion = await getPackageVersion("@mastra/core") ?? "unknown";
|
|
577
|
+
let resource = resourceFromAttributes({
|
|
578
|
+
[ATTR_SERVICE_NAME]: this.params.serviceName || "mastra-service",
|
|
579
|
+
[ATTR_SERVICE_VERSION]: serviceVersion,
|
|
580
|
+
[ATTR_TELEMETRY_SDK_NAME]: this.params.packageName,
|
|
581
|
+
[ATTR_TELEMETRY_SDK_VERSION]: packageVersion,
|
|
582
|
+
[ATTR_TELEMETRY_SDK_LANGUAGE]: "nodejs"
|
|
583
|
+
});
|
|
584
|
+
if (this.params.config?.resourceAttributes) resource = resource.merge(resourceFromAttributes(this.params.config.resourceAttributes));
|
|
585
|
+
this.resource = resource;
|
|
586
|
+
this.scope = {
|
|
587
|
+
name: this.params.packageName,
|
|
588
|
+
version: packageVersion
|
|
589
|
+
};
|
|
590
|
+
})();
|
|
591
|
+
return this.initPromise;
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Convert a Mastra Span to an OpenTelemetry ReadableSpan
|
|
595
|
+
*/
|
|
596
|
+
async convertSpan(span) {
|
|
597
|
+
await this.initIfNeeded();
|
|
598
|
+
if (!this.resource || !this.scope) throw new Error("SpanConverter not initialized correctly");
|
|
599
|
+
const name = getSpanName(span);
|
|
600
|
+
const kind = getSpanKind(span.type);
|
|
601
|
+
const attributes = getAttributes(span);
|
|
602
|
+
if (span.metadata) for (const [k, v] of Object.entries(span.metadata)) {
|
|
603
|
+
if (v === null || v === void 0) continue;
|
|
604
|
+
attributes[`mastra.metadata.${k}`] = typeof v === "object" ? JSON.stringify(v) : v;
|
|
605
|
+
}
|
|
606
|
+
if (span.isRootSpan && span.tags?.length) attributes["mastra.tags"] = JSON.stringify(span.tags);
|
|
607
|
+
const startTime = dateToHrTime(span.startTime);
|
|
608
|
+
const endTime = span.endTime ? dateToHrTime(span.endTime) : startTime;
|
|
609
|
+
const duration = computeDuration(span.startTime, span.endTime);
|
|
610
|
+
const { status, events } = buildStatusAndEvents(span, startTime);
|
|
611
|
+
const spanContext = {
|
|
612
|
+
traceId: span.traceId,
|
|
613
|
+
spanId: span.id,
|
|
614
|
+
traceFlags: TraceFlags.SAMPLED,
|
|
615
|
+
isRemote: false
|
|
616
|
+
};
|
|
617
|
+
return {
|
|
618
|
+
name,
|
|
619
|
+
kind,
|
|
620
|
+
spanContext: () => spanContext,
|
|
621
|
+
parentSpanContext: span.parentSpanId ? {
|
|
622
|
+
traceId: span.traceId,
|
|
623
|
+
spanId: span.parentSpanId,
|
|
624
|
+
traceFlags: TraceFlags.SAMPLED,
|
|
625
|
+
isRemote: false
|
|
626
|
+
} : void 0,
|
|
627
|
+
startTime,
|
|
628
|
+
endTime,
|
|
629
|
+
status,
|
|
630
|
+
attributes,
|
|
631
|
+
links: [],
|
|
632
|
+
events,
|
|
633
|
+
duration,
|
|
634
|
+
ended: !!span.endTime,
|
|
635
|
+
resource: this.resource,
|
|
636
|
+
instrumentationScope: this.scope,
|
|
637
|
+
droppedAttributesCount: 0,
|
|
638
|
+
droppedEventsCount: 0,
|
|
639
|
+
droppedLinksCount: 0
|
|
640
|
+
};
|
|
641
|
+
}
|
|
741
642
|
};
|
|
742
643
|
async function getPackageVersion(pkgName) {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
return void 0;
|
|
750
|
-
}
|
|
644
|
+
try {
|
|
645
|
+
const path = fileURLToPath(new URL(await import.meta.resolve(`${pkgName}/package.json`)));
|
|
646
|
+
return JSON.parse(readFileSync(path, "utf8")).version;
|
|
647
|
+
} catch {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
751
650
|
}
|
|
651
|
+
/**
|
|
652
|
+
* Get the appropriate Otel SpanKind based on Mastra SpanType.
|
|
653
|
+
*
|
|
654
|
+
* @param type - The Mastra span type
|
|
655
|
+
* @returns The appropriate OTEL SpanKind
|
|
656
|
+
*/
|
|
752
657
|
function getSpanKind(type) {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
return SpanKind.INTERNAL;
|
|
760
|
-
}
|
|
658
|
+
switch (type) {
|
|
659
|
+
case SpanType.MODEL_GENERATION:
|
|
660
|
+
case SpanType.RAG_EMBEDDING:
|
|
661
|
+
case SpanType.MCP_TOOL_CALL: return SpanKind.CLIENT;
|
|
662
|
+
default: return SpanKind.INTERNAL;
|
|
663
|
+
}
|
|
761
664
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
665
|
+
/**
|
|
666
|
+
* Convert JavaScript Date to hrtime format
|
|
667
|
+
*/
|
|
668
|
+
function dateToHrTime(date) {
|
|
669
|
+
const ms = date.getTime();
|
|
670
|
+
return [Math.floor(ms / 1e3), ms % 1e3 * 1e6];
|
|
767
671
|
}
|
|
768
672
|
function computeDuration(start, end) {
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
673
|
+
if (!end) return [0, 0];
|
|
674
|
+
const diffMs = end.getTime() - start.getTime();
|
|
675
|
+
return [Math.floor(diffMs / 1e3), diffMs % 1e3 * 1e6];
|
|
772
676
|
}
|
|
677
|
+
/**
|
|
678
|
+
* Build status + events from span.errorInfo (if present)
|
|
679
|
+
*/
|
|
773
680
|
function buildStatusAndEvents(span, defaultTime) {
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
};
|
|
681
|
+
const events = [];
|
|
682
|
+
if (span.errorInfo) {
|
|
683
|
+
const status = {
|
|
684
|
+
code: SpanStatusCode.ERROR,
|
|
685
|
+
message: span.errorInfo.message
|
|
686
|
+
};
|
|
687
|
+
events.push({
|
|
688
|
+
name: "exception",
|
|
689
|
+
attributes: {
|
|
690
|
+
"exception.message": span.errorInfo.message,
|
|
691
|
+
"exception.type": "Error",
|
|
692
|
+
...span.errorInfo.details?.stack && { "exception.stacktrace": span.errorInfo.details.stack }
|
|
693
|
+
},
|
|
694
|
+
time: defaultTime,
|
|
695
|
+
droppedAttributesCount: 0
|
|
696
|
+
});
|
|
697
|
+
return {
|
|
698
|
+
status,
|
|
699
|
+
events
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
if (span.endTime) return {
|
|
703
|
+
status: { code: SpanStatusCode.OK },
|
|
704
|
+
events
|
|
705
|
+
};
|
|
706
|
+
return {
|
|
707
|
+
status: { code: SpanStatusCode.UNSET },
|
|
708
|
+
events
|
|
709
|
+
};
|
|
804
710
|
}
|
|
805
|
-
|
|
806
|
-
|
|
711
|
+
//#endregion
|
|
712
|
+
//#region src/tracing.ts
|
|
713
|
+
/**
|
|
714
|
+
* Wrapper around a SpanExporter that logs export results when debug mode is enabled.
|
|
715
|
+
* The OTel SDK intentionally does not log on success, making debugging difficult.
|
|
716
|
+
* This wrapper adds visibility into each batch export's outcome.
|
|
717
|
+
*/
|
|
807
718
|
var DebugSpanExporterWrapper = class {
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
async forceFlush() {
|
|
829
|
-
return this.inner.forceFlush?.();
|
|
830
|
-
}
|
|
719
|
+
inner;
|
|
720
|
+
debugLog;
|
|
721
|
+
constructor(inner, debugLog) {
|
|
722
|
+
this.inner = inner;
|
|
723
|
+
this.debugLog = debugLog;
|
|
724
|
+
}
|
|
725
|
+
export(spans, resultCallback) {
|
|
726
|
+
const count = spans.length;
|
|
727
|
+
this.inner.export(spans, (result) => {
|
|
728
|
+
if (result.code === ExportResultCode.SUCCESS) this.debugLog(`[OtelExporter] Export completed: ${count} spans sent successfully`);
|
|
729
|
+
else this.debugLog(`[OtelExporter] Export FAILED: ${count} spans, error: ${result.error?.message ?? "unknown"}`);
|
|
730
|
+
resultCallback(result);
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
async shutdown() {
|
|
734
|
+
return this.inner.shutdown();
|
|
735
|
+
}
|
|
736
|
+
async forceFlush() {
|
|
737
|
+
return this.inner.forceFlush?.();
|
|
738
|
+
}
|
|
831
739
|
};
|
|
740
|
+
/**
|
|
741
|
+
* Wrapper around a LogRecordExporter that logs export results when debug mode is enabled.
|
|
742
|
+
* Same pattern as DebugSpanExporterWrapper but for log records.
|
|
743
|
+
*/
|
|
832
744
|
var DebugLogExporterWrapper = class {
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
async forceFlush() {
|
|
854
|
-
return this.inner.forceFlush?.();
|
|
855
|
-
}
|
|
745
|
+
inner;
|
|
746
|
+
debugLog;
|
|
747
|
+
constructor(inner, debugLog) {
|
|
748
|
+
this.inner = inner;
|
|
749
|
+
this.debugLog = debugLog;
|
|
750
|
+
}
|
|
751
|
+
export(logs, resultCallback) {
|
|
752
|
+
const count = logs.length;
|
|
753
|
+
this.inner.export(logs, (result) => {
|
|
754
|
+
if (result.code === ExportResultCode.SUCCESS) this.debugLog(`[OtelExporter] Log export completed: ${count} logs sent successfully`);
|
|
755
|
+
else this.debugLog(`[OtelExporter] Log export FAILED: ${count} logs, error: ${result.error?.message ?? "unknown"}`);
|
|
756
|
+
resultCallback(result);
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
async shutdown() {
|
|
760
|
+
return this.inner.shutdown();
|
|
761
|
+
}
|
|
762
|
+
async forceFlush() {
|
|
763
|
+
return this.inner.forceFlush?.();
|
|
764
|
+
}
|
|
856
765
|
};
|
|
857
766
|
var OtelExporter = class extends BaseExporter {
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
timestamp: logParams.timestamp,
|
|
1132
|
-
severityNumber: logParams.severityNumber,
|
|
1133
|
-
severityText: logParams.severityText,
|
|
1134
|
-
body: logParams.body,
|
|
1135
|
-
attributes,
|
|
1136
|
-
context: logContext
|
|
1137
|
-
});
|
|
1138
|
-
this.debugLog(`Exported log (level: ${event.log.level}, trace: ${event.log.traceId || "none"})`);
|
|
1139
|
-
} catch (error) {
|
|
1140
|
-
this.logger.error("[OtelExporter] Failed to export log:", error);
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
// ===========================================================================
|
|
1144
|
-
// Lifecycle
|
|
1145
|
-
// ===========================================================================
|
|
1146
|
-
/**
|
|
1147
|
-
* Force flush any buffered data without shutting down the exporter.
|
|
1148
|
-
* Delegates to all active processors/providers.
|
|
1149
|
-
*/
|
|
1150
|
-
async flush() {
|
|
1151
|
-
if (this.setupPromise) {
|
|
1152
|
-
await this.setupPromise;
|
|
1153
|
-
}
|
|
1154
|
-
const flushPromises = [];
|
|
1155
|
-
const signals = [];
|
|
1156
|
-
if (this.processor) {
|
|
1157
|
-
flushPromises.push(this.processor.forceFlush());
|
|
1158
|
-
signals.push("traces");
|
|
1159
|
-
}
|
|
1160
|
-
if (this.loggerProvider) {
|
|
1161
|
-
flushPromises.push(this.loggerProvider.forceFlush());
|
|
1162
|
-
signals.push("logs");
|
|
1163
|
-
}
|
|
1164
|
-
if (flushPromises.length > 0) {
|
|
1165
|
-
this.debugLog(`Flushing signals: ${signals.join(", ")}...`);
|
|
1166
|
-
await Promise.all(flushPromises);
|
|
1167
|
-
this.debugLog("Flushed all pending data");
|
|
1168
|
-
} else {
|
|
1169
|
-
this.debugLog("Flush called but no active exporters");
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
async shutdown() {
|
|
1173
|
-
if (this.setupPromise) {
|
|
1174
|
-
await this.setupPromise;
|
|
1175
|
-
}
|
|
1176
|
-
const shutdownPromises = [];
|
|
1177
|
-
if (this.processor) {
|
|
1178
|
-
shutdownPromises.push(this.processor.shutdown());
|
|
1179
|
-
}
|
|
1180
|
-
if (this.loggerProvider) {
|
|
1181
|
-
shutdownPromises.push(this.loggerProvider.shutdown());
|
|
1182
|
-
}
|
|
1183
|
-
if (shutdownPromises.length > 0) {
|
|
1184
|
-
await Promise.all(shutdownPromises);
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
767
|
+
config;
|
|
768
|
+
observabilityConfig;
|
|
769
|
+
spanConverter;
|
|
770
|
+
processor;
|
|
771
|
+
exporter;
|
|
772
|
+
loggerProvider;
|
|
773
|
+
otelLogger;
|
|
774
|
+
resolvedConfig;
|
|
775
|
+
providerName;
|
|
776
|
+
setupPromise;
|
|
777
|
+
name = "opentelemetry";
|
|
778
|
+
constructor(config) {
|
|
779
|
+
super(config);
|
|
780
|
+
this.config = config;
|
|
781
|
+
if (config.logLevel === "debug") diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO);
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Initialize with observability configuration and eagerly set up the trace
|
|
785
|
+
* and log signal exporters in parallel.
|
|
786
|
+
*
|
|
787
|
+
* Called by Mastra during component registration. The async setup runs in the
|
|
788
|
+
* background — event handlers await the resulting promise before processing.
|
|
789
|
+
*/
|
|
790
|
+
init(options) {
|
|
791
|
+
this.observabilityConfig = options.config;
|
|
792
|
+
this.setupPromise = this.setupAllSignals();
|
|
793
|
+
}
|
|
794
|
+
get isDebug() {
|
|
795
|
+
return this.config.logLevel === "debug";
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Debug logging that bypasses the Mastra logger.
|
|
799
|
+
*
|
|
800
|
+
* The Mastra framework replaces our logger via __setLogger() with one at
|
|
801
|
+
* INFO level, which silently swallows all this.logger.debug() calls.
|
|
802
|
+
* For debug output we need to go through console.info directly.
|
|
803
|
+
*/
|
|
804
|
+
debugLog = (...args) => this.isDebug && console.info("[OtelExporter:debug]", ...args);
|
|
805
|
+
resolveProvider() {
|
|
806
|
+
if (this.resolvedConfig !== void 0) return this.resolvedConfig;
|
|
807
|
+
if (!this.config.provider) {
|
|
808
|
+
this.setDisabled("[OtelExporter] Provider configuration is required. Use the \"custom\" provider for generic endpoints.");
|
|
809
|
+
this.resolvedConfig = null;
|
|
810
|
+
return null;
|
|
811
|
+
}
|
|
812
|
+
this.providerName = Object.keys(this.config.provider)[0];
|
|
813
|
+
const resolved = resolveProviderConfig(this.config.provider, this.isDebug);
|
|
814
|
+
if (!resolved) {
|
|
815
|
+
this.setDisabled("[OtelExporter] Provider configuration validation failed.");
|
|
816
|
+
this.resolvedConfig = null;
|
|
817
|
+
return null;
|
|
818
|
+
}
|
|
819
|
+
this.resolvedConfig = resolved;
|
|
820
|
+
return resolved;
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Derive the endpoint for a specific signal from the resolved provider config.
|
|
824
|
+
* Strips trailing slashes and any existing signal-path suffix so we never
|
|
825
|
+
* produce a doubled "//v1/logs" or leave a stale "/v1/traces" on a logs URL.
|
|
826
|
+
*/
|
|
827
|
+
getSignalEndpoint(resolved, signal) {
|
|
828
|
+
const signalPaths = {
|
|
829
|
+
traces: "/v1/traces",
|
|
830
|
+
logs: "/v1/logs"
|
|
831
|
+
};
|
|
832
|
+
let base = resolved.endpoint;
|
|
833
|
+
while (base.endsWith("/")) base = base.slice(0, -1);
|
|
834
|
+
for (const path of Object.values(signalPaths)) if (base.endsWith(path)) {
|
|
835
|
+
base = base.slice(0, -path.length);
|
|
836
|
+
break;
|
|
837
|
+
}
|
|
838
|
+
return base + signalPaths[signal];
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Build exporter constructor options for the given signal endpoint.
|
|
842
|
+
* For gRPC, converts headers to Metadata. For HTTP protocols, passes headers directly.
|
|
843
|
+
*/
|
|
844
|
+
async buildExporterOptions(protocol, url, headers) {
|
|
845
|
+
if (protocol === "grpc") {
|
|
846
|
+
const metadata = new (await (import("@grpc/grpc-js"))).Metadata();
|
|
847
|
+
for (const [key, value] of Object.entries(headers)) metadata.set(key, value);
|
|
848
|
+
return {
|
|
849
|
+
url,
|
|
850
|
+
metadata,
|
|
851
|
+
timeoutMillis: this.config.timeout
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
return {
|
|
855
|
+
url,
|
|
856
|
+
headers,
|
|
857
|
+
timeoutMillis: this.config.timeout
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Wait for setup to complete. If init() was called, this awaits the
|
|
862
|
+
* already-in-flight setup promise. If init() was never called (standalone
|
|
863
|
+
* usage without Mastra), this triggers setup on first use.
|
|
864
|
+
*/
|
|
865
|
+
ensureSetup() {
|
|
866
|
+
if (!this.setupPromise) this.setupPromise = this.setupAllSignals();
|
|
867
|
+
return this.setupPromise;
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Resolve provider config once and set up all enabled signals in parallel.
|
|
871
|
+
* Each signal setup catches its own errors — this method never rejects.
|
|
872
|
+
*/
|
|
873
|
+
async setupAllSignals() {
|
|
874
|
+
const resolved = this.resolveProvider();
|
|
875
|
+
if (!resolved) {
|
|
876
|
+
this.debugLog("Setup skipped: provider not resolved");
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
const protocol = resolved.protocol;
|
|
880
|
+
const setupTasks = [];
|
|
881
|
+
if (this.config.signals?.traces !== false) setupTasks.push(this.setupTraces(resolved, protocol));
|
|
882
|
+
else this.debugLog("Trace export disabled via config");
|
|
883
|
+
if (this.config.signals?.logs !== false) setupTasks.push(this.setupLogs(resolved, protocol));
|
|
884
|
+
else this.debugLog("Log export disabled via config");
|
|
885
|
+
await Promise.all(setupTasks);
|
|
886
|
+
this.debugLog(`Setup complete [traces=${!!this.processor}, logs=${!!this.otelLogger}]`);
|
|
887
|
+
}
|
|
888
|
+
async setupTraces(resolved, protocol) {
|
|
889
|
+
try {
|
|
890
|
+
if (this.config.exporter) this.exporter = this.config.exporter;
|
|
891
|
+
else {
|
|
892
|
+
const headers = resolved.headers;
|
|
893
|
+
const endpoint = protocol === "zipkin" ? resolved.endpoint : this.getSignalEndpoint(resolved, "traces");
|
|
894
|
+
this.debugLog(`Setting up trace exporter: protocol=${protocol}, endpoint=${endpoint}`);
|
|
895
|
+
const ExporterClass = await loadExporter(protocol, this.providerName);
|
|
896
|
+
if (!ExporterClass) {
|
|
897
|
+
this.debugLog(`Trace exporter not available for protocol: ${protocol}`);
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
const exporterOptions = protocol === "zipkin" ? {
|
|
901
|
+
url: endpoint,
|
|
902
|
+
headers
|
|
903
|
+
} : await this.buildExporterOptions(protocol, endpoint, headers);
|
|
904
|
+
this.exporter = new ExporterClass(exporterOptions);
|
|
905
|
+
this.debugLog(`Trace exporter created: ${this.exporter?.constructor?.name ?? "unknown"} -> ${endpoint}`);
|
|
906
|
+
}
|
|
907
|
+
const serviceName = this.observabilityConfig?.serviceName || "mastra-service";
|
|
908
|
+
this.spanConverter = new SpanConverter({
|
|
909
|
+
packageName: "@mastra/otel-exporter",
|
|
910
|
+
serviceName,
|
|
911
|
+
config: this.config,
|
|
912
|
+
format: "GenAI_v1_38_0"
|
|
913
|
+
});
|
|
914
|
+
const exporterForProcessor = this.isDebug ? new DebugSpanExporterWrapper(this.exporter, (msg) => this.debugLog(msg)) : this.exporter;
|
|
915
|
+
this.processor = new BatchSpanProcessor(exporterForProcessor, {
|
|
916
|
+
maxExportBatchSize: this.config.batchSize || 512,
|
|
917
|
+
maxQueueSize: 2048,
|
|
918
|
+
scheduledDelayMillis: 5e3,
|
|
919
|
+
exportTimeoutMillis: this.config.timeout || 3e4
|
|
920
|
+
});
|
|
921
|
+
this.debugLog(`Trace export initialized (service.name: "${serviceName}", batch size: ${this.config.batchSize || 512}, delay: 5s)`);
|
|
922
|
+
} catch (error) {
|
|
923
|
+
this.logger.warn("[OtelExporter] Failed to initialize trace export");
|
|
924
|
+
this.debugLog("Trace setup error:", error);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
async setupLogs(resolved, protocol) {
|
|
928
|
+
try {
|
|
929
|
+
const LogExporterClass = await loadSignalExporter("logs", protocol, this.providerName);
|
|
930
|
+
if (!LogExporterClass) {
|
|
931
|
+
this.debugLog(`Log exporter package not available for protocol "${protocol}". Log export disabled.`);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
const logEndpoint = this.getSignalEndpoint(resolved, "logs");
|
|
935
|
+
const headers = resolved.headers;
|
|
936
|
+
this.debugLog(`Setting up log exporter: protocol=${protocol}, endpoint=${logEndpoint}`);
|
|
937
|
+
const logExporter = new LogExporterClass(await this.buildExporterOptions(protocol, logEndpoint, headers));
|
|
938
|
+
const exporterForProcessor = this.isDebug ? new DebugLogExporterWrapper(logExporter, (msg) => this.debugLog(msg)) : logExporter;
|
|
939
|
+
const resource = resourceFromAttributes({
|
|
940
|
+
...this.config.resourceAttributes ?? {},
|
|
941
|
+
[ATTR_SERVICE_NAME]: this.observabilityConfig?.serviceName || "mastra-service"
|
|
942
|
+
});
|
|
943
|
+
this.loggerProvider = new LoggerProvider({
|
|
944
|
+
resource,
|
|
945
|
+
processors: [new BatchLogRecordProcessor(exporterForProcessor, {
|
|
946
|
+
maxExportBatchSize: this.config.batchSize || 512,
|
|
947
|
+
maxQueueSize: 2048,
|
|
948
|
+
scheduledDelayMillis: 5e3,
|
|
949
|
+
exportTimeoutMillis: this.config.timeout || 3e4
|
|
950
|
+
})]
|
|
951
|
+
});
|
|
952
|
+
this.otelLogger = this.loggerProvider.getLogger("@mastra/otel-exporter");
|
|
953
|
+
this.debugLog(`Log export initialized (endpoint: ${logEndpoint})`);
|
|
954
|
+
} catch (error) {
|
|
955
|
+
this.logger.warn("[OtelExporter] Failed to initialize log export");
|
|
956
|
+
this.debugLog("Log setup error:", error);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
async _exportTracingEvent(event) {
|
|
960
|
+
if (this.config.signals?.traces === false) return;
|
|
961
|
+
if (event.type !== TracingEventType.SPAN_ENDED) return;
|
|
962
|
+
await this.ensureSetup();
|
|
963
|
+
if (this.isDisabled || !this.processor) return;
|
|
964
|
+
const span = event.exportedSpan;
|
|
965
|
+
try {
|
|
966
|
+
const otelSpan = await this.spanConverter.convertSpan(span);
|
|
967
|
+
this.processor.onEnd(otelSpan);
|
|
968
|
+
this.debugLog(`Queued span ${span.id} (trace: ${span.traceId}, parent: ${span.parentSpanId || "none"}, type: ${span.type})`);
|
|
969
|
+
} catch (error) {
|
|
970
|
+
this.logger.error(`[OtelExporter] Failed to export span ${span.id}:`, error);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
async onLogEvent(event) {
|
|
974
|
+
this.debugLog(`onLogEvent received (level: ${event.log.level}, message: "${event.log.message}")`);
|
|
975
|
+
await this.ensureSetup();
|
|
976
|
+
if (this.isDisabled) {
|
|
977
|
+
this.debugLog("Log event skipped: exporter is disabled");
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
if (!this.otelLogger) {
|
|
981
|
+
this.debugLog("Log event skipped: log exporter not available");
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
try {
|
|
985
|
+
const logParams = convertLog(event.log);
|
|
986
|
+
const attributes = { ...logParams.attributes };
|
|
987
|
+
let logContext = context.active();
|
|
988
|
+
if (logParams.traceId && logParams.spanId) {
|
|
989
|
+
attributes["mastra.traceId"] = logParams.traceId;
|
|
990
|
+
attributes["mastra.spanId"] = logParams.spanId;
|
|
991
|
+
logContext = trace.setSpanContext(logContext, {
|
|
992
|
+
traceId: logParams.traceId,
|
|
993
|
+
spanId: logParams.spanId,
|
|
994
|
+
traceFlags: TraceFlags.SAMPLED,
|
|
995
|
+
isRemote: false
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
this.otelLogger.emit({
|
|
999
|
+
timestamp: logParams.timestamp,
|
|
1000
|
+
severityNumber: logParams.severityNumber,
|
|
1001
|
+
severityText: logParams.severityText,
|
|
1002
|
+
body: logParams.body,
|
|
1003
|
+
attributes,
|
|
1004
|
+
context: logContext
|
|
1005
|
+
});
|
|
1006
|
+
this.debugLog(`Exported log (level: ${event.log.level}, trace: ${event.log.traceId || "none"})`);
|
|
1007
|
+
} catch (error) {
|
|
1008
|
+
this.logger.error("[OtelExporter] Failed to export log:", error);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Force flush any buffered data without shutting down the exporter.
|
|
1013
|
+
* Delegates to all active processors/providers.
|
|
1014
|
+
*/
|
|
1015
|
+
async flush() {
|
|
1016
|
+
if (this.setupPromise) await this.setupPromise;
|
|
1017
|
+
const flushPromises = [];
|
|
1018
|
+
const signals = [];
|
|
1019
|
+
if (this.processor) {
|
|
1020
|
+
flushPromises.push(this.processor.forceFlush());
|
|
1021
|
+
signals.push("traces");
|
|
1022
|
+
}
|
|
1023
|
+
if (this.loggerProvider) {
|
|
1024
|
+
flushPromises.push(this.loggerProvider.forceFlush());
|
|
1025
|
+
signals.push("logs");
|
|
1026
|
+
}
|
|
1027
|
+
if (flushPromises.length > 0) {
|
|
1028
|
+
this.debugLog(`Flushing signals: ${signals.join(", ")}...`);
|
|
1029
|
+
await Promise.all(flushPromises);
|
|
1030
|
+
this.debugLog("Flushed all pending data");
|
|
1031
|
+
} else this.debugLog("Flush called but no active exporters");
|
|
1032
|
+
}
|
|
1033
|
+
async shutdown() {
|
|
1034
|
+
if (this.setupPromise) await this.setupPromise;
|
|
1035
|
+
const shutdownPromises = [];
|
|
1036
|
+
if (this.processor) shutdownPromises.push(this.processor.shutdown());
|
|
1037
|
+
if (this.loggerProvider) shutdownPromises.push(this.loggerProvider.shutdown());
|
|
1038
|
+
if (shutdownPromises.length > 0) await Promise.all(shutdownPromises);
|
|
1039
|
+
}
|
|
1187
1040
|
};
|
|
1188
|
-
|
|
1041
|
+
//#endregion
|
|
1189
1042
|
export { OtelExporter, SpanConverter, buildLogAttributes, convertLog, getAttributes, getSpanKind, getSpanName, mapSeverity };
|
|
1190
|
-
|
|
1043
|
+
|
|
1191
1044
|
//# sourceMappingURL=index.js.map
|