@traccia2/sdk 0.0.1
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/LICENSE +23 -0
- package/README.md +503 -0
- package/dist/auto.d.ts +27 -0
- package/dist/auto.d.ts.map +1 -0
- package/dist/auto.js +171 -0
- package/dist/auto.js.map +1 -0
- package/dist/config/env-config.d.ts +21 -0
- package/dist/config/env-config.d.ts.map +1 -0
- package/dist/config/env-config.js +111 -0
- package/dist/config/env-config.js.map +1 -0
- package/dist/config/pricing-config.d.ts +27 -0
- package/dist/config/pricing-config.d.ts.map +1 -0
- package/dist/config/pricing-config.js +74 -0
- package/dist/config/pricing-config.js.map +1 -0
- package/dist/config/runtime-config.d.ts +65 -0
- package/dist/config/runtime-config.d.ts.map +1 -0
- package/dist/config/runtime-config.js +97 -0
- package/dist/config/runtime-config.js.map +1 -0
- package/dist/context/context.d.ts +29 -0
- package/dist/context/context.d.ts.map +1 -0
- package/dist/context/context.js +48 -0
- package/dist/context/context.js.map +1 -0
- package/dist/exporter/console-exporter.d.ts +18 -0
- package/dist/exporter/console-exporter.d.ts.map +1 -0
- package/dist/exporter/console-exporter.js +39 -0
- package/dist/exporter/console-exporter.js.map +1 -0
- package/dist/exporter/http-exporter.d.ts +57 -0
- package/dist/exporter/http-exporter.d.ts.map +1 -0
- package/dist/exporter/http-exporter.js +181 -0
- package/dist/exporter/http-exporter.js.map +1 -0
- package/dist/exporter/index.d.ts +7 -0
- package/dist/exporter/index.d.ts.map +1 -0
- package/dist/exporter/index.js +12 -0
- package/dist/exporter/index.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/index.d.ts +9 -0
- package/dist/integrations/index.d.ts.map +1 -0
- package/dist/integrations/index.js +16 -0
- package/dist/integrations/index.js.map +1 -0
- package/dist/integrations/langchain-callback.d.ts +72 -0
- package/dist/integrations/langchain-callback.d.ts.map +1 -0
- package/dist/integrations/langchain-callback.js +201 -0
- package/dist/integrations/langchain-callback.js.map +1 -0
- package/dist/integrations/langgraph-instrumentation.d.ts +57 -0
- package/dist/integrations/langgraph-instrumentation.d.ts.map +1 -0
- package/dist/integrations/langgraph-instrumentation.js +162 -0
- package/dist/integrations/langgraph-instrumentation.js.map +1 -0
- package/dist/processor/batch-processor.d.ts +68 -0
- package/dist/processor/batch-processor.d.ts.map +1 -0
- package/dist/processor/batch-processor.js +150 -0
- package/dist/processor/batch-processor.js.map +1 -0
- package/dist/processor/cost-processor.d.ts +16 -0
- package/dist/processor/cost-processor.d.ts.map +1 -0
- package/dist/processor/cost-processor.js +50 -0
- package/dist/processor/cost-processor.js.map +1 -0
- package/dist/processor/index.d.ts +9 -0
- package/dist/processor/index.d.ts.map +1 -0
- package/dist/processor/index.js +18 -0
- package/dist/processor/index.js.map +1 -0
- package/dist/processor/logging-processor.d.ts +13 -0
- package/dist/processor/logging-processor.d.ts.map +1 -0
- package/dist/processor/logging-processor.js +26 -0
- package/dist/processor/logging-processor.js.map +1 -0
- package/dist/processor/sampler.d.ts +20 -0
- package/dist/processor/sampler.d.ts.map +1 -0
- package/dist/processor/sampler.js +33 -0
- package/dist/processor/sampler.js.map +1 -0
- package/dist/processor/token-counter.d.ts +13 -0
- package/dist/processor/token-counter.d.ts.map +1 -0
- package/dist/processor/token-counter.js +40 -0
- package/dist/processor/token-counter.js.map +1 -0
- package/dist/tracer/index.d.ts +8 -0
- package/dist/tracer/index.d.ts.map +1 -0
- package/dist/tracer/index.js +15 -0
- package/dist/tracer/index.js.map +1 -0
- package/dist/tracer/provider.d.ts +59 -0
- package/dist/tracer/provider.d.ts.map +1 -0
- package/dist/tracer/provider.js +114 -0
- package/dist/tracer/provider.js.map +1 -0
- package/dist/tracer/span-context.d.ts +23 -0
- package/dist/tracer/span-context.d.ts.map +1 -0
- package/dist/tracer/span-context.js +34 -0
- package/dist/tracer/span-context.js.map +1 -0
- package/dist/tracer/span.d.ts +49 -0
- package/dist/tracer/span.d.ts.map +1 -0
- package/dist/tracer/span.js +118 -0
- package/dist/tracer/span.js.map +1 -0
- package/dist/tracer/tracer.d.ts +28 -0
- package/dist/tracer/tracer.d.ts.map +1 -0
- package/dist/tracer/tracer.js +75 -0
- package/dist/tracer/tracer.js.map +1 -0
- package/dist/types.d.ts +135 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/package.json +79 -0
- package/src/__tests__/exporter.test.ts +62 -0
- package/src/__tests__/integrations-langchain.test.ts +384 -0
- package/src/__tests__/integrations-langgraph.test.ts +479 -0
- package/src/__tests__/processor.test.ts +89 -0
- package/src/__tests__/span.test.ts +103 -0
- package/src/__tests__/tracer.test.ts +89 -0
- package/src/auto.ts +198 -0
- package/src/config/env-config.ts +93 -0
- package/src/config/pricing-config.ts +84 -0
- package/src/config/runtime-config.ts +108 -0
- package/src/context/context.ts +52 -0
- package/src/exporter/console-exporter.ts +38 -0
- package/src/exporter/http-exporter.ts +188 -0
- package/src/exporter/index.ts +7 -0
- package/src/index.ts +51 -0
- package/src/integrations/README.md +287 -0
- package/src/integrations/index.ts +13 -0
- package/src/integrations/langchain-callback.ts +229 -0
- package/src/integrations/langgraph-instrumentation.ts +174 -0
- package/src/processor/batch-processor.ts +180 -0
- package/src/processor/cost-processor.ts +57 -0
- package/src/processor/index.ts +9 -0
- package/src/processor/logging-processor.ts +26 -0
- package/src/processor/sampler.ts +35 -0
- package/src/processor/token-counter.ts +42 -0
- package/src/tracer/index.ts +8 -0
- package/src/tracer/provider.ts +130 -0
- package/src/tracer/span-context.ts +46 -0
- package/src/tracer/span.ts +145 -0
- package/src/tracer/tracer.ts +100 -0
- package/src/types.ts +155 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LangChain callback handler for automatic tracing.
|
|
3
|
+
* Integrates with LangChain's callback system to automatically instrument
|
|
4
|
+
* LLM calls, chains, agents, and tools.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ISpan } from '../types';
|
|
8
|
+
import { getTracer } from '../auto';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* LangChain Callback Handler for Traccia SDK.
|
|
12
|
+
* Automatically traces LLM calls, chains, agents, and tools.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* import { ChatOpenAI } from 'langchain/chat_models/openai';
|
|
16
|
+
* import { LLMChain } from 'langchain/chains';
|
|
17
|
+
* import { TraciaCallbackHandler } from '@traccia/sdk/integrations/langchain';
|
|
18
|
+
*
|
|
19
|
+
* const handler = new TraciaCallbackHandler();
|
|
20
|
+
* const chain = new LLMChain({
|
|
21
|
+
* llm: new ChatOpenAI(),
|
|
22
|
+
* callbacks: [handler],
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* await chain.run({ input: 'What is 2+2?' });
|
|
26
|
+
* // Automatically traced with spans for LLM, chain execution, tokens, etc.
|
|
27
|
+
*/
|
|
28
|
+
export class TraciaCallbackHandler {
|
|
29
|
+
private tracer = getTracer('langchain');
|
|
30
|
+
private spanStack: Map<string, ISpan> = new Map();
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Handle LLM start - called when an LLM begins execution.
|
|
34
|
+
*/
|
|
35
|
+
public async handleLLMStart(
|
|
36
|
+
llm: any,
|
|
37
|
+
prompts: string[],
|
|
38
|
+
runId: string,
|
|
39
|
+
_parentRunId?: string
|
|
40
|
+
): Promise<void> {
|
|
41
|
+
const span = this.tracer.startSpan('llm', {
|
|
42
|
+
attributes: {
|
|
43
|
+
type: 'llm',
|
|
44
|
+
model: llm.name || llm._modelType || 'unknown',
|
|
45
|
+
prompt_count: prompts.length,
|
|
46
|
+
first_prompt_length: prompts[0]?.length || 0,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
this.spanStack.set(runId, span);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Handle LLM end - called when an LLM finishes execution.
|
|
55
|
+
*/
|
|
56
|
+
public async handleLLMEnd(output: any, runId: string): Promise<void> {
|
|
57
|
+
const span = this.spanStack.get(runId);
|
|
58
|
+
if (span) {
|
|
59
|
+
try {
|
|
60
|
+
const tokenUsage = output?.llmOutput?.token_usage || output?.token_usage;
|
|
61
|
+
if (tokenUsage) {
|
|
62
|
+
span.setAttribute('prompt_tokens', tokenUsage.prompt_tokens);
|
|
63
|
+
span.setAttribute('completion_tokens', tokenUsage.completion_tokens);
|
|
64
|
+
span.setAttribute(
|
|
65
|
+
'total_tokens',
|
|
66
|
+
(tokenUsage.prompt_tokens || 0) + (tokenUsage.completion_tokens || 0)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (output?.text) {
|
|
71
|
+
span.setAttribute('output_length', output.text.length);
|
|
72
|
+
}
|
|
73
|
+
} catch (error) {
|
|
74
|
+
// Silently fail on attribute setting
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
span.end();
|
|
78
|
+
this.spanStack.delete(runId);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Handle LLM error.
|
|
84
|
+
*/
|
|
85
|
+
public async handleLLMError(error: Error, runId: string): Promise<void> {
|
|
86
|
+
const span = this.spanStack.get(runId);
|
|
87
|
+
if (span) {
|
|
88
|
+
span.recordException(error, { source: 'langchain-llm' });
|
|
89
|
+
span.end();
|
|
90
|
+
this.spanStack.delete(runId);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Handle chain start - called when a chain begins execution.
|
|
96
|
+
*/
|
|
97
|
+
public async handleChainStart(
|
|
98
|
+
chain: any,
|
|
99
|
+
inputs: any,
|
|
100
|
+
runId: string,
|
|
101
|
+
_parentRunId?: string
|
|
102
|
+
): Promise<void> {
|
|
103
|
+
const chainName = chain.name || chain._chainType || 'chain';
|
|
104
|
+
const span = this.tracer.startSpan(`chain:${chainName}`, {
|
|
105
|
+
attributes: {
|
|
106
|
+
type: 'chain',
|
|
107
|
+
chain_name: chainName,
|
|
108
|
+
chain_type: chain._chainType,
|
|
109
|
+
input_keys: Object.keys(inputs).join(','),
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
this.spanStack.set(runId, span);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Handle chain end - called when a chain finishes execution.
|
|
118
|
+
*/
|
|
119
|
+
public async handleChainEnd(output: any, runId: string): Promise<void> {
|
|
120
|
+
const span = this.spanStack.get(runId);
|
|
121
|
+
if (span) {
|
|
122
|
+
try {
|
|
123
|
+
if (output) {
|
|
124
|
+
const outputStr = typeof output === 'string' ? output : JSON.stringify(output);
|
|
125
|
+
span.setAttribute('output_length', outputStr.length);
|
|
126
|
+
}
|
|
127
|
+
} catch (error) {
|
|
128
|
+
// Silently fail
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
span.end();
|
|
132
|
+
this.spanStack.delete(runId);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Handle chain error.
|
|
138
|
+
*/
|
|
139
|
+
public async handleChainError(error: Error, runId: string): Promise<void> {
|
|
140
|
+
const span = this.spanStack.get(runId);
|
|
141
|
+
if (span) {
|
|
142
|
+
span.recordException(error, { source: 'langchain-chain' });
|
|
143
|
+
span.end();
|
|
144
|
+
this.spanStack.delete(runId);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Handle tool start - called when a tool is invoked.
|
|
150
|
+
*/
|
|
151
|
+
public async handleToolStart(
|
|
152
|
+
tool: any,
|
|
153
|
+
input: string,
|
|
154
|
+
runId: string,
|
|
155
|
+
_parentRunId?: string
|
|
156
|
+
): Promise<void> {
|
|
157
|
+
const toolName = tool.name || 'unknown-tool';
|
|
158
|
+
const span = this.tracer.startSpan(`tool:${toolName}`, {
|
|
159
|
+
attributes: {
|
|
160
|
+
type: 'tool',
|
|
161
|
+
tool_name: toolName,
|
|
162
|
+
tool_description: tool.description,
|
|
163
|
+
input_length: typeof input === 'string' ? input.length : 0,
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
this.spanStack.set(runId, span);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Handle tool end - called when a tool finishes execution.
|
|
172
|
+
*/
|
|
173
|
+
public async handleToolEnd(output: string, runId: string): Promise<void> {
|
|
174
|
+
const span = this.spanStack.get(runId);
|
|
175
|
+
if (span) {
|
|
176
|
+
try {
|
|
177
|
+
span.setAttribute('output_length', output?.length || 0);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
// Silently fail
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
span.end();
|
|
183
|
+
this.spanStack.delete(runId);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Handle tool error.
|
|
189
|
+
*/
|
|
190
|
+
public async handleToolError(error: Error, runId: string): Promise<void> {
|
|
191
|
+
const span = this.spanStack.get(runId);
|
|
192
|
+
if (span) {
|
|
193
|
+
span.recordException(error, { source: 'langchain-tool' });
|
|
194
|
+
span.end();
|
|
195
|
+
this.spanStack.delete(runId);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Handle agent action.
|
|
201
|
+
*/
|
|
202
|
+
public async handleAgentAction(action: any, runId: string): Promise<void> {
|
|
203
|
+
const span = this.spanStack.get(runId);
|
|
204
|
+
if (span) {
|
|
205
|
+
try {
|
|
206
|
+
span.setAttribute('agent_action', action.tool);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
// Silently fail
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Handle agent finish.
|
|
215
|
+
*/
|
|
216
|
+
public async handleAgentFinish(finish: any, runId: string): Promise<void> {
|
|
217
|
+
const span = this.spanStack.get(runId);
|
|
218
|
+
if (span) {
|
|
219
|
+
try {
|
|
220
|
+
span.setAttribute('agent_finish_output', JSON.stringify(finish.returnValues));
|
|
221
|
+
} catch (error) {
|
|
222
|
+
// Silently fail
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
span.end();
|
|
226
|
+
this.spanStack.delete(runId);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LangGraph instrumentation for automatic tracing.
|
|
3
|
+
* Provides utilities to instrument LangGraph state graphs for automatic tracing.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getTracer } from '../auto';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Instrument a LangGraph to automatically trace execution.
|
|
10
|
+
* Wraps graph invocation to create spans for the entire graph execution.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* import { StateGraph } from 'langchain/graph';
|
|
14
|
+
* import { instrumentLangGraph } from '@traccia/sdk/integrations/langgraph';
|
|
15
|
+
*
|
|
16
|
+
* const graph = new StateGraph(AgentState)
|
|
17
|
+
* .addNode('agent', agentNode)
|
|
18
|
+
* .addNode('tools', toolsNode)
|
|
19
|
+
* .addEdge('agent', 'tools')
|
|
20
|
+
* .addEdge('tools', 'agent');
|
|
21
|
+
*
|
|
22
|
+
* const instrumented = instrumentLangGraph(graph, { graphName: 'my-agent' });
|
|
23
|
+
* const compiled = instrumented.compile();
|
|
24
|
+
*
|
|
25
|
+
* await compiled.invoke({ messages: [...] });
|
|
26
|
+
* // Automatically traced with span for graph execution
|
|
27
|
+
*/
|
|
28
|
+
export function instrumentLangGraph(graph: any, options?: { graphName?: string }): any {
|
|
29
|
+
const tracer = getTracer('langgraph');
|
|
30
|
+
const graphName = options?.graphName || graph.graph_name || 'langgraph';
|
|
31
|
+
|
|
32
|
+
// Store original compile method
|
|
33
|
+
const originalCompile = graph.compile.bind(graph);
|
|
34
|
+
|
|
35
|
+
// Override compile to wrap invocation
|
|
36
|
+
graph.compile = function() {
|
|
37
|
+
const compiled = originalCompile();
|
|
38
|
+
const originalInvoke = compiled.invoke.bind(compiled);
|
|
39
|
+
const originalStream = compiled.stream?.bind(compiled);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Wrap invoke calls with tracing.
|
|
43
|
+
*/
|
|
44
|
+
compiled.invoke = async function(input: any, config?: any) {
|
|
45
|
+
const span = tracer.startSpan('langgraph-invoke', {
|
|
46
|
+
attributes: {
|
|
47
|
+
graph_name: graphName,
|
|
48
|
+
config_thread_id: config?.configurable?.thread_id,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const result = await originalInvoke(input, config);
|
|
54
|
+
span.end();
|
|
55
|
+
return result;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
span.recordException(error as Error, { context: 'langgraph-invoke' });
|
|
58
|
+
span.end();
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Wrap stream calls with tracing.
|
|
65
|
+
*/
|
|
66
|
+
if (originalStream) {
|
|
67
|
+
compiled.stream = async function*(input: any, config?: any) {
|
|
68
|
+
const span = tracer.startSpan('langgraph-stream', {
|
|
69
|
+
attributes: {
|
|
70
|
+
graph_name: graphName,
|
|
71
|
+
config_thread_id: config?.configurable?.thread_id,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
let eventCount = 0;
|
|
76
|
+
try {
|
|
77
|
+
for await (const event of originalStream(input, config)) {
|
|
78
|
+
eventCount++;
|
|
79
|
+
yield event;
|
|
80
|
+
}
|
|
81
|
+
span.setAttribute('stream_events', eventCount);
|
|
82
|
+
span.end();
|
|
83
|
+
} catch (error) {
|
|
84
|
+
span.recordException(error as Error, { context: 'langgraph-stream' });
|
|
85
|
+
span.setAttribute('stream_events', eventCount);
|
|
86
|
+
span.end();
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return compiled;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return graph;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Create a traced node function wrapper for LangGraph nodes.
|
|
100
|
+
* Automatically wraps node execution with tracing spans.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* import { createTracedNode } from '@traccia/sdk/integrations/langgraph';
|
|
104
|
+
*
|
|
105
|
+
* const agentNode = createTracedNode('agent', async (state) => {
|
|
106
|
+
* // Your agent logic here
|
|
107
|
+
* return { messages: [...] };
|
|
108
|
+
* });
|
|
109
|
+
*
|
|
110
|
+
* graph.addNode('agent', agentNode);
|
|
111
|
+
*/
|
|
112
|
+
export function createTracedNode(
|
|
113
|
+
nodeName: string,
|
|
114
|
+
nodeFunc: (state: any) => Promise<any>
|
|
115
|
+
): (state: any) => Promise<any> {
|
|
116
|
+
const tracer = getTracer('langgraph');
|
|
117
|
+
|
|
118
|
+
return async function tracedNode(state: any) {
|
|
119
|
+
const span = tracer.startSpan(`node:${nodeName}`, {
|
|
120
|
+
attributes: {
|
|
121
|
+
node_name: nodeName,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const result = await nodeFunc(state);
|
|
127
|
+
span.end();
|
|
128
|
+
return result;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
span.recordException(error as Error, { node: nodeName });
|
|
131
|
+
span.end();
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Create a traced conditional edge function for LangGraph.
|
|
139
|
+
* Wraps conditional routing logic with tracing.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* import { createTracedConditional } from '@traccia/sdk/integrations/langgraph';
|
|
143
|
+
*
|
|
144
|
+
* const shouldContinue = createTracedConditional('should_continue', (state) => {
|
|
145
|
+
* return state.messages.length > 10 ? 'end' : 'continue';
|
|
146
|
+
* });
|
|
147
|
+
*
|
|
148
|
+
* graph.addConditionalEdges('agent', shouldContinue);
|
|
149
|
+
*/
|
|
150
|
+
export function createTracedConditional(
|
|
151
|
+
conditionName: string,
|
|
152
|
+
conditionFunc: (state: any) => string
|
|
153
|
+
): (state: any) => string {
|
|
154
|
+
const tracer = getTracer('langgraph');
|
|
155
|
+
|
|
156
|
+
return function tracedConditional(state: any) {
|
|
157
|
+
const span = tracer.startSpan(`condition:${conditionName}`, {
|
|
158
|
+
attributes: {
|
|
159
|
+
condition_name: conditionName,
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const result = conditionFunc(state);
|
|
165
|
+
span.setAttribute('condition_result', result);
|
|
166
|
+
span.end();
|
|
167
|
+
return result;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
span.recordException(error as Error, { condition: conditionName });
|
|
170
|
+
span.end();
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Batch span processor with queue management and background flush.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ISpan, ISpanProcessor, ISpanExporter } from '../types';
|
|
6
|
+
import { ISampler } from '../types';
|
|
7
|
+
|
|
8
|
+
const DEFAULT_MAX_QUEUE_SIZE = 5000;
|
|
9
|
+
const DEFAULT_MAX_EXPORT_BATCH_SIZE = 512;
|
|
10
|
+
const DEFAULT_SCHEDULE_DELAY_MS = 5000;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Drop policy for handling queue overflow.
|
|
14
|
+
*/
|
|
15
|
+
export enum DropPolicy {
|
|
16
|
+
DROP_OLDEST = 'drop_oldest',
|
|
17
|
+
DROP_NEWEST = 'drop_newest',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Batch span processor.
|
|
22
|
+
*/
|
|
23
|
+
export class BatchSpanProcessor implements ISpanProcessor {
|
|
24
|
+
private exporter?: ISpanExporter;
|
|
25
|
+
private maxQueueSize: number;
|
|
26
|
+
private maxExportBatchSize: number;
|
|
27
|
+
private scheduleDelayMs: number;
|
|
28
|
+
private dropPolicy: DropPolicy;
|
|
29
|
+
private sampler?: ISampler;
|
|
30
|
+
|
|
31
|
+
private queue: ISpan[] = [];
|
|
32
|
+
private _shutdown = false;
|
|
33
|
+
private timer?: NodeJS.Timeout;
|
|
34
|
+
private processing = false;
|
|
35
|
+
|
|
36
|
+
constructor(options: {
|
|
37
|
+
exporter?: ISpanExporter;
|
|
38
|
+
maxQueueSize?: number;
|
|
39
|
+
maxExportBatchSize?: number;
|
|
40
|
+
scheduleDelayMs?: number;
|
|
41
|
+
dropPolicy?: DropPolicy;
|
|
42
|
+
sampler?: ISampler;
|
|
43
|
+
} = {}) {
|
|
44
|
+
this.exporter = options.exporter;
|
|
45
|
+
this.maxQueueSize = options.maxQueueSize || DEFAULT_MAX_QUEUE_SIZE;
|
|
46
|
+
this.maxExportBatchSize = options.maxExportBatchSize || DEFAULT_MAX_EXPORT_BATCH_SIZE;
|
|
47
|
+
this.scheduleDelayMs = options.scheduleDelayMs || DEFAULT_SCHEDULE_DELAY_MS;
|
|
48
|
+
this.dropPolicy = options.dropPolicy || DropPolicy.DROP_OLDEST;
|
|
49
|
+
this.sampler = options.sampler;
|
|
50
|
+
|
|
51
|
+
this.startSchedule();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Handle span end.
|
|
56
|
+
*/
|
|
57
|
+
onEnd(span: ISpan): void {
|
|
58
|
+
if (this._shutdown) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Apply head-based sampling: drop non-sampled traces
|
|
63
|
+
if (this.sampler && span.context.traceFlags === 0) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
this.enqueue(span);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Force flush pending spans.
|
|
72
|
+
*/
|
|
73
|
+
async forceFlush(timeout?: number): Promise<void> {
|
|
74
|
+
const deadline = timeout ? Date.now() + timeout : undefined;
|
|
75
|
+
|
|
76
|
+
// eslint-disable-next-line no-constant-condition
|
|
77
|
+
while (true) {
|
|
78
|
+
const flushed = await this.flushOnce(deadline);
|
|
79
|
+
if (!flushed || (deadline && Date.now() >= deadline)) {
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Shutdown the processor.
|
|
87
|
+
*/
|
|
88
|
+
async shutdown(): Promise<void> {
|
|
89
|
+
this._shutdown = true;
|
|
90
|
+
if (this.timer) {
|
|
91
|
+
clearTimeout(this.timer);
|
|
92
|
+
}
|
|
93
|
+
await this.forceFlush();
|
|
94
|
+
if (this.exporter) {
|
|
95
|
+
await this.exporter.shutdown();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Enqueue a span.
|
|
101
|
+
*/
|
|
102
|
+
private enqueue(span: ISpan): void {
|
|
103
|
+
if (this.queue.length >= this.maxQueueSize) {
|
|
104
|
+
if (this.dropPolicy === DropPolicy.DROP_OLDEST) {
|
|
105
|
+
this.queue.shift();
|
|
106
|
+
} else {
|
|
107
|
+
return; // Drop newest
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
this.queue.push(span);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Flush once (export one batch).
|
|
115
|
+
*/
|
|
116
|
+
private async flushOnce(deadline?: number): Promise<boolean> {
|
|
117
|
+
if (this.processing) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (deadline && Date.now() >= deadline) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const batch = this.drainQueue(this.maxExportBatchSize);
|
|
126
|
+
if (batch.length === 0) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
await this.export(batch);
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Drain queue.
|
|
136
|
+
*/
|
|
137
|
+
private drainQueue(limit: number): ISpan[] {
|
|
138
|
+
const batch: ISpan[] = [];
|
|
139
|
+
while (batch.length < limit && this.queue.length > 0) {
|
|
140
|
+
const span = this.queue.shift();
|
|
141
|
+
if (span) {
|
|
142
|
+
batch.push(span);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return batch;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Export spans.
|
|
150
|
+
*/
|
|
151
|
+
private async export(spans: ISpan[]): Promise<void> {
|
|
152
|
+
if (!this.exporter) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
this.processing = true;
|
|
157
|
+
try {
|
|
158
|
+
await this.exporter.export(spans);
|
|
159
|
+
} catch {
|
|
160
|
+
// Export errors are swallowed for resilience
|
|
161
|
+
} finally {
|
|
162
|
+
this.processing = false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Start the background scheduling.
|
|
168
|
+
*/
|
|
169
|
+
private startSchedule(): void {
|
|
170
|
+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
171
|
+
this.timer = setInterval(async () => {
|
|
172
|
+
await this.flushOnce();
|
|
173
|
+
}, this.scheduleDelayMs);
|
|
174
|
+
|
|
175
|
+
// Ensure the timer doesn't keep the process alive
|
|
176
|
+
if (this.timer.unref) {
|
|
177
|
+
this.timer.unref();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cost annotation processor.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ISpan, ISpanProcessor } from '../types';
|
|
6
|
+
import { PricingTable } from '../config/pricing-config';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Cost annotation processor.
|
|
10
|
+
*/
|
|
11
|
+
export class CostAnnotatingProcessor implements ISpanProcessor {
|
|
12
|
+
private pricingTable: PricingTable;
|
|
13
|
+
|
|
14
|
+
constructor(pricingTable: PricingTable) {
|
|
15
|
+
this.pricingTable = pricingTable;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
onEnd(span: ISpan): void {
|
|
19
|
+
try {
|
|
20
|
+
const model = span.attributes['model'] as string | undefined;
|
|
21
|
+
if (!model) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const pricing = this.pricingTable[model];
|
|
26
|
+
if (!pricing) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const inputTokens = span.attributes['input_tokens'] as number | undefined;
|
|
31
|
+
const outputTokens = span.attributes['output_tokens'] as number | undefined;
|
|
32
|
+
|
|
33
|
+
let cost = 0;
|
|
34
|
+
if (inputTokens) {
|
|
35
|
+
cost += (inputTokens / 1000) * pricing.inputCost;
|
|
36
|
+
}
|
|
37
|
+
if (outputTokens) {
|
|
38
|
+
cost += (outputTokens / 1000) * pricing.outputCost;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (cost > 0) {
|
|
42
|
+
// Direct attribute modification for processors (after span ends)
|
|
43
|
+
span.attributes['cost_usd'] = cost;
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
// Silently fail
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async shutdown(): Promise<void> {
|
|
51
|
+
// No-op
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async forceFlush(): Promise<void> {
|
|
55
|
+
// No-op
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Processor module exports.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { Sampler } from './sampler';
|
|
6
|
+
export { BatchSpanProcessor, DropPolicy } from './batch-processor';
|
|
7
|
+
export { TokenCountingProcessor } from './token-counter';
|
|
8
|
+
export { CostAnnotatingProcessor } from './cost-processor';
|
|
9
|
+
export { LoggingSpanProcessor } from './logging-processor';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logging span processor for debugging.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ISpan, ISpanProcessor } from '../types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Logging span processor.
|
|
9
|
+
*/
|
|
10
|
+
export class LoggingSpanProcessor implements ISpanProcessor {
|
|
11
|
+
onEnd(span: ISpan): void {
|
|
12
|
+
console.log(`[Span] ${span.name} (${span.context.traceId})`, {
|
|
13
|
+
spanId: span.context.spanId,
|
|
14
|
+
duration: span.durationNs,
|
|
15
|
+
attributes: span.attributes,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async shutdown(): Promise<void> {
|
|
20
|
+
// No-op
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async forceFlush(): Promise<void> {
|
|
24
|
+
// No-op
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sampler for making sampling decisions.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ISampler, SamplingResult } from '../types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Probability-based sampler.
|
|
9
|
+
*/
|
|
10
|
+
export class Sampler implements ISampler {
|
|
11
|
+
private sampleRate: number;
|
|
12
|
+
|
|
13
|
+
constructor(sampleRate: number = 1.0) {
|
|
14
|
+
if (sampleRate < 0 || sampleRate > 1) {
|
|
15
|
+
throw new Error('sampleRate must be between 0.0 and 1.0');
|
|
16
|
+
}
|
|
17
|
+
this.sampleRate = sampleRate;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Decide whether to sample.
|
|
22
|
+
*/
|
|
23
|
+
shouldSample(): SamplingResult {
|
|
24
|
+
return {
|
|
25
|
+
sampled: Math.random() <= this.sampleRate,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get the current sample rate.
|
|
31
|
+
*/
|
|
32
|
+
getSampleRate(): number {
|
|
33
|
+
return this.sampleRate;
|
|
34
|
+
}
|
|
35
|
+
}
|