@introspection-sdk/introspection-node 0.4.1 → 0.4.3
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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-check-types.log +4 -0
- package/.turbo/turbo-lint.log +5 -0
- package/dist/ai-sdk-integration.d.ts.map +1 -1
- package/dist/ai-sdk-integration.js +2 -2
- package/dist/claude-hooks.d.ts.map +1 -1
- package/dist/claude-hooks.js +3 -3
- package/dist/client.d.ts +42 -132
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +66 -365
- package/dist/client.js.map +1 -0
- package/dist/converters/claude.d.ts +5 -1
- package/dist/converters/claude.d.ts.map +1 -1
- package/dist/converters/claude.js +7 -0
- package/dist/converters/claude.js.map +1 -0
- package/dist/converters/gemini.d.ts +90 -0
- package/dist/converters/gemini.d.ts.map +1 -0
- package/dist/converters/gemini.js +171 -0
- package/dist/converters/gemini.js.map +1 -0
- package/dist/converters/index.d.ts +1 -1
- package/dist/converters/index.d.ts.map +1 -1
- package/dist/converters/index.js +2 -1
- package/dist/converters/index.js.map +1 -0
- package/dist/converters/openai.js +1 -0
- package/dist/converters/openai.js.map +1 -0
- package/dist/converters/openclaw.d.ts +16 -0
- package/dist/converters/openclaw.d.ts.map +1 -0
- package/dist/converters/openclaw.js +49 -0
- package/dist/converters/openinference.js +1 -0
- package/dist/converters/openinference.js.map +1 -0
- package/dist/converters/vercel.d.ts.map +1 -1
- package/dist/converters/vercel.js +31 -3
- package/dist/converters/vercel.js.map +1 -0
- package/dist/http.d.ts +36 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +145 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +24 -27
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -32
- package/dist/index.js.map +1 -0
- package/dist/langchain-handler.d.ts.map +1 -1
- package/dist/langchain-handler.js +2 -2
- package/dist/mastra-exporter.d.ts.map +1 -1
- package/dist/mastra-exporter.js +2 -2
- package/dist/otel/anthropic.d.ts +62 -0
- package/dist/otel/anthropic.d.ts.map +1 -0
- package/dist/otel/anthropic.js +425 -0
- package/dist/otel/anthropic.js.map +1 -0
- package/dist/otel/claude-hooks.d.ts +346 -0
- package/dist/otel/claude-hooks.d.ts.map +1 -0
- package/dist/otel/claude-hooks.js +645 -0
- package/dist/otel/claude-hooks.js.map +1 -0
- package/dist/otel/claude-wrapper.d.ts +93 -0
- package/dist/otel/claude-wrapper.d.ts.map +1 -0
- package/dist/otel/claude-wrapper.js +155 -0
- package/dist/otel/claude-wrapper.js.map +1 -0
- package/dist/otel/gemini.d.ts +48 -0
- package/dist/otel/gemini.d.ts.map +1 -0
- package/dist/otel/gemini.js +297 -0
- package/dist/otel/gemini.js.map +1 -0
- package/dist/otel/index.d.ts +27 -0
- package/dist/otel/index.d.ts.map +1 -0
- package/dist/otel/index.js +33 -0
- package/dist/otel/index.js.map +1 -0
- package/dist/otel/langchain-handler.d.ts +112 -0
- package/dist/otel/langchain-handler.d.ts.map +1 -0
- package/dist/otel/langchain-handler.js +555 -0
- package/dist/otel/langchain-handler.js.map +1 -0
- package/dist/otel/logs.d.ts +82 -0
- package/dist/otel/logs.d.ts.map +1 -0
- package/dist/otel/logs.js +271 -0
- package/dist/otel/logs.js.map +1 -0
- package/dist/otel/mastra-exporter.d.ts +87 -0
- package/dist/otel/mastra-exporter.d.ts.map +1 -0
- package/dist/otel/mastra-exporter.js +525 -0
- package/dist/otel/mastra-exporter.js.map +1 -0
- package/dist/otel/pi.d.ts +59 -0
- package/dist/otel/pi.d.ts.map +1 -0
- package/dist/otel/pi.js +67 -0
- package/dist/otel/pi.js.map +1 -0
- package/dist/otel/setup.d.ts +23 -0
- package/dist/otel/setup.d.ts.map +1 -0
- package/dist/otel/setup.js +127 -0
- package/dist/otel/setup.js.map +1 -0
- package/dist/otel/span-processor.d.ts +62 -0
- package/dist/otel/span-processor.d.ts.map +1 -0
- package/dist/otel/span-processor.js +319 -0
- package/dist/otel/span-processor.js.map +1 -0
- package/dist/otel/tracing-processor.d.ts +139 -0
- package/dist/otel/tracing-processor.d.ts.map +1 -0
- package/dist/otel/tracing-processor.js +418 -0
- package/dist/otel/tracing-processor.js.map +1 -0
- package/dist/resources/experiments.d.ts +31 -0
- package/dist/resources/experiments.d.ts.map +1 -0
- package/dist/resources/experiments.js +133 -0
- package/dist/resources/experiments.js.map +1 -0
- package/dist/resources/recipes.d.ts +22 -0
- package/dist/resources/recipes.d.ts.map +1 -0
- package/dist/resources/recipes.js +61 -0
- package/dist/resources/recipes.js.map +1 -0
- package/dist/resources/runtimes.d.ts +76 -0
- package/dist/resources/runtimes.d.ts.map +1 -0
- package/dist/resources/runtimes.js +179 -0
- package/dist/resources/runtimes.js.map +1 -0
- package/dist/runner-resources/files.d.ts +35 -0
- package/dist/runner-resources/files.d.ts.map +1 -0
- package/dist/runner-resources/files.js +131 -0
- package/dist/runner-resources/files.js.map +1 -0
- package/dist/runner-resources/tasks.d.ts +39 -0
- package/dist/runner-resources/tasks.d.ts.map +1 -0
- package/dist/runner-resources/tasks.js +129 -0
- package/dist/runner-resources/tasks.js.map +1 -0
- package/dist/runner.d.ts +80 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +159 -0
- package/dist/runner.js.map +1 -0
- package/dist/span-processor.d.ts.map +1 -1
- package/dist/span-processor.js +3 -3
- package/dist/streaming.d.ts +8 -0
- package/dist/streaming.d.ts.map +1 -0
- package/dist/streaming.js +67 -0
- package/dist/streaming.js.map +1 -0
- package/dist/tracing-processor.d.ts.map +1 -1
- package/dist/tracing-processor.js +3 -3
- package/dist/types/genai.d.ts +165 -0
- package/dist/types/genai.d.ts.map +1 -0
- package/dist/types/genai.js +82 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +14 -0
- package/dist/utils.js.map +1 -0
- package/dist/version.js +1 -0
- package/dist/version.js.map +1 -0
- package/package.json +83 -25
- package/src/client.ts +76 -471
- package/src/converters/claude.ts +11 -1
- package/src/converters/gemini.ts +243 -0
- package/src/converters/index.ts +13 -7
- package/src/converters/vercel.ts +41 -3
- package/src/http.ts +163 -0
- package/src/index.ts +110 -75
- package/src/{anthropic.ts → otel/anthropic.ts} +95 -8
- package/src/{claude-hooks.ts → otel/claude-hooks.ts} +125 -24
- package/src/{claude-wrapper.ts → otel/claude-wrapper.ts} +1 -1
- package/src/otel/gemini.ts +397 -0
- package/src/otel/index.ts +80 -0
- package/src/{langchain-handler.ts → otel/langchain-handler.ts} +56 -7
- package/src/otel/logs.ts +394 -0
- package/src/{mastra-exporter.ts → otel/mastra-exporter.ts} +86 -23
- package/src/otel/pi.ts +88 -0
- package/src/otel/setup.ts +174 -0
- package/src/{span-processor.ts → otel/span-processor.ts} +17 -10
- package/src/{tracing-processor.ts → otel/tracing-processor.ts} +100 -23
- package/src/resources/experiments.ts +170 -0
- package/src/resources/recipes.ts +73 -0
- package/src/resources/runtimes.ts +231 -0
- package/src/runner-resources/files.ts +163 -0
- package/src/runner-resources/tasks.ts +156 -0
- package/src/runner.ts +194 -0
- package/src/streaming.ts +57 -0
- package/src/utils.ts +15 -0
- package/src/ai-sdk-integration.ts +0 -689
- package/src/converters/ai-sdk.ts +0 -323
package/src/otel/logs.ts
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IntrospectionLogs — OTel logs exporter for Introspection.
|
|
3
|
+
*
|
|
4
|
+
* Owns its own `LoggerProvider` and OTLP log exporter. Provides `track`,
|
|
5
|
+
* `feedback`, `identify`, and async-context helpers (`withBaggage`,
|
|
6
|
+
* `withAgent`, `withConversation`, etc.) that propagate identity and
|
|
7
|
+
* gen_ai context through OpenTelemetry baggage.
|
|
8
|
+
*
|
|
9
|
+
* Fully independent of {@link IntrospectionClient}. Construct it directly
|
|
10
|
+
* with the OTel peer dependencies installed.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import { IntrospectionLogs } from "@introspection-sdk/introspection-node/otel";
|
|
15
|
+
*
|
|
16
|
+
* const logs = new IntrospectionLogs({
|
|
17
|
+
* token: process.env.INTROSPECTION_TOKEN,
|
|
18
|
+
* serviceName: "my-service",
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* await logs.withAgent("support-bot", "agent_1", () =>
|
|
22
|
+
* logs.withConversation("conv_123", undefined, () => {
|
|
23
|
+
* logs.feedback("thumbs_up", { comments: "Great answer" });
|
|
24
|
+
* }),
|
|
25
|
+
* );
|
|
26
|
+
*
|
|
27
|
+
* await logs.shutdown();
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { SeverityNumber, type LogAttributes } from "@opentelemetry/api-logs";
|
|
32
|
+
import {
|
|
33
|
+
LoggerProvider,
|
|
34
|
+
BatchLogRecordProcessor,
|
|
35
|
+
} from "@opentelemetry/sdk-logs";
|
|
36
|
+
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-proto";
|
|
37
|
+
import {
|
|
38
|
+
resourceFromAttributes,
|
|
39
|
+
defaultResource,
|
|
40
|
+
} from "@opentelemetry/resources";
|
|
41
|
+
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
|
|
42
|
+
import { propagation, context } from "@opentelemetry/api";
|
|
43
|
+
|
|
44
|
+
import { logger as sdkLogger, withOtlpHttpsProxy } from "../utils.js";
|
|
45
|
+
import { VERSION } from "../version.js";
|
|
46
|
+
import {
|
|
47
|
+
generateEventId,
|
|
48
|
+
type FeedbackOptions,
|
|
49
|
+
type UserTraits,
|
|
50
|
+
type GenAiContext,
|
|
51
|
+
type IdentityContext,
|
|
52
|
+
} from "../types.js";
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Configuration for {@link IntrospectionLogs}.
|
|
56
|
+
*/
|
|
57
|
+
export interface IntrospectionLogsOptions {
|
|
58
|
+
/** Authentication token (env: INTROSPECTION_TOKEN). */
|
|
59
|
+
token?: string;
|
|
60
|
+
/** Service name for telemetry (env: INTROSPECTION_SERVICE_NAME, default: "introspection-client"). */
|
|
61
|
+
serviceName?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Base URL for the OTLP collector (env: INTROSPECTION_BASE_OTEL_URL,
|
|
64
|
+
* default: "https://otel.introspection.dev").
|
|
65
|
+
*/
|
|
66
|
+
baseOtelUrl?: string;
|
|
67
|
+
/** Additional HTTP headers to include in exporter requests. */
|
|
68
|
+
additionalHeaders?: Record<string, string>;
|
|
69
|
+
/** Flush interval in milliseconds (default: 5000). */
|
|
70
|
+
flushInterval?: number;
|
|
71
|
+
/** Maximum queue size before auto-flush (default: 100). */
|
|
72
|
+
maxBatchSize?: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class IntrospectionLogs {
|
|
76
|
+
private loggerProvider: LoggerProvider;
|
|
77
|
+
private otelLogger: ReturnType<LoggerProvider["getLogger"]>;
|
|
78
|
+
private userId: string | undefined;
|
|
79
|
+
private anonymousId: string | undefined;
|
|
80
|
+
private traits: Record<string, unknown> = {};
|
|
81
|
+
|
|
82
|
+
constructor(options: IntrospectionLogsOptions = {}) {
|
|
83
|
+
const token = options.token || process.env.INTROSPECTION_TOKEN || "";
|
|
84
|
+
const serviceName =
|
|
85
|
+
options.serviceName ||
|
|
86
|
+
process.env.INTROSPECTION_SERVICE_NAME ||
|
|
87
|
+
"introspection-client";
|
|
88
|
+
const baseOtelUrl =
|
|
89
|
+
options.baseOtelUrl ||
|
|
90
|
+
process.env.INTROSPECTION_BASE_OTEL_URL ||
|
|
91
|
+
"https://otel.introspection.dev";
|
|
92
|
+
|
|
93
|
+
if (!token) {
|
|
94
|
+
sdkLogger.warn(
|
|
95
|
+
"IntrospectionLogs: No token provided. Events will not be sent.",
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Construct endpoint URL for logs.
|
|
100
|
+
const endpoint = baseOtelUrl.endsWith("/v1/logs")
|
|
101
|
+
? baseOtelUrl
|
|
102
|
+
: `${baseOtelUrl.replace(/\/$/, "")}/v1/logs`;
|
|
103
|
+
|
|
104
|
+
const headers: Record<string, string> = {
|
|
105
|
+
Authorization: `Bearer ${token}`,
|
|
106
|
+
...(options.additionalHeaders || {}),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const exporter = new OTLPLogExporter(
|
|
110
|
+
withOtlpHttpsProxy({
|
|
111
|
+
url: endpoint,
|
|
112
|
+
headers,
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const processor = new BatchLogRecordProcessor(exporter, {
|
|
117
|
+
maxQueueSize: options.maxBatchSize ?? 100,
|
|
118
|
+
scheduledDelayMillis: options.flushInterval ?? 5000,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const baseResource = defaultResource();
|
|
122
|
+
const introspectionResource = resourceFromAttributes({
|
|
123
|
+
[ATTR_SERVICE_NAME]: serviceName,
|
|
124
|
+
});
|
|
125
|
+
const resource = baseResource.merge(introspectionResource);
|
|
126
|
+
|
|
127
|
+
this.loggerProvider = new LoggerProvider({
|
|
128
|
+
resource,
|
|
129
|
+
processors: [processor],
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
this.otelLogger = this.loggerProvider.getLogger(
|
|
133
|
+
"@introspection-sdk/introspection-node",
|
|
134
|
+
VERSION,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
sdkLogger.info(
|
|
138
|
+
`IntrospectionLogs initialized: service=${serviceName}, otlp=${endpoint}`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Get gen_ai context from baggage */
|
|
143
|
+
private getGenAiFromContext(): GenAiContext {
|
|
144
|
+
const baggage = propagation.getBaggage(context.active());
|
|
145
|
+
return {
|
|
146
|
+
conversationId: baggage?.getEntry("gen_ai.conversation.id")?.value,
|
|
147
|
+
previousResponseId: baggage?.getEntry(
|
|
148
|
+
"gen_ai.request.previous_response_id",
|
|
149
|
+
)?.value,
|
|
150
|
+
agentName: baggage?.getEntry("gen_ai.agent.name")?.value,
|
|
151
|
+
agentId: baggage?.getEntry("gen_ai.agent.id")?.value,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Get identity from baggage or instance state */
|
|
156
|
+
private getIdentityFromContext(): IdentityContext {
|
|
157
|
+
const baggage = propagation.getBaggage(context.active());
|
|
158
|
+
return {
|
|
159
|
+
userId: baggage?.getEntry("identity.user_id")?.value || this.userId,
|
|
160
|
+
anonymousId:
|
|
161
|
+
baggage?.getEntry("identity.anonymous_id")?.value || this.anonymousId,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private getTimestamp(): [number, number] {
|
|
166
|
+
const hrTimeNs = process.hrtime.bigint();
|
|
167
|
+
const epochNs = BigInt(Date.now()) * BigInt(1_000_000);
|
|
168
|
+
const offsetNs = hrTimeNs - process.hrtime.bigint() + epochNs;
|
|
169
|
+
const seconds = Number(offsetNs / BigInt(1_000_000_000));
|
|
170
|
+
const nanos = Number(offsetNs % BigInt(1_000_000_000));
|
|
171
|
+
return [seconds, nanos];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private buildAttributes(
|
|
175
|
+
eventName: string,
|
|
176
|
+
options: {
|
|
177
|
+
properties?: Record<string, unknown>;
|
|
178
|
+
traits?: Record<string, unknown>;
|
|
179
|
+
conversationId?: string;
|
|
180
|
+
previousResponseId?: string;
|
|
181
|
+
eventId?: string;
|
|
182
|
+
} = {},
|
|
183
|
+
): LogAttributes {
|
|
184
|
+
const { properties, traits, conversationId, previousResponseId, eventId } =
|
|
185
|
+
options;
|
|
186
|
+
|
|
187
|
+
const attributes: LogAttributes = {
|
|
188
|
+
"event.name": eventName,
|
|
189
|
+
"event.id": eventId || generateEventId(),
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const identity = this.getIdentityFromContext();
|
|
193
|
+
if (identity.userId) attributes["identity.user.id"] = identity.userId;
|
|
194
|
+
if (identity.anonymousId)
|
|
195
|
+
attributes["identity.anonymous.id"] = identity.anonymousId;
|
|
196
|
+
|
|
197
|
+
const genAi = this.getGenAiFromContext();
|
|
198
|
+
const finalConversationId = conversationId || genAi.conversationId;
|
|
199
|
+
const finalPreviousResponseId =
|
|
200
|
+
previousResponseId || genAi.previousResponseId;
|
|
201
|
+
|
|
202
|
+
if (finalConversationId)
|
|
203
|
+
attributes["gen_ai.conversation.id"] = finalConversationId;
|
|
204
|
+
if (finalPreviousResponseId)
|
|
205
|
+
attributes["gen_ai.request.previous_response_id"] =
|
|
206
|
+
finalPreviousResponseId;
|
|
207
|
+
if (genAi.agentName) attributes["gen_ai.agent.name"] = genAi.agentName;
|
|
208
|
+
if (genAi.agentId) attributes["gen_ai.agent.id"] = genAi.agentId;
|
|
209
|
+
|
|
210
|
+
if (properties) {
|
|
211
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
212
|
+
if (value != null) {
|
|
213
|
+
const attrKey = `properties.${key}`;
|
|
214
|
+
if (typeof value === "object") {
|
|
215
|
+
attributes[attrKey] = JSON.stringify(value);
|
|
216
|
+
} else if (
|
|
217
|
+
typeof value === "string" ||
|
|
218
|
+
typeof value === "number" ||
|
|
219
|
+
typeof value === "boolean"
|
|
220
|
+
) {
|
|
221
|
+
attributes[attrKey] = value;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (traits) {
|
|
228
|
+
for (const [key, value] of Object.entries(traits)) {
|
|
229
|
+
if (value != null) {
|
|
230
|
+
const attrKey = `context.traits.${key}`;
|
|
231
|
+
if (typeof value === "object") {
|
|
232
|
+
attributes[attrKey] = JSON.stringify(value);
|
|
233
|
+
} else if (
|
|
234
|
+
typeof value === "string" ||
|
|
235
|
+
typeof value === "number" ||
|
|
236
|
+
typeof value === "boolean"
|
|
237
|
+
) {
|
|
238
|
+
attributes[attrKey] = value;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return attributes;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
track(
|
|
248
|
+
eventName: string,
|
|
249
|
+
properties?: Record<string, unknown>,
|
|
250
|
+
options?: { eventId?: string },
|
|
251
|
+
): void {
|
|
252
|
+
const attributes = this.buildAttributes(eventName, {
|
|
253
|
+
properties,
|
|
254
|
+
eventId: options?.eventId,
|
|
255
|
+
});
|
|
256
|
+
this.otelLogger.emit({
|
|
257
|
+
timestamp: this.getTimestamp(),
|
|
258
|
+
context: context.active(),
|
|
259
|
+
severityNumber: SeverityNumber.INFO,
|
|
260
|
+
severityText: "INFO",
|
|
261
|
+
attributes,
|
|
262
|
+
});
|
|
263
|
+
sdkLogger.debug(`Tracked: ${eventName}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
feedback(name: string, options: FeedbackOptions = {}): void {
|
|
267
|
+
const { comments, conversationId, previousResponseId, eventId, ...extra } =
|
|
268
|
+
options;
|
|
269
|
+
const properties: Record<string, unknown> = { name, ...extra };
|
|
270
|
+
if (comments) properties.comments = comments;
|
|
271
|
+
const attributes = this.buildAttributes("introspection.feedback", {
|
|
272
|
+
properties,
|
|
273
|
+
conversationId: conversationId as string | undefined,
|
|
274
|
+
previousResponseId: previousResponseId as string | undefined,
|
|
275
|
+
eventId: eventId as string | undefined,
|
|
276
|
+
});
|
|
277
|
+
this.otelLogger.emit({
|
|
278
|
+
timestamp: this.getTimestamp(),
|
|
279
|
+
context: context.active(),
|
|
280
|
+
severityNumber: SeverityNumber.INFO,
|
|
281
|
+
severityText: "INFO",
|
|
282
|
+
attributes,
|
|
283
|
+
});
|
|
284
|
+
sdkLogger.debug(`Feedback: ${name}`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
identify(
|
|
288
|
+
userId: string,
|
|
289
|
+
traits?: UserTraits,
|
|
290
|
+
anonymousId?: string,
|
|
291
|
+
eventId?: string,
|
|
292
|
+
): void {
|
|
293
|
+
this.userId = userId;
|
|
294
|
+
if (anonymousId) this.anonymousId = anonymousId;
|
|
295
|
+
if (traits) this.traits = { ...this.traits, ...traits };
|
|
296
|
+
const attributes = this.buildAttributes("identify", { traits, eventId });
|
|
297
|
+
this.otelLogger.emit({
|
|
298
|
+
timestamp: this.getTimestamp(),
|
|
299
|
+
context: context.active(),
|
|
300
|
+
severityNumber: SeverityNumber.INFO,
|
|
301
|
+
severityText: "INFO",
|
|
302
|
+
attributes,
|
|
303
|
+
});
|
|
304
|
+
sdkLogger.debug(`Identified: ${userId}`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
createBaggageContext(values: Record<string, string>) {
|
|
308
|
+
const ctx = context.active();
|
|
309
|
+
let bag = propagation.getBaggage(ctx) || propagation.createBaggage();
|
|
310
|
+
for (const [key, value] of Object.entries(values)) {
|
|
311
|
+
bag = bag.setEntry(key, { value });
|
|
312
|
+
}
|
|
313
|
+
return propagation.setBaggage(ctx, bag);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async withBaggage<T>(
|
|
317
|
+
values: Record<string, string>,
|
|
318
|
+
callback: () => T | Promise<T>,
|
|
319
|
+
): Promise<T> {
|
|
320
|
+
const ctx = this.createBaggageContext(values);
|
|
321
|
+
return await context.with(ctx, callback);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async withAgent<T>(
|
|
325
|
+
agentName: string,
|
|
326
|
+
agentId: string | undefined,
|
|
327
|
+
callback: () => T | Promise<T>,
|
|
328
|
+
): Promise<T> {
|
|
329
|
+
const values: Record<string, string> = { "gen_ai.agent.name": agentName };
|
|
330
|
+
if (agentId) values["gen_ai.agent.id"] = agentId;
|
|
331
|
+
return await context.with(this.createBaggageContext(values), callback);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async withConversation<T>(
|
|
335
|
+
conversationId: string | undefined,
|
|
336
|
+
previousResponseId: string | undefined,
|
|
337
|
+
callback: () => T | Promise<T>,
|
|
338
|
+
): Promise<T> {
|
|
339
|
+
const values: Record<string, string> = {};
|
|
340
|
+
if (conversationId) values["gen_ai.conversation.id"] = conversationId;
|
|
341
|
+
if (previousResponseId)
|
|
342
|
+
values["gen_ai.request.previous_response_id"] = previousResponseId;
|
|
343
|
+
return await context.with(this.createBaggageContext(values), callback);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
setUserId(userId: string): void {
|
|
347
|
+
this.userId = userId;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
setAnonymousId(anonymousId: string): void {
|
|
351
|
+
this.anonymousId = anonymousId;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async withUserId<T>(
|
|
355
|
+
userId: string,
|
|
356
|
+
callback: () => T | Promise<T>,
|
|
357
|
+
): Promise<T> {
|
|
358
|
+
return await context.with(
|
|
359
|
+
this.createBaggageContext({ "identity.user_id": userId }),
|
|
360
|
+
callback,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async withAnonymousId<T>(
|
|
365
|
+
anonymousId: string,
|
|
366
|
+
callback: () => T | Promise<T>,
|
|
367
|
+
): Promise<T> {
|
|
368
|
+
return await context.with(
|
|
369
|
+
this.createBaggageContext({ "identity.anonymous_id": anonymousId }),
|
|
370
|
+
callback,
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
getAnonymousId(): string | undefined {
|
|
375
|
+
return this.anonymousId;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
reset(): void {
|
|
379
|
+
this.userId = undefined;
|
|
380
|
+
this.anonymousId = undefined;
|
|
381
|
+
this.traits = {};
|
|
382
|
+
sdkLogger.debug("IntrospectionLogs state reset");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async flush(): Promise<void> {
|
|
386
|
+
await this.loggerProvider.forceFlush();
|
|
387
|
+
sdkLogger.debug("Flushed pending log records");
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async shutdown(): Promise<void> {
|
|
391
|
+
await this.loggerProvider.shutdown();
|
|
392
|
+
sdkLogger.debug("IntrospectionLogs shutdown complete");
|
|
393
|
+
}
|
|
394
|
+
}
|
|
@@ -51,8 +51,8 @@ import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
|
51
51
|
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
|
|
52
52
|
import { randomUUID } from "crypto";
|
|
53
53
|
|
|
54
|
-
import { logger } from "
|
|
55
|
-
import { VERSION } from "
|
|
54
|
+
import { logger, withOtlpHttpsProxy } from "../utils.js";
|
|
55
|
+
import { VERSION } from "../version.js";
|
|
56
56
|
import type {
|
|
57
57
|
InputMessage,
|
|
58
58
|
OutputMessage,
|
|
@@ -77,7 +77,7 @@ export interface MastraExporterAdvancedOptions {
|
|
|
77
77
|
export interface IntrospectionMastraExporterOptions {
|
|
78
78
|
/** Authentication token (env: INTROSPECTION_TOKEN). */
|
|
79
79
|
token?: string;
|
|
80
|
-
/** Base URL for the API (env:
|
|
80
|
+
/** Base URL for the API (env: INTROSPECTION_BASE_OTEL_URL). */
|
|
81
81
|
baseUrl?: string;
|
|
82
82
|
/** Additional headers to include in requests. */
|
|
83
83
|
additionalHeaders?: Record<string, string>;
|
|
@@ -152,7 +152,7 @@ export class IntrospectionMastraExporter extends BaseExporter {
|
|
|
152
152
|
|
|
153
153
|
const baseUrl =
|
|
154
154
|
opts.baseUrl ||
|
|
155
|
-
process.env.
|
|
155
|
+
process.env.INTROSPECTION_BASE_OTEL_URL ||
|
|
156
156
|
"https://otel.introspection.dev";
|
|
157
157
|
const endpoint = baseUrl.endsWith("/v1/traces")
|
|
158
158
|
? baseUrl
|
|
@@ -164,7 +164,9 @@ export class IntrospectionMastraExporter extends BaseExporter {
|
|
|
164
164
|
...opts.additionalHeaders,
|
|
165
165
|
};
|
|
166
166
|
|
|
167
|
-
const spanExporter = new OTLPTraceExporter(
|
|
167
|
+
const spanExporter = new OTLPTraceExporter(
|
|
168
|
+
withOtlpHttpsProxy({ url: endpoint, headers }),
|
|
169
|
+
);
|
|
168
170
|
const effectiveBatchSize =
|
|
169
171
|
token.startsWith("intro_dev") || token.startsWith("intro_staging")
|
|
170
172
|
? 1
|
|
@@ -289,7 +291,26 @@ export class IntrospectionMastraExporter extends BaseExporter {
|
|
|
289
291
|
span.entityName || span.name || "agent",
|
|
290
292
|
);
|
|
291
293
|
|
|
292
|
-
|
|
294
|
+
// Prefer system messages from span.input (full prompt sent to LLM) over
|
|
295
|
+
// attrs.instructions (may be just the short placeholder string).
|
|
296
|
+
const inputMsgs = Array.isArray(span.input)
|
|
297
|
+
? (span.input as Record<string, unknown>[])
|
|
298
|
+
: undefined;
|
|
299
|
+
const systemMsgsFromInput = inputMsgs?.filter((m) => m.role === "system");
|
|
300
|
+
if (systemMsgsFromInput && systemMsgsFromInput.length > 0) {
|
|
301
|
+
rootSpan.setAttribute(
|
|
302
|
+
"gen_ai.system_instructions",
|
|
303
|
+
JSON.stringify(
|
|
304
|
+
systemMsgsFromInput.map((m) => ({
|
|
305
|
+
type: "text",
|
|
306
|
+
content: this._extractTextContent(m.content),
|
|
307
|
+
})),
|
|
308
|
+
),
|
|
309
|
+
);
|
|
310
|
+
} else if (
|
|
311
|
+
attrs?.["instructions"] &&
|
|
312
|
+
typeof attrs["instructions"] === "string"
|
|
313
|
+
) {
|
|
293
314
|
rootSpan.setAttribute(
|
|
294
315
|
"gen_ai.system_instructions",
|
|
295
316
|
JSON.stringify([{ type: "text", content: attrs["instructions"] }]),
|
|
@@ -343,12 +364,30 @@ export class IntrospectionMastraExporter extends BaseExporter {
|
|
|
343
364
|
otelSpan.setAttribute("gen_ai.response.model", responseModel);
|
|
344
365
|
if (responseId) otelSpan.setAttribute("gen_ai.response.id", responseId);
|
|
345
366
|
|
|
346
|
-
// Input messages —
|
|
367
|
+
// Input messages — span.input is either:
|
|
368
|
+
// (a) a flat normalized array { role, content }[] from Mastra's inputMessages path, or
|
|
369
|
+
// (b) an object { body: { input/messages, tools } } from the raw HTTP request path.
|
|
347
370
|
if (span.input != null) {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if (Array.isArray(
|
|
371
|
+
let apiMessages: Record<string, unknown>[] | undefined;
|
|
372
|
+
let tools: unknown[] | undefined;
|
|
373
|
+
|
|
374
|
+
if (Array.isArray(span.input)) {
|
|
375
|
+
// Path (a): flat array already normalized by Mastra's normalizeMessages()
|
|
376
|
+
apiMessages = span.input as Record<string, unknown>[];
|
|
377
|
+
} else {
|
|
378
|
+
// Path (b): nested request body
|
|
379
|
+
const rawInput = span.input as Record<string, unknown>;
|
|
380
|
+
const body = rawInput?.body as Record<string, unknown> | undefined;
|
|
381
|
+
const msgs = body?.input ?? body?.messages;
|
|
382
|
+
if (Array.isArray(msgs)) {
|
|
383
|
+
apiMessages = msgs as Record<string, unknown>[];
|
|
384
|
+
}
|
|
385
|
+
if (Array.isArray(body?.tools)) {
|
|
386
|
+
tools = body.tools as unknown[];
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (apiMessages && apiMessages.length > 0) {
|
|
352
391
|
const inputMessages = this._convertInput(apiMessages);
|
|
353
392
|
if (inputMessages.length > 0) {
|
|
354
393
|
otelSpan.setAttribute(
|
|
@@ -357,28 +396,22 @@ export class IntrospectionMastraExporter extends BaseExporter {
|
|
|
357
396
|
);
|
|
358
397
|
}
|
|
359
398
|
|
|
360
|
-
// System instructions
|
|
361
|
-
const systemMsgs = (
|
|
362
|
-
(m) => m.role === "system",
|
|
363
|
-
);
|
|
399
|
+
// System instructions — combine all system messages into one attribute
|
|
400
|
+
const systemMsgs = apiMessages.filter((m) => m.role === "system");
|
|
364
401
|
if (systemMsgs.length > 0) {
|
|
365
402
|
otelSpan.setAttribute(
|
|
366
403
|
"gen_ai.system_instructions",
|
|
367
404
|
JSON.stringify(
|
|
368
405
|
systemMsgs.map((m) => ({
|
|
369
406
|
type: "text",
|
|
370
|
-
content:
|
|
371
|
-
typeof m.content === "string"
|
|
372
|
-
? m.content
|
|
373
|
-
: JSON.stringify(m.content),
|
|
407
|
+
content: this._extractTextContent(m.content),
|
|
374
408
|
})),
|
|
375
409
|
),
|
|
376
410
|
);
|
|
377
411
|
}
|
|
378
412
|
}
|
|
379
413
|
|
|
380
|
-
// Tool definitions
|
|
381
|
-
const tools = body?.tools;
|
|
414
|
+
// Tool definitions (only present on path (b))
|
|
382
415
|
if (Array.isArray(tools) && tools.length > 0) {
|
|
383
416
|
otelSpan.setAttribute(
|
|
384
417
|
"gen_ai.tool.definitions",
|
|
@@ -429,6 +462,11 @@ export class IntrospectionMastraExporter extends BaseExporter {
|
|
|
429
462
|
"gen_ai.usage.cache_read.input_tokens",
|
|
430
463
|
inputDetails["cacheRead"],
|
|
431
464
|
);
|
|
465
|
+
if (typeof inputDetails?.["cacheWrite"] === "number")
|
|
466
|
+
otelSpan.setAttribute(
|
|
467
|
+
"gen_ai.usage.cache_creation.input_tokens",
|
|
468
|
+
inputDetails["cacheWrite"],
|
|
469
|
+
);
|
|
432
470
|
}
|
|
433
471
|
|
|
434
472
|
// Finish reason
|
|
@@ -503,11 +541,17 @@ export class IntrospectionMastraExporter extends BaseExporter {
|
|
|
503
541
|
// Skip system messages (handled separately)
|
|
504
542
|
if (msg.role === "system") continue;
|
|
505
543
|
|
|
506
|
-
// Standard role-based messages
|
|
544
|
+
// Standard role-based messages (AIv4 uses msg.content; AIv5 may use msg.parts)
|
|
507
545
|
if (msg.role) {
|
|
546
|
+
const parts =
|
|
547
|
+
msg.content != null
|
|
548
|
+
? this._extractParts(msg.content)
|
|
549
|
+
: Array.isArray(msg.parts)
|
|
550
|
+
? this._extractParts(msg.parts)
|
|
551
|
+
: [];
|
|
508
552
|
result.push({
|
|
509
553
|
role: msg.role as InputMessage["role"],
|
|
510
|
-
parts
|
|
554
|
+
parts,
|
|
511
555
|
});
|
|
512
556
|
continue;
|
|
513
557
|
}
|
|
@@ -586,6 +630,25 @@ export class IntrospectionMastraExporter extends BaseExporter {
|
|
|
586
630
|
return [{ role: "assistant", parts, finish_reason: finishReason }];
|
|
587
631
|
}
|
|
588
632
|
|
|
633
|
+
/** Extract plain text from a message content field (string, array of parts, or AIv5 parts). */
|
|
634
|
+
private _extractTextContent(content: unknown): string {
|
|
635
|
+
if (typeof content === "string") return content;
|
|
636
|
+
if (Array.isArray(content)) {
|
|
637
|
+
return content
|
|
638
|
+
.map((p: unknown) => {
|
|
639
|
+
const part = p as Record<string, unknown>;
|
|
640
|
+
return typeof part.text === "string"
|
|
641
|
+
? part.text
|
|
642
|
+
: typeof part.content === "string"
|
|
643
|
+
? part.content
|
|
644
|
+
: "";
|
|
645
|
+
})
|
|
646
|
+
.filter(Boolean)
|
|
647
|
+
.join("\n");
|
|
648
|
+
}
|
|
649
|
+
return typeof content === "undefined" ? "" : JSON.stringify(content);
|
|
650
|
+
}
|
|
651
|
+
|
|
589
652
|
private _extractParts(content: unknown): MessagePart[] {
|
|
590
653
|
if (typeof content === "string") {
|
|
591
654
|
return [{ type: "text", content }];
|
package/src/otel/pi.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* High-level pi Agent SDK integration for Introspection.
|
|
3
|
+
*
|
|
4
|
+
* Wraps {@link @introspection-sdk/introspection-pi} behind a zero-config
|
|
5
|
+
* interface so users don't need to manage OpenTelemetry tracers directly.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { IntrospectionPiInstrumentor, setupTracing } from "@introspection-sdk/introspection-node";
|
|
10
|
+
* import { Agent } from "@mariozechner/pi-agent-core";
|
|
11
|
+
*
|
|
12
|
+
* setupTracing({ serviceName: "my-app" });
|
|
13
|
+
* const instrumentor = new IntrospectionPiInstrumentor();
|
|
14
|
+
*
|
|
15
|
+
* const agent = new Agent({ ... });
|
|
16
|
+
* instrumentor.instrument(agent, {
|
|
17
|
+
* conversationId: "conv-123",
|
|
18
|
+
* agentId: "weather-agent",
|
|
19
|
+
* agentName: "Weather",
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* await agent.prompt("What's the weather in Tokyo?");
|
|
23
|
+
* instrumentor.stop();
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { trace } from "@opentelemetry/api";
|
|
28
|
+
import type { Agent } from "@mariozechner/pi-agent-core";
|
|
29
|
+
import {
|
|
30
|
+
instrumentStream,
|
|
31
|
+
instrumentAgent,
|
|
32
|
+
type AgentMeta,
|
|
33
|
+
type AgentInstrumentation,
|
|
34
|
+
} from "@introspection-sdk/introspection-pi";
|
|
35
|
+
import { VERSION } from "../version.js";
|
|
36
|
+
|
|
37
|
+
export type { AgentMeta } from "@introspection-sdk/introspection-pi";
|
|
38
|
+
|
|
39
|
+
export interface IntrospectionPiInstrumentorOptions {
|
|
40
|
+
/** Tracer name used for all spans produced by this instrumentor. */
|
|
41
|
+
tracerName?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Zero-config pi Agent SDK integration for Introspection.
|
|
46
|
+
*
|
|
47
|
+
* Uses the global OTel tracer provider (registered by {@link setupTracing}).
|
|
48
|
+
* Call {@link instrument} once per {@link Agent} instance, then {@link stop}
|
|
49
|
+
* to unsubscribe all tool instrumentation and finalize open spans.
|
|
50
|
+
*/
|
|
51
|
+
export class IntrospectionPiInstrumentor {
|
|
52
|
+
private _tracer: ReturnType<typeof trace.getTracer>;
|
|
53
|
+
private _instrumentations: AgentInstrumentation[] = [];
|
|
54
|
+
|
|
55
|
+
constructor(opts: IntrospectionPiInstrumentorOptions = {}) {
|
|
56
|
+
this._tracer = trace.getTracer(
|
|
57
|
+
opts.tracerName ?? "introspection-pi",
|
|
58
|
+
VERSION,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Instrument a pi {@link Agent}:
|
|
64
|
+
* - Wraps `agent.streamFn` to emit a `chat ${provider}` span per LLM call.
|
|
65
|
+
* - Subscribes to the agent loop to emit an `execute_tool ${name}` span per
|
|
66
|
+
* tool execution.
|
|
67
|
+
*
|
|
68
|
+
* @param agent - The pi Agent instance to instrument.
|
|
69
|
+
* @param meta - Identity metadata stamped on every span produced by this agent.
|
|
70
|
+
*/
|
|
71
|
+
instrument(agent: Agent, meta: AgentMeta): void {
|
|
72
|
+
agent.streamFn = instrumentStream(agent.streamFn, {
|
|
73
|
+
tracer: this._tracer,
|
|
74
|
+
meta,
|
|
75
|
+
});
|
|
76
|
+
this._instrumentations.push(
|
|
77
|
+
instrumentAgent(agent, { tracer: this._tracer, meta }),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Unsubscribe all active tool instrumentations and finalize any open spans.
|
|
83
|
+
*/
|
|
84
|
+
stop(): void {
|
|
85
|
+
for (const inst of this._instrumentations) inst.stop();
|
|
86
|
+
this._instrumentations = [];
|
|
87
|
+
}
|
|
88
|
+
}
|