@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.
@@ -1,10 +1,10 @@
1
1
  /**
2
- * OTEL attribute constants for AG-Kit observability.
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 AG-Kit specific attributes where OpenInference
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
- export const OBSERVABILITY_TRACER_NAME = "agkit-tracer";
25
- export const OBSERVABILITY_SDK_NAME = "@agkit/observability";
26
- // Version will be injected from package.json
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 by AG-Kit
32
+ * Provides a single namespace for all OTEL attributes used internally.
31
33
  *
32
- * Combines OpenInference SemanticConventions with AG-Kit specific attributes
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
- // AG-Kit Trace attributes (non-standard)
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
- // AG-Kit Observation attributes (non-standard)
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
- // AG-Kit LLM-specific (non-standard)
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
- // AG-Kit Retriever-specific (non-standard)
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
- // AG-Kit General (non-standard)
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];
@@ -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.js";
4
- import { getTracer } from "./tracerProvider.js";
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.js";
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.js";
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 AG-Kit OpenTelemetry tracer instance */
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?: "span" },
180
- ): ObservationSpan;
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.js");
198
- const { asType = "span" } = options || {};
219
+ const { startObservation: startObs } = require("../index");
220
+ const { asType = "chain" } = options || {};
199
221
 
200
222
  return startObs(name, attributes, {
201
- asType: asType as "span",
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";