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.
- package/README.md +52 -3
- package/dist/backends/index.d.ts +28 -0
- package/dist/backends/index.d.ts.map +1 -1
- package/dist/backends/local-jsonl.d.ts +29 -1
- package/dist/backends/local-jsonl.d.ts.map +1 -1
- package/dist/backends/local-jsonl.js +259 -27
- package/dist/backends/local-jsonl.js.map +1 -1
- package/dist/backends/local-jsonl.test.d.ts +2 -0
- package/dist/backends/local-jsonl.test.d.ts.map +1 -0
- package/dist/backends/local-jsonl.test.js +1638 -0
- package/dist/backends/local-jsonl.test.js.map +1 -0
- package/dist/backends/signoz-api.d.ts +9 -2
- package/dist/backends/signoz-api.d.ts.map +1 -1
- package/dist/backends/signoz-api.integration.test.d.ts +8 -0
- package/dist/backends/signoz-api.integration.test.d.ts.map +1 -0
- package/dist/backends/signoz-api.integration.test.js +137 -0
- package/dist/backends/signoz-api.integration.test.js.map +1 -0
- package/dist/backends/signoz-api.js +206 -115
- package/dist/backends/signoz-api.js.map +1 -1
- package/dist/backends/signoz-api.test.d.ts +2 -0
- package/dist/backends/signoz-api.test.d.ts.map +1 -0
- package/dist/backends/signoz-api.test.js +1080 -0
- package/dist/backends/signoz-api.test.js.map +1 -0
- package/dist/lib/constants.d.ts +28 -0
- package/dist/lib/constants.d.ts.map +1 -1
- package/dist/lib/constants.js +73 -0
- package/dist/lib/constants.js.map +1 -1
- package/dist/lib/constants.test.d.ts +5 -0
- package/dist/lib/constants.test.d.ts.map +1 -0
- package/dist/lib/constants.test.js +381 -0
- package/dist/lib/constants.test.js.map +1 -0
- package/dist/lib/file-utils.d.ts +53 -1
- package/dist/lib/file-utils.d.ts.map +1 -1
- package/dist/lib/file-utils.js +142 -3
- package/dist/lib/file-utils.js.map +1 -1
- package/dist/lib/file-utils.test.d.ts +2 -0
- package/dist/lib/file-utils.test.d.ts.map +1 -0
- package/dist/lib/file-utils.test.js +649 -0
- package/dist/lib/file-utils.test.js.map +1 -0
- package/dist/server.js +50 -63
- package/dist/server.js.map +1 -1
- package/dist/server.test.d.ts +5 -0
- package/dist/server.test.d.ts.map +1 -0
- package/dist/server.test.js +547 -0
- package/dist/server.test.js.map +1 -0
- package/dist/tools/context-stats.d.ts +2 -2
- package/dist/tools/context-stats.d.ts.map +1 -1
- package/dist/tools/context-stats.js +2 -1
- package/dist/tools/context-stats.js.map +1 -1
- package/dist/tools/context-stats.test.d.ts +5 -0
- package/dist/tools/context-stats.test.d.ts.map +1 -0
- package/dist/tools/context-stats.test.js +465 -0
- package/dist/tools/context-stats.test.js.map +1 -0
- package/dist/tools/get-trace-url.d.ts.map +1 -1
- package/dist/tools/get-trace-url.js +5 -1
- package/dist/tools/get-trace-url.js.map +1 -1
- package/dist/tools/get-trace-url.test.d.ts +5 -0
- package/dist/tools/get-trace-url.test.d.ts.map +1 -0
- package/dist/tools/get-trace-url.test.js +429 -0
- package/dist/tools/get-trace-url.test.js.map +1 -0
- package/dist/tools/health-check.d.ts +9 -2
- package/dist/tools/health-check.d.ts.map +1 -1
- package/dist/tools/health-check.js +66 -27
- package/dist/tools/health-check.js.map +1 -1
- package/dist/tools/health-check.test.d.ts +5 -0
- package/dist/tools/health-check.test.d.ts.map +1 -0
- package/dist/tools/health-check.test.js +386 -0
- package/dist/tools/health-check.test.js.map +1 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +1 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/query-llm-events.d.ts +82 -0
- package/dist/tools/query-llm-events.d.ts.map +1 -0
- package/dist/tools/query-llm-events.js +60 -0
- package/dist/tools/query-llm-events.js.map +1 -0
- package/dist/tools/query-llm-events.test.d.ts +5 -0
- package/dist/tools/query-llm-events.test.d.ts.map +1 -0
- package/dist/tools/query-llm-events.test.js +111 -0
- package/dist/tools/query-llm-events.test.js.map +1 -0
- package/dist/tools/query-logs.d.ts +15 -8
- package/dist/tools/query-logs.d.ts.map +1 -1
- package/dist/tools/query-logs.js +11 -10
- package/dist/tools/query-logs.js.map +1 -1
- package/dist/tools/query-logs.test.d.ts +5 -0
- package/dist/tools/query-logs.test.d.ts.map +1 -0
- package/dist/tools/query-logs.test.js +688 -0
- package/dist/tools/query-logs.test.js.map +1 -0
- package/dist/tools/query-metrics.d.ts +13 -15
- package/dist/tools/query-metrics.d.ts.map +1 -1
- package/dist/tools/query-metrics.js +12 -13
- package/dist/tools/query-metrics.js.map +1 -1
- package/dist/tools/query-metrics.test.d.ts +5 -0
- package/dist/tools/query-metrics.test.d.ts.map +1 -0
- package/dist/tools/query-metrics.test.js +597 -0
- package/dist/tools/query-metrics.test.js.map +1 -0
- package/dist/tools/query-traces.d.ts +19 -14
- package/dist/tools/query-traces.d.ts.map +1 -1
- package/dist/tools/query-traces.js +14 -14
- package/dist/tools/query-traces.js.map +1 -1
- package/dist/tools/query-traces.test.d.ts +5 -0
- package/dist/tools/query-traces.test.d.ts.map +1 -0
- package/dist/tools/query-traces.test.js +643 -0
- package/dist/tools/query-traces.test.js.map +1 -0
- package/dist/tools/setup-claudeignore.d.ts +36 -10
- package/dist/tools/setup-claudeignore.d.ts.map +1 -1
- package/dist/tools/setup-claudeignore.js +193 -33
- package/dist/tools/setup-claudeignore.js.map +1 -1
- package/dist/tools/setup-claudeignore.test.d.ts +2 -0
- package/dist/tools/setup-claudeignore.test.d.ts.map +1 -0
- package/dist/tools/setup-claudeignore.test.js +481 -0
- package/dist/tools/setup-claudeignore.test.js.map +1 -0
- package/dist/tools/signoz.integration.test.d.ts +8 -0
- package/dist/tools/signoz.integration.test.d.ts.map +1 -0
- package/dist/tools/signoz.integration.test.js +141 -0
- package/dist/tools/signoz.integration.test.js.map +1 -0
- 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
|
|
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({
|
|
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
|
-
|
|
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
|
|
package/dist/backends/index.d.ts
CHANGED
|
@@ -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;
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
//
|
|
132
|
-
const
|
|
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
|
|
153
|
-
return results
|
|
233
|
+
if (hasReachedLimit(results.length, offset, limit)) {
|
|
234
|
+
return paginateResults(results, offset, limit);
|
|
154
235
|
}
|
|
155
236
|
}
|
|
156
237
|
}
|
|
157
|
-
return results
|
|
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
|
-
//
|
|
166
|
-
const
|
|
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
|
|
180
|
-
return results
|
|
285
|
+
if (hasReachedLimit(results.length, offset, limit)) {
|
|
286
|
+
return paginateResults(results, offset, limit);
|
|
181
287
|
}
|
|
182
288
|
}
|
|
183
289
|
}
|
|
184
|
-
return results
|
|
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
|
-
//
|
|
193
|
-
const
|
|
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
|
|
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
|
|
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
|
-
|
|
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: ${
|
|
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
|
}
|