@root-signals/scorable-cli 0.7.0 → 0.9.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 +222 -0
- package/dist/client.d.ts +10 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +10 -6
- package/dist/commands/otel-filter/create.d.ts +3 -0
- package/dist/commands/otel-filter/create.d.ts.map +1 -0
- package/dist/commands/otel-filter/create.js +154 -0
- package/dist/commands/otel-filter/delete.d.ts +3 -0
- package/dist/commands/otel-filter/delete.d.ts.map +1 -0
- package/dist/commands/otel-filter/delete.js +25 -0
- package/dist/commands/otel-filter/index.d.ts +3 -0
- package/dist/commands/otel-filter/index.d.ts.map +1 -0
- package/dist/commands/otel-filter/index.js +15 -0
- package/dist/commands/otel-filter/list.d.ts +3 -0
- package/dist/commands/otel-filter/list.d.ts.map +1 -0
- package/dist/commands/otel-filter/list.js +29 -0
- package/dist/commands/otel-filter/update.d.ts +3 -0
- package/dist/commands/otel-filter/update.d.ts.map +1 -0
- package/dist/commands/otel-filter/update.js +53 -0
- package/dist/commands/otel-filter/validate.d.ts +3 -0
- package/dist/commands/otel-filter/validate.d.ts.map +1 -0
- package/dist/commands/otel-filter/validate.js +72 -0
- package/dist/commands/otel-trace/index.d.ts +3 -0
- package/dist/commands/otel-trace/index.d.ts.map +1 -0
- package/dist/commands/otel-trace/index.js +9 -0
- package/dist/commands/otel-trace/list.d.ts +3 -0
- package/dist/commands/otel-trace/list.d.ts.map +1 -0
- package/dist/commands/otel-trace/list.js +280 -0
- package/dist/commands/otel-trace/shared.d.ts +17 -0
- package/dist/commands/otel-trace/shared.d.ts.map +1 -0
- package/dist/commands/otel-trace/shared.js +73 -0
- package/dist/commands/otel-trace/spans.d.ts +3 -0
- package/dist/commands/otel-trace/spans.d.ts.map +1 -0
- package/dist/commands/otel-trace/spans.js +135 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/lib/filter-yaml.d.ts +179 -0
- package/dist/lib/filter-yaml.d.ts.map +1 -0
- package/dist/lib/filter-yaml.js +111 -0
- package/examples/otel-filters/claude-code.yaml +31 -0
- package/examples/otel-filters/genai-explicit.yaml +16 -0
- package/examples/otel-filters/openinference-agent.yaml +17 -0
- package/package.json +4 -2
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import { requireApiKey } from "../../auth.js";
|
|
4
|
+
import { apiRequestStatus } from "../../client.js";
|
|
5
|
+
import { printError, printSuccess, handleSdkError } from "../../output.js";
|
|
6
|
+
import { loadFilterYaml } from "../../lib/filter-yaml.js";
|
|
7
|
+
import { CliError } from "../../types.js";
|
|
8
|
+
export function registerValidateCommand(otelFilter) {
|
|
9
|
+
otelFilter
|
|
10
|
+
.command("validate")
|
|
11
|
+
.description("Validate an OTEL trace evaluation filter spec without saving")
|
|
12
|
+
.requiredOption("-f, --from-file <path>", "Filter spec path (YAML or JSON)")
|
|
13
|
+
.addHelpText("after", `
|
|
14
|
+
Useful in CI to fail builds on a bad manifest before applying.
|
|
15
|
+
|
|
16
|
+
Exit codes:
|
|
17
|
+
0 schema valid; no warnings
|
|
18
|
+
0 schema valid; warnings written to stderr (e.g. rules don't match any recent span)
|
|
19
|
+
2 schema invalid (local Zod failure or server 400)
|
|
20
|
+
|
|
21
|
+
Example:
|
|
22
|
+
$ scorable otel-filter validate -f ./filter.yaml`)
|
|
23
|
+
.action(async (opts) => {
|
|
24
|
+
let source;
|
|
25
|
+
try {
|
|
26
|
+
source = fs.readFileSync(opts.fromFile, "utf8");
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
30
|
+
printError(`Failed to read --from-file: ${msg}`);
|
|
31
|
+
throw new CliError(1, msg);
|
|
32
|
+
}
|
|
33
|
+
let payload;
|
|
34
|
+
try {
|
|
35
|
+
payload = loadFilterYaml(source);
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
39
|
+
printError(msg);
|
|
40
|
+
throw new CliError(2, msg);
|
|
41
|
+
}
|
|
42
|
+
const spinner = ora("Validating filter...").start();
|
|
43
|
+
try {
|
|
44
|
+
const apiKey = await requireApiKey();
|
|
45
|
+
const result = await apiRequestStatus("POST", "v1/otel/evaluation-filters/validate", {
|
|
46
|
+
payload,
|
|
47
|
+
apiKey,
|
|
48
|
+
});
|
|
49
|
+
spinner.stop();
|
|
50
|
+
if (!result.ok) {
|
|
51
|
+
if (result.status === 400) {
|
|
52
|
+
throw new CliError(2, "Filter schema validation failed.");
|
|
53
|
+
}
|
|
54
|
+
throw new CliError(1, `Validation request failed (status ${result.status}).`);
|
|
55
|
+
}
|
|
56
|
+
const data = (result.data ?? {});
|
|
57
|
+
const warnings = Array.isArray(data.warnings) ? data.warnings : [];
|
|
58
|
+
if (warnings.length > 0) {
|
|
59
|
+
for (const w of warnings) {
|
|
60
|
+
console.error(`warning: ${w}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
printSuccess("OK");
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
spinner.stop();
|
|
67
|
+
if (e instanceof CliError)
|
|
68
|
+
throw e;
|
|
69
|
+
handleSdkError(e);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/otel-trace/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIpC,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAOhE"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { registerListCommand } from "./list.js";
|
|
2
|
+
import { registerSpansCommand } from "./spans.js";
|
|
3
|
+
export function registerOtelTraceCommands(program) {
|
|
4
|
+
const otelTrace = program
|
|
5
|
+
.command("otel-trace")
|
|
6
|
+
.description("Query OTEL traces ingested by Scorable");
|
|
7
|
+
registerListCommand(otelTrace);
|
|
8
|
+
registerSpansCommand(otelTrace);
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../../src/commands/otel-trace/list.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAoFpC,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,OAAO,GAAG,IAAI,CA0P5D"}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import ora from "ora";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import Table from "cli-table3";
|
|
4
|
+
import { requireApiKey } from "../../auth.js";
|
|
5
|
+
import { apiRequest } from "../../client.js";
|
|
6
|
+
import { printJson, printMessage, printInfo, handleSdkError } from "../../output.js";
|
|
7
|
+
import { buildTimeFilters, parseOutputFormat, resolveTimeWindow, toCsv } from "./shared.js";
|
|
8
|
+
const UNICODE_CHARS = {
|
|
9
|
+
top: "─",
|
|
10
|
+
"top-mid": "┬",
|
|
11
|
+
"top-left": "┌",
|
|
12
|
+
"top-right": "┐",
|
|
13
|
+
bottom: "─",
|
|
14
|
+
"bottom-mid": "┴",
|
|
15
|
+
"bottom-left": "└",
|
|
16
|
+
"bottom-right": "┘",
|
|
17
|
+
left: "│",
|
|
18
|
+
"left-mid": "├",
|
|
19
|
+
mid: "─",
|
|
20
|
+
"mid-mid": "┼",
|
|
21
|
+
right: "│",
|
|
22
|
+
"right-mid": "┤",
|
|
23
|
+
middle: "│",
|
|
24
|
+
};
|
|
25
|
+
function truncate(s, max) {
|
|
26
|
+
return s.length > max ? s.slice(0, max - 1) + "…" : s;
|
|
27
|
+
}
|
|
28
|
+
function printTraceTable(traces) {
|
|
29
|
+
const table = new Table({
|
|
30
|
+
head: ["Trace ID", "Root Span", "Spans", "First Span"].map((h) => chalk.bold.cyan(h)),
|
|
31
|
+
chars: UNICODE_CHARS,
|
|
32
|
+
colWidths: [34, 42, 7, 22],
|
|
33
|
+
wordWrap: true,
|
|
34
|
+
});
|
|
35
|
+
for (const t of traces) {
|
|
36
|
+
const ts = (t.first_span_at ?? "").slice(0, 19).replace("T", " ");
|
|
37
|
+
table.push([t.trace_id, truncate(t.root_span_name ?? "", 40), String(t.span_count), ts]);
|
|
38
|
+
}
|
|
39
|
+
console.log(table.toString());
|
|
40
|
+
}
|
|
41
|
+
function printTraceCsv(traces) {
|
|
42
|
+
const headers = [
|
|
43
|
+
"trace_id",
|
|
44
|
+
"root_span_name",
|
|
45
|
+
"span_count",
|
|
46
|
+
"first_span_at",
|
|
47
|
+
"last_span_at",
|
|
48
|
+
"input_preview",
|
|
49
|
+
"output_preview",
|
|
50
|
+
];
|
|
51
|
+
const rows = traces.map((t) => [
|
|
52
|
+
t.trace_id,
|
|
53
|
+
t.root_span_name,
|
|
54
|
+
t.span_count,
|
|
55
|
+
t.first_span_at,
|
|
56
|
+
t.last_span_at,
|
|
57
|
+
t.input_preview,
|
|
58
|
+
t.output_preview,
|
|
59
|
+
]);
|
|
60
|
+
process.stdout.write(toCsv(headers, rows));
|
|
61
|
+
}
|
|
62
|
+
export function registerListCommand(otelTrace) {
|
|
63
|
+
otelTrace
|
|
64
|
+
.command("list")
|
|
65
|
+
.description("List ingested OTEL traces, optionally filtered")
|
|
66
|
+
.option("--filter <expr>", "Filter expression (repeatable). Format: `column;type;key;operator;value`. See examples below.", (value, previous = []) => [...previous, value], [])
|
|
67
|
+
.option("--since <duration>", "Only traces from the last <duration>. Forms: 30s, 5m, 2h, 7d. Mutually exclusive with --start-time/--end-time.")
|
|
68
|
+
.option("--start-time <iso>", "Earliest first_span_at, inclusive (ISO 8601, e.g. 2026-04-30T00:00:00Z)")
|
|
69
|
+
.option("--end-time <iso>", "Latest first_span_at, exclusive (ISO 8601). Defaults to now.")
|
|
70
|
+
.option("--service-name <name>", "Match resource.service.name (=). Shortcut for --filter.")
|
|
71
|
+
.option("--has-error", "Only traces where some span ended with status ERROR. Shortcut for --filter.")
|
|
72
|
+
.option("--root-name <substr>", "Substring match on the root span name. Shortcut for --filter.")
|
|
73
|
+
.option("--span-name <substr>", "Substring match on any span's name. Shortcut for --filter.")
|
|
74
|
+
.option("--agent-name <name>", "Match gen_ai.agent.name (=). Shortcut for --filter.")
|
|
75
|
+
.option("--model <name>", "Match gen_ai.request.model (=). Shortcut for --filter.")
|
|
76
|
+
.option("--tool <name>", "Match gen_ai.tool.name (=). Shortcut for --filter.")
|
|
77
|
+
.option("--page-size <n>", "Number of traces per page", (v) => parseInt(v, 10), 25)
|
|
78
|
+
.option("--cursor <cursor>", "Pagination cursor (from a previous `next` response)")
|
|
79
|
+
.option("--output <format>", "Output format: table | json | csv", "table")
|
|
80
|
+
.addHelpText("after", `
|
|
81
|
+
Filtering — three layers, pick the lowest one that fits
|
|
82
|
+
|
|
83
|
+
1. Convenience flags (covers ~80% of queries):
|
|
84
|
+
--service-name <name> resource.service.name = <name>
|
|
85
|
+
--has-error at least one span errored
|
|
86
|
+
--root-name <substr> root span name contains <substr>
|
|
87
|
+
--span-name <substr> any span name contains <substr>
|
|
88
|
+
--agent-name <name> gen_ai.agent.name = <name>
|
|
89
|
+
--model <name> gen_ai.request.model = <name>
|
|
90
|
+
--tool <name> gen_ai.tool.name = <name>
|
|
91
|
+
All composable with each other and with --filter (AND-combined).
|
|
92
|
+
|
|
93
|
+
2. Time-window shortcuts:
|
|
94
|
+
--since 30s | 5m | 2h | 7d
|
|
95
|
+
--start-time / --end-time ISO 8601 (inclusive start, exclusive end)
|
|
96
|
+
Mutually exclusive group; compile down to first_span_at filters.
|
|
97
|
+
|
|
98
|
+
3. Raw --filter (full power, anything the matcher supports):
|
|
99
|
+
One expression per --filter, repeatable, AND-combined.
|
|
100
|
+
|
|
101
|
+
Raw filter expression format
|
|
102
|
+
column;type;key;operator;value
|
|
103
|
+
|
|
104
|
+
column - what to match against (see "Columns" below)
|
|
105
|
+
type - value type: string | number | boolean | datetime | stringOptions
|
|
106
|
+
key - same as column for built-ins; for sentinel columns
|
|
107
|
+
(resource, attribute) this is the attribute name
|
|
108
|
+
operator - one of: = != contains "starts with" "any of" "none of"
|
|
109
|
+
> >= < <= (numeric and datetime)
|
|
110
|
+
value - the value to match. For "any of" / "none of",
|
|
111
|
+
pipe-separate multiple values: ok|error|unset.
|
|
112
|
+
For datetime, ISO 8601: 2026-04-30T00:00:00Z
|
|
113
|
+
|
|
114
|
+
Columns
|
|
115
|
+
Trace-record level (per-trace metadata):
|
|
116
|
+
name root span name
|
|
117
|
+
span_count total spans in trace
|
|
118
|
+
first_span_at earliest span timestamp (datetime)
|
|
119
|
+
last_span_at latest span timestamp (datetime)
|
|
120
|
+
input_preview first input message snippet
|
|
121
|
+
output_preview final output snippet
|
|
122
|
+
|
|
123
|
+
Span level (matches if any span in the trace satisfies):
|
|
124
|
+
span_name span name (e.g. "agent run", "execute_tool")
|
|
125
|
+
has_error boolean: span ended with status code ERROR
|
|
126
|
+
kind 1=INTERNAL 2=SERVER 3=CLIENT 4=PRODUCER 5=CONSUMER
|
|
127
|
+
status OK | ERROR | UNSET (use "any of" / "none of")
|
|
128
|
+
|
|
129
|
+
Sentinel columns (key carries the attribute name):
|
|
130
|
+
resource resource attribute, e.g. service.name
|
|
131
|
+
<attribute-key> any span attribute. If your instrumentation follows the
|
|
132
|
+
OpenTelemetry GenAI semantic conventions (pydantic-ai,
|
|
133
|
+
OpenLLMetry, Logfire, OpenAI/Anthropic SDKs with otel
|
|
134
|
+
all do), every documented attribute is filterable here
|
|
135
|
+
without extra setup. Common ones:
|
|
136
|
+
|
|
137
|
+
Request: gen_ai.request.model
|
|
138
|
+
gen_ai.request.temperature
|
|
139
|
+
gen_ai.request.max_tokens
|
|
140
|
+
Response: gen_ai.response.model
|
|
141
|
+
gen_ai.response.finish_reasons
|
|
142
|
+
Agent/Op: gen_ai.agent.name / .id / .version
|
|
143
|
+
gen_ai.operation.name (chat|embeddings|...)
|
|
144
|
+
gen_ai.workflow.name
|
|
145
|
+
gen_ai.provider.name (openai|aws.bedrock|...)
|
|
146
|
+
Tools: gen_ai.tool.name / .type / .call.id
|
|
147
|
+
Tracking: gen_ai.conversation.id
|
|
148
|
+
Tokens: gen_ai.usage.input_tokens
|
|
149
|
+
gen_ai.usage.output_tokens
|
|
150
|
+
gen_ai.usage.reasoning.output_tokens
|
|
151
|
+
gen_ai.usage.cache_read.input_tokens
|
|
152
|
+
|
|
153
|
+
Full registry:
|
|
154
|
+
https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/
|
|
155
|
+
|
|
156
|
+
Examples — convenience flags (the common case)
|
|
157
|
+
|
|
158
|
+
# All traces from a specific service, last hour
|
|
159
|
+
$ scorable otel-trace list --since 1h --service-name my_agent
|
|
160
|
+
|
|
161
|
+
# Errored traces in the last 24h, exported as CSV for spreadsheet analysis
|
|
162
|
+
$ scorable otel-trace list --since 24h --has-error --output csv > errors.csv
|
|
163
|
+
|
|
164
|
+
# All traces of a specific pydantic-ai agent on a specific model
|
|
165
|
+
$ scorable otel-trace list --agent-name my_agent --model gpt-5-mini
|
|
166
|
+
|
|
167
|
+
# Drill into traces that hit a specific tool
|
|
168
|
+
$ scorable otel-trace list --since 7d --tool fetch_customer_data
|
|
169
|
+
|
|
170
|
+
# Mix shortcuts and raw filter — they AND together
|
|
171
|
+
$ scorable otel-trace list \\
|
|
172
|
+
--service-name my_agent \\
|
|
173
|
+
--filter 'gen_ai.usage.input_tokens;number;gen_ai.usage.input_tokens;>;1000'
|
|
174
|
+
|
|
175
|
+
Examples — raw --filter (when shortcuts don't fit)
|
|
176
|
+
|
|
177
|
+
# Numeric comparison (no shortcut)
|
|
178
|
+
$ scorable otel-trace list \\
|
|
179
|
+
--filter 'gen_ai.usage.input_tokens;number;gen_ai.usage.input_tokens;>;1000'
|
|
180
|
+
|
|
181
|
+
# "any of" semantics with pipe-separated values
|
|
182
|
+
$ scorable otel-trace list \\
|
|
183
|
+
--filter 'kind;stringOptions;kind;any of;3|5'
|
|
184
|
+
|
|
185
|
+
# Multi-turn debugging — every trace in a single conversation
|
|
186
|
+
$ scorable otel-trace list \\
|
|
187
|
+
--filter 'gen_ai.conversation.id;string;gen_ai.conversation.id;=;conv_5j66'
|
|
188
|
+
|
|
189
|
+
# Expensive runs — over 5k input tokens
|
|
190
|
+
$ scorable otel-trace list --since 24h \\
|
|
191
|
+
--filter 'gen_ai.usage.input_tokens;number;gen_ai.usage.input_tokens;>;5000'
|
|
192
|
+
|
|
193
|
+
# Filter by provider — e.g. only Bedrock traffic
|
|
194
|
+
$ scorable otel-trace list \\
|
|
195
|
+
--filter 'gen_ai.provider.name;string;gen_ai.provider.name;=;aws.bedrock'
|
|
196
|
+
|
|
197
|
+
# Specific time range (inclusive start, exclusive end)
|
|
198
|
+
$ scorable otel-trace list \\
|
|
199
|
+
--start-time 2026-04-30T00:00:00Z \\
|
|
200
|
+
--end-time 2026-05-01T00:00:00Z
|
|
201
|
+
|
|
202
|
+
# Pagination — pass the cursor returned in the next-page hint
|
|
203
|
+
$ scorable otel-trace list --since 7d --cursor cD0yMDI2LTA0LTMwKzEx...
|
|
204
|
+
|
|
205
|
+
# Pipe JSON to jq for ad-hoc analysis
|
|
206
|
+
$ scorable otel-trace list --since 1h --output json | jq '.results[].trace_id'`)
|
|
207
|
+
.action(async (opts) => {
|
|
208
|
+
let format;
|
|
209
|
+
let timeFilters;
|
|
210
|
+
try {
|
|
211
|
+
format = parseOutputFormat(opts.output);
|
|
212
|
+
const window = resolveTimeWindow({
|
|
213
|
+
since: opts.since,
|
|
214
|
+
startTime: opts.startTime,
|
|
215
|
+
endTime: opts.endTime,
|
|
216
|
+
});
|
|
217
|
+
timeFilters = buildTimeFilters(window);
|
|
218
|
+
}
|
|
219
|
+
catch (e) {
|
|
220
|
+
handleSdkError(e);
|
|
221
|
+
}
|
|
222
|
+
const shortcutFilters = [];
|
|
223
|
+
if (opts.serviceName)
|
|
224
|
+
shortcutFilters.push(`resource;string;service.name;=;${opts.serviceName}`);
|
|
225
|
+
if (opts.hasError)
|
|
226
|
+
shortcutFilters.push("has_error;boolean;has_error;=;true");
|
|
227
|
+
if (opts.rootName)
|
|
228
|
+
shortcutFilters.push(`name;string;name;contains;${opts.rootName}`);
|
|
229
|
+
if (opts.spanName)
|
|
230
|
+
shortcutFilters.push(`span_name;string;span_name;contains;${opts.spanName}`);
|
|
231
|
+
if (opts.agentName)
|
|
232
|
+
shortcutFilters.push(`gen_ai.agent.name;string;gen_ai.agent.name;=;${opts.agentName}`);
|
|
233
|
+
if (opts.model)
|
|
234
|
+
shortcutFilters.push(`gen_ai.request.model;string;gen_ai.request.model;=;${opts.model}`);
|
|
235
|
+
if (opts.tool)
|
|
236
|
+
shortcutFilters.push(`gen_ai.tool.name;string;gen_ai.tool.name;=;${opts.tool}`);
|
|
237
|
+
const params = {};
|
|
238
|
+
const allFilters = [...timeFilters, ...shortcutFilters, ...opts.filter];
|
|
239
|
+
if (allFilters.length > 0)
|
|
240
|
+
params.filters = allFilters.join(",");
|
|
241
|
+
if (opts.pageSize !== undefined)
|
|
242
|
+
params.page_size = opts.pageSize;
|
|
243
|
+
if (opts.cursor)
|
|
244
|
+
params.cursor = opts.cursor;
|
|
245
|
+
const showSpinner = format === "table";
|
|
246
|
+
const spinner = showSpinner ? ora("Fetching traces...").start() : null;
|
|
247
|
+
try {
|
|
248
|
+
const apiKey = await requireApiKey();
|
|
249
|
+
const result = (await apiRequest("GET", "v1/otel/traces", {
|
|
250
|
+
params,
|
|
251
|
+
apiKey,
|
|
252
|
+
}));
|
|
253
|
+
spinner?.stop();
|
|
254
|
+
if (!result)
|
|
255
|
+
return;
|
|
256
|
+
if (format === "json") {
|
|
257
|
+
printJson(result);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (format === "csv") {
|
|
261
|
+
printTraceCsv(result.results ?? []);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (!result.results || result.results.length === 0) {
|
|
265
|
+
printMessage("No traces found.");
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
printTraceTable(result.results);
|
|
269
|
+
if (result.next) {
|
|
270
|
+
const cursor = new URL(result.next).searchParams.get("cursor");
|
|
271
|
+
if (cursor)
|
|
272
|
+
printInfo(`Next page available. Use --cursor "${cursor}"`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
catch (e) {
|
|
276
|
+
spinner?.stop();
|
|
277
|
+
handleSdkError(e);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type OutputFormat = "table" | "json" | "csv";
|
|
2
|
+
export declare function parseOutputFormat(value: string | undefined): OutputFormat;
|
|
3
|
+
export declare function parseDuration(input: string): number;
|
|
4
|
+
export declare function resolveTimeWindow(opts: {
|
|
5
|
+
since?: string;
|
|
6
|
+
startTime?: string;
|
|
7
|
+
endTime?: string;
|
|
8
|
+
}): {
|
|
9
|
+
start?: string;
|
|
10
|
+
end?: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function buildTimeFilters(window: {
|
|
13
|
+
start?: string;
|
|
14
|
+
end?: string;
|
|
15
|
+
}): string[];
|
|
16
|
+
export declare function toCsv(headers: string[], rows: unknown[][]): string;
|
|
17
|
+
//# sourceMappingURL=shared.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../../src/commands/otel-trace/shared.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,MAAM,GAAG,KAAK,CAAC;AAEpD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,YAAY,CAIzE;AAaD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CASnD;AAID,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG;IACjG,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CA6BA;AAGD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,EAAE,CASnF;AAWD,wBAAgB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,GAAG,MAAM,CAIlE"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export function parseOutputFormat(value) {
|
|
2
|
+
const v = (value ?? "table").toLowerCase();
|
|
3
|
+
if (v === "table" || v === "json" || v === "csv")
|
|
4
|
+
return v;
|
|
5
|
+
throw new Error(`Invalid --output format "${value}". Use: table, json, csv`);
|
|
6
|
+
}
|
|
7
|
+
const DURATION_PATTERN = /^(\d+(?:\.\d+)?)(ms|s|m|h|d)$/i;
|
|
8
|
+
const UNIT_TO_MS = {
|
|
9
|
+
ms: 1,
|
|
10
|
+
s: 1000,
|
|
11
|
+
m: 60_000,
|
|
12
|
+
h: 3_600_000,
|
|
13
|
+
d: 86_400_000,
|
|
14
|
+
};
|
|
15
|
+
export function parseDuration(input) {
|
|
16
|
+
const match = DURATION_PATTERN.exec(input.trim());
|
|
17
|
+
if (!match) {
|
|
18
|
+
throw new Error(`Invalid duration "${input}". Use forms like 30s, 5m, 2h, 7d (suffixes: ms, s, m, h, d).`);
|
|
19
|
+
}
|
|
20
|
+
const [, num, unit] = match;
|
|
21
|
+
return Number(num) * UNIT_TO_MS[unit.toLowerCase()];
|
|
22
|
+
}
|
|
23
|
+
export function resolveTimeWindow(opts) {
|
|
24
|
+
if (opts.since && (opts.startTime || opts.endTime)) {
|
|
25
|
+
throw new Error("--since cannot be combined with --start-time / --end-time.");
|
|
26
|
+
}
|
|
27
|
+
const end = opts.endTime ? new Date(opts.endTime) : undefined;
|
|
28
|
+
let start;
|
|
29
|
+
if (opts.since) {
|
|
30
|
+
const ms = parseDuration(opts.since);
|
|
31
|
+
start = new Date(Date.now() - ms);
|
|
32
|
+
}
|
|
33
|
+
else if (opts.startTime) {
|
|
34
|
+
start = new Date(opts.startTime);
|
|
35
|
+
}
|
|
36
|
+
if (start && Number.isNaN(start.getTime())) {
|
|
37
|
+
throw new Error(`Invalid --start-time: "${opts.startTime}".`);
|
|
38
|
+
}
|
|
39
|
+
if (end && Number.isNaN(end.getTime())) {
|
|
40
|
+
throw new Error(`Invalid --end-time: "${opts.endTime}".`);
|
|
41
|
+
}
|
|
42
|
+
if (start && end && start.getTime() >= end.getTime()) {
|
|
43
|
+
throw new Error("--start-time must be before --end-time.");
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
start: start?.toISOString(),
|
|
47
|
+
end: end?.toISOString(),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export function buildTimeFilters(window) {
|
|
51
|
+
const filters = [];
|
|
52
|
+
if (window.start) {
|
|
53
|
+
filters.push(`first_span_at;datetime;first_span_at;>=;${window.start}`);
|
|
54
|
+
}
|
|
55
|
+
if (window.end) {
|
|
56
|
+
filters.push(`first_span_at;datetime;first_span_at;<;${window.end}`);
|
|
57
|
+
}
|
|
58
|
+
return filters;
|
|
59
|
+
}
|
|
60
|
+
function csvEscape(value) {
|
|
61
|
+
if (value === null || value === undefined)
|
|
62
|
+
return "";
|
|
63
|
+
const s = typeof value === "string" ? value : String(value);
|
|
64
|
+
if (/[",\n\r]/.test(s))
|
|
65
|
+
return `"${s.replace(/"/g, '""')}"`;
|
|
66
|
+
return s;
|
|
67
|
+
}
|
|
68
|
+
export function toCsv(headers, rows) {
|
|
69
|
+
const lines = [headers.map(csvEscape).join(",")];
|
|
70
|
+
for (const row of rows)
|
|
71
|
+
lines.push(row.map(csvEscape).join(","));
|
|
72
|
+
return lines.join("\n") + "\n";
|
|
73
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spans.d.ts","sourceRoot":"","sources":["../../../src/commands/otel-trace/spans.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAoFpC,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,OAAO,GAAG,IAAI,CA+E7D"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import ora from "ora";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import Table from "cli-table3";
|
|
4
|
+
import { requireApiKey } from "../../auth.js";
|
|
5
|
+
import { apiRequest } from "../../client.js";
|
|
6
|
+
import { printJson, printMessage, handleSdkError } from "../../output.js";
|
|
7
|
+
import { parseOutputFormat, toCsv } from "./shared.js";
|
|
8
|
+
const UNICODE_CHARS = {
|
|
9
|
+
top: "─",
|
|
10
|
+
"top-mid": "┬",
|
|
11
|
+
"top-left": "┌",
|
|
12
|
+
"top-right": "┐",
|
|
13
|
+
bottom: "─",
|
|
14
|
+
"bottom-mid": "┴",
|
|
15
|
+
"bottom-left": "└",
|
|
16
|
+
"bottom-right": "┘",
|
|
17
|
+
left: "│",
|
|
18
|
+
"left-mid": "├",
|
|
19
|
+
mid: "─",
|
|
20
|
+
"mid-mid": "┼",
|
|
21
|
+
right: "│",
|
|
22
|
+
"right-mid": "┤",
|
|
23
|
+
middle: "│",
|
|
24
|
+
};
|
|
25
|
+
function truncate(s, max) {
|
|
26
|
+
return s.length > max ? s.slice(0, max - 1) + "…" : s;
|
|
27
|
+
}
|
|
28
|
+
function printSpanTable(spans) {
|
|
29
|
+
const table = new Table({
|
|
30
|
+
head: ["Span ID", "Parent", "Name", "Error", "Timestamp"].map((h) => chalk.bold.cyan(h)),
|
|
31
|
+
chars: UNICODE_CHARS,
|
|
32
|
+
colWidths: [18, 18, 38, 7, 22],
|
|
33
|
+
wordWrap: true,
|
|
34
|
+
});
|
|
35
|
+
for (const s of spans) {
|
|
36
|
+
const ts = (s.timestamp ?? "").slice(0, 19).replace("T", " ");
|
|
37
|
+
const parent = s.parent_span_id || chalk.dim("(root)");
|
|
38
|
+
const errorMark = s.span?.has_error ? chalk.red("✖") : "";
|
|
39
|
+
table.push([s.span_id, parent, truncate(s.span?.name ?? "", 36), errorMark, ts]);
|
|
40
|
+
}
|
|
41
|
+
console.log(table.toString());
|
|
42
|
+
}
|
|
43
|
+
function printSpanCsv(spans) {
|
|
44
|
+
const headers = [
|
|
45
|
+
"span_id",
|
|
46
|
+
"parent_span_id",
|
|
47
|
+
"name",
|
|
48
|
+
"kind",
|
|
49
|
+
"has_error",
|
|
50
|
+
"status_code",
|
|
51
|
+
"timestamp",
|
|
52
|
+
];
|
|
53
|
+
const rows = spans.map((s) => [
|
|
54
|
+
s.span_id,
|
|
55
|
+
s.parent_span_id,
|
|
56
|
+
s.span?.name ?? "",
|
|
57
|
+
s.span?.kind ?? "",
|
|
58
|
+
s.span?.has_error ?? "",
|
|
59
|
+
s.span?.status?.code ?? "",
|
|
60
|
+
s.timestamp,
|
|
61
|
+
]);
|
|
62
|
+
process.stdout.write(toCsv(headers, rows));
|
|
63
|
+
}
|
|
64
|
+
export function registerSpansCommand(otelTrace) {
|
|
65
|
+
otelTrace
|
|
66
|
+
.command("spans <trace_id>")
|
|
67
|
+
.description("List all spans for a given trace")
|
|
68
|
+
.option("--output <format>", "Output format: table | json | csv", "table")
|
|
69
|
+
.addHelpText("after", `
|
|
70
|
+
Output formats
|
|
71
|
+
table Default. Pretty-printed columnar view: span_id, parent, name, error, timestamp.
|
|
72
|
+
json Full payload — including attributes, events, links, status, kind, resource_attributes.
|
|
73
|
+
Use this for debugging or scripting.
|
|
74
|
+
csv Flattened columns: span_id, parent_span_id, name, kind, has_error, status_code, timestamp.
|
|
75
|
+
Useful for spreadsheet analysis.
|
|
76
|
+
|
|
77
|
+
Examples
|
|
78
|
+
# Tabular overview of all spans in a trace
|
|
79
|
+
$ scorable otel-trace spans 7b6e5bd99494c4ee46fbfd39b194c499
|
|
80
|
+
|
|
81
|
+
# Full span data for jq-piping
|
|
82
|
+
$ scorable otel-trace spans 7b6e5bd99494c4ee46fbfd39b194c499 --output json | jq '.[0].span'
|
|
83
|
+
|
|
84
|
+
# Inspect just the span attributes
|
|
85
|
+
$ scorable otel-trace spans <trace_id> --output json | \\
|
|
86
|
+
jq '.[].span | {name, attributes}'
|
|
87
|
+
|
|
88
|
+
# Pull resource attributes (e.g. service.name) from the first span
|
|
89
|
+
$ scorable otel-trace spans <trace_id> --output json | jq '.[0].span.resource_attributes'
|
|
90
|
+
|
|
91
|
+
# CSV for a quick latency / error overview
|
|
92
|
+
$ scorable otel-trace spans <trace_id> --output csv > spans.csv
|
|
93
|
+
|
|
94
|
+
Tip
|
|
95
|
+
Find a trace_id first with \`scorable otel-trace list\`, then drill down here.`)
|
|
96
|
+
.action(async (traceId, opts) => {
|
|
97
|
+
let format;
|
|
98
|
+
try {
|
|
99
|
+
format = parseOutputFormat(opts.output);
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
handleSdkError(e);
|
|
103
|
+
}
|
|
104
|
+
const showSpinner = format === "table";
|
|
105
|
+
const spinner = showSpinner ? ora(`Fetching spans for trace ${traceId}...`).start() : null;
|
|
106
|
+
try {
|
|
107
|
+
const apiKey = await requireApiKey();
|
|
108
|
+
const result = (await apiRequest("GET", `v1/otel/traces/${encodeURIComponent(traceId)}/spans`, { apiKey }));
|
|
109
|
+
spinner?.stop();
|
|
110
|
+
if (result === null)
|
|
111
|
+
return;
|
|
112
|
+
if (!Array.isArray(result)) {
|
|
113
|
+
throw new Error("Unexpected spans response: expected an array.");
|
|
114
|
+
}
|
|
115
|
+
const spans = result;
|
|
116
|
+
if (format === "json") {
|
|
117
|
+
printJson(spans);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (format === "csv") {
|
|
121
|
+
printSpanCsv(spans);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (spans.length === 0) {
|
|
125
|
+
printMessage(`No spans found for trace ${traceId}.`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
printSpanTable(spans);
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
spinner?.stop();
|
|
132
|
+
handleSdkError(e);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAIA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAIA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmDpC,wBAAgB,SAAS,IAAI,OAAO,CAuBnC"}
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,8 @@ import { registerExecutionLogCommands } from "./commands/execution-log/index.js"
|
|
|
11
11
|
import { registerAuthCommands } from "./commands/auth/index.js";
|
|
12
12
|
import { registerSkillsAddCommand } from "./commands/skills-add.js";
|
|
13
13
|
import { registerFileCommands } from "./commands/file/index.js";
|
|
14
|
+
import { registerOtelFilterCommands } from "./commands/otel-filter/index.js";
|
|
15
|
+
import { registerOtelTraceCommands } from "./commands/otel-trace/index.js";
|
|
14
16
|
const { version } = createRequire(import.meta.url)("../package.json");
|
|
15
17
|
function buildBanner(ver) {
|
|
16
18
|
const logo = chalk.hex("#4D9FFF");
|
|
@@ -63,6 +65,8 @@ export function createCli() {
|
|
|
63
65
|
registerAuthCommands(program);
|
|
64
66
|
registerSkillsAddCommand(program);
|
|
65
67
|
registerFileCommands(program);
|
|
68
|
+
registerOtelFilterCommands(program);
|
|
69
|
+
registerOtelTraceCommands(program);
|
|
66
70
|
return program;
|
|
67
71
|
}
|
|
68
72
|
if (realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1])) {
|