@cloudbase/agent-observability 0.0.20 → 0.0.21

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudbase/agent-observability",
3
- "version": "0.0.20",
3
+ "version": "0.0.21",
4
4
  "description": "OpenInference-compatible observability for AG-Kit",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -64,19 +64,16 @@
64
64
  "dependencies": {
65
65
  "@arizeai/openinference-semantic-conventions": "^2.1.7",
66
66
  "@opentelemetry/api": "^1.9.0",
67
+ "@opentelemetry/exporter-trace-otlp-http": "^0.211.0",
68
+ "@opentelemetry/resources": "^2.5.0",
69
+ "@opentelemetry/sdk-trace-base": "^2.5.0",
70
+ "@opentelemetry/sdk-trace-node": "^2.5.0",
67
71
  "@opentelemetry/semantic-conventions": "^1.39.0",
68
- "@cloudbase/agent-shared": "^0.0.20"
69
- },
70
- "optionalDependencies": {
71
- "@opentelemetry/exporter-trace-otlp-http": "^0.210.0",
72
- "@opentelemetry/resources": "^2.4.0",
73
- "@opentelemetry/sdk-node": "^0.210.0",
74
- "@opentelemetry/sdk-trace-base": "^2.4.0",
75
- "@opentelemetry/sdk-trace-node": "^2.4.0"
72
+ "@cloudbase/agent-shared": "^0.0.21"
76
73
  },
77
74
  "devDependencies": {
78
75
  "@langchain/core": "^1.0.2",
79
- "@opentelemetry/exporter-trace-otlp-http": "^0.210.0",
76
+ "@opentelemetry/exporter-trace-otlp-http": "^0.211.0",
80
77
  "@types/node": "^20.0.0",
81
78
  "tsup": "^8.5.0",
82
79
  "typescript": "^5.0.0"
package/src/index.ts CHANGED
@@ -4,7 +4,10 @@
4
4
  * @packageDocumentation
5
5
  */
6
6
 
7
- import { trace, context, TimeInput, SpanStatusCode, Span, SpanContext, Link } from "@opentelemetry/api";
7
+ import { trace, context, TimeInput, SpanStatusCode, Span, SpanContext, Link, INVALID_SPAN_CONTEXT } from "@opentelemetry/api";
8
+
9
+ // Re-export SpanContext type for adapters to use without depending on @opentelemetry/api directly
10
+ export type { SpanContext } from "@opentelemetry/api";
8
11
 
9
12
  import {
10
13
  createObservationAttributes,
@@ -164,6 +167,50 @@ function createParentContext(
164
167
  return trace.setSpanContext(context.active(), parentSpanContext);
165
168
  }
166
169
 
170
+ /**
171
+ * Creates a NonRecordingSpan that safely accepts all Span operations as no-ops.
172
+ * Used as a fallback when observation creation fails, ensuring observability
173
+ * never blocks business logic.
174
+ *
175
+ * @internal
176
+ */
177
+ function createNoopSpan(): Span {
178
+ return trace.wrapSpanContext(INVALID_SPAN_CONTEXT);
179
+ }
180
+
181
+ /**
182
+ * Creates a no-op observation of the specified type.
183
+ * All methods (update, end, setErrorStatus, etc.) are safe to call but do nothing.
184
+ *
185
+ * @param asType - The observation type to create
186
+ * @returns A no-op observation instance
187
+ * @internal
188
+ */
189
+ function createNoopObservation(asType: ObservationType = "chain"): Observation {
190
+ const otelSpan = createNoopSpan();
191
+ switch (asType) {
192
+ case "llm":
193
+ return new ObservationLLM({ otelSpan });
194
+ case "embedding":
195
+ return new ObservationEmbedding({ otelSpan });
196
+ case "agent":
197
+ return new ObservationAgent({ otelSpan });
198
+ case "tool":
199
+ return new ObservationTool({ otelSpan });
200
+ case "retriever":
201
+ return new ObservationRetriever({ otelSpan });
202
+ case "reranker":
203
+ return new ObservationReranker({ otelSpan });
204
+ case "evaluator":
205
+ return new ObservationEvaluator({ otelSpan });
206
+ case "guardrail":
207
+ return new ObservationGuardrail({ otelSpan });
208
+ case "chain":
209
+ default:
210
+ return new ObservationChain({ otelSpan });
211
+ }
212
+ }
213
+
167
214
  // Function overloads for proper type inference
168
215
  // Generic overload for dynamic asType (returns Observation union)
169
216
  export function startObservation(
@@ -284,71 +331,80 @@ export function startObservation(
284
331
  ): Observation {
285
332
  const { asType = "chain", ...observationOptions } = options || {};
286
333
 
287
- const otelSpan = createOtelSpan({
288
- name,
289
- ...observationOptions,
290
- });
291
-
292
- switch (asType) {
293
- case "llm":
294
- return new ObservationLLM({
295
- otelSpan,
296
- attributes: attributes as LLMAttributes,
297
- });
334
+ try {
335
+ const otelSpan = createOtelSpan({
336
+ name,
337
+ ...observationOptions,
338
+ });
339
+
340
+ switch (asType) {
341
+ case "llm":
342
+ return new ObservationLLM({
343
+ otelSpan,
344
+ attributes: attributes as LLMAttributes,
345
+ });
298
346
 
299
- case "embedding":
300
- return new ObservationEmbedding({
301
- otelSpan,
302
- attributes: attributes as EmbeddingAttributes,
303
- });
347
+ case "embedding":
348
+ return new ObservationEmbedding({
349
+ otelSpan,
350
+ attributes: attributes as EmbeddingAttributes,
351
+ });
304
352
 
305
- case "agent":
306
- return new ObservationAgent({
307
- otelSpan,
308
- attributes: attributes as AgentAttributes,
309
- });
353
+ case "agent":
354
+ return new ObservationAgent({
355
+ otelSpan,
356
+ attributes: attributes as AgentAttributes,
357
+ });
310
358
 
311
- case "tool":
312
- return new ObservationTool({
313
- otelSpan,
314
- attributes: attributes as ToolAttributes,
315
- });
359
+ case "tool":
360
+ return new ObservationTool({
361
+ otelSpan,
362
+ attributes: attributes as ToolAttributes,
363
+ });
316
364
 
317
- case "chain":
318
- return new ObservationChain({
319
- otelSpan,
320
- attributes: attributes as ChainAttributes,
321
- });
365
+ case "chain":
366
+ return new ObservationChain({
367
+ otelSpan,
368
+ attributes: attributes as ChainAttributes,
369
+ });
322
370
 
323
- case "retriever":
324
- return new ObservationRetriever({
325
- otelSpan,
326
- attributes: attributes as RetrieverAttributes,
327
- });
371
+ case "retriever":
372
+ return new ObservationRetriever({
373
+ otelSpan,
374
+ attributes: attributes as RetrieverAttributes,
375
+ });
328
376
 
329
- case "reranker":
330
- return new ObservationReranker({
331
- otelSpan,
332
- attributes: attributes as RerankerAttributes,
333
- });
377
+ case "reranker":
378
+ return new ObservationReranker({
379
+ otelSpan,
380
+ attributes: attributes as RerankerAttributes,
381
+ });
334
382
 
335
- case "evaluator":
336
- return new ObservationEvaluator({
337
- otelSpan,
338
- attributes: attributes as EvaluatorAttributes,
339
- });
383
+ case "evaluator":
384
+ return new ObservationEvaluator({
385
+ otelSpan,
386
+ attributes: attributes as EvaluatorAttributes,
387
+ });
340
388
 
341
- case "guardrail":
342
- return new ObservationGuardrail({
343
- otelSpan,
344
- attributes: attributes as GuardrailAttributes,
345
- });
389
+ case "guardrail":
390
+ return new ObservationGuardrail({
391
+ otelSpan,
392
+ attributes: attributes as GuardrailAttributes,
393
+ });
346
394
 
347
- default:
348
- return new ObservationChain({
349
- otelSpan,
350
- attributes: attributes as BaseSpanAttributes,
351
- });
395
+ default:
396
+ return new ObservationChain({
397
+ otelSpan,
398
+ attributes: attributes as BaseSpanAttributes,
399
+ });
400
+ }
401
+ } catch (err) {
402
+ // Observability must never block business logic - return a no-op observation
403
+ console.warn(
404
+ `[Observability] Failed to create observation "${name}":`,
405
+ err instanceof Error ? err.message : err,
406
+ );
407
+ return createNoopObservation(asType);
352
408
  }
353
409
  }
354
410
 
@@ -547,71 +603,92 @@ export function startActiveObservation<
547
603
  >(name: string, fn: F, options?: StartActiveObservationOpts): ReturnType<F> {
548
604
  const { asType = "chain", endOnExit, ...observationOptions } = options || {};
549
605
 
550
- return getTracer().startActiveSpan(
551
- name,
552
- { startTime: observationOptions?.startTime },
553
- createParentContext(observationOptions?.parentSpanContext) ??
554
- context.active(),
555
- (span) => {
556
- try {
557
- let observation: Observation;
558
-
559
- switch (asType) {
560
- case "llm":
561
- observation = new ObservationLLM({ otelSpan: span });
562
- break;
563
- case "embedding":
564
- observation = new ObservationEmbedding({ otelSpan: span });
565
- break;
566
- case "agent":
567
- observation = new ObservationAgent({ otelSpan: span });
568
- break;
569
- case "tool":
570
- observation = new ObservationTool({ otelSpan: span });
571
- break;
572
- case "retriever":
573
- observation = new ObservationRetriever({ otelSpan: span });
574
- break;
575
- case "reranker":
576
- observation = new ObservationReranker({ otelSpan: span });
577
- break;
578
- case "evaluator":
579
- observation = new ObservationEvaluator({ otelSpan: span });
580
- break;
581
- case "guardrail":
582
- observation = new ObservationGuardrail({ otelSpan: span });
583
- break;
584
- case "chain":
585
- default:
586
- observation = new ObservationChain({ otelSpan: span });
587
- }
606
+ // Track whether fn has been called to distinguish observability errors from business errors.
607
+ // If fn was called and threw, we must re-throw (business error).
608
+ // If fn was never called, the error is from observability infrastructure and we can fall back.
609
+ let fnCalled = false;
588
610
 
589
- const result = fn(observation as Parameters<F>[0]);
611
+ try {
612
+ return getTracer().startActiveSpan(
613
+ name,
614
+ { startTime: observationOptions?.startTime },
615
+ createParentContext(observationOptions?.parentSpanContext) ??
616
+ context.active(),
617
+ (span) => {
618
+ try {
619
+ let observation: Observation;
620
+
621
+ switch (asType) {
622
+ case "llm":
623
+ observation = new ObservationLLM({ otelSpan: span });
624
+ break;
625
+ case "embedding":
626
+ observation = new ObservationEmbedding({ otelSpan: span });
627
+ break;
628
+ case "agent":
629
+ observation = new ObservationAgent({ otelSpan: span });
630
+ break;
631
+ case "tool":
632
+ observation = new ObservationTool({ otelSpan: span });
633
+ break;
634
+ case "retriever":
635
+ observation = new ObservationRetriever({ otelSpan: span });
636
+ break;
637
+ case "reranker":
638
+ observation = new ObservationReranker({ otelSpan: span });
639
+ break;
640
+ case "evaluator":
641
+ observation = new ObservationEvaluator({ otelSpan: span });
642
+ break;
643
+ case "guardrail":
644
+ observation = new ObservationGuardrail({ otelSpan: span });
645
+ break;
646
+ case "chain":
647
+ default:
648
+ observation = new ObservationChain({ otelSpan: span });
649
+ }
590
650
 
591
- if (result instanceof Promise) {
592
- return wrapPromise(
593
- result,
594
- span,
595
- endOnExit,
596
- ) as ReturnType<F>;
597
- } else {
651
+ fnCalled = true;
652
+ const result = fn(observation as Parameters<F>[0]);
653
+
654
+ if (result instanceof Promise) {
655
+ return wrapPromise(
656
+ result,
657
+ span,
658
+ endOnExit,
659
+ ) as ReturnType<F>;
660
+ } else {
661
+ if (endOnExit !== false) {
662
+ span.end();
663
+ }
664
+ return result as ReturnType<F>;
665
+ }
666
+ } catch (err) {
667
+ span.setStatus({
668
+ code: SpanStatusCode.ERROR,
669
+ message: err instanceof Error ? err.message : "Unknown error",
670
+ });
598
671
  if (endOnExit !== false) {
599
672
  span.end();
600
673
  }
601
- return result as ReturnType<F>;
602
- }
603
- } catch (err) {
604
- span.setStatus({
605
- code: SpanStatusCode.ERROR,
606
- message: err instanceof Error ? err.message : "Unknown error",
607
- });
608
- if (endOnExit !== false) {
609
- span.end();
674
+ throw err;
610
675
  }
611
- throw err;
612
- }
613
- },
614
- );
676
+ },
677
+ );
678
+ } catch (err) {
679
+ // If fn was already called, the error is from business logic - re-throw
680
+ if (fnCalled) {
681
+ throw err;
682
+ }
683
+ // Error is from observability infrastructure (getTracer, startActiveSpan) -
684
+ // fall back to executing fn with a no-op observation
685
+ console.warn(
686
+ `[Observability] startActiveObservation "${name}" failed, falling back to no-op:`,
687
+ err instanceof Error ? err.message : err,
688
+ );
689
+ const noopObservation = createNoopObservation(asType);
690
+ return fn(noopObservation as Parameters<F>[0]) as ReturnType<F>;
691
+ }
615
692
  }
616
693
 
617
694
  /**
@@ -732,53 +809,75 @@ export function observe<T extends (...args: any[]) => any>(
732
809
  ): ReturnType<T> {
733
810
  const name = fn.name || "anonymous-function";
734
811
 
735
- // Prepare input data
736
- const inputData = captureInput ? _captureArguments(args) : undefined;
737
-
738
- // Create the observation
739
- const observation = startObservation(
740
- name,
741
- inputData ? { input: inputData } : {},
742
- {
743
- ...observationOptions,
744
- asType: asType as "chain",
745
- },
746
- );
747
-
748
- // Set the observation span as active in the context
749
- const activeContext = trace.setSpan(context.active(), observation.otelSpan);
750
-
751
- // Execute the function within the observation context
752
- const result = context.with(activeContext, () => fn.apply(this, args));
753
-
754
- // Handle promises
755
- if (result instanceof Promise) {
756
- return result.then(
757
- (value) => {
758
- if (captureOutput) {
759
- observation.update({ output: value });
760
- }
761
- observation.end();
762
- return value;
812
+ let observation: Observation;
813
+ let fnCalled = false;
814
+ try {
815
+ // Prepare input data
816
+ const inputData = captureInput ? _captureArguments(args) : undefined;
817
+
818
+ // Create the observation (safe - returns no-op on failure)
819
+ observation = startObservation(
820
+ name,
821
+ inputData ? { input: inputData } : {},
822
+ {
823
+ ...observationOptions,
824
+ asType: asType as "chain",
763
825
  },
764
- (err: unknown) => {
765
- observation.update({
766
- level: "ERROR",
767
- statusMessage: err instanceof Error ? err.message : "Unknown error",
768
- });
769
- observation.end();
770
- throw err;
771
- },
772
- ) as ReturnType<T>;
773
- }
826
+ );
827
+
828
+ // Set the observation span as active in the context
829
+ const activeContext = trace.setSpan(context.active(), observation.otelSpan);
830
+
831
+ // Execute the function within the observation context
832
+ fnCalled = true;
833
+ const result = context.with(activeContext, () => fn.apply(this, args));
834
+
835
+ // Handle promises
836
+ if (result instanceof Promise) {
837
+ return result.then(
838
+ (value) => {
839
+ if (captureOutput) {
840
+ observation.update({ output: value });
841
+ }
842
+ observation.end();
843
+ return value;
844
+ },
845
+ (err: unknown) => {
846
+ observation.update({
847
+ level: "ERROR",
848
+ statusMessage: err instanceof Error ? err.message : "Unknown error",
849
+ });
850
+ observation.end();
851
+ throw err;
852
+ },
853
+ ) as ReturnType<T>;
854
+ }
774
855
 
775
- // Handle synchronous functions
776
- if (captureOutput) {
777
- observation.update({ output: result });
778
- }
779
- observation.end();
856
+ // Handle synchronous functions
857
+ if (captureOutput) {
858
+ observation.update({ output: result });
859
+ }
860
+ observation.end();
780
861
 
781
- return result as ReturnType<T>;
862
+ return result as ReturnType<T>;
863
+ } catch (err) {
864
+ // If fn was already called, the error is from business logic - re-throw
865
+ if (fnCalled) {
866
+ throw err;
867
+ }
868
+ // Observability infra error (trace.setSpan, context.with) -
869
+ // fall back to calling fn directly without observability context
870
+ console.warn(
871
+ `[Observability] observe "${name}" failed, falling back to direct call:`,
872
+ err instanceof Error ? (err as Error).message : err,
873
+ );
874
+ try {
875
+ observation!?.end();
876
+ } catch {
877
+ // Ignore end() failures on cleanup
878
+ }
879
+ return fn.apply(this, args);
880
+ }
782
881
  };
783
882
 
784
883
  // Preserve function properties
@@ -766,6 +766,12 @@ export class CallbackHandler extends BaseCallbackHandler {
766
766
  );
767
767
  this.runMap.set(runId, observation);
768
768
 
769
+ if (this.runMap.size > 1000) {
770
+ this.logger.warn?.(
771
+ `runMap size (${this.runMap.size}) exceeds 1000. Possible missing end callbacks.`
772
+ );
773
+ }
774
+
769
775
  return observation;
770
776
  }
771
777
 
@@ -1,27 +0,0 @@
1
- var __defProp = Object.defineProperty;
2
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
- var __getOwnPropNames = Object.getOwnPropertyNames;
4
- var __hasOwnProp = Object.prototype.hasOwnProperty;
5
- var __esm = (fn, res) => function __init() {
6
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
7
- };
8
- var __export = (target, all) => {
9
- for (var name in all)
10
- __defProp(target, name, { get: all[name], enumerable: true });
11
- };
12
- var __copyProps = (to, from, except, desc) => {
13
- if (from && typeof from === "object" || typeof from === "function") {
14
- for (let key of __getOwnPropNames(from))
15
- if (!__hasOwnProp.call(to, key) && key !== except)
16
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
- }
18
- return to;
19
- };
20
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
21
-
22
- export {
23
- __esm,
24
- __export,
25
- __toCommonJS
26
- };
27
- //# sourceMappingURL=chunk-NFEGQTCC.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}