@moduna/otel 1.1.2 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -121,10 +121,16 @@ const result = await model.invoke("Hello, world!", {
|
|
|
121
121
|
});
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
+
Enable callback trace lifecycle logs while debugging instrumentation.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
const handler = otel.langChainHandler({}, { debug: true });
|
|
128
|
+
```
|
|
129
|
+
|
|
124
130
|
Or register it globally for all LangChain runs.
|
|
125
131
|
|
|
126
132
|
```ts
|
|
127
|
-
otel.registerGlobalLangChainHandler();
|
|
133
|
+
otel.registerGlobalLangChainHandler({}, { debug: true });
|
|
128
134
|
|
|
129
135
|
const result = await model.invoke("Hello, world!", {
|
|
130
136
|
metadata: {
|
|
@@ -7,6 +7,14 @@ import type { ModunaTraceContext } from "../types/TraceContext.js";
|
|
|
7
7
|
* Configuration for Moduna's LangChain callback handler.
|
|
8
8
|
*/
|
|
9
9
|
export interface ModunaLangChainCallbackHandlerConfig {
|
|
10
|
+
/**
|
|
11
|
+
* Enables trace lifecycle logging for LangChain callback events.
|
|
12
|
+
*/
|
|
13
|
+
debug?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Logger used when debug trace logging is enabled.
|
|
16
|
+
*/
|
|
17
|
+
logger?: Pick<Console, "debug" | "error">;
|
|
10
18
|
/**
|
|
11
19
|
* Default trace identifiers used when a LangChain call has no metadata.
|
|
12
20
|
*/
|
|
@@ -20,7 +28,9 @@ export declare class ModunaLangChainCallbackHandler extends BaseCallbackHandler
|
|
|
20
28
|
* Stable handler name used by LangChain callback manager de-duplication.
|
|
21
29
|
*/
|
|
22
30
|
name: string;
|
|
31
|
+
private readonly debug;
|
|
23
32
|
private readonly defaultTraceContext;
|
|
33
|
+
private readonly logger;
|
|
24
34
|
private readonly runs;
|
|
25
35
|
/**
|
|
26
36
|
* Creates a LangChain callback handler for Moduna spans.
|
|
@@ -77,9 +87,174 @@ export declare class ModunaLangChainCallbackHandler extends BaseCallbackHandler
|
|
|
77
87
|
*/
|
|
78
88
|
handleLLMError(error: unknown, runId: string): void;
|
|
79
89
|
private startRun;
|
|
90
|
+
/**
|
|
91
|
+
* Applies model/provider attributes using LangChain serialization metadata.
|
|
92
|
+
*
|
|
93
|
+
* @param span Span receiving model attributes.
|
|
94
|
+
* @param llm Serialized LangChain model metadata.
|
|
95
|
+
* @param extraParams Provider-specific invocation parameters.
|
|
96
|
+
*/
|
|
80
97
|
private applyModelAttributes;
|
|
98
|
+
/**
|
|
99
|
+
* Applies Moduna conversation and session identifiers to a span.
|
|
100
|
+
*
|
|
101
|
+
* @param span Span receiving trace context attributes.
|
|
102
|
+
* @param metadata LangChain metadata supplied on the run.
|
|
103
|
+
*/
|
|
81
104
|
private applyTraceContext;
|
|
105
|
+
/**
|
|
106
|
+
* Applies prompt input attributes in LangSmith-compatible GenAI format.
|
|
107
|
+
*
|
|
108
|
+
* @param span Span receiving prompt attributes.
|
|
109
|
+
* @param messages Normalized prompt messages.
|
|
110
|
+
*/
|
|
111
|
+
private applyPromptAttributes;
|
|
112
|
+
/**
|
|
113
|
+
* Applies request parameter attributes in GenAI semantic format.
|
|
114
|
+
*
|
|
115
|
+
* @param span Span receiving invocation parameter attributes.
|
|
116
|
+
* @param extraParams Provider-specific invocation parameters.
|
|
117
|
+
*/
|
|
118
|
+
private applyInvocationAttributes;
|
|
119
|
+
/**
|
|
120
|
+
* Applies model completion output attributes from a LangChain result.
|
|
121
|
+
*
|
|
122
|
+
* @param span Span receiving completion attributes.
|
|
123
|
+
* @param output LangChain LLM output.
|
|
124
|
+
*/
|
|
125
|
+
private applyCompletionAttributes;
|
|
126
|
+
/**
|
|
127
|
+
* Applies token usage attributes from a LangChain result.
|
|
128
|
+
*
|
|
129
|
+
* @param span Span receiving usage attributes.
|
|
130
|
+
* @param output LangChain LLM output.
|
|
131
|
+
* @param streamedTokenCount Token count observed from streaming callbacks.
|
|
132
|
+
*/
|
|
133
|
+
private applyUsageAttributes;
|
|
134
|
+
/**
|
|
135
|
+
* Extracts token usage from LangChain result metadata and provider outputs.
|
|
136
|
+
*
|
|
137
|
+
* @param output LangChain LLM output.
|
|
138
|
+
* @returns Normalized token usage when present.
|
|
139
|
+
*/
|
|
140
|
+
private extractUsage;
|
|
141
|
+
/**
|
|
142
|
+
* Finds the first message usage metadata in a LangChain result.
|
|
143
|
+
*
|
|
144
|
+
* @param output LangChain LLM output.
|
|
145
|
+
* @returns Usage metadata record when present.
|
|
146
|
+
*/
|
|
147
|
+
private getFirstUsageMetadata;
|
|
148
|
+
/**
|
|
149
|
+
* Finds the model name returned by the provider, when available.
|
|
150
|
+
*
|
|
151
|
+
* @param output LangChain LLM output.
|
|
152
|
+
* @returns Provider response model name when present.
|
|
153
|
+
*/
|
|
154
|
+
private getResponseModel;
|
|
155
|
+
/**
|
|
156
|
+
* Counts all generated candidates in a LangChain result.
|
|
157
|
+
*
|
|
158
|
+
* @param output LangChain LLM output.
|
|
159
|
+
* @returns Total generation candidate count.
|
|
160
|
+
*/
|
|
161
|
+
private countGenerations;
|
|
162
|
+
/**
|
|
163
|
+
* Converts batches of LangChain messages to flat normalized messages.
|
|
164
|
+
*
|
|
165
|
+
* @param messageBatches LangChain message batches.
|
|
166
|
+
* @returns Flat list of normalized prompt messages.
|
|
167
|
+
*/
|
|
168
|
+
private normalizeMessageBatches;
|
|
169
|
+
/**
|
|
170
|
+
* Converts a LangChain message to a GenAI-compatible role/content pair.
|
|
171
|
+
*
|
|
172
|
+
* @param message LangChain base message.
|
|
173
|
+
* @returns Normalized message.
|
|
174
|
+
*/
|
|
175
|
+
private normalizeMessage;
|
|
176
|
+
/**
|
|
177
|
+
* Converts LangChain message types to GenAI/OpenAI role names.
|
|
178
|
+
*
|
|
179
|
+
* @param type LangChain message type.
|
|
180
|
+
* @returns Role name for telemetry.
|
|
181
|
+
*/
|
|
182
|
+
private mapMessageRole;
|
|
183
|
+
/**
|
|
184
|
+
* Extracts a normalized chat generation message when one exists.
|
|
185
|
+
*
|
|
186
|
+
* @param generation LangChain generation candidate.
|
|
187
|
+
* @returns Normalized generation message when present.
|
|
188
|
+
*/
|
|
189
|
+
private getGenerationMessage;
|
|
190
|
+
/**
|
|
191
|
+
* Extracts a message-shaped value from a LangChain generation.
|
|
192
|
+
*
|
|
193
|
+
* @param generation LangChain generation candidate.
|
|
194
|
+
* @returns Message-shaped record when present.
|
|
195
|
+
*/
|
|
196
|
+
private getMessageLike;
|
|
197
|
+
/**
|
|
198
|
+
* Infers a provider name from serialized model metadata.
|
|
199
|
+
*
|
|
200
|
+
* @param llm Serialized LangChain model metadata.
|
|
201
|
+
* @param modelName Model name used for the request.
|
|
202
|
+
* @returns Provider name for GenAI attributes.
|
|
203
|
+
*/
|
|
204
|
+
private inferProvider;
|
|
205
|
+
/**
|
|
206
|
+
* Sets a span attribute when the value is OpenTelemetry-compatible.
|
|
207
|
+
*
|
|
208
|
+
* @param span Span receiving the attribute.
|
|
209
|
+
* @param key Attribute key.
|
|
210
|
+
* @param value Attribute value.
|
|
211
|
+
*/
|
|
212
|
+
private setAttributeIfSupported;
|
|
213
|
+
/**
|
|
214
|
+
* Emits a debug trace lifecycle log when debug mode is enabled.
|
|
215
|
+
*
|
|
216
|
+
* @param event Trace lifecycle event name.
|
|
217
|
+
* @param span Span associated with the lifecycle event.
|
|
218
|
+
* @param payload Additional event fields.
|
|
219
|
+
*/
|
|
220
|
+
private debugLog;
|
|
221
|
+
/**
|
|
222
|
+
* Converts arbitrary values to supported OpenTelemetry attribute values.
|
|
223
|
+
*
|
|
224
|
+
* @param value Unknown source value.
|
|
225
|
+
* @returns OpenTelemetry attribute value when representable.
|
|
226
|
+
*/
|
|
227
|
+
private toAttributeValue;
|
|
228
|
+
/**
|
|
229
|
+
* Reads an object-valued property from a record-like value.
|
|
230
|
+
*
|
|
231
|
+
* @param value Record-like source value.
|
|
232
|
+
* @param key Property key.
|
|
233
|
+
* @returns Nested record when present.
|
|
234
|
+
*/
|
|
235
|
+
private getRecord;
|
|
236
|
+
/**
|
|
237
|
+
* Reads a string-valued property from a record-like value.
|
|
238
|
+
*
|
|
239
|
+
* @param value Record-like source value.
|
|
240
|
+
* @param key Property key.
|
|
241
|
+
* @returns String property when present.
|
|
242
|
+
*/
|
|
82
243
|
private getString;
|
|
244
|
+
/**
|
|
245
|
+
* Reads a number-valued property from a record-like value.
|
|
246
|
+
*
|
|
247
|
+
* @param value Record-like source value.
|
|
248
|
+
* @param key Property key.
|
|
249
|
+
* @returns Number property when present.
|
|
250
|
+
*/
|
|
251
|
+
private getNumber;
|
|
252
|
+
/**
|
|
253
|
+
* Normalizes unknown errors to Error instances.
|
|
254
|
+
*
|
|
255
|
+
* @param error Unknown error value.
|
|
256
|
+
* @returns Error instance.
|
|
257
|
+
*/
|
|
83
258
|
private toError;
|
|
84
259
|
}
|
|
85
260
|
/**
|
|
@@ -11,7 +11,9 @@ export class ModunaLangChainCallbackHandler extends BaseCallbackHandler {
|
|
|
11
11
|
* Stable handler name used by LangChain callback manager de-duplication.
|
|
12
12
|
*/
|
|
13
13
|
name = "moduna_otel_langchain_callback_handler";
|
|
14
|
+
debug;
|
|
14
15
|
defaultTraceContext;
|
|
16
|
+
logger;
|
|
15
17
|
runs = new Map();
|
|
16
18
|
/**
|
|
17
19
|
* Creates a LangChain callback handler for Moduna spans.
|
|
@@ -20,7 +22,9 @@ export class ModunaLangChainCallbackHandler extends BaseCallbackHandler {
|
|
|
20
22
|
*/
|
|
21
23
|
constructor(config = {}) {
|
|
22
24
|
super();
|
|
25
|
+
this.debug = config.debug ?? false;
|
|
23
26
|
this.defaultTraceContext = config.traceContext ?? {};
|
|
27
|
+
this.logger = config.logger ?? console;
|
|
24
28
|
}
|
|
25
29
|
/**
|
|
26
30
|
* Starts a span for a LangChain chat model run.
|
|
@@ -37,7 +41,8 @@ export class ModunaLangChainCallbackHandler extends BaseCallbackHandler {
|
|
|
37
41
|
handleChatModelStart(llm, messages, runId, parentRunId, extraParams, tags, metadata, runName) {
|
|
38
42
|
this.startRun({
|
|
39
43
|
extraParams,
|
|
40
|
-
inputCount: messages.length,
|
|
44
|
+
inputCount: messages.flat().length,
|
|
45
|
+
inputMessages: this.normalizeMessageBatches(messages),
|
|
41
46
|
llm,
|
|
42
47
|
metadata,
|
|
43
48
|
parentRunId,
|
|
@@ -63,6 +68,10 @@ export class ModunaLangChainCallbackHandler extends BaseCallbackHandler {
|
|
|
63
68
|
this.startRun({
|
|
64
69
|
extraParams,
|
|
65
70
|
inputCount: prompts.length,
|
|
71
|
+
inputMessages: prompts.map((prompt) => ({
|
|
72
|
+
content: prompt,
|
|
73
|
+
role: "user",
|
|
74
|
+
})),
|
|
66
75
|
llm,
|
|
67
76
|
metadata,
|
|
68
77
|
parentRunId,
|
|
@@ -80,7 +89,21 @@ export class ModunaLangChainCallbackHandler extends BaseCallbackHandler {
|
|
|
80
89
|
* @param runId LangChain run identifier.
|
|
81
90
|
*/
|
|
82
91
|
handleLLMNewToken(_token, _idx, runId) {
|
|
83
|
-
this.runs.get(runId)
|
|
92
|
+
const run = this.runs.get(runId);
|
|
93
|
+
if (!run) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
run.streamedTokenCount += 1;
|
|
97
|
+
run.span.addEvent("gen_ai.content.completion", {
|
|
98
|
+
content: _token,
|
|
99
|
+
role: "assistant",
|
|
100
|
+
});
|
|
101
|
+
this.debugLog("token", run.span, {
|
|
102
|
+
runId,
|
|
103
|
+
runType: run.runType,
|
|
104
|
+
streamedTokenCount: run.streamedTokenCount,
|
|
105
|
+
tokenLength: _token.length,
|
|
106
|
+
});
|
|
84
107
|
}
|
|
85
108
|
/**
|
|
86
109
|
* Ends the span for a successful LangChain run.
|
|
@@ -94,6 +117,17 @@ export class ModunaLangChainCallbackHandler extends BaseCallbackHandler {
|
|
|
94
117
|
return;
|
|
95
118
|
}
|
|
96
119
|
run.span.setAttribute("langchain.output.generations", output.generations.length);
|
|
120
|
+
run.span.setAttribute("langchain.output.candidates", this.countGenerations(output));
|
|
121
|
+
this.applyCompletionAttributes(run.span, output);
|
|
122
|
+
this.applyUsageAttributes(run.span, output, run.streamedTokenCount);
|
|
123
|
+
this.debugLog("end", run.span, {
|
|
124
|
+
candidateCount: this.countGenerations(output),
|
|
125
|
+
generationCount: output.generations.length,
|
|
126
|
+
runId,
|
|
127
|
+
runType: run.runType,
|
|
128
|
+
streamedTokenCount: run.streamedTokenCount,
|
|
129
|
+
usage: this.extractUsage(output),
|
|
130
|
+
});
|
|
97
131
|
run.span.setStatus({ code: SpanStatusCode.OK });
|
|
98
132
|
run.span.end();
|
|
99
133
|
this.runs.delete(runId);
|
|
@@ -109,10 +143,17 @@ export class ModunaLangChainCallbackHandler extends BaseCallbackHandler {
|
|
|
109
143
|
if (!run) {
|
|
110
144
|
return;
|
|
111
145
|
}
|
|
112
|
-
|
|
146
|
+
const normalizedError = this.toError(error);
|
|
147
|
+
run.span.recordException(normalizedError);
|
|
113
148
|
run.span.setStatus({
|
|
114
149
|
code: SpanStatusCode.ERROR,
|
|
115
|
-
message:
|
|
150
|
+
message: normalizedError.message,
|
|
151
|
+
});
|
|
152
|
+
this.debugLog("error", run.span, {
|
|
153
|
+
errorName: normalizedError.name,
|
|
154
|
+
errorMessage: normalizedError.message,
|
|
155
|
+
runId,
|
|
156
|
+
runType: run.runType,
|
|
116
157
|
});
|
|
117
158
|
run.span.end();
|
|
118
159
|
this.runs.delete(runId);
|
|
@@ -127,25 +168,66 @@ export class ModunaLangChainCallbackHandler extends BaseCallbackHandler {
|
|
|
127
168
|
span.setAttribute("langchain.run.id", input.runId);
|
|
128
169
|
span.setAttribute("langchain.run.type", input.runType);
|
|
129
170
|
span.setAttribute("langchain.input.count", input.inputCount);
|
|
171
|
+
span.setAttribute("langsmith.span.kind", "llm");
|
|
172
|
+
span.setAttribute("gen_ai.operation.name", input.runType === "chat_model" ? "chat" : "completion");
|
|
173
|
+
span.setAttribute("llm.request.type", input.runType === "chat_model" ? "chat" : "completion");
|
|
130
174
|
if (input.parentRunId) {
|
|
131
175
|
span.setAttribute("langchain.parent_run.id", input.parentRunId);
|
|
132
176
|
}
|
|
177
|
+
if (input.runName) {
|
|
178
|
+
span.setAttribute("langsmith.trace.name", input.runName);
|
|
179
|
+
}
|
|
133
180
|
if (input.tags?.length) {
|
|
134
181
|
span.setAttribute("langchain.tags", input.tags.join(","));
|
|
182
|
+
span.setAttribute("langsmith.span.tags", input.tags.join(","));
|
|
135
183
|
}
|
|
136
184
|
this.applyModelAttributes(span, input.llm, input.extraParams);
|
|
185
|
+
this.applyPromptAttributes(span, input.inputMessages);
|
|
186
|
+
this.applyInvocationAttributes(span, input.extraParams);
|
|
137
187
|
this.applyTraceContext(span, input.metadata);
|
|
138
|
-
this.runs.set(input.runId, {
|
|
188
|
+
this.runs.set(input.runId, {
|
|
189
|
+
runType: input.runType,
|
|
190
|
+
span,
|
|
191
|
+
streamedTokenCount: 0,
|
|
192
|
+
});
|
|
193
|
+
this.debugLog("start", span, {
|
|
194
|
+
inputCount: input.inputCount,
|
|
195
|
+
parentRunId: input.parentRunId,
|
|
196
|
+
runId: input.runId,
|
|
197
|
+
runName: input.runName,
|
|
198
|
+
runType: input.runType,
|
|
199
|
+
tags: input.tags,
|
|
200
|
+
});
|
|
139
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* Applies model/provider attributes using LangChain serialization metadata.
|
|
204
|
+
*
|
|
205
|
+
* @param span Span receiving model attributes.
|
|
206
|
+
* @param llm Serialized LangChain model metadata.
|
|
207
|
+
* @param extraParams Provider-specific invocation parameters.
|
|
208
|
+
*/
|
|
140
209
|
applyModelAttributes(span, llm, extraParams) {
|
|
141
|
-
const
|
|
142
|
-
|
|
210
|
+
const invocationParams = this.getRecord(extraParams, "invocation_params");
|
|
211
|
+
const modelName = this.getString(invocationParams, "model") ??
|
|
212
|
+
this.getString(invocationParams, "modelName") ??
|
|
213
|
+
this.getString(invocationParams, "model_name") ??
|
|
143
214
|
this.getString(llm, "name") ??
|
|
144
215
|
llm.id.join(".");
|
|
145
|
-
|
|
216
|
+
const provider = this.getString(invocationParams, "model_provider") ??
|
|
217
|
+
this.getString(invocationParams, "provider") ??
|
|
218
|
+
this.inferProvider(llm, modelName);
|
|
219
|
+
span.setAttribute("gen_ai.system", provider);
|
|
146
220
|
span.setAttribute("gen_ai.request.model", modelName);
|
|
221
|
+
span.setAttribute("llm.model_name", modelName);
|
|
222
|
+
span.setAttribute("metadata.ls_model_name", modelName);
|
|
147
223
|
span.setAttribute("langchain.serialized.id", llm.id.join("."));
|
|
148
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Applies Moduna conversation and session identifiers to a span.
|
|
227
|
+
*
|
|
228
|
+
* @param span Span receiving trace context attributes.
|
|
229
|
+
* @param metadata LangChain metadata supplied on the run.
|
|
230
|
+
*/
|
|
149
231
|
applyTraceContext(span, metadata) {
|
|
150
232
|
const traceContext = {
|
|
151
233
|
conversationId: this.getString(metadata, "conversationId") ??
|
|
@@ -157,11 +239,378 @@ export class ModunaLangChainCallbackHandler extends BaseCallbackHandler {
|
|
|
157
239
|
};
|
|
158
240
|
if (traceContext.conversationId) {
|
|
159
241
|
span.setAttribute("moduna.conversation.id", traceContext.conversationId);
|
|
242
|
+
span.setAttribute("langsmith.metadata.conversation_id", traceContext.conversationId);
|
|
160
243
|
}
|
|
161
244
|
if (traceContext.sessionId) {
|
|
162
245
|
span.setAttribute("moduna.session.id", traceContext.sessionId);
|
|
246
|
+
span.setAttribute("langsmith.metadata.session_id", traceContext.sessionId);
|
|
247
|
+
span.setAttribute("langsmith.trace.session_id", traceContext.sessionId);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Applies prompt input attributes in LangSmith-compatible GenAI format.
|
|
252
|
+
*
|
|
253
|
+
* @param span Span receiving prompt attributes.
|
|
254
|
+
* @param messages Normalized prompt messages.
|
|
255
|
+
*/
|
|
256
|
+
applyPromptAttributes(span, messages) {
|
|
257
|
+
if (messages.length === 0) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
for (const [index, message] of messages.entries()) {
|
|
261
|
+
span.setAttribute(`gen_ai.prompt.${index}.role`, message.role);
|
|
262
|
+
span.setAttribute(`gen_ai.prompt.${index}.content`, message.content);
|
|
263
|
+
}
|
|
264
|
+
span.setAttribute("gen_ai.input.messages", JSON.stringify(messages));
|
|
265
|
+
span.addEvent("gen_ai.content.prompt", {
|
|
266
|
+
content: JSON.stringify(messages),
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Applies request parameter attributes in GenAI semantic format.
|
|
271
|
+
*
|
|
272
|
+
* @param span Span receiving invocation parameter attributes.
|
|
273
|
+
* @param extraParams Provider-specific invocation parameters.
|
|
274
|
+
*/
|
|
275
|
+
applyInvocationAttributes(span, extraParams) {
|
|
276
|
+
const invocationParams = this.getRecord(extraParams, "invocation_params");
|
|
277
|
+
if (!invocationParams) {
|
|
278
|
+
return;
|
|
163
279
|
}
|
|
280
|
+
const mappings = [
|
|
281
|
+
["temperature", "gen_ai.request.temperature"],
|
|
282
|
+
["top_p", "gen_ai.request.top_p"],
|
|
283
|
+
["max_tokens", "gen_ai.request.max_tokens"],
|
|
284
|
+
["maxOutputTokens", "gen_ai.request.max_tokens"],
|
|
285
|
+
["frequency_penalty", "gen_ai.request.frequency_penalty"],
|
|
286
|
+
["presence_penalty", "gen_ai.request.presence_penalty"],
|
|
287
|
+
["seed", "gen_ai.request.seed"],
|
|
288
|
+
["stop", "gen_ai.request.stop_sequences"],
|
|
289
|
+
["stop_sequences", "gen_ai.request.stop_sequences"],
|
|
290
|
+
["top_k", "gen_ai.request.top_k"],
|
|
291
|
+
["encoding_formats", "gen_ai.request.encoding_formats"],
|
|
292
|
+
["tools", "tools"],
|
|
293
|
+
];
|
|
294
|
+
for (const [sourceKey, attributeKey] of mappings) {
|
|
295
|
+
this.setAttributeIfSupported(span, attributeKey, invocationParams[sourceKey]);
|
|
296
|
+
}
|
|
297
|
+
span.setAttribute("llm.invocation_parameters", JSON.stringify(invocationParams));
|
|
164
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* Applies model completion output attributes from a LangChain result.
|
|
301
|
+
*
|
|
302
|
+
* @param span Span receiving completion attributes.
|
|
303
|
+
* @param output LangChain LLM output.
|
|
304
|
+
*/
|
|
305
|
+
applyCompletionAttributes(span, output) {
|
|
306
|
+
const messages = [];
|
|
307
|
+
for (const generationGroup of output.generations) {
|
|
308
|
+
for (const generation of generationGroup) {
|
|
309
|
+
const message = this.getGenerationMessage(generation);
|
|
310
|
+
messages.push(message ?? {
|
|
311
|
+
content: generation.text,
|
|
312
|
+
role: "assistant",
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
for (const [index, message] of messages.entries()) {
|
|
317
|
+
span.setAttribute(`gen_ai.completion.${index}.role`, message.role);
|
|
318
|
+
span.setAttribute(`gen_ai.completion.${index}.content`, message.content);
|
|
319
|
+
}
|
|
320
|
+
if (messages.length > 0) {
|
|
321
|
+
span.setAttribute("gen_ai.output.messages", JSON.stringify(messages));
|
|
322
|
+
span.addEvent("gen_ai.content.completion", {
|
|
323
|
+
content: JSON.stringify(messages),
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
const responseModel = this.getResponseModel(output);
|
|
327
|
+
if (responseModel) {
|
|
328
|
+
span.setAttribute("gen_ai.response.model", responseModel);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Applies token usage attributes from a LangChain result.
|
|
333
|
+
*
|
|
334
|
+
* @param span Span receiving usage attributes.
|
|
335
|
+
* @param output LangChain LLM output.
|
|
336
|
+
* @param streamedTokenCount Token count observed from streaming callbacks.
|
|
337
|
+
*/
|
|
338
|
+
applyUsageAttributes(span, output, streamedTokenCount) {
|
|
339
|
+
const usage = this.extractUsage(output);
|
|
340
|
+
if (usage.promptTokens !== undefined) {
|
|
341
|
+
span.setAttribute("gen_ai.usage.input_tokens", usage.promptTokens);
|
|
342
|
+
span.setAttribute("gen_ai.usage.prompt_tokens", usage.promptTokens);
|
|
343
|
+
span.setAttribute("llm.token_count.prompt", usage.promptTokens);
|
|
344
|
+
}
|
|
345
|
+
const completionTokens = usage.completionTokens ??
|
|
346
|
+
(streamedTokenCount > 0 ? streamedTokenCount : undefined);
|
|
347
|
+
if (completionTokens !== undefined) {
|
|
348
|
+
span.setAttribute("gen_ai.usage.output_tokens", completionTokens);
|
|
349
|
+
span.setAttribute("gen_ai.usage.completion_tokens", completionTokens);
|
|
350
|
+
span.setAttribute("llm.token_count.completion", completionTokens);
|
|
351
|
+
}
|
|
352
|
+
if (usage.totalTokens !== undefined) {
|
|
353
|
+
span.setAttribute("gen_ai.usage.total_tokens", usage.totalTokens);
|
|
354
|
+
span.setAttribute("llm.token_count.total", usage.totalTokens);
|
|
355
|
+
span.setAttribute("llm.usage.total_tokens", usage.totalTokens);
|
|
356
|
+
}
|
|
357
|
+
if (usage.reasoningTokens !== undefined) {
|
|
358
|
+
span.setAttribute("gen_ai.usage.details.reasoning_tokens", usage.reasoningTokens);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Extracts token usage from LangChain result metadata and provider outputs.
|
|
363
|
+
*
|
|
364
|
+
* @param output LangChain LLM output.
|
|
365
|
+
* @returns Normalized token usage when present.
|
|
366
|
+
*/
|
|
367
|
+
extractUsage(output) {
|
|
368
|
+
const usageMetadata = this.getFirstUsageMetadata(output);
|
|
369
|
+
const tokenUsage = this.getRecord(output.llmOutput, "tokenUsage");
|
|
370
|
+
const estimatedTokenUsage = this.getRecord(output.llmOutput, "estimatedTokenUsage");
|
|
371
|
+
return {
|
|
372
|
+
completionTokens: this.getNumber(usageMetadata, "output_tokens") ??
|
|
373
|
+
this.getNumber(tokenUsage, "completionTokens") ??
|
|
374
|
+
this.getNumber(tokenUsage, "completion_tokens") ??
|
|
375
|
+
this.getNumber(estimatedTokenUsage, "completionTokens"),
|
|
376
|
+
promptTokens: this.getNumber(usageMetadata, "input_tokens") ??
|
|
377
|
+
this.getNumber(tokenUsage, "promptTokens") ??
|
|
378
|
+
this.getNumber(tokenUsage, "prompt_tokens") ??
|
|
379
|
+
this.getNumber(estimatedTokenUsage, "promptTokens"),
|
|
380
|
+
reasoningTokens: this.getNumber(this.getRecord(usageMetadata, "output_token_details"), "reasoning"),
|
|
381
|
+
totalTokens: this.getNumber(usageMetadata, "total_tokens") ??
|
|
382
|
+
this.getNumber(tokenUsage, "totalTokens") ??
|
|
383
|
+
this.getNumber(tokenUsage, "total_tokens") ??
|
|
384
|
+
this.getNumber(estimatedTokenUsage, "totalTokens"),
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Finds the first message usage metadata in a LangChain result.
|
|
389
|
+
*
|
|
390
|
+
* @param output LangChain LLM output.
|
|
391
|
+
* @returns Usage metadata record when present.
|
|
392
|
+
*/
|
|
393
|
+
getFirstUsageMetadata(output) {
|
|
394
|
+
for (const generationGroup of output.generations) {
|
|
395
|
+
for (const generation of generationGroup) {
|
|
396
|
+
const message = this.getMessageLike(generation);
|
|
397
|
+
const usageMetadata = this.getRecord(message, "usage_metadata");
|
|
398
|
+
if (usageMetadata) {
|
|
399
|
+
return usageMetadata;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return undefined;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Finds the model name returned by the provider, when available.
|
|
407
|
+
*
|
|
408
|
+
* @param output LangChain LLM output.
|
|
409
|
+
* @returns Provider response model name when present.
|
|
410
|
+
*/
|
|
411
|
+
getResponseModel(output) {
|
|
412
|
+
for (const generationGroup of output.generations) {
|
|
413
|
+
for (const generation of generationGroup) {
|
|
414
|
+
const message = this.getMessageLike(generation);
|
|
415
|
+
const responseMetadata = this.getRecord(message, "response_metadata");
|
|
416
|
+
const model = this.getString(responseMetadata, "model_name") ??
|
|
417
|
+
this.getString(responseMetadata, "model") ??
|
|
418
|
+
this.getString(output.llmOutput, "model");
|
|
419
|
+
if (model) {
|
|
420
|
+
return model;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return this.getString(output.llmOutput, "model");
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Counts all generated candidates in a LangChain result.
|
|
428
|
+
*
|
|
429
|
+
* @param output LangChain LLM output.
|
|
430
|
+
* @returns Total generation candidate count.
|
|
431
|
+
*/
|
|
432
|
+
countGenerations(output) {
|
|
433
|
+
return output.generations.reduce((count, generationGroup) => count + generationGroup.length, 0);
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Converts batches of LangChain messages to flat normalized messages.
|
|
437
|
+
*
|
|
438
|
+
* @param messageBatches LangChain message batches.
|
|
439
|
+
* @returns Flat list of normalized prompt messages.
|
|
440
|
+
*/
|
|
441
|
+
normalizeMessageBatches(messageBatches) {
|
|
442
|
+
return messageBatches.flatMap((messages) => messages.map((message) => this.normalizeMessage(message)));
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Converts a LangChain message to a GenAI-compatible role/content pair.
|
|
446
|
+
*
|
|
447
|
+
* @param message LangChain base message.
|
|
448
|
+
* @returns Normalized message.
|
|
449
|
+
*/
|
|
450
|
+
normalizeMessage(message) {
|
|
451
|
+
return {
|
|
452
|
+
content: typeof message.content === "string"
|
|
453
|
+
? message.content
|
|
454
|
+
: JSON.stringify(message.content),
|
|
455
|
+
role: this.mapMessageRole(message.type),
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Converts LangChain message types to GenAI/OpenAI role names.
|
|
460
|
+
*
|
|
461
|
+
* @param type LangChain message type.
|
|
462
|
+
* @returns Role name for telemetry.
|
|
463
|
+
*/
|
|
464
|
+
mapMessageRole(type) {
|
|
465
|
+
const roleMap = {
|
|
466
|
+
ai: "assistant",
|
|
467
|
+
human: "user",
|
|
468
|
+
system: "system",
|
|
469
|
+
tool: "tool",
|
|
470
|
+
};
|
|
471
|
+
return roleMap[type] ?? type;
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Extracts a normalized chat generation message when one exists.
|
|
475
|
+
*
|
|
476
|
+
* @param generation LangChain generation candidate.
|
|
477
|
+
* @returns Normalized generation message when present.
|
|
478
|
+
*/
|
|
479
|
+
getGenerationMessage(generation) {
|
|
480
|
+
const message = this.getMessageLike(generation);
|
|
481
|
+
if (!message) {
|
|
482
|
+
return undefined;
|
|
483
|
+
}
|
|
484
|
+
const content = message.content;
|
|
485
|
+
const type = this.getString(message, "type");
|
|
486
|
+
if (typeof content !== "string" && !Array.isArray(content)) {
|
|
487
|
+
return undefined;
|
|
488
|
+
}
|
|
489
|
+
return {
|
|
490
|
+
content: typeof content === "string" ? content : JSON.stringify(content),
|
|
491
|
+
role: this.mapMessageRole(type ?? "assistant"),
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Extracts a message-shaped value from a LangChain generation.
|
|
496
|
+
*
|
|
497
|
+
* @param generation LangChain generation candidate.
|
|
498
|
+
* @returns Message-shaped record when present.
|
|
499
|
+
*/
|
|
500
|
+
getMessageLike(generation) {
|
|
501
|
+
return this.getRecord(generation, "message");
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Infers a provider name from serialized model metadata.
|
|
505
|
+
*
|
|
506
|
+
* @param llm Serialized LangChain model metadata.
|
|
507
|
+
* @param modelName Model name used for the request.
|
|
508
|
+
* @returns Provider name for GenAI attributes.
|
|
509
|
+
*/
|
|
510
|
+
inferProvider(llm, modelName) {
|
|
511
|
+
const serializedId = llm.id.join(".").toLowerCase();
|
|
512
|
+
const model = modelName.toLowerCase();
|
|
513
|
+
if (serializedId.includes("google") || model.includes("gemini")) {
|
|
514
|
+
return "google";
|
|
515
|
+
}
|
|
516
|
+
if (serializedId.includes("openai") || model.includes("gpt")) {
|
|
517
|
+
return "openai";
|
|
518
|
+
}
|
|
519
|
+
if (serializedId.includes("anthropic") || model.includes("claude")) {
|
|
520
|
+
return "anthropic";
|
|
521
|
+
}
|
|
522
|
+
return llm.id.at(-1) ?? "unknown";
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Sets a span attribute when the value is OpenTelemetry-compatible.
|
|
526
|
+
*
|
|
527
|
+
* @param span Span receiving the attribute.
|
|
528
|
+
* @param key Attribute key.
|
|
529
|
+
* @param value Attribute value.
|
|
530
|
+
*/
|
|
531
|
+
setAttributeIfSupported(span, key, value) {
|
|
532
|
+
const attributeValue = this.toAttributeValue(value);
|
|
533
|
+
if (attributeValue !== undefined) {
|
|
534
|
+
span.setAttribute(key, attributeValue);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Emits a debug trace lifecycle log when debug mode is enabled.
|
|
539
|
+
*
|
|
540
|
+
* @param event Trace lifecycle event name.
|
|
541
|
+
* @param span Span associated with the lifecycle event.
|
|
542
|
+
* @param payload Additional event fields.
|
|
543
|
+
*/
|
|
544
|
+
debugLog(event, span, payload) {
|
|
545
|
+
if (!this.debug) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
const spanContext = span.spanContext();
|
|
549
|
+
const logPayload = {
|
|
550
|
+
event,
|
|
551
|
+
spanId: spanContext.spanId,
|
|
552
|
+
traceId: spanContext.traceId,
|
|
553
|
+
...payload,
|
|
554
|
+
};
|
|
555
|
+
if (event === "error") {
|
|
556
|
+
this.logger.error("[moduna:langchain]", logPayload);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
this.logger.debug("[moduna:langchain]", logPayload);
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Converts arbitrary values to supported OpenTelemetry attribute values.
|
|
563
|
+
*
|
|
564
|
+
* @param value Unknown source value.
|
|
565
|
+
* @returns OpenTelemetry attribute value when representable.
|
|
566
|
+
*/
|
|
567
|
+
toAttributeValue(value) {
|
|
568
|
+
if (typeof value === "string" ||
|
|
569
|
+
typeof value === "number" ||
|
|
570
|
+
typeof value === "boolean") {
|
|
571
|
+
return value;
|
|
572
|
+
}
|
|
573
|
+
if (Array.isArray(value)) {
|
|
574
|
+
if (value.every((item) => typeof item === "string")) {
|
|
575
|
+
return value;
|
|
576
|
+
}
|
|
577
|
+
if (value.every((item) => typeof item === "number")) {
|
|
578
|
+
return value;
|
|
579
|
+
}
|
|
580
|
+
if (value.every((item) => typeof item === "boolean")) {
|
|
581
|
+
return value;
|
|
582
|
+
}
|
|
583
|
+
return JSON.stringify(value);
|
|
584
|
+
}
|
|
585
|
+
if (value && typeof value === "object") {
|
|
586
|
+
return JSON.stringify(value);
|
|
587
|
+
}
|
|
588
|
+
return undefined;
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Reads an object-valued property from a record-like value.
|
|
592
|
+
*
|
|
593
|
+
* @param value Record-like source value.
|
|
594
|
+
* @param key Property key.
|
|
595
|
+
* @returns Nested record when present.
|
|
596
|
+
*/
|
|
597
|
+
getRecord(value, key) {
|
|
598
|
+
if (!value || typeof value !== "object") {
|
|
599
|
+
return undefined;
|
|
600
|
+
}
|
|
601
|
+
const record = value;
|
|
602
|
+
const result = record[key];
|
|
603
|
+
return result && typeof result === "object" && !Array.isArray(result)
|
|
604
|
+
? result
|
|
605
|
+
: undefined;
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Reads a string-valued property from a record-like value.
|
|
609
|
+
*
|
|
610
|
+
* @param value Record-like source value.
|
|
611
|
+
* @param key Property key.
|
|
612
|
+
* @returns String property when present.
|
|
613
|
+
*/
|
|
165
614
|
getString(value, key) {
|
|
166
615
|
if (!value || typeof value !== "object") {
|
|
167
616
|
return undefined;
|
|
@@ -170,6 +619,27 @@ export class ModunaLangChainCallbackHandler extends BaseCallbackHandler {
|
|
|
170
619
|
const result = record[key];
|
|
171
620
|
return typeof result === "string" ? result : undefined;
|
|
172
621
|
}
|
|
622
|
+
/**
|
|
623
|
+
* Reads a number-valued property from a record-like value.
|
|
624
|
+
*
|
|
625
|
+
* @param value Record-like source value.
|
|
626
|
+
* @param key Property key.
|
|
627
|
+
* @returns Number property when present.
|
|
628
|
+
*/
|
|
629
|
+
getNumber(value, key) {
|
|
630
|
+
if (!value || typeof value !== "object") {
|
|
631
|
+
return undefined;
|
|
632
|
+
}
|
|
633
|
+
const record = value;
|
|
634
|
+
const result = record[key];
|
|
635
|
+
return typeof result === "number" ? result : undefined;
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Normalizes unknown errors to Error instances.
|
|
639
|
+
*
|
|
640
|
+
* @param error Unknown error value.
|
|
641
|
+
* @returns Error instance.
|
|
642
|
+
*/
|
|
173
643
|
toError(error) {
|
|
174
644
|
return error instanceof Error ? error : new Error(String(error));
|
|
175
645
|
}
|
|
@@ -2,6 +2,7 @@ import type { AttributeValue } from "@opentelemetry/api";
|
|
|
2
2
|
import type { ModunaOTELConfig } from "../interface/ModunaOTELConfig.js";
|
|
3
3
|
import type { TraceCallback } from "../types/TraceCallback.js";
|
|
4
4
|
import type { ModunaTelemetryMetadata, ModunaTraceContext } from "../types/TraceContext.js";
|
|
5
|
+
import type { ModunaLangChainCallbackHandlerConfig } from "./ModunaLangChainCallbackHandler.js";
|
|
5
6
|
import { ModunaLangChainCallbackHandler } from "./ModunaLangChainCallbackHandler.js";
|
|
6
7
|
/**
|
|
7
8
|
* Vercel AI SDK compatible telemetry settings.
|
|
@@ -58,16 +59,18 @@ export declare class ModunaOTEL {
|
|
|
58
59
|
* Creates a LangChain callback handler for per-call usage.
|
|
59
60
|
*
|
|
60
61
|
* @param context Default conversation or session identifiers.
|
|
62
|
+
* @param config Optional LangChain handler settings.
|
|
61
63
|
* @returns LangChain callback handler that emits Moduna spans.
|
|
62
64
|
*/
|
|
63
|
-
langChainHandler(context?: ModunaTraceContext): ModunaLangChainCallbackHandler;
|
|
65
|
+
langChainHandler(context?: ModunaTraceContext, config?: Omit<ModunaLangChainCallbackHandlerConfig, "traceContext">): ModunaLangChainCallbackHandler;
|
|
64
66
|
/**
|
|
65
67
|
* Registers a LangChain callback handler for all LangChain runs.
|
|
66
68
|
*
|
|
67
69
|
* @param context Default conversation or session identifiers.
|
|
70
|
+
* @param config Optional LangChain handler settings.
|
|
68
71
|
* @returns The globally registered LangChain callback handler.
|
|
69
72
|
*/
|
|
70
|
-
registerGlobalLangChainHandler(context?: ModunaTraceContext): ModunaLangChainCallbackHandler;
|
|
73
|
+
registerGlobalLangChainHandler(context?: ModunaTraceContext, config?: Omit<ModunaLangChainCallbackHandlerConfig, "traceContext">): ModunaLangChainCallbackHandler;
|
|
71
74
|
/**
|
|
72
75
|
* Instruments a callback with a Moduna span.
|
|
73
76
|
*
|
|
@@ -133,10 +133,12 @@ export class ModunaOTEL {
|
|
|
133
133
|
* Creates a LangChain callback handler for per-call usage.
|
|
134
134
|
*
|
|
135
135
|
* @param context Default conversation or session identifiers.
|
|
136
|
+
* @param config Optional LangChain handler settings.
|
|
136
137
|
* @returns LangChain callback handler that emits Moduna spans.
|
|
137
138
|
*/
|
|
138
|
-
langChainHandler(context = {}) {
|
|
139
|
+
langChainHandler(context = {}, config = {}) {
|
|
139
140
|
return new ModunaLangChainCallbackHandler({
|
|
141
|
+
...config,
|
|
140
142
|
traceContext: context,
|
|
141
143
|
});
|
|
142
144
|
}
|
|
@@ -144,10 +146,11 @@ export class ModunaOTEL {
|
|
|
144
146
|
* Registers a LangChain callback handler for all LangChain runs.
|
|
145
147
|
*
|
|
146
148
|
* @param context Default conversation or session identifiers.
|
|
149
|
+
* @param config Optional LangChain handler settings.
|
|
147
150
|
* @returns The globally registered LangChain callback handler.
|
|
148
151
|
*/
|
|
149
|
-
registerGlobalLangChainHandler(context = {}) {
|
|
150
|
-
const handler = this.langChainHandler(context);
|
|
152
|
+
registerGlobalLangChainHandler(context = {}, config = {}) {
|
|
153
|
+
const handler = this.langChainHandler(context, config);
|
|
151
154
|
registerGlobalModunaLangChainHandler(handler);
|
|
152
155
|
return handler;
|
|
153
156
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moduna/otel",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "One-line OpenTelemetry setup for Moduna AI traces.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,10 +35,32 @@
|
|
|
35
35
|
"@opentelemetry/exporter-trace-otlp-http": "^0.218.0",
|
|
36
36
|
"@opentelemetry/resources": "^2.7.1",
|
|
37
37
|
"@opentelemetry/sdk-node": "^0.218.0",
|
|
38
|
-
"@opentelemetry/semantic-conventions": "^1.41.1"
|
|
38
|
+
"@opentelemetry/semantic-conventions": "^1.41.1"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@ai-sdk/google": "^3.0.75",
|
|
42
|
+
"@langchain/google-genai": "^2.1.30",
|
|
43
|
+
"ai": "^6.0.184",
|
|
39
44
|
"langchain": "^1.4.0",
|
|
40
45
|
"openai": "^6.38.0"
|
|
41
46
|
},
|
|
47
|
+
"peerDependenciesMeta": {
|
|
48
|
+
"@ai-sdk/google": {
|
|
49
|
+
"optional": true
|
|
50
|
+
},
|
|
51
|
+
"@langchain/google-genai": {
|
|
52
|
+
"optional": true
|
|
53
|
+
},
|
|
54
|
+
"ai": {
|
|
55
|
+
"optional": true
|
|
56
|
+
},
|
|
57
|
+
"langchain": {
|
|
58
|
+
"optional": true
|
|
59
|
+
},
|
|
60
|
+
"openai": {
|
|
61
|
+
"optional": true
|
|
62
|
+
}
|
|
63
|
+
},
|
|
42
64
|
"devDependencies": {
|
|
43
65
|
"@ai-sdk/google": "^3.0.75",
|
|
44
66
|
"@changesets/cli": "^2.31.0",
|