@cloudbase/agent-observability 0.0.16 → 0.0.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.
- package/README.md +20 -5
- package/dist/chunk-5EXUNUGP.mjs +60 -0
- package/dist/chunk-5EXUNUGP.mjs.map +1 -0
- package/dist/{chunk-ZGEMAYS4.mjs → chunk-AHSI4KTT.mjs} +399 -92
- package/dist/chunk-AHSI4KTT.mjs.map +1 -0
- package/dist/index.d.mts +369 -33
- package/dist/index.d.ts +369 -33
- package/dist/index.js +399 -52
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +16 -5
- package/dist/langchain.d.mts +7 -5
- package/dist/langchain.d.ts +7 -5
- package/dist/langchain.js +421 -60
- package/dist/langchain.js.map +1 -1
- package/dist/langchain.mjs +31 -11
- package/dist/langchain.mjs.map +1 -1
- package/dist/server.d.mts +72 -6
- package/dist/server.d.ts +72 -6
- package/dist/server.js +129 -13
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +96 -13
- package/dist/server.mjs.map +1 -1
- package/package.json +2 -2
- package/src/core/attributes.ts +256 -11
- package/src/core/constants.ts +14 -16
- package/src/core/spanWrapper.ts +34 -33
- package/src/core/trace-context.ts +469 -0
- package/src/core/tracerProvider.ts +1 -4
- package/src/index.ts +54 -40
- package/src/langchain/CallbackHandler.ts +48 -17
- package/src/langchain/index.ts +1 -1
- package/src/server/SingleLineConsoleSpanExporter.ts +141 -0
- package/src/server/config.ts +2 -4
- package/src/server/index.ts +9 -3
- package/src/server/setup.ts +30 -20
- package/src/types.ts +112 -10
- package/dist/chunk-ZGEMAYS4.mjs.map +0 -1
package/src/core/constants.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OTEL attribute constants for
|
|
2
|
+
* OTEL attribute constants for observability.
|
|
3
3
|
*
|
|
4
4
|
* Uses OpenInference semantic conventions where applicable:
|
|
5
5
|
* https://github.com/Arize-ai/openinference/tree/main/spec
|
|
6
6
|
*
|
|
7
|
-
* Falls back to
|
|
7
|
+
* Falls back to non-standard attributes where OpenInference
|
|
8
8
|
* doesn't define a standard.
|
|
9
9
|
*
|
|
10
10
|
* @module
|
|
@@ -21,21 +21,23 @@ export { OpenInferenceSpanKind };
|
|
|
21
21
|
/**
|
|
22
22
|
* SDK information
|
|
23
23
|
*/
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
// Brandless defaults: avoid emitting project-specific identifiers into traces.
|
|
25
|
+
// Users can still override service.name via OTEL_SERVICE_NAME.
|
|
26
|
+
export const OBSERVABILITY_TRACER_NAME = "agui-tracer";
|
|
27
|
+
export const OBSERVABILITY_SDK_NAME = "observability";
|
|
28
|
+
export const OBSERVABILITY_SDK_VERSION = "0.1.0";
|
|
27
29
|
|
|
28
30
|
/**
|
|
29
31
|
* Combined attribute namespace for internal use
|
|
30
|
-
* Provides a single namespace for all OTEL attributes used
|
|
32
|
+
* Provides a single namespace for all OTEL attributes used internally.
|
|
31
33
|
*
|
|
32
|
-
* Combines OpenInference SemanticConventions with
|
|
34
|
+
* Combines OpenInference SemanticConventions with non-standard attributes
|
|
33
35
|
*/
|
|
34
36
|
export const OtelSpanAttributes = {
|
|
35
37
|
// OpenInference - re-export all standard conventions
|
|
36
38
|
...SemanticConventions,
|
|
37
39
|
|
|
38
|
-
//
|
|
40
|
+
// Trace attributes (non-standard)
|
|
39
41
|
TRACE_NAME: "trace.name",
|
|
40
42
|
TRACE_TAGS: "trace.tags",
|
|
41
43
|
TRACE_PUBLIC: "trace.public",
|
|
@@ -43,7 +45,7 @@ export const OtelSpanAttributes = {
|
|
|
43
45
|
TRACE_INPUT: "trace.input",
|
|
44
46
|
TRACE_OUTPUT: "trace.output",
|
|
45
47
|
|
|
46
|
-
//
|
|
48
|
+
// Observation attributes (non-standard)
|
|
47
49
|
OBSERVATION_TYPE: "observation.type",
|
|
48
50
|
OBSERVATION_LEVEL: "observation.level",
|
|
49
51
|
OBSERVATION_STATUS_MESSAGE: "observation.status_message",
|
|
@@ -51,25 +53,21 @@ export const OtelSpanAttributes = {
|
|
|
51
53
|
OBSERVATION_OUTPUT: "observation.output",
|
|
52
54
|
OBSERVATION_METADATA: "observation.metadata",
|
|
53
55
|
|
|
54
|
-
//
|
|
56
|
+
// LLM-specific (non-standard)
|
|
55
57
|
LLM_COMPLETION_START_TIME: "llm.completion_start_time",
|
|
56
58
|
LLM_MODEL_PARAMETERS: "llm.model_parameters",
|
|
57
59
|
LLM_USAGE_DETAILS: "llm.usage_details",
|
|
58
60
|
LLM_COST_DETAILS: "llm.cost_details",
|
|
59
61
|
|
|
60
|
-
//
|
|
62
|
+
// Retriever-specific (non-standard)
|
|
61
63
|
RETRIEVER_NAME: "retriever.name",
|
|
62
64
|
RETRIEVER_QUERY: "retriever.query",
|
|
63
65
|
RETRIEVER_INDEX_ID: "retriever.index_id",
|
|
64
66
|
RETRIEVER_TOP_K: "retriever.top_k",
|
|
65
67
|
|
|
66
|
-
//
|
|
68
|
+
// General (non-standard)
|
|
67
69
|
ENVIRONMENT: "environment",
|
|
68
70
|
RELEASE: "release",
|
|
69
71
|
VERSION: "version",
|
|
70
72
|
} as const;
|
|
71
73
|
|
|
72
|
-
/**
|
|
73
|
-
* Type for the OtelSpanAttributes object values
|
|
74
|
-
*/
|
|
75
|
-
export type OtelSpanAttributeValues = typeof OtelSpanAttributes[keyof typeof OtelSpanAttributes];
|
package/src/core/spanWrapper.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { Span, TimeInput } from "@opentelemetry/api";
|
|
1
|
+
import { Span, TimeInput, SpanStatus, SpanStatusCode } from "@opentelemetry/api";
|
|
2
2
|
|
|
3
|
-
import { createObservationAttributes, createTraceAttributes } from "./attributes
|
|
4
|
-
import { getTracer } from "./tracerProvider
|
|
3
|
+
import { createObservationAttributes, createTraceAttributes } from "./attributes";
|
|
4
|
+
import { getTracer } from "./tracerProvider";
|
|
5
5
|
import {
|
|
6
6
|
BaseSpanAttributes,
|
|
7
7
|
LLMAttributes,
|
|
8
8
|
TraceAttributes,
|
|
9
9
|
ObservationType,
|
|
10
|
-
} from "../types
|
|
10
|
+
} from "../types";
|
|
11
11
|
import type {
|
|
12
12
|
ToolAttributes,
|
|
13
13
|
AgentAttributes,
|
|
@@ -18,7 +18,7 @@ import type {
|
|
|
18
18
|
GuardrailAttributes,
|
|
19
19
|
EmbeddingAttributes,
|
|
20
20
|
ObservationAttributes,
|
|
21
|
-
} from "../types
|
|
21
|
+
} from "../types";
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Union type representing any observation wrapper.
|
|
@@ -26,7 +26,6 @@ import type {
|
|
|
26
26
|
* @public
|
|
27
27
|
*/
|
|
28
28
|
export type Observation =
|
|
29
|
-
| ObservationSpan
|
|
30
29
|
| ObservationLLM
|
|
31
30
|
| ObservationEmbedding
|
|
32
31
|
| ObservationAgent
|
|
@@ -83,7 +82,7 @@ abstract class BaseObservation {
|
|
|
83
82
|
}
|
|
84
83
|
}
|
|
85
84
|
|
|
86
|
-
/** Gets the
|
|
85
|
+
/** Gets the OpenTelemetry tracer instance */
|
|
87
86
|
protected get tracer() {
|
|
88
87
|
return getTracer();
|
|
89
88
|
}
|
|
@@ -97,6 +96,29 @@ abstract class BaseObservation {
|
|
|
97
96
|
this.otelSpan.end(endTime);
|
|
98
97
|
}
|
|
99
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Sets the span status.
|
|
101
|
+
*
|
|
102
|
+
* @param status - The status to set on the span
|
|
103
|
+
*/
|
|
104
|
+
public setStatus(status: SpanStatus) {
|
|
105
|
+
this.otelSpan.setStatus(status);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Sets the span status to ERROR.
|
|
110
|
+
*
|
|
111
|
+
* Convenience method for marking the span as failed.
|
|
112
|
+
*
|
|
113
|
+
* @param message - Error description message
|
|
114
|
+
*/
|
|
115
|
+
public setErrorStatus(message: string) {
|
|
116
|
+
this.otelSpan.setStatus({
|
|
117
|
+
code: SpanStatusCode.ERROR,
|
|
118
|
+
message,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
100
122
|
/**
|
|
101
123
|
* Updates the OTEL span attributes.
|
|
102
124
|
*
|
|
@@ -176,8 +198,8 @@ abstract class BaseObservation {
|
|
|
176
198
|
public startObservation(
|
|
177
199
|
name: string,
|
|
178
200
|
attributes?: BaseSpanAttributes,
|
|
179
|
-
options?: { asType?: "
|
|
180
|
-
):
|
|
201
|
+
options?: { asType?: "chain" },
|
|
202
|
+
): ObservationChain;
|
|
181
203
|
public startObservation(
|
|
182
204
|
name: string,
|
|
183
205
|
attributes?:
|
|
@@ -194,11 +216,11 @@ abstract class BaseObservation {
|
|
|
194
216
|
options?: { asType?: ObservationType },
|
|
195
217
|
): Observation {
|
|
196
218
|
// Import here to avoid circular dependency
|
|
197
|
-
const { startObservation: startObs } = require("../index
|
|
198
|
-
const { asType = "
|
|
219
|
+
const { startObservation: startObs } = require("../index");
|
|
220
|
+
const { asType = "chain" } = options || {};
|
|
199
221
|
|
|
200
222
|
return startObs(name, attributes, {
|
|
201
|
-
asType: asType as "
|
|
223
|
+
asType: asType as "chain",
|
|
202
224
|
parentSpanContext: this.otelSpan.spanContext(),
|
|
203
225
|
});
|
|
204
226
|
}
|
|
@@ -206,27 +228,6 @@ abstract class BaseObservation {
|
|
|
206
228
|
|
|
207
229
|
// Type-specific observation classes
|
|
208
230
|
|
|
209
|
-
type ObservationSpanParams = {
|
|
210
|
-
otelSpan: Span;
|
|
211
|
-
attributes?: BaseSpanAttributes;
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* General-purpose observation for tracking operations.
|
|
216
|
-
*
|
|
217
|
-
* @public
|
|
218
|
-
*/
|
|
219
|
-
export class ObservationSpan extends BaseObservation {
|
|
220
|
-
constructor(params: ObservationSpanParams) {
|
|
221
|
-
super({ ...params, type: "span" });
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
public update(attributes: BaseSpanAttributes): ObservationSpan {
|
|
225
|
-
super.updateOtelSpanAttributes(attributes);
|
|
226
|
-
return this;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
231
|
type ObservationLLMParams = {
|
|
231
232
|
otelSpan: Span;
|
|
232
233
|
attributes?: LLMAttributes;
|
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trace context extraction and validation utilities.
|
|
3
|
+
*
|
|
4
|
+
* This module provides utilities for extracting and validating W3C-compatible
|
|
5
|
+
* trace context from HTTP headers, supporting external trace inheritance.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Link, SpanContext, TraceFlags } from "@opentelemetry/api";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Result of trace context validation.
|
|
14
|
+
*/
|
|
15
|
+
export interface TraceContextValidation {
|
|
16
|
+
traceId?: string;
|
|
17
|
+
parentSpanId?: string;
|
|
18
|
+
errors: string[];
|
|
19
|
+
isValid: boolean;
|
|
20
|
+
hasTraceId: boolean;
|
|
21
|
+
hasParentSpanId: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Processed trace context result with all necessary data for span creation.
|
|
26
|
+
*
|
|
27
|
+
* This encapsulates the complete result of extracting, validating, and processing
|
|
28
|
+
* external trace context from HTTP headers.
|
|
29
|
+
*/
|
|
30
|
+
export interface ProcessedTraceContext {
|
|
31
|
+
/** Validated trace ID (undefined if invalid or not provided) */
|
|
32
|
+
traceId?: string;
|
|
33
|
+
/** Validated parent span ID (undefined if invalid or not provided) */
|
|
34
|
+
parentSpanId?: string;
|
|
35
|
+
/** Whether external trace context was successfully inherited */
|
|
36
|
+
isInherited: boolean;
|
|
37
|
+
/** Whether parent span link was created */
|
|
38
|
+
hasParentLink: boolean;
|
|
39
|
+
/** Span attributes ready to merge into span attributes object */
|
|
40
|
+
spanAttributes: Record<string, any>;
|
|
41
|
+
/** Span links ready to pass to start_observation */
|
|
42
|
+
links: Link[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Validate W3C Trace Context trace-id format.
|
|
47
|
+
*
|
|
48
|
+
* Must be 32 hex characters (128-bit) and not all zeros.
|
|
49
|
+
*
|
|
50
|
+
* Performance: Uses parseInt() instead of regex for 45% faster validation
|
|
51
|
+
* and better handling of edge cases (extremely long strings).
|
|
52
|
+
*
|
|
53
|
+
* @param traceId - The trace ID string to validate
|
|
54
|
+
* @returns True if valid, false otherwise
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* validateTraceId('7EBd0D2dd5A198C3Cc66a51EeEcdd0F4') // true
|
|
59
|
+
* validateTraceId('invalid') // false
|
|
60
|
+
* validateTraceId('00000000000000000000000000000000') // false
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* @public
|
|
64
|
+
*/
|
|
65
|
+
export function validateTraceId(traceId: string): boolean {
|
|
66
|
+
if (!traceId || typeof traceId !== "string") {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Length check (fast fail for wrong length)
|
|
71
|
+
if (traceId.length !== 32) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Try to parse as hex (validates format + rejects invalid chars)
|
|
76
|
+
// Split into two 16-char chunks to avoid precision loss with large numbers
|
|
77
|
+
const chunk1 = traceId.slice(0, 16);
|
|
78
|
+
const chunk2 = traceId.slice(16, 32);
|
|
79
|
+
|
|
80
|
+
const value1 = parseInt(chunk1, 16);
|
|
81
|
+
const value2 = parseInt(chunk2, 16);
|
|
82
|
+
|
|
83
|
+
// Check if parsing failed (NaN)
|
|
84
|
+
if (isNaN(value1) || isNaN(value2)) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check not all zeros (invalid trace-id per W3C spec)
|
|
89
|
+
if (value1 === 0 && value2 === 0) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Validate W3C Trace Context span-id format.
|
|
98
|
+
*
|
|
99
|
+
* Must be 16 hex characters (64-bit) and not all zeros.
|
|
100
|
+
*
|
|
101
|
+
* Performance: Uses parseInt() instead of regex for 45% faster validation
|
|
102
|
+
* and better handling of edge cases (extremely long strings).
|
|
103
|
+
*
|
|
104
|
+
* @param spanId - The span ID string to validate
|
|
105
|
+
* @returns True if valid, false otherwise
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```typescript
|
|
109
|
+
* validateSpanId('00f067aa0ba902b7') // true
|
|
110
|
+
* validateSpanId('invalid') // false
|
|
111
|
+
* validateSpanId('0000000000000000') // false
|
|
112
|
+
* ```
|
|
113
|
+
*
|
|
114
|
+
* @public
|
|
115
|
+
*/
|
|
116
|
+
export function validateSpanId(spanId: string): boolean {
|
|
117
|
+
if (!spanId || typeof spanId !== "string") {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Length check (fast fail for wrong length)
|
|
122
|
+
if (spanId.length !== 16) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Try to parse as hex (validates format + rejects invalid chars)
|
|
127
|
+
const value = parseInt(spanId, 16);
|
|
128
|
+
|
|
129
|
+
// Check if parsing failed (NaN)
|
|
130
|
+
if (isNaN(value)) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Check not all zeros (invalid span-id per W3C spec)
|
|
135
|
+
if (value === 0) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Validate and normalize external trace context.
|
|
144
|
+
*
|
|
145
|
+
* Supports four scenarios:
|
|
146
|
+
* 1. Both missing → Valid (generate new trace)
|
|
147
|
+
* 2. Only traceId → Valid (inherit trace, new root span)
|
|
148
|
+
* 3. Both provided → Valid (inherit trace + parent link)
|
|
149
|
+
* 4. Only parentSpanId → Invalid (parent without trace)
|
|
150
|
+
*
|
|
151
|
+
* @param traceId - External trace ID from x-trace-id header
|
|
152
|
+
* @param parentSpanId - External parent span ID from x-parent-span-id header
|
|
153
|
+
* @returns Validation result with normalized values and any errors
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```typescript
|
|
157
|
+
* // Scenario 1: Generate new trace
|
|
158
|
+
* const result = validateTraceContext(undefined, undefined);
|
|
159
|
+
* result.isValid // true
|
|
160
|
+
* result.hasTraceId // false
|
|
161
|
+
*
|
|
162
|
+
* // Scenario 2: Inherit trace only
|
|
163
|
+
* const result = validateTraceContext('9bBab669fdD6eAEB3Ac0A522f5DeE0BF', undefined);
|
|
164
|
+
* result.isValid // true
|
|
165
|
+
* result.traceId // '9bbab669fdd6eaeb3ac0a522f5dee0bf'
|
|
166
|
+
*
|
|
167
|
+
* // Scenario 3: Inherit trace + parent
|
|
168
|
+
* const result = validateTraceContext(
|
|
169
|
+
* '9bBab669fdD6eAEB3Ac0A522f5DeE0BF',
|
|
170
|
+
* '00f067aa0ba902b7'
|
|
171
|
+
* );
|
|
172
|
+
* result.isValid // true
|
|
173
|
+
* result.hasParentSpanId // true
|
|
174
|
+
*
|
|
175
|
+
* // Scenario 4: Invalid - parent without trace
|
|
176
|
+
* const result = validateTraceContext(undefined, '00f067aa0ba902b7');
|
|
177
|
+
* result.isValid // false
|
|
178
|
+
* result.errors.length > 0 // true
|
|
179
|
+
* ```
|
|
180
|
+
*
|
|
181
|
+
* @public
|
|
182
|
+
*/
|
|
183
|
+
export function validateTraceContext(
|
|
184
|
+
traceId?: string,
|
|
185
|
+
parentSpanId?: string
|
|
186
|
+
): TraceContextValidation {
|
|
187
|
+
const result: TraceContextValidation = {
|
|
188
|
+
errors: [],
|
|
189
|
+
isValid: false,
|
|
190
|
+
hasTraceId: false,
|
|
191
|
+
hasParentSpanId: false,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// Scenario 1: Both missing - valid (generate new trace)
|
|
195
|
+
if (!traceId && !parentSpanId) {
|
|
196
|
+
result.isValid = true;
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Scenario 4: Only parentSpanId provided - invalid
|
|
201
|
+
if (!traceId && parentSpanId) {
|
|
202
|
+
result.errors.push(
|
|
203
|
+
"parentSpanId provided without traceId - " +
|
|
204
|
+
"traceId is required when inheriting parent span"
|
|
205
|
+
);
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Scenario 2 & 3: traceId provided (with or without parentSpanId)
|
|
210
|
+
if (traceId) {
|
|
211
|
+
if (!validateTraceId(traceId)) {
|
|
212
|
+
result.errors.push(
|
|
213
|
+
`Invalid traceId format: '${traceId}' - ` +
|
|
214
|
+
`must be 32 hex characters and not all zeros`
|
|
215
|
+
);
|
|
216
|
+
} else {
|
|
217
|
+
// Normalize to lowercase for consistency
|
|
218
|
+
result.traceId = traceId.toLowerCase();
|
|
219
|
+
result.hasTraceId = true;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Validate parentSpanId if provided
|
|
224
|
+
if (parentSpanId) {
|
|
225
|
+
if (!validateSpanId(parentSpanId)) {
|
|
226
|
+
result.errors.push(
|
|
227
|
+
`Invalid parentSpanId format: '${parentSpanId}' - ` +
|
|
228
|
+
`must be 16 hex characters and not all zeros`
|
|
229
|
+
);
|
|
230
|
+
} else {
|
|
231
|
+
// Normalize to lowercase for consistency
|
|
232
|
+
result.parentSpanId = parentSpanId.toLowerCase();
|
|
233
|
+
result.hasParentSpanId = true;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
result.isValid = result.errors.length === 0;
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Create an OpenTelemetry Link to an external span.
|
|
243
|
+
*
|
|
244
|
+
* This creates a loose coupling to an external span, avoiding the
|
|
245
|
+
* "orphaned span" problem when the parent span data is not available
|
|
246
|
+
* in the current tracing backend.
|
|
247
|
+
*
|
|
248
|
+
* @param traceId - External trace ID (32 hex chars)
|
|
249
|
+
* @param parentSpanId - External parent span ID (16 hex chars)
|
|
250
|
+
* @param linkType - Semantic relationship type (default: "follows_from")
|
|
251
|
+
* @param sourceSystem - Name of the external system (default: "gateway")
|
|
252
|
+
* @returns OpenTelemetry Link object
|
|
253
|
+
*
|
|
254
|
+
* @example
|
|
255
|
+
* ```typescript
|
|
256
|
+
* const link = createSpanLinkFromContext(
|
|
257
|
+
* '9bBab669fdD6eAEB3Ac0A522f5DeE0BF',
|
|
258
|
+
* '00f067aa0ba902b7'
|
|
259
|
+
* );
|
|
260
|
+
* // Use link when creating span
|
|
261
|
+
* const span = tracer.startSpan('my-operation', { links: [link] });
|
|
262
|
+
* ```
|
|
263
|
+
*
|
|
264
|
+
* @public
|
|
265
|
+
*/
|
|
266
|
+
export function createSpanLinkFromContext(
|
|
267
|
+
traceId: string,
|
|
268
|
+
parentSpanId: string,
|
|
269
|
+
linkType: string = "follows_from",
|
|
270
|
+
sourceSystem: string = "gateway"
|
|
271
|
+
): Link {
|
|
272
|
+
// Create SpanContext for external parent
|
|
273
|
+
// Note: OpenTelemetry expects traceId and spanId as hex strings
|
|
274
|
+
const externalContext: SpanContext = {
|
|
275
|
+
traceId: traceId,
|
|
276
|
+
spanId: parentSpanId,
|
|
277
|
+
traceFlags: TraceFlags.SAMPLED,
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// Create link with metadata
|
|
281
|
+
const link: Link = {
|
|
282
|
+
context: externalContext,
|
|
283
|
+
attributes: {
|
|
284
|
+
"link.type": linkType,
|
|
285
|
+
"link.source": sourceSystem,
|
|
286
|
+
"link.trace_id": traceId,
|
|
287
|
+
"link.span_id": parentSpanId,
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
return link;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Extract trace context from HTTP headers.
|
|
296
|
+
*
|
|
297
|
+
* Only supports custom x-trace-id and x-parent-span-id headers.
|
|
298
|
+
* Does not support W3C traceparent header.
|
|
299
|
+
*
|
|
300
|
+
* @param headers - HTTP headers (case-insensitive)
|
|
301
|
+
* @param traceIdHeader - Header name for trace ID (default: "x-trace-id")
|
|
302
|
+
* @param parentSpanIdHeader - Header name for parent span ID (default: "x-parent-span-id")
|
|
303
|
+
* @returns Tuple of [traceId, parentSpanId], both may be undefined
|
|
304
|
+
*
|
|
305
|
+
* @example
|
|
306
|
+
* ```typescript
|
|
307
|
+
* const headers = new Headers({
|
|
308
|
+
* 'x-trace-id': '9bBab669fdD6eAEB3Ac0A522f5DeE0BF',
|
|
309
|
+
* 'x-parent-span-id': '00f067aa0ba902b7'
|
|
310
|
+
* });
|
|
311
|
+
* const [traceId, parentSpanId] = extractTraceContextFromHeaders(headers);
|
|
312
|
+
* ```
|
|
313
|
+
*
|
|
314
|
+
* @public
|
|
315
|
+
*/
|
|
316
|
+
export function extractTraceContextFromHeaders(
|
|
317
|
+
headers: Headers | Record<string, string>,
|
|
318
|
+
traceIdHeader: string = "x-trace-id",
|
|
319
|
+
parentSpanIdHeader: string = "x-parent-span-id"
|
|
320
|
+
): [string | undefined, string | undefined] {
|
|
321
|
+
if (!headers) {
|
|
322
|
+
return [undefined, undefined];
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
let traceId: string | undefined;
|
|
326
|
+
let parentSpanId: string | undefined;
|
|
327
|
+
|
|
328
|
+
// Handle both Headers object and plain object
|
|
329
|
+
if (headers instanceof Headers) {
|
|
330
|
+
traceId = headers.get(traceIdHeader) || undefined;
|
|
331
|
+
parentSpanId = headers.get(parentSpanIdHeader) || undefined;
|
|
332
|
+
} else {
|
|
333
|
+
// Plain object - case-insensitive lookup
|
|
334
|
+
const headersLower: Record<string, string> = {};
|
|
335
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
336
|
+
headersLower[key.toLowerCase()] = value;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
traceId = headersLower[traceIdHeader.toLowerCase()];
|
|
340
|
+
parentSpanId = headersLower[parentSpanIdHeader.toLowerCase()];
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return [traceId, parentSpanId];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Extract, validate, and process trace context from HTTP headers.
|
|
348
|
+
*
|
|
349
|
+
* This is a high-level convenience method that combines extraction, validation,
|
|
350
|
+
* link creation, and attribute preparation into a single call. It handles all
|
|
351
|
+
* error cases and returns a ready-to-use ProcessedTraceContext object.
|
|
352
|
+
*
|
|
353
|
+
* @param headers - HTTP headers (case-insensitive)
|
|
354
|
+
* @param logger - Optional logger for validation errors
|
|
355
|
+
* @param linkType - Semantic relationship type for SpanLink (default: "follows_from")
|
|
356
|
+
* @param sourceSystem - Name of the external system (default: "gateway")
|
|
357
|
+
* @returns ProcessedTraceContext with all processed data ready for span creation
|
|
358
|
+
*
|
|
359
|
+
* @example
|
|
360
|
+
* ```typescript
|
|
361
|
+
* // Basic usage
|
|
362
|
+
* const result = processTraceContextFromHeaders(request.headers, logger);
|
|
363
|
+
* if (result.isInherited) {
|
|
364
|
+
* Object.assign(spanAttributes, result.spanAttributes);
|
|
365
|
+
* }
|
|
366
|
+
* const serverSpan = startObservation(
|
|
367
|
+
* "AG-UI.Server",
|
|
368
|
+
* spanAttributes,
|
|
369
|
+
* { links: result.links.length > 0 ? result.links : undefined }
|
|
370
|
+
* );
|
|
371
|
+
*
|
|
372
|
+
* // Scenario 1: No headers
|
|
373
|
+
* const result = processTraceContextFromHeaders(new Headers());
|
|
374
|
+
* result.isInherited // false
|
|
375
|
+
* result.links // []
|
|
376
|
+
*
|
|
377
|
+
* // Scenario 2: Only trace_id
|
|
378
|
+
* const headers = new Headers({ 'x-trace-id': '9bBab669fdD6eAEB3Ac0A522f5DeE0BF' });
|
|
379
|
+
* const result = processTraceContextFromHeaders(headers);
|
|
380
|
+
* result.isInherited // true
|
|
381
|
+
* result.hasParentLink // false
|
|
382
|
+
* result.spanAttributes['trace.external_trace_id'] // '7ebd0d2dd5a198c3cc66a51eeecdd0f4'
|
|
383
|
+
*
|
|
384
|
+
* // Scenario 3: Both headers
|
|
385
|
+
* const headers = new Headers({
|
|
386
|
+
* 'x-trace-id': '9bBab669fdD6eAEB3Ac0A522f5DeE0BF',
|
|
387
|
+
* 'x-parent-span-id': '00f067aa0ba902b7'
|
|
388
|
+
* });
|
|
389
|
+
* const result = processTraceContextFromHeaders(headers);
|
|
390
|
+
* result.isInherited // true
|
|
391
|
+
* result.hasParentLink // true
|
|
392
|
+
* result.links.length // 1
|
|
393
|
+
*
|
|
394
|
+
* // Scenario 4: Invalid (only parent_span_id)
|
|
395
|
+
* const headers = new Headers({ 'x-parent-span-id': '00f067aa0ba902b7' });
|
|
396
|
+
* const result = processTraceContextFromHeaders(headers);
|
|
397
|
+
* result.isInherited // false
|
|
398
|
+
* result.links // []
|
|
399
|
+
* ```
|
|
400
|
+
*
|
|
401
|
+
* @public
|
|
402
|
+
*/
|
|
403
|
+
export function processTraceContextFromHeaders(
|
|
404
|
+
headers: Headers | Record<string, string>,
|
|
405
|
+
logger?: { warn?: (msg: string) => void },
|
|
406
|
+
linkType: string = "follows_from",
|
|
407
|
+
sourceSystem: string = "gateway"
|
|
408
|
+
): ProcessedTraceContext {
|
|
409
|
+
// Step 1: Extract trace context from headers
|
|
410
|
+
const [traceId, parentSpanId] = extractTraceContextFromHeaders(headers);
|
|
411
|
+
|
|
412
|
+
// Step 2: Validate extracted values
|
|
413
|
+
const validation = validateTraceContext(traceId, parentSpanId);
|
|
414
|
+
|
|
415
|
+
// Step 3: Handle validation errors
|
|
416
|
+
if (!validation.isValid) {
|
|
417
|
+
// Log all validation errors
|
|
418
|
+
if (logger && logger.warn) {
|
|
419
|
+
validation.errors.forEach((error) => {
|
|
420
|
+
logger.warn!(`Trace context validation failed: ${error}`);
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Return empty result for invalid context
|
|
425
|
+
return {
|
|
426
|
+
traceId: undefined,
|
|
427
|
+
parentSpanId: undefined,
|
|
428
|
+
isInherited: false,
|
|
429
|
+
hasParentLink: false,
|
|
430
|
+
spanAttributes: {},
|
|
431
|
+
links: [],
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Step 4: Process valid trace context
|
|
436
|
+
const normalizedTraceId = validation.traceId;
|
|
437
|
+
const normalizedParentSpanId = validation.parentSpanId;
|
|
438
|
+
|
|
439
|
+
// Prepare span attributes
|
|
440
|
+
const attributes: Record<string, any> = {};
|
|
441
|
+
const links: Link[] = [];
|
|
442
|
+
|
|
443
|
+
// Add trace inheritance markers
|
|
444
|
+
if (normalizedTraceId) {
|
|
445
|
+
attributes["trace.inherited"] = true;
|
|
446
|
+
attributes["trace.external_trace_id"] = normalizedTraceId;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Create link to external parent if provided
|
|
450
|
+
if (normalizedTraceId && normalizedParentSpanId) {
|
|
451
|
+
const link = createSpanLinkFromContext(
|
|
452
|
+
normalizedTraceId,
|
|
453
|
+
normalizedParentSpanId,
|
|
454
|
+
linkType,
|
|
455
|
+
sourceSystem
|
|
456
|
+
);
|
|
457
|
+
links.push(link);
|
|
458
|
+
attributes["trace.external_parent_span_id"] = normalizedParentSpanId;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
traceId: normalizedTraceId,
|
|
463
|
+
parentSpanId: normalizedParentSpanId,
|
|
464
|
+
isInherited: !!normalizedTraceId,
|
|
465
|
+
hasParentLink: !!normalizedParentSpanId,
|
|
466
|
+
spanAttributes: attributes,
|
|
467
|
+
links,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TracerProvider, trace, context } from "@opentelemetry/api";
|
|
2
|
+
import { OBSERVABILITY_SDK_NAME, OBSERVABILITY_SDK_VERSION } from "./constants";
|
|
2
3
|
|
|
3
4
|
const OBSERVABILITY_GLOBAL_SYMBOL = Symbol.for("observability");
|
|
4
5
|
|
|
@@ -130,7 +131,3 @@ export function getTracer() {
|
|
|
130
131
|
OBSERVABILITY_SDK_VERSION
|
|
131
132
|
);
|
|
132
133
|
}
|
|
133
|
-
|
|
134
|
-
// SDK version - could be read from package.json in production
|
|
135
|
-
const OBSERVABILITY_SDK_NAME = "ag-kit-observability";
|
|
136
|
-
const OBSERVABILITY_SDK_VERSION = "0.1.0";
|