observability-toolkit 1.5.0 → 1.8.0

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.
Files changed (66) hide show
  1. package/README.md +203 -91
  2. package/dist/backends/index.d.ts +71 -0
  3. package/dist/backends/index.d.ts.map +1 -1
  4. package/dist/backends/index.js +49 -1
  5. package/dist/backends/index.js.map +1 -1
  6. package/dist/backends/local-jsonl.d.ts +8 -0
  7. package/dist/backends/local-jsonl.d.ts.map +1 -1
  8. package/dist/backends/local-jsonl.js +133 -9
  9. package/dist/backends/local-jsonl.js.map +1 -1
  10. package/dist/backends/local-jsonl.test.js +361 -0
  11. package/dist/backends/local-jsonl.test.js.map +1 -1
  12. package/dist/backends/signoz-api.d.ts.map +1 -1
  13. package/dist/backends/signoz-api.js +47 -1
  14. package/dist/backends/signoz-api.js.map +1 -1
  15. package/dist/backends/signoz-api.test.js +97 -0
  16. package/dist/backends/signoz-api.test.js.map +1 -1
  17. package/dist/lib/cache.d.ts +11 -0
  18. package/dist/lib/cache.d.ts.map +1 -1
  19. package/dist/lib/cache.js +21 -2
  20. package/dist/lib/cache.js.map +1 -1
  21. package/dist/lib/cache.test.d.ts +5 -0
  22. package/dist/lib/cache.test.d.ts.map +1 -0
  23. package/dist/lib/cache.test.js +105 -0
  24. package/dist/lib/cache.test.js.map +1 -0
  25. package/dist/lib/file-utils.d.ts +76 -0
  26. package/dist/lib/file-utils.d.ts.map +1 -1
  27. package/dist/lib/file-utils.js +135 -6
  28. package/dist/lib/file-utils.js.map +1 -1
  29. package/dist/lib/file-utils.test.js +214 -1
  30. package/dist/lib/file-utils.test.js.map +1 -1
  31. package/dist/lib/indexer.d.ts +10 -0
  32. package/dist/lib/indexer.d.ts.map +1 -1
  33. package/dist/lib/indexer.js +34 -7
  34. package/dist/lib/indexer.js.map +1 -1
  35. package/dist/lib/indexer.test.js +186 -0
  36. package/dist/lib/indexer.test.js.map +1 -1
  37. package/dist/tools/health-check.d.ts +2 -0
  38. package/dist/tools/health-check.d.ts.map +1 -1
  39. package/dist/tools/health-check.js +3 -1
  40. package/dist/tools/health-check.js.map +1 -1
  41. package/dist/tools/health-check.test.js +36 -0
  42. package/dist/tools/health-check.test.js.map +1 -1
  43. package/dist/tools/query-llm-events.d.ts +53 -4
  44. package/dist/tools/query-llm-events.d.ts.map +1 -1
  45. package/dist/tools/query-llm-events.js +39 -6
  46. package/dist/tools/query-llm-events.js.map +1 -1
  47. package/dist/tools/query-llm-events.test.js +253 -0
  48. package/dist/tools/query-llm-events.test.js.map +1 -1
  49. package/dist/tools/query-logs.d.ts +6 -0
  50. package/dist/tools/query-logs.d.ts.map +1 -1
  51. package/dist/tools/query-logs.js +11 -2
  52. package/dist/tools/query-logs.js.map +1 -1
  53. package/dist/tools/query-logs.test.js +180 -0
  54. package/dist/tools/query-logs.test.js.map +1 -1
  55. package/dist/tools/query-metrics.d.ts +6 -6
  56. package/dist/tools/query-metrics.js +1 -1
  57. package/dist/tools/query-metrics.js.map +1 -1
  58. package/dist/tools/query-metrics.test.js +21 -0
  59. package/dist/tools/query-metrics.test.js.map +1 -1
  60. package/dist/tools/query-traces.d.ts +42 -0
  61. package/dist/tools/query-traces.d.ts.map +1 -1
  62. package/dist/tools/query-traces.js +20 -1
  63. package/dist/tools/query-traces.js.map +1 -1
  64. package/dist/tools/query-traces.test.js +222 -0
  65. package/dist/tools/query-traces.test.js.map +1 -1
  66. package/package.json +1 -1
@@ -993,6 +993,19 @@ describe('LocalJsonlBackend', () => {
993
993
  assert.strictEqual(results.length, 1);
994
994
  assert.strictEqual(results[0].traceId, 'trace1');
995
995
  });
996
+ it('should complete queries with timing (timing is logged for slow queries)', async () => {
997
+ // This test verifies that query timing is active and doesn't break normal queries
998
+ // Timing warnings are logged for queries > 500ms
999
+ const today = getTestDate();
1000
+ const mockSpans = [
1001
+ { traceId: 'trace1', spanId: 'span1', name: 'op1', startTime: [1700000000, 0] },
1002
+ { traceId: 'trace2', spanId: 'span2', name: 'op2', startTime: [1700000001, 0] },
1003
+ ];
1004
+ writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
1005
+ // Query should complete successfully with timing active
1006
+ const results = await backend.queryTraces({});
1007
+ assert.strictEqual(results.length, 2);
1008
+ });
996
1009
  });
997
1010
  describe('queryLogs', () => {
998
1011
  it('should read and normalize log records from JSONL files', async () => {
@@ -2623,6 +2636,287 @@ describe('LocalJsonlBackend', () => {
2623
2636
  assert.strictEqual(results.length, 1);
2624
2637
  assert.strictEqual(results[0].name, 'llm.completion');
2625
2638
  });
2639
+ it('should use OTel GenAI provider fallback: gen_ai.provider.name -> gen_ai.system -> provider', async () => {
2640
+ const today = getTestDate();
2641
+ const mockEvents = [
2642
+ {
2643
+ timestamp: '2026-01-28T10:00:00Z',
2644
+ name: 'llm.completion',
2645
+ attributes: { 'gen_ai.provider.name': 'anthropic-new' }, // should match
2646
+ },
2647
+ {
2648
+ timestamp: '2026-01-28T10:01:00Z',
2649
+ name: 'llm.completion',
2650
+ attributes: { 'gen_ai.system': 'anthropic-new', 'provider': 'legacy' }, // should match via gen_ai.system
2651
+ },
2652
+ {
2653
+ timestamp: '2026-01-28T10:02:00Z',
2654
+ name: 'llm.completion',
2655
+ attributes: { 'provider': 'anthropic-new' }, // should match via provider fallback
2656
+ },
2657
+ {
2658
+ timestamp: '2026-01-28T10:03:00Z',
2659
+ name: 'llm.completion',
2660
+ attributes: { 'gen_ai.provider.name': 'other-provider' }, // should NOT match
2661
+ },
2662
+ ];
2663
+ writeJsonlFile(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
2664
+ const results = await backend.queryLLMEvents({ provider: 'anthropic-new' });
2665
+ assert.strictEqual(results.length, 3);
2666
+ });
2667
+ it('should filter events by operationName', async () => {
2668
+ const today = getTestDate();
2669
+ const mockEvents = [
2670
+ {
2671
+ timestamp: '2026-01-28T10:00:00Z',
2672
+ name: 'llm.chat',
2673
+ attributes: { 'gen_ai.operation.name': 'chat' },
2674
+ },
2675
+ {
2676
+ timestamp: '2026-01-28T10:01:00Z',
2677
+ name: 'llm.embedding',
2678
+ attributes: { 'gen_ai.operation.name': 'embeddings' },
2679
+ },
2680
+ {
2681
+ timestamp: '2026-01-28T10:02:00Z',
2682
+ name: 'agent.invoke',
2683
+ attributes: { 'gen_ai.operation.name': 'invoke_agent' },
2684
+ },
2685
+ ];
2686
+ writeJsonlFile(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
2687
+ const results = await backend.queryLLMEvents({ operationName: 'chat' });
2688
+ assert.strictEqual(results.length, 1);
2689
+ assert.strictEqual(results[0].name, 'llm.chat');
2690
+ });
2691
+ it('should filter events by conversationId', async () => {
2692
+ const today = getTestDate();
2693
+ const mockEvents = [
2694
+ {
2695
+ timestamp: '2026-01-28T10:00:00Z',
2696
+ name: 'llm.chat',
2697
+ attributes: { 'gen_ai.conversation.id': 'conv-abc123' },
2698
+ },
2699
+ {
2700
+ timestamp: '2026-01-28T10:01:00Z',
2701
+ name: 'llm.chat',
2702
+ attributes: { 'gen_ai.conversation.id': 'conv-xyz789' },
2703
+ },
2704
+ {
2705
+ timestamp: '2026-01-28T10:02:00Z',
2706
+ name: 'llm.chat',
2707
+ attributes: { 'gen_ai.conversation.id': 'conv-abc123' },
2708
+ },
2709
+ ];
2710
+ writeJsonlFile(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
2711
+ const results = await backend.queryLLMEvents({ conversationId: 'conv-abc123' });
2712
+ assert.strictEqual(results.length, 2);
2713
+ });
2714
+ it('should combine OTel GenAI filters with other filters', async () => {
2715
+ const today = getTestDate();
2716
+ const mockEvents = [
2717
+ {
2718
+ timestamp: '2026-01-28T10:00:00Z',
2719
+ name: 'llm.chat',
2720
+ attributes: {
2721
+ 'gen_ai.operation.name': 'chat',
2722
+ 'gen_ai.conversation.id': 'conv-abc123',
2723
+ 'gen_ai.request.model': 'claude-3-opus',
2724
+ },
2725
+ },
2726
+ {
2727
+ timestamp: '2026-01-28T10:01:00Z',
2728
+ name: 'llm.chat',
2729
+ attributes: {
2730
+ 'gen_ai.operation.name': 'chat',
2731
+ 'gen_ai.conversation.id': 'conv-abc123',
2732
+ 'gen_ai.request.model': 'gpt-4',
2733
+ },
2734
+ },
2735
+ ];
2736
+ writeJsonlFile(path.join(tempDir, `llm-events-${today}.jsonl`), mockEvents);
2737
+ const results = await backend.queryLLMEvents({
2738
+ operationName: 'chat',
2739
+ conversationId: 'conv-abc123',
2740
+ model: 'claude-3-opus',
2741
+ });
2742
+ assert.strictEqual(results.length, 1);
2743
+ assert.strictEqual(results[0].attributes['gen_ai.request.model'], 'claude-3-opus');
2744
+ });
2745
+ });
2746
+ describe('queryTraces OTel GenAI agent/tool filters', () => {
2747
+ it('should filter traces by agentId', async () => {
2748
+ const today = getTestDate();
2749
+ const mockSpans = [
2750
+ {
2751
+ traceId: 'trace1',
2752
+ spanId: 'span1',
2753
+ name: 'agent.invoke',
2754
+ startTime: [1700000000, 0],
2755
+ attributes: { 'gen_ai.agent.id': 'agent-001' },
2756
+ },
2757
+ {
2758
+ traceId: 'trace1',
2759
+ spanId: 'span2',
2760
+ name: 'agent.invoke',
2761
+ startTime: [1700000001, 0],
2762
+ attributes: { 'gen_ai.agent.id': 'agent-002' },
2763
+ },
2764
+ ];
2765
+ writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
2766
+ const results = await backend.queryTraces({ agentId: 'agent-001' });
2767
+ assert.strictEqual(results.length, 1);
2768
+ assert.strictEqual(results[0].attributes?.['gen_ai.agent.id'], 'agent-001');
2769
+ });
2770
+ it('should filter traces by agentName', async () => {
2771
+ const today = getTestDate();
2772
+ const mockSpans = [
2773
+ {
2774
+ traceId: 'trace1',
2775
+ spanId: 'span1',
2776
+ name: 'agent.invoke',
2777
+ startTime: [1700000000, 0],
2778
+ attributes: { 'gen_ai.agent.name': 'Explore' },
2779
+ },
2780
+ {
2781
+ traceId: 'trace1',
2782
+ spanId: 'span2',
2783
+ name: 'agent.invoke',
2784
+ startTime: [1700000001, 0],
2785
+ attributes: { 'gen_ai.agent.name': 'Plan' },
2786
+ },
2787
+ ];
2788
+ writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
2789
+ const results = await backend.queryTraces({ agentName: 'Explore' });
2790
+ assert.strictEqual(results.length, 1);
2791
+ assert.strictEqual(results[0].attributes?.['gen_ai.agent.name'], 'Explore');
2792
+ });
2793
+ it('should filter traces by toolName', async () => {
2794
+ const today = getTestDate();
2795
+ const mockSpans = [
2796
+ {
2797
+ traceId: 'trace1',
2798
+ spanId: 'span1',
2799
+ name: 'tool.execute',
2800
+ startTime: [1700000000, 0],
2801
+ attributes: { 'gen_ai.tool.name': 'Read' },
2802
+ },
2803
+ {
2804
+ traceId: 'trace1',
2805
+ spanId: 'span2',
2806
+ name: 'tool.execute',
2807
+ startTime: [1700000001, 0],
2808
+ attributes: { 'gen_ai.tool.name': 'Write' },
2809
+ },
2810
+ ];
2811
+ writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
2812
+ const results = await backend.queryTraces({ toolName: 'Read' });
2813
+ assert.strictEqual(results.length, 1);
2814
+ assert.strictEqual(results[0].attributes?.['gen_ai.tool.name'], 'Read');
2815
+ });
2816
+ it('should filter traces by toolCallId', async () => {
2817
+ const today = getTestDate();
2818
+ const mockSpans = [
2819
+ {
2820
+ traceId: 'trace1',
2821
+ spanId: 'span1',
2822
+ name: 'tool.execute',
2823
+ startTime: [1700000000, 0],
2824
+ attributes: { 'gen_ai.tool.call.id': 'toolu_abc123' },
2825
+ },
2826
+ {
2827
+ traceId: 'trace1',
2828
+ spanId: 'span2',
2829
+ name: 'tool.execute',
2830
+ startTime: [1700000001, 0],
2831
+ attributes: { 'gen_ai.tool.call.id': 'toolu_xyz789' },
2832
+ },
2833
+ ];
2834
+ writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
2835
+ const results = await backend.queryTraces({ toolCallId: 'toolu_abc123' });
2836
+ assert.strictEqual(results.length, 1);
2837
+ assert.strictEqual(results[0].attributes?.['gen_ai.tool.call.id'], 'toolu_abc123');
2838
+ });
2839
+ it('should filter traces by toolType', async () => {
2840
+ const today = getTestDate();
2841
+ const mockSpans = [
2842
+ {
2843
+ traceId: 'trace1',
2844
+ spanId: 'span1',
2845
+ name: 'tool.execute',
2846
+ startTime: [1700000000, 0],
2847
+ attributes: { 'gen_ai.tool.type': 'function' },
2848
+ },
2849
+ {
2850
+ traceId: 'trace1',
2851
+ spanId: 'span2',
2852
+ name: 'tool.execute',
2853
+ startTime: [1700000001, 0],
2854
+ attributes: { 'gen_ai.tool.type': 'mcp' },
2855
+ },
2856
+ ];
2857
+ writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
2858
+ const results = await backend.queryTraces({ toolType: 'function' });
2859
+ assert.strictEqual(results.length, 1);
2860
+ assert.strictEqual(results[0].attributes?.['gen_ai.tool.type'], 'function');
2861
+ });
2862
+ it('should filter traces by operationName', async () => {
2863
+ const today = getTestDate();
2864
+ const mockSpans = [
2865
+ {
2866
+ traceId: 'trace1',
2867
+ spanId: 'span1',
2868
+ name: 'llm.call',
2869
+ startTime: [1700000000, 0],
2870
+ attributes: { 'gen_ai.operation.name': 'chat' },
2871
+ },
2872
+ {
2873
+ traceId: 'trace1',
2874
+ spanId: 'span2',
2875
+ name: 'tool.execute',
2876
+ startTime: [1700000001, 0],
2877
+ attributes: { 'gen_ai.operation.name': 'execute_tool' },
2878
+ },
2879
+ ];
2880
+ writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
2881
+ const results = await backend.queryTraces({ operationName: 'chat' });
2882
+ assert.strictEqual(results.length, 1);
2883
+ assert.strictEqual(results[0].attributes?.['gen_ai.operation.name'], 'chat');
2884
+ });
2885
+ it('should combine agent/tool filters with other trace filters', async () => {
2886
+ const today = getTestDate();
2887
+ const mockSpans = [
2888
+ {
2889
+ traceId: 'trace1',
2890
+ spanId: 'span1',
2891
+ name: 'agent.explore',
2892
+ startTime: [1700000000, 0],
2893
+ duration: [0, 100000000], // 100ms
2894
+ attributes: {
2895
+ 'gen_ai.agent.name': 'Explore',
2896
+ 'gen_ai.tool.name': 'Grep',
2897
+ },
2898
+ },
2899
+ {
2900
+ traceId: 'trace1',
2901
+ spanId: 'span2',
2902
+ name: 'agent.explore',
2903
+ startTime: [1700000001, 0],
2904
+ duration: [0, 200000000], // 200ms
2905
+ attributes: {
2906
+ 'gen_ai.agent.name': 'Explore',
2907
+ 'gen_ai.tool.name': 'Read',
2908
+ },
2909
+ },
2910
+ ];
2911
+ writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
2912
+ const results = await backend.queryTraces({
2913
+ agentName: 'Explore',
2914
+ toolName: 'Grep',
2915
+ spanName: 'agent',
2916
+ });
2917
+ assert.strictEqual(results.length, 1);
2918
+ assert.strictEqual(results[0].attributes?.['gen_ai.tool.name'], 'Grep');
2919
+ });
2626
2920
  });
2627
2921
  describe('healthCheck', () => {
2628
2922
  it('should return error when telemetry directory does not exist', async () => {
@@ -2997,6 +3291,46 @@ describe('QueryCache', () => {
2997
3291
  assert.strictEqual(result2.length, 1, 'Should return cached LLM events result');
2998
3292
  });
2999
3293
  });
3294
+ describe('getCacheStats', () => {
3295
+ it('should return correct cache stats structure', () => {
3296
+ const stats = backend.getCacheStats();
3297
+ assert.ok(stats.traces, 'Should have traces stats');
3298
+ assert.ok(stats.logs, 'Should have logs stats');
3299
+ assert.ok(stats.metrics, 'Should have metrics stats');
3300
+ assert.ok(stats.llmEvents, 'Should have llmEvents stats');
3301
+ // Check structure of each cache stat
3302
+ for (const key of ['traces', 'logs', 'metrics', 'llmEvents']) {
3303
+ const stat = stats[key];
3304
+ assert.strictEqual(typeof stat.hits, 'number', `${key} should have hits`);
3305
+ assert.strictEqual(typeof stat.misses, 'number', `${key} should have misses`);
3306
+ assert.strictEqual(typeof stat.evictions, 'number', `${key} should have evictions`);
3307
+ assert.strictEqual(typeof stat.size, 'number', `${key} should have size`);
3308
+ assert.strictEqual(typeof stat.hitRate, 'number', `${key} should have hitRate`);
3309
+ }
3310
+ });
3311
+ it('should track cache hits and misses', async () => {
3312
+ const today = getTestDate();
3313
+ const mockSpans = [
3314
+ { traceId: 'stats-test-1', spanId: 'span1', name: 'test-op', startTime: [1700000000, 0] },
3315
+ ];
3316
+ writeJsonlFile(path.join(tempDir, `traces-${today}.jsonl`), mockSpans);
3317
+ // Initial stats should be zero
3318
+ let stats = backend.getCacheStats();
3319
+ assert.strictEqual(stats.traces.hits, 0);
3320
+ assert.strictEqual(stats.traces.misses, 0);
3321
+ // First query - cache miss
3322
+ await backend.queryTraces({ spanName: 'test-op' });
3323
+ stats = backend.getCacheStats();
3324
+ assert.strictEqual(stats.traces.misses, 1);
3325
+ assert.strictEqual(stats.traces.hits, 0);
3326
+ // Second query with same options - cache hit
3327
+ await backend.queryTraces({ spanName: 'test-op' });
3328
+ stats = backend.getCacheStats();
3329
+ assert.strictEqual(stats.traces.hits, 1);
3330
+ assert.strictEqual(stats.traces.misses, 1);
3331
+ assert.ok(stats.traces.hitRate > 0, 'Hit rate should be > 0');
3332
+ });
3333
+ });
3000
3334
  describe('indexed queries', () => {
3001
3335
  it('should use index for trace queries when available', async () => {
3002
3336
  const today = getTestDate();
@@ -3367,6 +3701,33 @@ describe('MultiDirectoryBackend', () => {
3367
3701
  assert.ok(health.message?.includes('telemetry director'));
3368
3702
  });
3369
3703
  });
3704
+ describe('getCacheStats', () => {
3705
+ it('should return aggregated cache stats from all backends', () => {
3706
+ const localTelemetry = path.join(projectDir, 'telemetry');
3707
+ fs.mkdirSync(localTelemetry, { recursive: true });
3708
+ const backend = new MultiDirectoryBackend(projectDir);
3709
+ const stats = backend.getCacheStats();
3710
+ assert.ok(stats.traces, 'Should have traces stats');
3711
+ assert.ok(stats.logs, 'Should have logs stats');
3712
+ assert.ok(stats.metrics, 'Should have metrics stats');
3713
+ assert.ok(stats.llmEvents, 'Should have llmEvents stats');
3714
+ });
3715
+ it('should have correct structure for aggregated stats', () => {
3716
+ const localTelemetry = path.join(projectDir, 'telemetry');
3717
+ fs.mkdirSync(localTelemetry, { recursive: true });
3718
+ const backend = new MultiDirectoryBackend(projectDir);
3719
+ const stats = backend.getCacheStats();
3720
+ for (const key of ['traces', 'logs', 'metrics', 'llmEvents']) {
3721
+ const stat = stats[key];
3722
+ assert.strictEqual(typeof stat.hits, 'number', `${key} should have hits`);
3723
+ assert.strictEqual(typeof stat.misses, 'number', `${key} should have misses`);
3724
+ assert.strictEqual(typeof stat.evictions, 'number', `${key} should have evictions`);
3725
+ assert.strictEqual(typeof stat.size, 'number', `${key} should have size`);
3726
+ assert.strictEqual(typeof stat.hitRate, 'number', `${key} should have hitRate`);
3727
+ assert.ok(stat.hitRate >= 0 && stat.hitRate <= 1, `${key} hitRate should be between 0 and 1`);
3728
+ }
3729
+ });
3730
+ });
3370
3731
  });
3371
3732
  /**
3372
3733
  * OTLP Export Tests