@cloudbase/agent-observability 1.0.1-alpha.16 → 1.0.1-alpha.18

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.
@@ -26,7 +26,7 @@ import {
26
26
  type ObservationTool,
27
27
  type Observation,
28
28
  type ObservationAttributes,
29
- } from "../index.js";
29
+ } from "../index";
30
30
  import type { SpanContext } from "@opentelemetry/api";
31
31
  import { type Logger, noopLogger } from "@cloudbase/agent-shared";
32
32
 
@@ -104,6 +104,9 @@ export class CallbackHandler extends BaseCallbackHandler {
104
104
  // External parent context from AG-UI.Server span
105
105
  private externalParentSpanContext?: SpanContext;
106
106
 
107
+ // External metadata from AG-UI.Server (e.g., threadId, runId)
108
+ private externalMetadata?: Record<string, string>;
109
+
107
110
  // Adapter name for ROOT span prefix
108
111
  private adapterName?: string;
109
112
 
@@ -130,10 +133,12 @@ export class CallbackHandler extends BaseCallbackHandler {
130
133
  * to the server-level span, creating a unified trace hierarchy.
131
134
  *
132
135
  * @param spanContext - SpanContext from the AG-UI.Server span
136
+ * @param metadata - Optional metadata from server (e.g., threadId, runId)
133
137
  * @public
134
138
  */
135
- setExternalParentContext(spanContext: SpanContext): void {
139
+ setExternalParentContext(spanContext: SpanContext, metadata?: Record<string, string>): void {
136
140
  this.externalParentSpanContext = spanContext;
141
+ this.externalMetadata = metadata;
137
142
  }
138
143
 
139
144
  async handleLLMNewToken(
@@ -199,7 +204,7 @@ export class CallbackHandler extends BaseCallbackHandler {
199
204
  parentRunId,
200
205
  runId,
201
206
  tags,
202
- metadata,
207
+ metadata: this.joinTagsAndMetaData(tags, metadata),
203
208
  attributes: {
204
209
  input: finalInput,
205
210
  },
@@ -328,6 +333,8 @@ export class CallbackHandler extends BaseCallbackHandler {
328
333
  }
329
334
 
330
335
  let extractedModelName: string | undefined;
336
+ let extractedSystem: string | undefined;
337
+ let extractedProvider: string | undefined;
331
338
  if (extraParams) {
332
339
  const invocationParamsModelName = (
333
340
  extraParams.invocation_params as InvocationParams
@@ -338,6 +345,11 @@ export class CallbackHandler extends BaseCallbackHandler {
338
345
  : undefined;
339
346
 
340
347
  extractedModelName = invocationParamsModelName ?? metadataModelName;
348
+
349
+ // Extract provider from metadata (e.g., "ls_provider" from LangChain)
350
+ if (metadata && "ls_provider" in metadata) {
351
+ extractedProvider = metadata["ls_provider"] as string;
352
+ }
341
353
  }
342
354
 
343
355
  const registeredPrompt = this.promptToParentRunMap.get(
@@ -479,7 +491,7 @@ export class CallbackHandler extends BaseCallbackHandler {
479
491
  attributes: {
480
492
  input,
481
493
  },
482
- metadata,
494
+ metadata: this.joinTagsAndMetaData(tags, metadata),
483
495
  tags,
484
496
  asType: "tool",
485
497
  });
@@ -507,7 +519,7 @@ export class CallbackHandler extends BaseCallbackHandler {
507
519
  input: query,
508
520
  },
509
521
  tags,
510
- metadata,
522
+ metadata: this.joinTagsAndMetaData(tags, metadata),
511
523
  asType: "span",
512
524
  });
513
525
  } catch (e) {
@@ -736,12 +748,19 @@ export class CallbackHandler extends BaseCallbackHandler {
736
748
  finalRunName = `Adapter.${this.adapterName}`;
737
749
  }
738
750
 
751
+ // Add agui.thread_id and agui.run_id to ALL spans if external metadata available
752
+ // This ensures consistent tagging across the entire trace hierarchy
753
+ const serverMetadata = this.externalMetadata
754
+ ? { "agui.thread_id": this.externalMetadata.threadId, "agui.run_id": this.externalMetadata.runId }
755
+ : {};
756
+
739
757
  const observation = startObservation(
740
758
  finalRunName,
741
759
  {
742
760
  version: this.version,
743
761
  metadata: this.joinTagsAndMetaData(tags, metadata),
744
762
  ...attributes,
763
+ ...serverMetadata,
745
764
  },
746
765
  {
747
766
  asType: asType ?? "span",
@@ -765,6 +784,21 @@ export class CallbackHandler extends BaseCallbackHandler {
765
784
  return;
766
785
  }
767
786
 
787
+ // Check if this is an error and set span status accordingly
788
+ const level = attributes.level as string | undefined;
789
+ const statusMessage = attributes.statusMessage as string | undefined;
790
+ if (level === "ERROR") {
791
+ try {
792
+ const { SpanStatusCode } = require("@opentelemetry/api");
793
+ observation.otelSpan.setStatus({
794
+ code: SpanStatusCode.ERROR,
795
+ message: statusMessage || "Unknown error",
796
+ });
797
+ } catch (e) {
798
+ this.logger.debug?.("Failed to set span error status:", e);
799
+ }
800
+ }
801
+
768
802
  // Type-safe update: cast to ObservationAttributes which is the union of all observation attribute types
769
803
  observation.update(attributes as ObservationAttributes).end();
770
804
 
@@ -797,7 +831,10 @@ export class CallbackHandler extends BaseCallbackHandler {
797
831
  return;
798
832
  }
799
833
 
800
- const reservedKeys = ["promptInfo", "userId", "sessionId"];
834
+ // Filter out keys that should not be in metadata:
835
+ // - thread_id, run_id: Become agui.thread_id, agui.run_id at span level
836
+ // - promptInfo, userId, sessionId: Reserved for internal use
837
+ const reservedKeys = ["promptInfo", "userId", "sessionId", "thread_id", "runId", "run_id"];
801
838
 
802
839
  return Object.fromEntries(
803
840
  Object.entries(metadata).filter(([key, _]) => !reservedKeys.includes(key))
@@ -893,4 +930,5 @@ export class CallbackHandler extends BaseCallbackHandler {
893
930
 
894
931
  return response;
895
932
  }
933
+
896
934
  }
@@ -4,4 +4,4 @@
4
4
  * @module
5
5
  */
6
6
 
7
- export { CallbackHandler } from "./CallbackHandler.js";
7
+ export { CallbackHandler } from "./CallbackHandler";
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Custom console exporter that outputs single-line JSON.
3
+ *
4
+ * This exporter outputs spans as single-line JSON for easier parsing
5
+ * with line-based tools (grep, jq, etc.).
6
+ *
7
+ * To switch back to standard multi-line output, use ConsoleSpanExporter instead.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * // Single-line (current default)
12
+ * const exporter = new SingleLineConsoleSpanExporter();
13
+ *
14
+ * // Multi-line (standard OTel)
15
+ * import { ConsoleSpanExporter } from '@opentelemetry/sdk-trace-base';
16
+ * const exporter = new ConsoleSpanExporter();
17
+ * ```
18
+ *
19
+ * @packageDocumentation
20
+ */
21
+
22
+ import type { SpanExporter, ReadableSpan } from '@opentelemetry/sdk-trace-base';
23
+
24
+ /**
25
+ * Export result codes.
26
+ */
27
+ export enum ExportResultCode {
28
+ SUCCESS = 0,
29
+ FAILED = 1,
30
+ }
31
+
32
+ /**
33
+ * Export result interface.
34
+ */
35
+ export interface ExportResult {
36
+ code: ExportResultCode;
37
+ error?: Error;
38
+ }
39
+
40
+ /**
41
+ * Custom console exporter that outputs single-line JSON.
42
+ *
43
+ * This exporter outputs spans as single-line JSON for easier parsing
44
+ * with line-based tools (grep, jq, etc.).
45
+ */
46
+ export class SingleLineConsoleSpanExporter implements SpanExporter {
47
+ /**
48
+ * Export spans as single-line JSON.
49
+ */
50
+ export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void {
51
+ try {
52
+ for (const span of spans) {
53
+ const spanDict = this.spanToDict(span);
54
+ // Single-line JSON output
55
+ const jsonLine = JSON.stringify(spanDict);
56
+ console.log(jsonLine);
57
+ }
58
+ resultCallback({ code: ExportResultCode.SUCCESS });
59
+ } catch (error) {
60
+ resultCallback({ code: ExportResultCode.FAILED, error: error as Error });
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Shutdown the exporter.
66
+ */
67
+ shutdown(): Promise<void> {
68
+ return Promise.resolve();
69
+ }
70
+
71
+ /**
72
+ * Force flush the exporter.
73
+ */
74
+ forceFlush?(): Promise<void> {
75
+ return Promise.resolve();
76
+ }
77
+
78
+ /**
79
+ * Convert a ReadableSpan to a dictionary for JSON serialization.
80
+ */
81
+ private spanToDict(span: ReadableSpan): Record<string, unknown> {
82
+ const context = span.spanContext();
83
+
84
+ // Get parent span ID from parentSpanContext if available
85
+ // Some versions use parentSpanId directly, others use parentSpanContext.spanId
86
+ const parentId = (span as any).parentSpanId || span.parentSpanContext?.spanId;
87
+
88
+ return {
89
+ name: span.name,
90
+ context: {
91
+ trace_id: context.traceId,
92
+ span_id: context.spanId,
93
+ trace_flags: context.traceFlags,
94
+ },
95
+ kind: span.kind,
96
+ parent_id: parentId,
97
+ start_time: span.startTime,
98
+ end_time: span.endTime,
99
+ status: {
100
+ status_code: span.status.code,
101
+ description: span.status.message,
102
+ },
103
+ attributes: span.attributes,
104
+ events: span.events.map((event) => ({
105
+ name: event.name,
106
+ timestamp: event.time,
107
+ attributes: event.attributes,
108
+ })),
109
+ links: span.links.map((link) => ({
110
+ context: {
111
+ trace_id: link.context.traceId,
112
+ span_id: link.context.spanId,
113
+ },
114
+ attributes: link.attributes,
115
+ })),
116
+ resource: {
117
+ attributes: span.resource.attributes,
118
+ },
119
+ };
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Check if single-line console exporter should be used.
125
+ *
126
+ * Set CONSOLE_EXPORTER_SINGLE_LINE=false to use standard multi-line output.
127
+ */
128
+ export function isSingleLineConsoleExporterEnabled(): boolean {
129
+ const value = process.env.CONSOLE_EXPORTER_SINGLE_LINE?.toLowerCase() || 'true';
130
+ return ['true', '1', 'yes', 'on'].includes(value);
131
+ }
@@ -15,7 +15,13 @@ export {
15
15
  type OTLPTraceConfig,
16
16
  type CustomTraceConfig,
17
17
  type BatchConfig,
18
- } from './setup.js';
18
+ } from './setup';
19
19
 
20
20
  // Exporter type constants
21
- export { ExporterType } from './config.js';
21
+ export { ExporterType } from './config';
22
+
23
+ // Single-line console exporter for easier parsing
24
+ export {
25
+ SingleLineConsoleSpanExporter,
26
+ isSingleLineConsoleExporterEnabled,
27
+ } from './SingleLineConsoleSpanExporter';
@@ -14,6 +14,10 @@ import type {
14
14
  OTLPTraceConfig,
15
15
  CustomTraceConfig,
16
16
  } from './config';
17
+ import {
18
+ SingleLineConsoleSpanExporter,
19
+ isSingleLineConsoleExporterEnabled,
20
+ } from './SingleLineConsoleSpanExporter';
17
21
 
18
22
  export type {
19
23
  BatchConfig,
@@ -22,6 +26,7 @@ export type {
22
26
  OTLPTraceConfig,
23
27
  CustomTraceConfig,
24
28
  } from './config';
29
+ export { SingleLineConsoleSpanExporter } from './SingleLineConsoleSpanExporter';
25
30
 
26
31
  /**
27
32
  * Environment variable truthy values.
@@ -122,6 +127,9 @@ async function safeSetup(
122
127
 
123
128
  /**
124
129
  * Setup console exporter.
130
+ *
131
+ * Uses SingleLineConsoleSpanExporter by default for easier parsing with line-based tools.
132
+ * Set CONSOLE_EXPORTER_SINGLE_LINE=false to use standard multi-line output.
125
133
  */
126
134
  async function setupConsoleExporter(config: ConsoleTraceConfig): Promise<void> {
127
135
  const { trace } = await import('@opentelemetry/api');
@@ -133,18 +141,26 @@ async function setupConsoleExporter(config: ConsoleTraceConfig): Promise<void> {
133
141
 
134
142
  const batchConfig = resolveBatchConfig(config.batch);
135
143
 
144
+ // Choose exporter type based on CONSOLE_EXPORTER_SINGLE_LINE env var
145
+ // Single-line: easier for parsing with line-based tools (grep, jq, etc.)
146
+ // Multi-line: more human-readable for debugging
147
+ const useSingleLine = isSingleLineConsoleExporterEnabled();
148
+ const exporter = useSingleLine
149
+ ? new SingleLineConsoleSpanExporter()
150
+ : new ConsoleSpanExporter();
151
+ const exporterType = useSingleLine ? 'single-line' : 'multi-line';
152
+
136
153
  // Check if a real TracerProvider already exists
137
154
  let provider = trace.getTracerProvider();
138
155
  const isRealProvider = 'addSpanProcessor' in provider;
139
156
 
140
157
  if (isRealProvider) {
141
158
  // Add processor to existing provider
142
- const exporter = new ConsoleSpanExporter();
143
159
  const processor = new BatchSpanProcessor(exporter, batchConfig);
144
160
  (provider as any).addSpanProcessor(processor);
145
161
 
146
162
  console.info(
147
- `[Observability] Console exporter configured (batch=${batchConfig.maxExportBatchSize}, ` +
163
+ `[Observability] Console exporter configured (${exporterType}, batch=${batchConfig.maxExportBatchSize}, ` +
148
164
  `delay=${batchConfig.scheduledDelayMillis}ms)`
149
165
  );
150
166
  } else {
@@ -154,7 +170,6 @@ async function setupConsoleExporter(config: ConsoleTraceConfig): Promise<void> {
154
170
  'service.version': '1.0.0',
155
171
  });
156
172
 
157
- const exporter = new ConsoleSpanExporter();
158
173
  const processor = new BatchSpanProcessor(exporter, batchConfig);
159
174
 
160
175
  const tracerProvider = new NodeTracerProvider({
@@ -165,7 +180,7 @@ async function setupConsoleExporter(config: ConsoleTraceConfig): Promise<void> {
165
180
  tracerProvider.register();
166
181
 
167
182
  console.info(
168
- `[Observability] Console exporter configured (batch=${batchConfig.maxExportBatchSize}, ` +
183
+ `[Observability] Console exporter configured (${exporterType}, batch=${batchConfig.maxExportBatchSize}, ` +
169
184
  `delay=${batchConfig.scheduledDelayMillis}ms)`
170
185
  );
171
186
  }
package/src/types.ts CHANGED
@@ -55,6 +55,72 @@ export type TokenUsage = {
55
55
  totalTokens?: number;
56
56
  };
57
57
 
58
+ /**
59
+ * LLM message for input/output tracking.
60
+ *
61
+ * Follows OpenInference semantic convention:
62
+ * - llm.input_messages.{i}.message.role
63
+ * - llm.input_messages.{i}.message.content
64
+ * - llm.output_messages.{i}.message.role
65
+ * - llm.output_messages.{i}.message.content
66
+ *
67
+ * @public
68
+ */
69
+ export type LLMMessage = {
70
+ /** Message role (e.g., 'user', 'assistant', 'system', 'tool') */
71
+ role: string;
72
+ /** Message content */
73
+ content?: string;
74
+ /** Tool calls for assistant messages */
75
+ toolCalls?: ToolCall[];
76
+ /** Tool call ID for tool messages */
77
+ toolCallId?: string;
78
+ };
79
+
80
+ /**
81
+ * Tool call details.
82
+ *
83
+ * Follows OpenInference semantic convention:
84
+ * - tool_call.id
85
+ * - tool_call.function.name
86
+ * - tool_call.function.arguments
87
+ *
88
+ * @public
89
+ */
90
+ export type ToolCall = {
91
+ /** Tool call ID */
92
+ id: string;
93
+ /** Function name */
94
+ function: {
95
+ /** Function name */
96
+ name: string;
97
+ /** Function arguments as JSON string */
98
+ arguments: string;
99
+ };
100
+ };
101
+
102
+ /**
103
+ * Document for retrieval tracking.
104
+ *
105
+ * Follows OpenInference semantic convention:
106
+ * - retrieval.documents.{i}.document.id
107
+ * - retrieval.documents.{i}.document.content
108
+ * - retrieval.documents.{i}.document.score
109
+ * - retrieval.documents.{i}.document.metadata
110
+ *
111
+ * @public
112
+ */
113
+ export type Document = {
114
+ /** Document ID */
115
+ id?: string;
116
+ /** Document content */
117
+ content?: string;
118
+ /** Relevance score */
119
+ score?: number;
120
+ /** Document metadata */
121
+ metadata?: Record<string, unknown>;
122
+ };
123
+
58
124
  /**
59
125
  * Base attributes for all observation types.
60
126
  *
@@ -79,6 +145,8 @@ export type BaseSpanAttributes = {
79
145
  version?: string;
80
146
  /** Environment where the operation is running (e.g., 'production', 'staging') */
81
147
  environment?: string;
148
+ /** Allow additional custom attributes (e.g., http.*, agui.*) */
149
+ [key: string]: unknown;
82
150
  };
83
151
 
84
152
  /**
@@ -86,7 +154,10 @@ export type BaseSpanAttributes = {
86
154
  *
87
155
  * Uses attributes like:
88
156
  * - llm.model_name
89
- * - llm.parameters.*
157
+ * - llm.system
158
+ * - llm.provider
159
+ * - llm.input_messages
160
+ * - llm.output_messages
90
161
  * - llm.token_count.*
91
162
  * - llm.invocation_parameters
92
163
  *
@@ -97,10 +168,22 @@ export type LLMAttributes = BaseSpanAttributes & {
97
168
  completionStartTime?: Date;
98
169
  /** Name of the language model (e.g., 'gpt-4', 'claude-3-opus') */
99
170
  model?: string;
171
+ /** AI system/vendor identifier (e.g., 'openai', 'anthropic') */
172
+ system?: string;
173
+ /** Hosting provider (e.g., 'openai', 'azure', 'aws') */
174
+ provider?: string;
100
175
  /** Parameters passed to the model (temperature, max_tokens, etc.) */
101
176
  modelParameters?: Record<string, string | number>;
102
177
  /** Token usage metrics */
103
178
  usageDetails?: TokenUsage | Record<string, number>;
179
+ /** Input messages for chat completions */
180
+ inputMessages?: LLMMessage[];
181
+ /** Output messages from the model */
182
+ outputMessages?: LLMMessage[];
183
+ /** MIME type of input (e.g., 'application/json') */
184
+ inputMimeType?: string;
185
+ /** MIME type of output (e.g., 'application/json') */
186
+ outputMimeType?: string;
104
187
  };
105
188
 
106
189
  /**
@@ -121,10 +204,22 @@ export type EmbeddingAttributes = LLMAttributes;
121
204
  * - tool.name
122
205
  * - tool.description
123
206
  * - tool.parameters
207
+ * - tool_call.id
208
+ * - tool_call.function.name
209
+ * - tool_call.function.arguments
124
210
  *
125
211
  * @public
126
212
  */
127
- export type ToolAttributes = BaseSpanAttributes;
213
+ export type ToolAttributes = BaseSpanAttributes & {
214
+ /** Tool name */
215
+ toolName?: string;
216
+ /** Tool description */
217
+ toolDescription?: string;
218
+ /** Tool parameters schema */
219
+ toolParameters?: Record<string, unknown>;
220
+ /** Tool call details */
221
+ toolCall?: ToolCall;
222
+ };
128
223
 
129
224
  /**
130
225
  * Agent-specific attributes following OpenInference semantic conventions.
@@ -135,7 +230,10 @@ export type ToolAttributes = BaseSpanAttributes;
135
230
  *
136
231
  * @public
137
232
  */
138
- export type AgentAttributes = BaseSpanAttributes;
233
+ export type AgentAttributes = BaseSpanAttributes & {
234
+ /** Agent name */
235
+ agentName?: string;
236
+ };
139
237
 
140
238
  /**
141
239
  * Chain-specific attributes following OpenInference semantic conventions.
@@ -154,11 +252,16 @@ export type ChainAttributes = BaseSpanAttributes;
154
252
  * Uses attributes like:
155
253
  * - retriever.name
156
254
  * - retriever.query
157
- * - retriever.*
255
+ * - retrieval.documents
158
256
  *
159
257
  * @public
160
258
  */
161
- export type RetrieverAttributes = BaseSpanAttributes;
259
+ export type RetrieverAttributes = BaseSpanAttributes & {
260
+ /** Retrieved documents */
261
+ documents?: Document[];
262
+ /** Query used for retrieval */
263
+ query?: string;
264
+ };
162
265
 
163
266
  /**
164
267
  * Reranker-specific attributes following OpenInference semantic conventions.