observability-toolkit 1.1.0 → 1.4.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 (117) hide show
  1. package/README.md +52 -3
  2. package/dist/backends/index.d.ts +28 -0
  3. package/dist/backends/index.d.ts.map +1 -1
  4. package/dist/backends/local-jsonl.d.ts +29 -1
  5. package/dist/backends/local-jsonl.d.ts.map +1 -1
  6. package/dist/backends/local-jsonl.js +259 -27
  7. package/dist/backends/local-jsonl.js.map +1 -1
  8. package/dist/backends/local-jsonl.test.d.ts +2 -0
  9. package/dist/backends/local-jsonl.test.d.ts.map +1 -0
  10. package/dist/backends/local-jsonl.test.js +1638 -0
  11. package/dist/backends/local-jsonl.test.js.map +1 -0
  12. package/dist/backends/signoz-api.d.ts +9 -2
  13. package/dist/backends/signoz-api.d.ts.map +1 -1
  14. package/dist/backends/signoz-api.integration.test.d.ts +8 -0
  15. package/dist/backends/signoz-api.integration.test.d.ts.map +1 -0
  16. package/dist/backends/signoz-api.integration.test.js +137 -0
  17. package/dist/backends/signoz-api.integration.test.js.map +1 -0
  18. package/dist/backends/signoz-api.js +206 -115
  19. package/dist/backends/signoz-api.js.map +1 -1
  20. package/dist/backends/signoz-api.test.d.ts +2 -0
  21. package/dist/backends/signoz-api.test.d.ts.map +1 -0
  22. package/dist/backends/signoz-api.test.js +1080 -0
  23. package/dist/backends/signoz-api.test.js.map +1 -0
  24. package/dist/lib/constants.d.ts +28 -0
  25. package/dist/lib/constants.d.ts.map +1 -1
  26. package/dist/lib/constants.js +73 -0
  27. package/dist/lib/constants.js.map +1 -1
  28. package/dist/lib/constants.test.d.ts +5 -0
  29. package/dist/lib/constants.test.d.ts.map +1 -0
  30. package/dist/lib/constants.test.js +381 -0
  31. package/dist/lib/constants.test.js.map +1 -0
  32. package/dist/lib/file-utils.d.ts +53 -1
  33. package/dist/lib/file-utils.d.ts.map +1 -1
  34. package/dist/lib/file-utils.js +142 -3
  35. package/dist/lib/file-utils.js.map +1 -1
  36. package/dist/lib/file-utils.test.d.ts +2 -0
  37. package/dist/lib/file-utils.test.d.ts.map +1 -0
  38. package/dist/lib/file-utils.test.js +649 -0
  39. package/dist/lib/file-utils.test.js.map +1 -0
  40. package/dist/server.js +50 -63
  41. package/dist/server.js.map +1 -1
  42. package/dist/server.test.d.ts +5 -0
  43. package/dist/server.test.d.ts.map +1 -0
  44. package/dist/server.test.js +547 -0
  45. package/dist/server.test.js.map +1 -0
  46. package/dist/tools/context-stats.d.ts +2 -2
  47. package/dist/tools/context-stats.d.ts.map +1 -1
  48. package/dist/tools/context-stats.js +2 -1
  49. package/dist/tools/context-stats.js.map +1 -1
  50. package/dist/tools/context-stats.test.d.ts +5 -0
  51. package/dist/tools/context-stats.test.d.ts.map +1 -0
  52. package/dist/tools/context-stats.test.js +465 -0
  53. package/dist/tools/context-stats.test.js.map +1 -0
  54. package/dist/tools/get-trace-url.d.ts.map +1 -1
  55. package/dist/tools/get-trace-url.js +5 -1
  56. package/dist/tools/get-trace-url.js.map +1 -1
  57. package/dist/tools/get-trace-url.test.d.ts +5 -0
  58. package/dist/tools/get-trace-url.test.d.ts.map +1 -0
  59. package/dist/tools/get-trace-url.test.js +429 -0
  60. package/dist/tools/get-trace-url.test.js.map +1 -0
  61. package/dist/tools/health-check.d.ts +9 -2
  62. package/dist/tools/health-check.d.ts.map +1 -1
  63. package/dist/tools/health-check.js +66 -27
  64. package/dist/tools/health-check.js.map +1 -1
  65. package/dist/tools/health-check.test.d.ts +5 -0
  66. package/dist/tools/health-check.test.d.ts.map +1 -0
  67. package/dist/tools/health-check.test.js +386 -0
  68. package/dist/tools/health-check.test.js.map +1 -0
  69. package/dist/tools/index.d.ts +1 -0
  70. package/dist/tools/index.d.ts.map +1 -1
  71. package/dist/tools/index.js +1 -0
  72. package/dist/tools/index.js.map +1 -1
  73. package/dist/tools/query-llm-events.d.ts +82 -0
  74. package/dist/tools/query-llm-events.d.ts.map +1 -0
  75. package/dist/tools/query-llm-events.js +60 -0
  76. package/dist/tools/query-llm-events.js.map +1 -0
  77. package/dist/tools/query-llm-events.test.d.ts +5 -0
  78. package/dist/tools/query-llm-events.test.d.ts.map +1 -0
  79. package/dist/tools/query-llm-events.test.js +111 -0
  80. package/dist/tools/query-llm-events.test.js.map +1 -0
  81. package/dist/tools/query-logs.d.ts +15 -8
  82. package/dist/tools/query-logs.d.ts.map +1 -1
  83. package/dist/tools/query-logs.js +11 -10
  84. package/dist/tools/query-logs.js.map +1 -1
  85. package/dist/tools/query-logs.test.d.ts +5 -0
  86. package/dist/tools/query-logs.test.d.ts.map +1 -0
  87. package/dist/tools/query-logs.test.js +688 -0
  88. package/dist/tools/query-logs.test.js.map +1 -0
  89. package/dist/tools/query-metrics.d.ts +13 -15
  90. package/dist/tools/query-metrics.d.ts.map +1 -1
  91. package/dist/tools/query-metrics.js +12 -13
  92. package/dist/tools/query-metrics.js.map +1 -1
  93. package/dist/tools/query-metrics.test.d.ts +5 -0
  94. package/dist/tools/query-metrics.test.d.ts.map +1 -0
  95. package/dist/tools/query-metrics.test.js +597 -0
  96. package/dist/tools/query-metrics.test.js.map +1 -0
  97. package/dist/tools/query-traces.d.ts +19 -14
  98. package/dist/tools/query-traces.d.ts.map +1 -1
  99. package/dist/tools/query-traces.js +14 -14
  100. package/dist/tools/query-traces.js.map +1 -1
  101. package/dist/tools/query-traces.test.d.ts +5 -0
  102. package/dist/tools/query-traces.test.d.ts.map +1 -0
  103. package/dist/tools/query-traces.test.js +643 -0
  104. package/dist/tools/query-traces.test.js.map +1 -0
  105. package/dist/tools/setup-claudeignore.d.ts +36 -10
  106. package/dist/tools/setup-claudeignore.d.ts.map +1 -1
  107. package/dist/tools/setup-claudeignore.js +193 -33
  108. package/dist/tools/setup-claudeignore.js.map +1 -1
  109. package/dist/tools/setup-claudeignore.test.d.ts +2 -0
  110. package/dist/tools/setup-claudeignore.test.d.ts.map +1 -0
  111. package/dist/tools/setup-claudeignore.test.js +481 -0
  112. package/dist/tools/setup-claudeignore.test.js.map +1 -0
  113. package/dist/tools/signoz.integration.test.d.ts +8 -0
  114. package/dist/tools/signoz.integration.test.d.ts.map +1 -0
  115. package/dist/tools/signoz.integration.test.js +141 -0
  116. package/dist/tools/signoz.integration.test.js.map +1 -0
  117. package/package.json +6 -3
package/README.md CHANGED
@@ -21,10 +21,11 @@ claude mcp add observability-toolkit -- node ~/.claude/mcp-servers/observability
21
21
  | `obs_query_traces` | Query traces from local JSONL or SigNoz |
22
22
  | `obs_query_metrics` | Query metrics with aggregation support |
23
23
  | `obs_query_logs` | Query logs by severity, search text, trace ID |
24
+ | `obs_query_llm_events` | Query LLM events with token usage and duration metrics |
24
25
  | `obs_health_check` | Check telemetry system health |
25
26
  | `obs_context_stats` | Get context window utilization stats |
26
27
  | `obs_get_trace_url` | Get SigNoz trace viewer URL (requires SigNoz) |
27
- | `obs_setup_claudeignore` | Add telemetry/ to .claudeignore |
28
+ | `obs_setup_claudeignore` | Add entries to .claudeignore (telemetry, test files, coverage) |
28
29
 
29
30
  ## Configuration
30
31
 
@@ -44,8 +45,26 @@ The server works standalone with local JSONL files. SigNoz integration is option
44
45
  obs_query_traces({ backend: "local", limit: 10 })
45
46
  obs_query_traces({ traceId: "abc123..." })
46
47
  obs_query_traces({ serviceName: "claude-code", minDurationMs: 100 })
48
+ obs_query_traces({ attributeFilter: { "agent.source_type": "lazy" } })
47
49
  ```
48
50
 
51
+ ### Filterable Attributes
52
+
53
+ Use `attributeFilter` to query by span attributes:
54
+
55
+ | Attribute | Values | Description |
56
+ |-----------|--------|-------------|
57
+ | `agent.type` | string | Agent name (e.g., "Explore", "auto-error-resolver") |
58
+ | `agent.source_type` | `active`, `lazy`, `builtin`, `settings` | Where agent is defined |
59
+ | `agent.category` | string | Agent category (e.g., "error-handling", "code") |
60
+ | `plugin.name` | string | Skill/plugin name |
61
+ | `plugin.source_type` | `active`, `lazy`, `settings` | Where skill is defined |
62
+ | `plugin.category` | string | Skill category |
63
+ | `mcp.server` | string | MCP server name |
64
+ | `mcp.tool` | string | MCP tool name |
65
+ | `builtin.tool` | string | Built-in tool name (Read, Write, Bash, etc.) |
66
+ | `hook.type` | `agent`, `plugin`, `mcp`, `builtin` | Hook handler type |
67
+
49
68
  ### Query logs
50
69
 
51
70
  ```
@@ -61,6 +80,14 @@ obs_query_metrics({ metricName: "session.context.size" })
61
80
  obs_query_metrics({ metricName: "gen_ai.client.token.usage", aggregation: "sum" })
62
81
  ```
63
82
 
83
+ ### Query LLM events
84
+
85
+ ```
86
+ obs_query_llm_events({})
87
+ obs_query_llm_events({ model: "claude-opus-4-5", limit: 10 })
88
+ obs_query_llm_events({ provider: "anthropic", startDate: "2026-01-28" })
89
+ ```
90
+
64
91
  ### Health check
65
92
 
66
93
  ```
@@ -85,17 +112,31 @@ obs_get_trace_url({ traceId: "abc123..." })
85
112
  ```
86
113
  obs_setup_claudeignore({})
87
114
  obs_setup_claudeignore({ dryRun: true })
88
- obs_setup_claudeignore({ entry: "telemetry/", path: "/path/to/.claudeignore" })
115
+ obs_setup_claudeignore({ includeDefaults: false, entry: "custom/" })
116
+ obs_setup_claudeignore({ entries: ["logs/", "tmp/", "*.bak"] })
89
117
  ```
90
118
 
119
+ Default entries added when `includeDefaults: true` (default):
120
+ - `telemetry/` - telemetry data
121
+ - `*.test.ts` - TypeScript test files
122
+ - `*.test.js` - JavaScript test files
123
+ - `coverage/` - coverage reports
124
+
91
125
  ## Data Sources
92
126
 
93
127
  ### Local JSONL (Default)
94
128
 
95
- Reads telemetry data from `~/.claude/telemetry/`:
129
+ Automatically scans multiple telemetry directories:
130
+ - **Global**: `~/.claude/telemetry/` (always checked)
131
+ - **Project-local**: `.claude/telemetry/`, `telemetry/`, `.telemetry/` (checked if they exist in cwd)
132
+
133
+ This allows querying both global Claude Code telemetry and project-specific telemetry.
134
+
135
+ File patterns:
96
136
  - `traces-YYYY-MM-DD.jsonl` - Trace spans
97
137
  - `logs-YYYY-MM-DD.jsonl` - Log records
98
138
  - `metrics-YYYY-MM-DD.jsonl` - Metric data points
139
+ - `llm-events-YYYY-MM-DD.jsonl` - LLM events
99
140
 
100
141
  ### SigNoz Cloud (Optional)
101
142
 
@@ -112,7 +153,15 @@ Use `backend: "signoz"` in queries to explicitly use SigNoz, or `backend: "auto"
112
153
  cd ~/.claude/mcp-servers/observability-toolkit
113
154
  npm install
114
155
  npm run build
156
+ npm test # 617 tests
115
157
  npm run start
116
158
  ```
117
159
 
160
+ ## Documentation
161
+
162
+ - [ROADMAP.md](docs/ROADMAP.md) - Improvement roadmap with priorities
163
+ - [code-review.md](docs/code-review.md) - Code review findings
164
+ - [security-audit.md](docs/security-audit.md) - Security audit report
165
+ - [observability-audit.md](docs/observability-audit.md) - Observability best practices audit
166
+
118
167
 
@@ -14,6 +14,8 @@ export interface TraceSpan {
14
14
  code: number;
15
15
  message?: string;
16
16
  };
17
+ /** Human-readable status code: 'UNSET' (0), 'OK' (1), 'ERROR' (2) */
18
+ statusCode?: 'UNSET' | 'OK' | 'ERROR';
17
19
  attributes?: Record<string, unknown>;
18
20
  events?: Array<{
19
21
  name: string;
@@ -24,6 +26,7 @@ export interface TraceSpan {
24
26
  export interface LogRecord {
25
27
  timestamp: string;
26
28
  severity: string;
29
+ severityNumber?: number;
27
30
  body: string;
28
31
  traceId?: string;
29
32
  spanId?: string;
@@ -48,22 +51,47 @@ export interface TraceQueryOptions extends QueryOptions {
48
51
  spanName?: string;
49
52
  minDurationMs?: number;
50
53
  maxDurationMs?: number;
54
+ attributeFilter?: Record<string, string | number | boolean>;
55
+ /** Exclude spans matching this name (substring match) */
56
+ excludeSpanName?: string;
57
+ /** Only include spans where these attributes exist */
58
+ attributeExists?: string[];
59
+ /** Exclude spans where these attributes exist */
60
+ attributeNotExists?: string[];
51
61
  }
52
62
  export interface LogQueryOptions extends QueryOptions {
53
63
  severity?: string;
54
64
  search?: string;
55
65
  traceId?: string;
66
+ /** Exclude logs containing this text (case-insensitive) */
67
+ excludeSearch?: string;
68
+ /** Only include logs where these attributes exist */
69
+ attributeExists?: string[];
70
+ /** Exclude logs where these attributes exist */
71
+ attributeNotExists?: string[];
56
72
  }
57
73
  export interface MetricQueryOptions extends QueryOptions {
58
74
  metricName?: string;
59
75
  aggregation?: 'sum' | 'avg' | 'min' | 'max' | 'count';
60
76
  groupBy?: string[];
61
77
  }
78
+ export interface LLMEvent {
79
+ timestamp: string;
80
+ name: string;
81
+ attributes: Record<string, unknown>;
82
+ }
83
+ export interface LLMEventQueryOptions extends QueryOptions {
84
+ eventName?: string;
85
+ model?: string;
86
+ provider?: string;
87
+ search?: string;
88
+ }
62
89
  export interface TelemetryBackend {
63
90
  name: string;
64
91
  queryTraces(options: TraceQueryOptions): Promise<TraceSpan[]>;
65
92
  queryLogs(options: LogQueryOptions): Promise<LogRecord[]>;
66
93
  queryMetrics(options: MetricQueryOptions): Promise<MetricDataPoint[]>;
94
+ queryLLMEvents?(options: LLMEventQueryOptions): Promise<LLMEvent[]>;
67
95
  healthCheck(): Promise<{
68
96
  status: 'ok' | 'error';
69
97
  message?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/backends/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;CAC3F;AAED,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAkB,SAAQ,YAAY;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,eAAgB,SAAQ,YAAY;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,kBAAmB,SAAQ,YAAY;IACtD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG,OAAO,CAAC;IACtD,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IAEb,WAAW,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IAC9D,SAAS,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IAC1D,YAAY,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;IACtE,WAAW,IAAI,OAAO,CAAC;QAAE,MAAM,EAAE,IAAI,GAAG,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACtE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/backends/index.ts"],"names":[],"mappings":"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"}
@@ -4,7 +4,7 @@
4
4
  * The local telemetry files use a flat JSONL format where each line is a complete
5
5
  * span or log record, not the batched OpenTelemetry export format.
6
6
  */
7
- import { TelemetryBackend, TraceSpan, LogRecord, MetricDataPoint, TraceQueryOptions, LogQueryOptions, MetricQueryOptions } from './index.js';
7
+ import { TelemetryBackend, TraceSpan, LogRecord, MetricDataPoint, LLMEvent, TraceQueryOptions, LogQueryOptions, MetricQueryOptions, LLMEventQueryOptions } from './index.js';
8
8
  export declare class LocalJsonlBackend implements TelemetryBackend {
9
9
  name: string;
10
10
  private telemetryDir;
@@ -13,9 +13,37 @@ export declare class LocalJsonlBackend implements TelemetryBackend {
13
13
  queryLogs(options: LogQueryOptions): Promise<LogRecord[]>;
14
14
  queryMetrics(options: MetricQueryOptions): Promise<MetricDataPoint[]>;
15
15
  private aggregate;
16
+ queryLLMEvents(options: LLMEventQueryOptions): Promise<LLMEvent[]>;
16
17
  healthCheck(): Promise<{
17
18
  status: 'ok' | 'error';
18
19
  message?: string;
19
20
  }>;
20
21
  }
22
+ /**
23
+ * Multi-directory backend that queries all telemetry directories
24
+ * (global ~/.claude/telemetry + local project directories)
25
+ */
26
+ export declare class MultiDirectoryBackend implements TelemetryBackend {
27
+ name: string;
28
+ private backends;
29
+ private directories;
30
+ constructor(cwd?: string);
31
+ getDirectories(): Array<{
32
+ path: string;
33
+ source: 'global' | 'local';
34
+ }>;
35
+ queryTraces(options: TraceQueryOptions): Promise<TraceSpan[]>;
36
+ queryLogs(options: LogQueryOptions): Promise<LogRecord[]>;
37
+ queryMetrics(options: MetricQueryOptions): Promise<MetricDataPoint[]>;
38
+ queryLLMEvents(options: LLMEventQueryOptions): Promise<LLMEvent[]>;
39
+ healthCheck(): Promise<{
40
+ status: 'ok' | 'error';
41
+ message?: string;
42
+ directories?: Array<{
43
+ path: string;
44
+ source: string;
45
+ status: string;
46
+ }>;
47
+ }>;
48
+ }
21
49
  //# sourceMappingURL=local-jsonl.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"local-jsonl.d.ts","sourceRoot":"","sources":["../../src/backends/local-jsonl.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EACL,gBAAgB,EAChB,SAAS,EACT,SAAS,EACT,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAwKpB,qBAAa,iBAAkB,YAAW,gBAAgB;IACxD,IAAI,SAAiB;IACrB,OAAO,CAAC,YAAY,CAAS;gBAEjB,YAAY,CAAC,EAAE,MAAM;IAI3B,WAAW,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAyC7D,SAAS,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAoCzD,YAAY,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAyC3E,OAAO,CAAC,SAAS;IAyDX,WAAW,IAAI,OAAO,CAAC;QAAE,MAAM,EAAE,IAAI,GAAG,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAqB3E"}
1
+ {"version":3,"file":"local-jsonl.d.ts","sourceRoot":"","sources":["../../src/backends/local-jsonl.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EACL,gBAAgB,EAChB,SAAS,EACT,SAAS,EACT,eAAe,EACf,QAAQ,EACR,iBAAiB,EACjB,eAAe,EACf,kBAAkB,EAClB,oBAAoB,EACrB,MAAM,YAAY,CAAC;AAsOpB,qBAAa,iBAAkB,YAAW,gBAAgB;IACxD,IAAI,SAAiB;IACrB,OAAO,CAAC,YAAY,CAAS;gBAEjB,YAAY,CAAC,EAAE,MAAM;IAI3B,WAAW,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAyE7D,SAAS,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAyDzD,YAAY,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAqC3E,OAAO,CAAC,SAAS;IAyDX,cAAc,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAgDlE,WAAW,IAAI,OAAO,CAAC;QAAE,MAAM,EAAE,IAAI,GAAG,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CA6B3E;AAED;;;GAGG;AACH,qBAAa,qBAAsB,YAAW,gBAAgB;IAC5D,IAAI,SAAqB;IACzB,OAAO,CAAC,QAAQ,CAAsB;IACtC,OAAO,CAAC,WAAW,CAAsD;gBAE7D,GAAG,CAAC,EAAE,MAAM;IAKxB,cAAc,IAAI,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAA;KAAE,CAAC;IAI/D,WAAW,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAwB7D,SAAS,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAwBzD,YAAY,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAarE,cAAc,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAwBlE,WAAW,IAAI,OAAO,CAAC;QAAE,MAAM,EAAE,IAAI,GAAG,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,CAAC;CAwBlJ"}
@@ -5,9 +5,49 @@
5
5
  * span or log record, not the batched OpenTelemetry export format.
6
6
  */
7
7
  import { join } from 'path';
8
- import { TELEMETRY_DIR } from '../lib/constants.js';
9
- import { listFiles, readJsonlSync, parseDateFromFilename, getDateString, } from '../lib/file-utils.js';
8
+ import { TELEMETRY_DIR, getTelemetryDirectories, getSpanKind, getStatusCodeName } from '../lib/constants.js';
9
+ import { listFiles, streamJsonl, parseDateFromFilename, getDateString, paginateResults, hasReachedLimit, } from '../lib/file-utils.js';
10
10
  import { existsSync } from 'fs';
11
+ /**
12
+ * Insert item into a sorted array, maintaining sort order and max size.
13
+ * More efficient than sort+slice for top-K selection (O(n) vs O(n log n)).
14
+ */
15
+ function insertSortedBounded(arr, item, maxSize, compareFn) {
16
+ // Find insertion point using binary search
17
+ let low = 0;
18
+ let high = arr.length;
19
+ while (low < high) {
20
+ const mid = (low + high) >>> 1;
21
+ if (compareFn(arr[mid], item) <= 0) {
22
+ low = mid + 1;
23
+ }
24
+ else {
25
+ high = mid;
26
+ }
27
+ }
28
+ // If array is full and item would go at end, skip it
29
+ if (arr.length >= maxSize && low >= maxSize) {
30
+ return;
31
+ }
32
+ // Insert at the correct position
33
+ arr.splice(low, 0, item);
34
+ // Remove excess element if over max size
35
+ if (arr.length > maxSize) {
36
+ arr.pop();
37
+ }
38
+ }
39
+ /**
40
+ * OTel-compliant severity number mapping
41
+ * https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber
42
+ */
43
+ const SEVERITY_MAP = {
44
+ TRACE: 1,
45
+ DEBUG: 5,
46
+ INFO: 9,
47
+ WARN: 13,
48
+ ERROR: 17,
49
+ FATAL: 21,
50
+ };
11
51
  /**
12
52
  * Convert flat span to normalized TraceSpan
13
53
  */
@@ -42,11 +82,12 @@ function normalizeSpan(raw) {
42
82
  spanId: raw.spanId,
43
83
  parentSpanId: raw.parentSpanId,
44
84
  name: raw.name,
45
- kind: raw.kind !== undefined ? ['INTERNAL', 'SERVER', 'CLIENT', 'PRODUCER', 'CONSUMER'][raw.kind] : undefined,
85
+ kind: getSpanKind(raw.kind),
46
86
  startTimeUnixNano,
47
87
  endTimeUnixNano,
48
88
  durationMs,
49
89
  status: raw.status?.code !== undefined ? { code: raw.status.code, message: raw.status.message } : undefined,
90
+ statusCode: getStatusCodeName(raw.status?.code),
50
91
  attributes,
51
92
  };
52
93
  }
@@ -67,9 +108,12 @@ function normalizeLog(raw) {
67
108
  else {
68
109
  timestamp = raw.timestamp;
69
110
  }
111
+ const severity = raw.severityText || raw.severity || 'INFO';
112
+ const severityNumber = SEVERITY_MAP[severity.toUpperCase()];
70
113
  return {
71
114
  timestamp,
72
- severity: raw.severityText || raw.severity || 'INFO',
115
+ severity,
116
+ severityNumber,
73
117
  body: raw.body || '',
74
118
  traceId: raw.traceId,
75
119
  spanId: raw.spanId,
@@ -128,9 +172,8 @@ export class LocalJsonlBackend {
128
172
  const limit = options.limit || 100;
129
173
  const offset = options.offset || 0;
130
174
  for (const file of files) {
131
- // Read flat span records (one per line)
132
- const spans = readJsonlSync(file);
133
- for (const raw of spans) {
175
+ // Stream flat span records (one per line) to avoid loading entire file into memory
176
+ for await (const raw of streamJsonl(file)) {
134
177
  const span = normalizeSpan(raw);
135
178
  if (!span)
136
179
  continue;
@@ -139,6 +182,8 @@ export class LocalJsonlBackend {
139
182
  continue;
140
183
  if (options.spanName && !span.name.includes(options.spanName))
141
184
  continue;
185
+ if (options.excludeSpanName && span.name.includes(options.excludeSpanName))
186
+ continue;
142
187
  if (options.minDurationMs && (span.durationMs || 0) < options.minDurationMs)
143
188
  continue;
144
189
  if (options.maxDurationMs && (span.durationMs || Infinity) > options.maxDurationMs)
@@ -148,13 +193,49 @@ export class LocalJsonlBackend {
148
193
  if (svc !== options.serviceName)
149
194
  continue;
150
195
  }
196
+ // Apply attribute filter
197
+ if (options.attributeFilter) {
198
+ let matches = true;
199
+ for (const [key, value] of Object.entries(options.attributeFilter)) {
200
+ if (span.attributes?.[key] !== value) {
201
+ matches = false;
202
+ break;
203
+ }
204
+ }
205
+ if (!matches)
206
+ continue;
207
+ }
208
+ // Apply attributeExists filter - all specified attributes must exist
209
+ if (options.attributeExists) {
210
+ let allExist = true;
211
+ for (const key of options.attributeExists) {
212
+ if (span.attributes?.[key] === undefined) {
213
+ allExist = false;
214
+ break;
215
+ }
216
+ }
217
+ if (!allExist)
218
+ continue;
219
+ }
220
+ // Apply attributeNotExists filter - exclude if any specified attribute exists
221
+ if (options.attributeNotExists) {
222
+ let anyExist = false;
223
+ for (const key of options.attributeNotExists) {
224
+ if (span.attributes?.[key] !== undefined) {
225
+ anyExist = true;
226
+ break;
227
+ }
228
+ }
229
+ if (anyExist)
230
+ continue;
231
+ }
151
232
  results.push(span);
152
- if (results.length >= offset + limit) {
153
- return results.slice(offset, offset + limit);
233
+ if (hasReachedLimit(results.length, offset, limit)) {
234
+ return paginateResults(results, offset, limit);
154
235
  }
155
236
  }
156
237
  }
157
- return results.slice(offset, offset + limit);
238
+ return paginateResults(results, offset, limit);
158
239
  }
159
240
  async queryLogs(options) {
160
241
  const files = getFilesInRange(this.telemetryDir, /logs-\d{4}-\d{2}-\d{2}\.jsonl$/, options.startDate, options.endDate);
@@ -162,9 +243,8 @@ export class LocalJsonlBackend {
162
243
  const limit = options.limit || 100;
163
244
  const offset = options.offset || 0;
164
245
  for (const file of files) {
165
- // Read flat log records (one per line)
166
- const logs = readJsonlSync(file);
167
- for (const raw of logs) {
246
+ // Stream flat log records (one per line) to avoid loading entire file into memory
247
+ for await (const raw of streamJsonl(file)) {
168
248
  const log = normalizeLog(raw);
169
249
  if (!log)
170
250
  continue;
@@ -175,23 +255,48 @@ export class LocalJsonlBackend {
175
255
  continue;
176
256
  if (options.search && !log.body.toLowerCase().includes(options.search.toLowerCase()))
177
257
  continue;
258
+ if (options.excludeSearch && log.body.toLowerCase().includes(options.excludeSearch.toLowerCase()))
259
+ continue;
260
+ // Apply attributeExists filter - all specified attributes must exist
261
+ if (options.attributeExists) {
262
+ let allExist = true;
263
+ for (const key of options.attributeExists) {
264
+ if (log.attributes?.[key] === undefined) {
265
+ allExist = false;
266
+ break;
267
+ }
268
+ }
269
+ if (!allExist)
270
+ continue;
271
+ }
272
+ // Apply attributeNotExists filter - exclude if any specified attribute exists
273
+ if (options.attributeNotExists) {
274
+ let anyExist = false;
275
+ for (const key of options.attributeNotExists) {
276
+ if (log.attributes?.[key] !== undefined) {
277
+ anyExist = true;
278
+ break;
279
+ }
280
+ }
281
+ if (anyExist)
282
+ continue;
283
+ }
178
284
  results.push(log);
179
- if (results.length >= offset + limit) {
180
- return results.slice(offset, offset + limit);
285
+ if (hasReachedLimit(results.length, offset, limit)) {
286
+ return paginateResults(results, offset, limit);
181
287
  }
182
288
  }
183
289
  }
184
- return results.slice(offset, offset + limit);
290
+ return paginateResults(results, offset, limit);
185
291
  }
186
292
  async queryMetrics(options) {
187
293
  const files = getFilesInRange(this.telemetryDir, /metrics-\d{4}-\d{2}-\d{2}\.jsonl$/, options.startDate, options.endDate);
188
294
  const results = [];
189
295
  const limit = options.limit || 100;
190
296
  const offset = options.offset || 0;
191
- for (const file of files) {
192
- // Read flat metric records (one per line)
193
- const metrics = readJsonlSync(file);
194
- for (const raw of metrics) {
297
+ outer: for (const file of files) {
298
+ // Stream flat metric records (one per line) to avoid loading entire file into memory
299
+ for await (const raw of streamJsonl(file)) {
195
300
  const point = normalizeMetric(raw);
196
301
  if (!point)
197
302
  continue;
@@ -199,18 +304,16 @@ export class LocalJsonlBackend {
199
304
  if (options.metricName && !point.name.includes(options.metricName))
200
305
  continue;
201
306
  results.push(point);
202
- if (results.length >= offset + limit) {
203
- break;
307
+ if (hasReachedLimit(results.length, offset, limit)) {
308
+ break outer;
204
309
  }
205
310
  }
206
- if (results.length >= offset + limit)
207
- break;
208
311
  }
209
312
  // Apply aggregation if requested
210
313
  if (options.aggregation && results.length > 0) {
211
314
  return this.aggregate(results, options.aggregation, options.groupBy);
212
315
  }
213
- return results.slice(offset, offset + limit);
316
+ return paginateResults(results, offset, limit);
214
317
  }
215
318
  aggregate(points, aggregation, groupBy) {
216
319
  // Group by metric name and optional attributes
@@ -260,6 +363,47 @@ export class LocalJsonlBackend {
260
363
  }
261
364
  return results;
262
365
  }
366
+ async queryLLMEvents(options) {
367
+ const files = getFilesInRange(this.telemetryDir, /llm-events-\d{4}-\d{2}-\d{2}\.jsonl$/, options.startDate, options.endDate);
368
+ const results = [];
369
+ const limit = options.limit || 100;
370
+ const offset = options.offset || 0;
371
+ for (const file of files) {
372
+ // Stream LLM event records (one per line) to avoid loading entire file into memory
373
+ for await (const event of streamJsonl(file)) {
374
+ if (!event.timestamp || !event.name)
375
+ continue;
376
+ // Apply filters
377
+ if (options.eventName && !event.name.includes(options.eventName))
378
+ continue;
379
+ if (options.model) {
380
+ const model = event.attributes?.['gen_ai.request.model'] || event.attributes?.['model'];
381
+ if (model !== options.model)
382
+ continue;
383
+ }
384
+ if (options.provider) {
385
+ const provider = event.attributes?.['gen_ai.system'] || event.attributes?.['provider'];
386
+ if (provider !== options.provider)
387
+ continue;
388
+ }
389
+ if (options.search) {
390
+ const searchLower = options.search.toLowerCase();
391
+ const attrStr = JSON.stringify(event.attributes).toLowerCase();
392
+ if (!attrStr.includes(searchLower) && !event.name.toLowerCase().includes(searchLower))
393
+ continue;
394
+ }
395
+ results.push({
396
+ timestamp: event.timestamp,
397
+ name: event.name,
398
+ attributes: event.attributes,
399
+ });
400
+ if (hasReachedLimit(results.length, offset, limit)) {
401
+ return paginateResults(results, offset, limit);
402
+ }
403
+ }
404
+ }
405
+ return paginateResults(results, offset, limit);
406
+ }
263
407
  async healthCheck() {
264
408
  if (!existsSync(this.telemetryDir)) {
265
409
  return { status: 'error', message: `Telemetry directory not found: ${this.telemetryDir}` };
@@ -267,14 +411,102 @@ export class LocalJsonlBackend {
267
411
  const today = getDateString();
268
412
  const tracesFile = join(this.telemetryDir, `traces-${today}.jsonl`);
269
413
  const logsFile = join(this.telemetryDir, `logs-${today}.jsonl`);
414
+ const llmEventsFile = join(this.telemetryDir, `llm-events-${today}.jsonl`);
270
415
  const hasTraces = existsSync(tracesFile);
271
416
  const hasLogs = existsSync(logsFile);
272
- if (!hasTraces && !hasLogs) {
417
+ const hasLLMEvents = existsSync(llmEventsFile);
418
+ const found = [
419
+ hasTraces ? 'traces' : '',
420
+ hasLogs ? 'logs' : '',
421
+ hasLLMEvents ? 'llm-events' : '',
422
+ ].filter(Boolean).join(', ');
423
+ if (!found) {
273
424
  return { status: 'ok', message: `No telemetry files for today (${today})` };
274
425
  }
275
426
  return {
276
427
  status: 'ok',
277
- message: `Found: ${hasTraces ? 'traces' : ''}${hasTraces && hasLogs ? ', ' : ''}${hasLogs ? 'logs' : ''} for ${today}`,
428
+ message: `Found: ${found} for ${today}`,
429
+ };
430
+ }
431
+ }
432
+ /**
433
+ * Multi-directory backend that queries all telemetry directories
434
+ * (global ~/.claude/telemetry + local project directories)
435
+ */
436
+ export class MultiDirectoryBackend {
437
+ name = 'multi-directory';
438
+ backends;
439
+ directories;
440
+ constructor(cwd) {
441
+ this.directories = getTelemetryDirectories(cwd);
442
+ this.backends = this.directories.map(d => new LocalJsonlBackend(d.path));
443
+ }
444
+ getDirectories() {
445
+ return this.directories;
446
+ }
447
+ async queryTraces(options) {
448
+ const limit = options.limit || 100;
449
+ // Query all backends in parallel
450
+ const allBackendResults = await Promise.all(this.backends.map(b => b.queryTraces({ ...options, limit })));
451
+ // Merge results using bounded insertion for efficient top-K selection
452
+ const topResults = [];
453
+ for (const results of allBackendResults) {
454
+ for (const span of results) {
455
+ insertSortedBounded(topResults, span, limit, (a, b) => (b.startTimeUnixNano || 0) - (a.startTimeUnixNano || 0));
456
+ }
457
+ }
458
+ return topResults;
459
+ }
460
+ async queryLogs(options) {
461
+ const limit = options.limit || 100;
462
+ // Query all backends in parallel
463
+ const allBackendResults = await Promise.all(this.backends.map(b => b.queryLogs({ ...options, limit })));
464
+ // Merge results using bounded insertion for efficient top-K selection
465
+ const topResults = [];
466
+ for (const results of allBackendResults) {
467
+ for (const log of results) {
468
+ insertSortedBounded(topResults, log, limit, (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
469
+ }
470
+ }
471
+ return topResults;
472
+ }
473
+ async queryMetrics(options) {
474
+ const limit = options.limit || 100;
475
+ // Query all backends in parallel
476
+ const allBackendResults = await Promise.all(this.backends.map(b => b.queryMetrics({ ...options, limit })));
477
+ // Flatten and limit results
478
+ const allResults = allBackendResults.flat();
479
+ return allResults.slice(0, limit);
480
+ }
481
+ async queryLLMEvents(options) {
482
+ const limit = options.limit || 100;
483
+ // Query all backends in parallel
484
+ const allBackendResults = await Promise.all(this.backends.map(b => b.queryLLMEvents({ ...options, limit })));
485
+ // Merge results using bounded insertion for efficient top-K selection
486
+ const topResults = [];
487
+ for (const results of allBackendResults) {
488
+ for (const event of results) {
489
+ insertSortedBounded(topResults, event, limit, (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
490
+ }
491
+ }
492
+ return topResults;
493
+ }
494
+ async healthCheck() {
495
+ if (this.backends.length === 0) {
496
+ return { status: 'error', message: 'No telemetry directories found' };
497
+ }
498
+ // Check all backends in parallel
499
+ const healthResults = await Promise.all(this.backends.map(b => b.healthCheck()));
500
+ const dirStatuses = healthResults.map((result, i) => ({
501
+ path: this.directories[i].path,
502
+ source: this.directories[i].source,
503
+ status: result.message || result.status,
504
+ }));
505
+ const hasOk = healthResults.some(r => r.status === 'ok');
506
+ return {
507
+ status: hasOk ? 'ok' : 'error',
508
+ message: `Found ${this.directories.length} telemetry director${this.directories.length === 1 ? 'y' : 'ies'}`,
509
+ directories: dirStatuses,
278
510
  };
279
511
  }
280
512
  }