autotel-cli 0.8.8 → 0.8.9
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/dist/index.js +1387 -18
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { Command } from "commander";
|
|
4
|
+
import { Command as Command11 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/init.ts
|
|
7
7
|
import * as fs4 from "fs";
|
|
@@ -4078,8 +4078,8 @@ async function runCodemodTrace(options) {
|
|
|
4078
4078
|
totalWrapped += result.wrappedCount;
|
|
4079
4079
|
totalChanged += 1;
|
|
4080
4080
|
if (!dryRun) {
|
|
4081
|
-
const
|
|
4082
|
-
|
|
4081
|
+
const fs9 = await import("fs");
|
|
4082
|
+
fs9.writeFileSync(filePath, result.modified, "utf8");
|
|
4083
4083
|
}
|
|
4084
4084
|
}
|
|
4085
4085
|
const showSummary = printFiles || dryRun || result.changed;
|
|
@@ -4307,6 +4307,142 @@ var COMMANDS = [
|
|
|
4307
4307
|
supportsJson: true
|
|
4308
4308
|
}
|
|
4309
4309
|
];
|
|
4310
|
+
var INVESTIGATE_FLAGS = [
|
|
4311
|
+
{ name: "--backend", takesValue: true, description: "Backend kind (env: AUTOTEL_BACKEND)" },
|
|
4312
|
+
{ name: "--jaeger-base-url", takesValue: true, description: "Jaeger base URL" },
|
|
4313
|
+
{ name: "--tempo-base-url", takesValue: true, description: "Tempo base URL" },
|
|
4314
|
+
{ name: "--prometheus-base-url", takesValue: true, description: "Prometheus base URL" },
|
|
4315
|
+
{ name: "--loki-base-url", takesValue: true, description: "Loki base URL" },
|
|
4316
|
+
{ name: "--collector-port", takesValue: true, description: "OTLP receiver port" },
|
|
4317
|
+
{ name: "--fixture-path", takesValue: true, description: "Fixture JSON path" },
|
|
4318
|
+
{ name: "--output-file", takesValue: true, description: "Persist JSON output to this path" },
|
|
4319
|
+
{ name: "--no-secrets-in-output", description: "Redact secret-shaped values" }
|
|
4320
|
+
];
|
|
4321
|
+
var STATIC_FLAGS = [
|
|
4322
|
+
{ name: "--output-file", takesValue: true, description: "Persist JSON output to this path" },
|
|
4323
|
+
{ name: "--no-secrets-in-output", description: "Redact secret-shaped values" }
|
|
4324
|
+
];
|
|
4325
|
+
function investigateCmd(name, description, extras = {}) {
|
|
4326
|
+
return {
|
|
4327
|
+
name,
|
|
4328
|
+
description,
|
|
4329
|
+
...extras.args ? { args: extras.args } : {},
|
|
4330
|
+
flags: [...extras.static ? STATIC_FLAGS : INVESTIGATE_FLAGS, ...extras.flags ?? []],
|
|
4331
|
+
mutating: false,
|
|
4332
|
+
network: extras.network ?? !extras.static,
|
|
4333
|
+
writesFiles: false,
|
|
4334
|
+
supportsDryRun: false,
|
|
4335
|
+
requiresPackageJson: false,
|
|
4336
|
+
mayReadEnv: false,
|
|
4337
|
+
supportsJson: true
|
|
4338
|
+
};
|
|
4339
|
+
}
|
|
4340
|
+
var traceIdArg = [
|
|
4341
|
+
{ name: "traceId", required: true, description: "Trace ID" }
|
|
4342
|
+
];
|
|
4343
|
+
var serviceNameArg = [
|
|
4344
|
+
{ name: "serviceName", required: true, description: "Service name" }
|
|
4345
|
+
];
|
|
4346
|
+
var INVESTIGATE_COMMANDS = [
|
|
4347
|
+
investigateCmd("health", "Backend health + signal coverage"),
|
|
4348
|
+
investigateCmd("capabilities", "Which signals the active backend serves"),
|
|
4349
|
+
investigateCmd("discover", "Discover services and field shapes (parent)"),
|
|
4350
|
+
investigateCmd("discover services", "Services with cross-signal metadata"),
|
|
4351
|
+
investigateCmd("discover trace-fields", "Trace/span field names from sampled traces", {
|
|
4352
|
+
flags: [{ name: "--search", takesValue: true, description: "Substring filter" }]
|
|
4353
|
+
}),
|
|
4354
|
+
investigateCmd("discover log-fields", "Log field names from sampled logs", {
|
|
4355
|
+
flags: [{ name: "--search", takesValue: true, description: "Substring filter" }]
|
|
4356
|
+
}),
|
|
4357
|
+
investigateCmd("query", "Query traces/spans/metrics/logs (parent)"),
|
|
4358
|
+
investigateCmd("query traces", "Search traces by service/op/status/tags/time/error"),
|
|
4359
|
+
investigateCmd("query spans", "Search individual spans (extra duration filters)"),
|
|
4360
|
+
investigateCmd("query metrics", "List metric series"),
|
|
4361
|
+
investigateCmd("query logs", "Search logs"),
|
|
4362
|
+
investigateCmd("trace", "Trace lookup commands (parent)"),
|
|
4363
|
+
investigateCmd("trace get", "Get a trace by ID", { args: traceIdArg }),
|
|
4364
|
+
investigateCmd("trace summary", "Compact incident-friendly trace summary", { args: traceIdArg }),
|
|
4365
|
+
investigateCmd("topology", "Service topology commands (parent)"),
|
|
4366
|
+
investigateCmd("topology services", "List known services"),
|
|
4367
|
+
investigateCmd("topology operations", "List operations for a service", { args: serviceNameArg }),
|
|
4368
|
+
investigateCmd("topology map", "Service dependency map with node/edge health"),
|
|
4369
|
+
investigateCmd("diagnose", "Anomaly / root-cause / errors / SLO diagnosis (parent)"),
|
|
4370
|
+
investigateCmd("diagnose anomalies", "Latency / error-rate outliers"),
|
|
4371
|
+
investigateCmd("diagnose root-cause", "Bottleneck span in a trace", { args: traceIdArg }),
|
|
4372
|
+
investigateCmd("diagnose errors", "Error spans grouped by service/operation"),
|
|
4373
|
+
investigateCmd("diagnose slos", "SLO violations for a service"),
|
|
4374
|
+
investigateCmd("correlate", "Cross-signal correlation (parent)"),
|
|
4375
|
+
investigateCmd("correlate trace", "Trace + metrics + logs for a trace ID", { args: traceIdArg }),
|
|
4376
|
+
investigateCmd("correlate explain-slowdown", "Anomalies + root cause + correlated signals"),
|
|
4377
|
+
investigateCmd("llm", "LLM analytics (parent)"),
|
|
4378
|
+
investigateCmd("llm usage", "Token usage + USD by model and service"),
|
|
4379
|
+
investigateCmd("llm models", "Discover LLM models in use"),
|
|
4380
|
+
investigateCmd("llm model-stats", "Per-model latency/token/error stats"),
|
|
4381
|
+
investigateCmd("llm expensive", "Top token-spend traces"),
|
|
4382
|
+
investigateCmd("llm slow", "Slowest LLM traces"),
|
|
4383
|
+
investigateCmd("llm tools", "Tool/function spans grouped by tool name"),
|
|
4384
|
+
investigateCmd("semconv", "Semantic conventions lookup (parent)", { static: true }),
|
|
4385
|
+
investigateCmd("semconv list", "List semconv namespaces", { static: true, network: true }),
|
|
4386
|
+
investigateCmd("semconv get", "Groups for one namespace", {
|
|
4387
|
+
static: true,
|
|
4388
|
+
network: true,
|
|
4389
|
+
args: [{ name: "namespace", required: true, description: "Namespace (e.g. http)" }]
|
|
4390
|
+
}),
|
|
4391
|
+
investigateCmd("semconv refresh", "Clear semconv cache", { static: true }),
|
|
4392
|
+
investigateCmd("score", "Score a span for instrumentation quality (JSON on stdin)", {
|
|
4393
|
+
static: true,
|
|
4394
|
+
flags: [{ name: "--span-file", takesValue: true, description: "Read span JSON from file" }]
|
|
4395
|
+
}),
|
|
4396
|
+
investigateCmd("score explain", "Explain the instrumentation scoring rubric", { static: true }),
|
|
4397
|
+
investigateCmd("collector", "OpenTelemetry Collector config + schema commands (parent)", {
|
|
4398
|
+
static: true
|
|
4399
|
+
}),
|
|
4400
|
+
investigateCmd("collector validate", "Validate OTLP receiver config", {
|
|
4401
|
+
static: true,
|
|
4402
|
+
flags: [{ name: "--config-file", takesValue: true, description: "Read JSON config from file" }]
|
|
4403
|
+
}),
|
|
4404
|
+
investigateCmd("collector suggest", "Minimal OTLP receiver config", { static: true }),
|
|
4405
|
+
investigateCmd("collector explain", "Receiver config shape + defaults", { static: true }),
|
|
4406
|
+
investigateCmd("collector versions", "Supported collector schema versions", { static: true, network: true }),
|
|
4407
|
+
investigateCmd("collector components", "Components for a version", {
|
|
4408
|
+
static: true,
|
|
4409
|
+
network: true,
|
|
4410
|
+
flags: [
|
|
4411
|
+
{ name: "--version", takesValue: true, description: "Collector version" },
|
|
4412
|
+
{ name: "--kind", takesValue: true, description: "Component kind filter" }
|
|
4413
|
+
]
|
|
4414
|
+
}),
|
|
4415
|
+
investigateCmd("collector schema", "JSON schema for a component", {
|
|
4416
|
+
static: true,
|
|
4417
|
+
network: true,
|
|
4418
|
+
flags: [
|
|
4419
|
+
{ name: "--kind", takesValue: true, description: "Component kind" },
|
|
4420
|
+
{ name: "--name", takesValue: true, description: "Component name" },
|
|
4421
|
+
{ name: "--version", takesValue: true, description: "Collector version" }
|
|
4422
|
+
]
|
|
4423
|
+
}),
|
|
4424
|
+
investigateCmd("collector readme", "README for a component", {
|
|
4425
|
+
static: true,
|
|
4426
|
+
network: true,
|
|
4427
|
+
flags: [
|
|
4428
|
+
{ name: "--kind", takesValue: true, description: "Component kind" },
|
|
4429
|
+
{ name: "--name", takesValue: true, description: "Component name" },
|
|
4430
|
+
{ name: "--version", takesValue: true, description: "Collector version" }
|
|
4431
|
+
]
|
|
4432
|
+
}),
|
|
4433
|
+
investigateCmd("collector validate-component", "Validate component config against upstream schema", {
|
|
4434
|
+
static: true,
|
|
4435
|
+
network: true,
|
|
4436
|
+
flags: [
|
|
4437
|
+
{ name: "--kind", takesValue: true, description: "Component kind" },
|
|
4438
|
+
{ name: "--name", takesValue: true, description: "Component name" },
|
|
4439
|
+
{ name: "--version", takesValue: true, description: "Collector version" },
|
|
4440
|
+
{ name: "--config-file", takesValue: true, description: "Read JSON config from file" }
|
|
4441
|
+
]
|
|
4442
|
+
}),
|
|
4443
|
+
investigateCmd("collector refresh", "Refresh in-memory collector metadata cache", { static: true, network: true })
|
|
4444
|
+
];
|
|
4445
|
+
COMMANDS.push(...INVESTIGATE_COMMANDS);
|
|
4310
4446
|
function getCommand(name) {
|
|
4311
4447
|
return COMMANDS.find((c) => c.name === name);
|
|
4312
4448
|
}
|
|
@@ -4461,14 +4597,1199 @@ function runVersion(opts) {
|
|
|
4461
4597
|
});
|
|
4462
4598
|
}
|
|
4463
4599
|
|
|
4600
|
+
// src/commands/investigate/health.ts
|
|
4601
|
+
import { Command } from "commander";
|
|
4602
|
+
|
|
4603
|
+
// src/commands/investigate/runtime.ts
|
|
4604
|
+
function applyFlagsToEnv(flags) {
|
|
4605
|
+
if (flags.backend !== void 0) process.env.AUTOTEL_BACKEND = flags.backend;
|
|
4606
|
+
if (flags.jaegerBaseUrl !== void 0)
|
|
4607
|
+
process.env.JAEGER_BASE_URL = flags.jaegerBaseUrl;
|
|
4608
|
+
if (flags.tempoBaseUrl !== void 0)
|
|
4609
|
+
process.env.TEMPO_BASE_URL = flags.tempoBaseUrl;
|
|
4610
|
+
if (flags.prometheusBaseUrl !== void 0)
|
|
4611
|
+
process.env.PROMETHEUS_BASE_URL = flags.prometheusBaseUrl;
|
|
4612
|
+
if (flags.lokiBaseUrl !== void 0)
|
|
4613
|
+
process.env.LOKI_BASE_URL = flags.lokiBaseUrl;
|
|
4614
|
+
if (flags.collectorPort !== void 0)
|
|
4615
|
+
process.env.AUTOTEL_COLLECTOR_PORT = String(flags.collectorPort);
|
|
4616
|
+
if (flags.fixturePath !== void 0)
|
|
4617
|
+
process.env.AUTOTEL_FIXTURE_PATH = flags.fixturePath;
|
|
4618
|
+
}
|
|
4619
|
+
async function openBackend(flags) {
|
|
4620
|
+
applyFlagsToEnv(flags);
|
|
4621
|
+
const { loadConfig, createBackend } = await import("autotel-mcp");
|
|
4622
|
+
const config = loadConfig();
|
|
4623
|
+
return createBackend(config);
|
|
4624
|
+
}
|
|
4625
|
+
async function runStatic(command, flags, fn) {
|
|
4626
|
+
configureJsonOutput({
|
|
4627
|
+
outputFile: flags.outputFile,
|
|
4628
|
+
noSecrets: flags.noSecrets
|
|
4629
|
+
});
|
|
4630
|
+
try {
|
|
4631
|
+
const data = await fn();
|
|
4632
|
+
printJson({ ok: true, command, data });
|
|
4633
|
+
} catch (error2) {
|
|
4634
|
+
throw toInvestigateError(command, error2);
|
|
4635
|
+
}
|
|
4636
|
+
}
|
|
4637
|
+
async function runInvestigate(command, flags, fn) {
|
|
4638
|
+
configureJsonOutput({
|
|
4639
|
+
outputFile: flags.outputFile,
|
|
4640
|
+
noSecrets: flags.noSecrets
|
|
4641
|
+
});
|
|
4642
|
+
let handle = null;
|
|
4643
|
+
try {
|
|
4644
|
+
handle = await openBackend(flags);
|
|
4645
|
+
await handle.start();
|
|
4646
|
+
const data = await fn(handle.backend);
|
|
4647
|
+
printJson({ ok: true, command, data });
|
|
4648
|
+
} catch (error2) {
|
|
4649
|
+
throw toInvestigateError(command, error2);
|
|
4650
|
+
} finally {
|
|
4651
|
+
if (handle) {
|
|
4652
|
+
try {
|
|
4653
|
+
await handle.stop();
|
|
4654
|
+
} catch {
|
|
4655
|
+
}
|
|
4656
|
+
}
|
|
4657
|
+
}
|
|
4658
|
+
}
|
|
4659
|
+
function toInvestigateError(command, error2) {
|
|
4660
|
+
if (error2 instanceof AutotelError) return error2;
|
|
4661
|
+
if (error2 !== null && typeof error2 === "object" && "issues" in error2 && Array.isArray(error2.issues)) {
|
|
4662
|
+
const issues = error2.issues;
|
|
4663
|
+
const first = issues[0];
|
|
4664
|
+
const path15 = first?.path?.join(".") ?? "";
|
|
4665
|
+
return new AutotelError({
|
|
4666
|
+
type: "validation",
|
|
4667
|
+
code: "AUTOTEL_E_INVALID_INPUT",
|
|
4668
|
+
message: `autotel ${command}: invalid input${path15 ? ` for "${path15}"` : ""} \u2014 ${first?.message ?? "validation failed"}`,
|
|
4669
|
+
retryable: false,
|
|
4670
|
+
expected: { issues }
|
|
4671
|
+
});
|
|
4672
|
+
}
|
|
4673
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
4674
|
+
return new AutotelError({
|
|
4675
|
+
type: "runtime",
|
|
4676
|
+
code: "AUTOTEL_E_UNKNOWN",
|
|
4677
|
+
message: `autotel ${command} failed: ${message}`,
|
|
4678
|
+
retryable: false
|
|
4679
|
+
});
|
|
4680
|
+
}
|
|
4681
|
+
|
|
4682
|
+
// src/commands/investigate/cli-helpers.ts
|
|
4683
|
+
var intArg = (v) => Number.parseInt(v, 10);
|
|
4684
|
+
var floatArg = (v) => Number.parseFloat(v);
|
|
4685
|
+
function addBackendFlags(cmd) {
|
|
4686
|
+
return cmd.option(
|
|
4687
|
+
"--backend <kind>",
|
|
4688
|
+
"Backend: collector|jaeger|tempo|prometheus|loki|stack|auto|fixture (env: AUTOTEL_BACKEND)"
|
|
4689
|
+
).option("--jaeger-base-url <url>", "Jaeger base URL (env: JAEGER_BASE_URL)").option("--tempo-base-url <url>", "Tempo base URL (env: TEMPO_BASE_URL)").option(
|
|
4690
|
+
"--prometheus-base-url <url>",
|
|
4691
|
+
"Prometheus base URL (env: PROMETHEUS_BASE_URL)"
|
|
4692
|
+
).option("--loki-base-url <url>", "Loki base URL (env: LOKI_BASE_URL)").option(
|
|
4693
|
+
"--collector-port <n>",
|
|
4694
|
+
"OTLP receiver port for the collector backend",
|
|
4695
|
+
intArg
|
|
4696
|
+
).option("--fixture-path <path>", "Fixture JSON for the fixture backend").option("--output-file <path>", "Persist JSON output to this file").option("--no-secrets-in-output", "Redact secret-shaped values");
|
|
4697
|
+
}
|
|
4698
|
+
function addStaticFlags(cmd) {
|
|
4699
|
+
return cmd.option("--output-file <path>", "Persist JSON output to this file").option("--no-secrets-in-output", "Redact secret-shaped values");
|
|
4700
|
+
}
|
|
4701
|
+
function backendFlagsFromOpts(opts) {
|
|
4702
|
+
return {
|
|
4703
|
+
backend: opts.backend,
|
|
4704
|
+
jaegerBaseUrl: opts.jaegerBaseUrl,
|
|
4705
|
+
tempoBaseUrl: opts.tempoBaseUrl,
|
|
4706
|
+
prometheusBaseUrl: opts.prometheusBaseUrl,
|
|
4707
|
+
lokiBaseUrl: opts.lokiBaseUrl,
|
|
4708
|
+
collectorPort: opts.collectorPort,
|
|
4709
|
+
fixturePath: opts.fixturePath,
|
|
4710
|
+
outputFile: opts.outputFile,
|
|
4711
|
+
noSecrets: opts.secretsInOutput === false
|
|
4712
|
+
};
|
|
4713
|
+
}
|
|
4714
|
+
function staticFlagsFromOpts(opts) {
|
|
4715
|
+
return {
|
|
4716
|
+
outputFile: opts.outputFile,
|
|
4717
|
+
noSecrets: opts.secretsInOutput === false
|
|
4718
|
+
};
|
|
4719
|
+
}
|
|
4720
|
+
function addTimeWindowFlags(cmd) {
|
|
4721
|
+
return cmd.option("--service-name <name>", "Filter by service name").option("--operation-name <name>", "Filter by operation name").option("--lookback-minutes <n>", "Lookback window in minutes", intArg).option("--from <iso>", "Start time (ISO 8601)").option("--to <iso>", "End time (ISO 8601)").option("--limit <n>", "Max results", intArg);
|
|
4722
|
+
}
|
|
4723
|
+
|
|
4724
|
+
// src/commands/investigate/health.ts
|
|
4725
|
+
async function runHealth(flags) {
|
|
4726
|
+
await runInvestigate("health", flags, async (backend) => {
|
|
4727
|
+
const [health, capabilities] = await Promise.all([
|
|
4728
|
+
backend.healthCheck(),
|
|
4729
|
+
Promise.resolve(backend.capabilities())
|
|
4730
|
+
]);
|
|
4731
|
+
return { ...health, signals: capabilities };
|
|
4732
|
+
});
|
|
4733
|
+
}
|
|
4734
|
+
async function runCapabilities(flags) {
|
|
4735
|
+
await runInvestigate(
|
|
4736
|
+
"capabilities",
|
|
4737
|
+
flags,
|
|
4738
|
+
async (backend) => backend.capabilities()
|
|
4739
|
+
);
|
|
4740
|
+
}
|
|
4741
|
+
function registerHealthCommands(program) {
|
|
4742
|
+
const healthCmd = new Command("health").description("Backend health check + signal coverage (JSON)").action(async function() {
|
|
4743
|
+
await runHealth(backendFlagsFromOpts(this.opts()));
|
|
4744
|
+
});
|
|
4745
|
+
addBackendFlags(healthCmd);
|
|
4746
|
+
program.addCommand(healthCmd);
|
|
4747
|
+
const capabilitiesCmd = new Command("capabilities").description("Which telemetry signals the active backend can serve (JSON)").action(async function() {
|
|
4748
|
+
await runCapabilities(backendFlagsFromOpts(this.opts()));
|
|
4749
|
+
});
|
|
4750
|
+
addBackendFlags(capabilitiesCmd);
|
|
4751
|
+
program.addCommand(capabilitiesCmd);
|
|
4752
|
+
}
|
|
4753
|
+
|
|
4754
|
+
// src/commands/investigate/discovery.ts
|
|
4755
|
+
import { Command as Command2 } from "commander";
|
|
4756
|
+
import {
|
|
4757
|
+
discoverServices,
|
|
4758
|
+
discoverTraceFields,
|
|
4759
|
+
discoverLogFields
|
|
4760
|
+
} from "autotel-mcp";
|
|
4761
|
+
async function runDiscoverServices(flags) {
|
|
4762
|
+
await runInvestigate("discover services", flags, async (backend) => {
|
|
4763
|
+
const limitServices = flags.limitServices ?? 100;
|
|
4764
|
+
const traceSample = flags.traceSample ?? 200;
|
|
4765
|
+
const logSample = flags.logSample ?? 200;
|
|
4766
|
+
const metricSample = flags.metricSample ?? 200;
|
|
4767
|
+
const caps = backend.capabilities();
|
|
4768
|
+
const servicesResult = await backend.listServices({ limit: limitServices });
|
|
4769
|
+
const services = servicesResult.services.slice(0, limitServices);
|
|
4770
|
+
const [traces, logs, metrics] = await Promise.all([
|
|
4771
|
+
caps.traces === "available" ? backend.searchTraces({ limit: traceSample }).then((r) => r.items) : Promise.resolve([]),
|
|
4772
|
+
caps.logs === "available" ? backend.searchLogs({ limit: logSample }).then((r) => r.items) : Promise.resolve([]),
|
|
4773
|
+
caps.metrics === "available" ? backend.listMetrics({ limit: metricSample }).then((r) => r.items) : Promise.resolve([])
|
|
4774
|
+
]);
|
|
4775
|
+
const discovered = discoverServices({ services, traces, logs, metrics });
|
|
4776
|
+
return { count: discovered.length, services: discovered };
|
|
4777
|
+
});
|
|
4778
|
+
}
|
|
4779
|
+
async function runDiscoverTraceFields(flags) {
|
|
4780
|
+
await runInvestigate("discover trace-fields", flags, async (backend) => {
|
|
4781
|
+
return discoverFields(backend, flags, "traces");
|
|
4782
|
+
});
|
|
4783
|
+
}
|
|
4784
|
+
async function runDiscoverLogFields(flags) {
|
|
4785
|
+
await runInvestigate("discover log-fields", flags, async (backend) => {
|
|
4786
|
+
return discoverFields(backend, flags, "logs");
|
|
4787
|
+
});
|
|
4788
|
+
}
|
|
4789
|
+
async function discoverFields(backend, flags, signal) {
|
|
4790
|
+
const sampleSize = flags.sampleSize ?? 200;
|
|
4791
|
+
if (signal === "traces") {
|
|
4792
|
+
const traces = await backend.searchTraces({ limit: sampleSize }).then((r) => r.items);
|
|
4793
|
+
return {
|
|
4794
|
+
search: flags.search ?? null,
|
|
4795
|
+
sampleSize: traces.length,
|
|
4796
|
+
...discoverTraceFields(traces, flags.search)
|
|
4797
|
+
};
|
|
4798
|
+
}
|
|
4799
|
+
const logs = await backend.searchLogs({ limit: sampleSize }).then((r) => r.items);
|
|
4800
|
+
return {
|
|
4801
|
+
search: flags.search ?? null,
|
|
4802
|
+
sampleSize: logs.length,
|
|
4803
|
+
...discoverLogFields(logs, flags.search)
|
|
4804
|
+
};
|
|
4805
|
+
}
|
|
4806
|
+
function registerDiscoveryCommands(program) {
|
|
4807
|
+
const discoverCmd = new Command2("discover").description(
|
|
4808
|
+
"Discover services and field shapes from the active backend (JSON)"
|
|
4809
|
+
);
|
|
4810
|
+
const servicesCmd = new Command2("services").description("Discover services with cross-signal metadata").option("--limit-services <n>", "Max services", intArg).option("--trace-sample <n>", "Trace sample size", intArg).option("--log-sample <n>", "Log sample size", intArg).option("--metric-sample <n>", "Metric sample size", intArg).action(async function() {
|
|
4811
|
+
const o = this.optsWithGlobals();
|
|
4812
|
+
await runDiscoverServices({
|
|
4813
|
+
...backendFlagsFromOpts(o),
|
|
4814
|
+
limitServices: o.limitServices,
|
|
4815
|
+
traceSample: o.traceSample,
|
|
4816
|
+
logSample: o.logSample,
|
|
4817
|
+
metricSample: o.metricSample
|
|
4818
|
+
});
|
|
4819
|
+
});
|
|
4820
|
+
const traceFieldsCmd = new Command2("trace-fields").description("Discover trace/span field names from sampled traces").option("--search <text>", "Filter field names by substring").option("--sample-size <n>", "Trace sample size", intArg).action(async function() {
|
|
4821
|
+
const o = this.optsWithGlobals();
|
|
4822
|
+
await runDiscoverTraceFields({
|
|
4823
|
+
...backendFlagsFromOpts(o),
|
|
4824
|
+
search: o.search,
|
|
4825
|
+
sampleSize: o.sampleSize
|
|
4826
|
+
});
|
|
4827
|
+
});
|
|
4828
|
+
const logFieldsCmd = new Command2("log-fields").description("Discover log field names from sampled logs").option("--search <text>", "Filter field names by substring").option("--sample-size <n>", "Log sample size", intArg).action(async function() {
|
|
4829
|
+
const o = this.optsWithGlobals();
|
|
4830
|
+
await runDiscoverLogFields({
|
|
4831
|
+
...backendFlagsFromOpts(o),
|
|
4832
|
+
search: o.search,
|
|
4833
|
+
sampleSize: o.sampleSize
|
|
4834
|
+
});
|
|
4835
|
+
});
|
|
4836
|
+
addBackendFlags(discoverCmd);
|
|
4837
|
+
discoverCmd.addCommand(servicesCmd);
|
|
4838
|
+
discoverCmd.addCommand(traceFieldsCmd);
|
|
4839
|
+
discoverCmd.addCommand(logFieldsCmd);
|
|
4840
|
+
program.addCommand(discoverCmd);
|
|
4841
|
+
}
|
|
4842
|
+
|
|
4843
|
+
// src/commands/investigate/investigation.ts
|
|
4844
|
+
import { Command as Command3 } from "commander";
|
|
4845
|
+
import {
|
|
4846
|
+
toTraceSearchQuery,
|
|
4847
|
+
toSpanSearchQuery
|
|
4848
|
+
} from "autotel-mcp";
|
|
4849
|
+
|
|
4850
|
+
// src/commands/investigate/signals.ts
|
|
4851
|
+
import {
|
|
4852
|
+
toMetricSearchQuery,
|
|
4853
|
+
toLogSearchQuery
|
|
4854
|
+
} from "autotel-mcp";
|
|
4855
|
+
async function runQueryMetrics(flags) {
|
|
4856
|
+
await runInvestigate(
|
|
4857
|
+
"query metrics",
|
|
4858
|
+
flags,
|
|
4859
|
+
async (backend) => backend.listMetrics(toMetricSearchQuery(flags))
|
|
4860
|
+
);
|
|
4861
|
+
}
|
|
4862
|
+
async function runQueryLogs(flags) {
|
|
4863
|
+
await runInvestigate(
|
|
4864
|
+
"query logs",
|
|
4865
|
+
flags,
|
|
4866
|
+
async (backend) => backend.searchLogs(toLogSearchQuery(flags))
|
|
4867
|
+
);
|
|
4868
|
+
}
|
|
4869
|
+
|
|
4870
|
+
// src/commands/investigate/investigation.ts
|
|
4871
|
+
async function runQueryTraces(flags) {
|
|
4872
|
+
await runInvestigate(
|
|
4873
|
+
"query traces",
|
|
4874
|
+
flags,
|
|
4875
|
+
async (backend) => backend.searchTraces(toTraceSearchQuery(flags))
|
|
4876
|
+
);
|
|
4877
|
+
}
|
|
4878
|
+
async function runQuerySpans(flags) {
|
|
4879
|
+
await runInvestigate(
|
|
4880
|
+
"query spans",
|
|
4881
|
+
flags,
|
|
4882
|
+
async (backend) => backend.searchSpans(toSpanSearchQuery(flags))
|
|
4883
|
+
);
|
|
4884
|
+
}
|
|
4885
|
+
async function runTraceGet(flags) {
|
|
4886
|
+
await runInvestigate(
|
|
4887
|
+
"trace get",
|
|
4888
|
+
flags,
|
|
4889
|
+
async (backend) => backend.getTrace(flags.traceId)
|
|
4890
|
+
);
|
|
4891
|
+
}
|
|
4892
|
+
async function runTraceSummary(flags) {
|
|
4893
|
+
await runInvestigate(
|
|
4894
|
+
"trace summary",
|
|
4895
|
+
flags,
|
|
4896
|
+
async (backend) => backend.summarizeTrace(flags.traceId)
|
|
4897
|
+
);
|
|
4898
|
+
}
|
|
4899
|
+
function registerQueryCommands(program) {
|
|
4900
|
+
const queryCmd = new Command3("query").description(
|
|
4901
|
+
"Query traces, spans, metrics, or logs (JSON)"
|
|
4902
|
+
);
|
|
4903
|
+
const tracesCmd = addTimeWindowFlags(new Command3("traces")).description("Search traces by service, op, status, tags, time, error").option("--error-only", "Only traces with errors").option("--status-code <code>", "OK | ERROR | UNSET").option("--min-duration-ms <n>", "Minimum duration", intArg).option("--max-duration-ms <n>", "Maximum duration", intArg).option("--gen-ai-system <name>", "gen_ai.system").option("--gen-ai-request-model <name>", "gen_ai.request.model").option("--gen-ai-response-model <name>", "gen_ai.response.model").action(async function() {
|
|
4904
|
+
const o = this.optsWithGlobals();
|
|
4905
|
+
await runQueryTraces({
|
|
4906
|
+
...backendFlagsFromOpts(o),
|
|
4907
|
+
serviceName: o.serviceName,
|
|
4908
|
+
operationName: o.operationName,
|
|
4909
|
+
lookbackMinutes: o.lookbackMinutes,
|
|
4910
|
+
from: o.from,
|
|
4911
|
+
to: o.to,
|
|
4912
|
+
limit: o.limit,
|
|
4913
|
+
errorOnly: o.errorOnly,
|
|
4914
|
+
statusCode: o.statusCode,
|
|
4915
|
+
minDurationMs: o.minDurationMs,
|
|
4916
|
+
maxDurationMs: o.maxDurationMs,
|
|
4917
|
+
genAiSystem: o.genAiSystem,
|
|
4918
|
+
genAiRequestModel: o.genAiRequestModel,
|
|
4919
|
+
genAiResponseModel: o.genAiResponseModel
|
|
4920
|
+
});
|
|
4921
|
+
});
|
|
4922
|
+
const spansCmd = addTimeWindowFlags(new Command3("spans")).description("Search individual spans by service/op/status/tags/duration").option("--error-only", "Only spans with errors").option("--status-code <code>", "OK | ERROR | UNSET").option("--min-duration-ms <n>", "Minimum span duration", intArg).option("--max-duration-ms <n>", "Maximum span duration", intArg).action(async function() {
|
|
4923
|
+
const o = this.optsWithGlobals();
|
|
4924
|
+
await runQuerySpans({
|
|
4925
|
+
...backendFlagsFromOpts(o),
|
|
4926
|
+
serviceName: o.serviceName,
|
|
4927
|
+
operationName: o.operationName,
|
|
4928
|
+
lookbackMinutes: o.lookbackMinutes,
|
|
4929
|
+
from: o.from,
|
|
4930
|
+
to: o.to,
|
|
4931
|
+
limit: o.limit,
|
|
4932
|
+
errorOnly: o.errorOnly,
|
|
4933
|
+
statusCode: o.statusCode,
|
|
4934
|
+
minDurationMs: o.minDurationMs,
|
|
4935
|
+
maxDurationMs: o.maxDurationMs
|
|
4936
|
+
});
|
|
4937
|
+
});
|
|
4938
|
+
const metricsCmd = addTimeWindowFlags(new Command3("metrics")).description("List metric series").option("--metric-name <name>", "Filter by metric name").action(async function() {
|
|
4939
|
+
const o = this.optsWithGlobals();
|
|
4940
|
+
await runQueryMetrics({
|
|
4941
|
+
...backendFlagsFromOpts(o),
|
|
4942
|
+
metricName: o.metricName,
|
|
4943
|
+
serviceName: o.serviceName,
|
|
4944
|
+
lookbackMinutes: o.lookbackMinutes,
|
|
4945
|
+
from: o.from,
|
|
4946
|
+
to: o.to,
|
|
4947
|
+
limit: o.limit
|
|
4948
|
+
});
|
|
4949
|
+
});
|
|
4950
|
+
const logsCmd = addTimeWindowFlags(new Command3("logs")).description("Search logs").option("--trace-id <id>", "Filter by trace id").option("--span-id <id>", "Filter by span id").option("--severity-text <text>", "Severity text").option("--text <text>", "Free-text search").action(async function() {
|
|
4951
|
+
const o = this.optsWithGlobals();
|
|
4952
|
+
await runQueryLogs({
|
|
4953
|
+
...backendFlagsFromOpts(o),
|
|
4954
|
+
serviceName: o.serviceName,
|
|
4955
|
+
traceId: o.traceId,
|
|
4956
|
+
spanId: o.spanId,
|
|
4957
|
+
severityText: o.severityText,
|
|
4958
|
+
text: o.text,
|
|
4959
|
+
lookbackMinutes: o.lookbackMinutes,
|
|
4960
|
+
from: o.from,
|
|
4961
|
+
to: o.to,
|
|
4962
|
+
limit: o.limit
|
|
4963
|
+
});
|
|
4964
|
+
});
|
|
4965
|
+
addBackendFlags(queryCmd);
|
|
4966
|
+
queryCmd.addCommand(tracesCmd);
|
|
4967
|
+
queryCmd.addCommand(spansCmd);
|
|
4968
|
+
queryCmd.addCommand(metricsCmd);
|
|
4969
|
+
queryCmd.addCommand(logsCmd);
|
|
4970
|
+
program.addCommand(queryCmd);
|
|
4971
|
+
}
|
|
4972
|
+
function registerTraceCommands(program) {
|
|
4973
|
+
const traceLookupCmd = new Command3("trace").description(
|
|
4974
|
+
"Trace lookup commands (JSON)"
|
|
4975
|
+
);
|
|
4976
|
+
const getCmd = new Command3("get").description("Get a trace by ID").argument("<traceId>", "Trace ID").action(async function(traceId) {
|
|
4977
|
+
await runTraceGet({
|
|
4978
|
+
...backendFlagsFromOpts(this.optsWithGlobals()),
|
|
4979
|
+
traceId
|
|
4980
|
+
});
|
|
4981
|
+
});
|
|
4982
|
+
const summaryCmd = new Command3("summary").description("Compact incident-friendly trace summary").argument("<traceId>", "Trace ID").action(async function(traceId) {
|
|
4983
|
+
await runTraceSummary({
|
|
4984
|
+
...backendFlagsFromOpts(this.optsWithGlobals()),
|
|
4985
|
+
traceId
|
|
4986
|
+
});
|
|
4987
|
+
});
|
|
4988
|
+
addBackendFlags(traceLookupCmd);
|
|
4989
|
+
traceLookupCmd.addCommand(getCmd);
|
|
4990
|
+
traceLookupCmd.addCommand(summaryCmd);
|
|
4991
|
+
program.addCommand(traceLookupCmd);
|
|
4992
|
+
}
|
|
4993
|
+
|
|
4994
|
+
// src/commands/investigate/topology.ts
|
|
4995
|
+
import { Command as Command4 } from "commander";
|
|
4996
|
+
async function runListServices(flags) {
|
|
4997
|
+
await runInvestigate(
|
|
4998
|
+
"topology services",
|
|
4999
|
+
flags,
|
|
5000
|
+
async (backend) => backend.listServices()
|
|
5001
|
+
);
|
|
5002
|
+
}
|
|
5003
|
+
async function runListOperations(flags) {
|
|
5004
|
+
await runInvestigate(
|
|
5005
|
+
"topology operations",
|
|
5006
|
+
flags,
|
|
5007
|
+
async (backend) => backend.listOperations(flags.serviceName)
|
|
5008
|
+
);
|
|
5009
|
+
}
|
|
5010
|
+
async function runServiceMap(flags) {
|
|
5011
|
+
await runInvestigate(
|
|
5012
|
+
"topology map",
|
|
5013
|
+
flags,
|
|
5014
|
+
async (backend) => backend.serviceMap(flags.lookbackMinutes ?? 60, flags.limit ?? 20)
|
|
5015
|
+
);
|
|
5016
|
+
}
|
|
5017
|
+
function registerTopologyCommands(program) {
|
|
5018
|
+
const topologyCmd = new Command4("topology").description(
|
|
5019
|
+
"Service topology commands (JSON)"
|
|
5020
|
+
);
|
|
5021
|
+
const servicesCmd = new Command4("services").description("List known services").action(async function() {
|
|
5022
|
+
await runListServices(backendFlagsFromOpts(this.optsWithGlobals()));
|
|
5023
|
+
});
|
|
5024
|
+
const operationsCmd = new Command4("operations").description("List operations for a service").argument("<serviceName>", "Service name").action(async function(serviceName) {
|
|
5025
|
+
await runListOperations({
|
|
5026
|
+
...backendFlagsFromOpts(this.optsWithGlobals()),
|
|
5027
|
+
serviceName
|
|
5028
|
+
});
|
|
5029
|
+
});
|
|
5030
|
+
const mapCmd = new Command4("map").description("Build a service dependency map").option("--lookback-minutes <n>", "Lookback in minutes", intArg).option("--limit <n>", "Max services", intArg).action(async function() {
|
|
5031
|
+
const o = this.optsWithGlobals();
|
|
5032
|
+
await runServiceMap({
|
|
5033
|
+
...backendFlagsFromOpts(o),
|
|
5034
|
+
lookbackMinutes: o.lookbackMinutes,
|
|
5035
|
+
limit: o.limit
|
|
5036
|
+
});
|
|
5037
|
+
});
|
|
5038
|
+
addBackendFlags(topologyCmd);
|
|
5039
|
+
topologyCmd.addCommand(servicesCmd);
|
|
5040
|
+
topologyCmd.addCommand(operationsCmd);
|
|
5041
|
+
topologyCmd.addCommand(mapCmd);
|
|
5042
|
+
program.addCommand(topologyCmd);
|
|
5043
|
+
}
|
|
5044
|
+
|
|
5045
|
+
// src/commands/investigate/diagnosis.ts
|
|
5046
|
+
import { Command as Command5 } from "commander";
|
|
5047
|
+
import {
|
|
5048
|
+
detectAnomalies,
|
|
5049
|
+
findRootCause,
|
|
5050
|
+
pickErrorMessage
|
|
5051
|
+
} from "autotel-mcp";
|
|
5052
|
+
async function runDiagnoseAnomalies(flags) {
|
|
5053
|
+
await runInvestigate("diagnose anomalies", flags, async (backend) => {
|
|
5054
|
+
const lookback = flags.lookbackMinutes ?? 60;
|
|
5055
|
+
const nowMs = Date.now();
|
|
5056
|
+
const result = await backend.searchTraces({
|
|
5057
|
+
service: flags.service,
|
|
5058
|
+
startTimeUnixMs: nowMs - lookback * 60 * 1e3,
|
|
5059
|
+
endTimeUnixMs: nowMs,
|
|
5060
|
+
limit: 100
|
|
5061
|
+
});
|
|
5062
|
+
return detectAnomalies(result.items, {
|
|
5063
|
+
service: flags.service,
|
|
5064
|
+
operation: flags.operation
|
|
5065
|
+
});
|
|
5066
|
+
});
|
|
5067
|
+
}
|
|
5068
|
+
async function runDiagnoseRootCause(flags) {
|
|
5069
|
+
await runInvestigate("diagnose root-cause", flags, async (backend) => {
|
|
5070
|
+
const trace = await backend.getTrace(flags.traceId);
|
|
5071
|
+
if (!trace) return { error: `Trace not found: ${flags.traceId}` };
|
|
5072
|
+
return findRootCause(trace);
|
|
5073
|
+
});
|
|
5074
|
+
}
|
|
5075
|
+
async function runDiagnoseErrors(flags) {
|
|
5076
|
+
await runInvestigate("diagnose errors", flags, async (backend) => {
|
|
5077
|
+
const lookback = flags.lookbackMinutes ?? 60;
|
|
5078
|
+
const limit = flags.limit ?? 20;
|
|
5079
|
+
const nowMs = Date.now();
|
|
5080
|
+
const result = await backend.searchTraces({
|
|
5081
|
+
service: flags.service,
|
|
5082
|
+
hasError: true,
|
|
5083
|
+
startTimeUnixMs: nowMs - lookback * 60 * 1e3,
|
|
5084
|
+
endTimeUnixMs: nowMs,
|
|
5085
|
+
limit
|
|
5086
|
+
});
|
|
5087
|
+
const groups = /* @__PURE__ */ new Map();
|
|
5088
|
+
for (const trace of result.items) {
|
|
5089
|
+
for (const span of trace.spans) {
|
|
5090
|
+
if (!span.hasError) continue;
|
|
5091
|
+
if (flags.service && span.serviceName !== flags.service) continue;
|
|
5092
|
+
const key = `${span.serviceName}::${span.operationName}`;
|
|
5093
|
+
if (!groups.has(key)) {
|
|
5094
|
+
groups.set(key, {
|
|
5095
|
+
service: span.serviceName,
|
|
5096
|
+
operation: span.operationName,
|
|
5097
|
+
count: 0,
|
|
5098
|
+
errorMessages: [],
|
|
5099
|
+
traceIds: []
|
|
5100
|
+
});
|
|
5101
|
+
}
|
|
5102
|
+
const group = groups.get(key);
|
|
5103
|
+
group.count++;
|
|
5104
|
+
const msg = pickErrorMessage(span.tags);
|
|
5105
|
+
if (msg && !group.errorMessages.includes(msg)) {
|
|
5106
|
+
group.errorMessages.push(msg);
|
|
5107
|
+
}
|
|
5108
|
+
if (!group.traceIds.includes(trace.traceId)) {
|
|
5109
|
+
group.traceIds.push(trace.traceId);
|
|
5110
|
+
}
|
|
5111
|
+
}
|
|
5112
|
+
}
|
|
5113
|
+
return {
|
|
5114
|
+
totalTraces: result.totalCount,
|
|
5115
|
+
groups: [...groups.values()].toSorted((a, b) => b.count - a.count)
|
|
5116
|
+
};
|
|
5117
|
+
});
|
|
5118
|
+
}
|
|
5119
|
+
async function runDiagnoseSlos(flags) {
|
|
5120
|
+
await runInvestigate("diagnose slos", flags, async (backend) => {
|
|
5121
|
+
const lookback = flags.lookbackMinutes ?? 60;
|
|
5122
|
+
const nowMs = Date.now();
|
|
5123
|
+
const result = await backend.searchTraces({
|
|
5124
|
+
service: flags.service,
|
|
5125
|
+
startTimeUnixMs: nowMs - lookback * 60 * 1e3,
|
|
5126
|
+
endTimeUnixMs: nowMs,
|
|
5127
|
+
limit: 100
|
|
5128
|
+
});
|
|
5129
|
+
const spans = result.items.flatMap(
|
|
5130
|
+
(t) => t.spans.filter((s) => s.serviceName === flags.service)
|
|
5131
|
+
);
|
|
5132
|
+
const violations = [];
|
|
5133
|
+
if (spans.length === 0) {
|
|
5134
|
+
return {
|
|
5135
|
+
service: flags.service,
|
|
5136
|
+
totalSpans: 0,
|
|
5137
|
+
violations,
|
|
5138
|
+
message: "No spans found for the given service and time window."
|
|
5139
|
+
};
|
|
5140
|
+
}
|
|
5141
|
+
const durations = spans.map((s) => s.durationMs).toSorted((a, b) => a - b);
|
|
5142
|
+
const p99Index = Math.floor(durations.length * 0.99);
|
|
5143
|
+
const actualP99 = durations[Math.min(p99Index, durations.length - 1)];
|
|
5144
|
+
if (flags.p99LatencyMs !== void 0 && actualP99 > flags.p99LatencyMs) {
|
|
5145
|
+
violations.push({
|
|
5146
|
+
type: "p99_latency",
|
|
5147
|
+
target: flags.p99LatencyMs,
|
|
5148
|
+
actual: actualP99,
|
|
5149
|
+
description: `p99 latency ${actualP99.toFixed(1)}ms exceeds target ${flags.p99LatencyMs}ms`
|
|
5150
|
+
});
|
|
5151
|
+
}
|
|
5152
|
+
const errorCount = spans.filter((s) => s.hasError).length;
|
|
5153
|
+
const actualErrorRate = errorCount / spans.length;
|
|
5154
|
+
if (flags.maxErrorRate !== void 0 && actualErrorRate > flags.maxErrorRate) {
|
|
5155
|
+
violations.push({
|
|
5156
|
+
type: "error_rate",
|
|
5157
|
+
target: flags.maxErrorRate,
|
|
5158
|
+
actual: actualErrorRate,
|
|
5159
|
+
description: `Error rate ${(actualErrorRate * 100).toFixed(2)}% exceeds target ${(flags.maxErrorRate * 100).toFixed(2)}%`
|
|
5160
|
+
});
|
|
5161
|
+
}
|
|
5162
|
+
return {
|
|
5163
|
+
service: flags.service,
|
|
5164
|
+
totalSpans: spans.length,
|
|
5165
|
+
p99LatencyMs: actualP99,
|
|
5166
|
+
errorRate: actualErrorRate,
|
|
5167
|
+
violations
|
|
5168
|
+
};
|
|
5169
|
+
});
|
|
5170
|
+
}
|
|
5171
|
+
function registerDiagnoseCommands(program) {
|
|
5172
|
+
const diagnoseCmd = new Command5("diagnose").description(
|
|
5173
|
+
"Anomaly / root-cause / errors / SLO diagnosis (JSON)"
|
|
5174
|
+
);
|
|
5175
|
+
const anomaliesCmd = new Command5("anomalies").description("Detect latency/error-rate outliers").option("--service <name>", "Service filter").option("--operation <name>", "Operation filter").option("--lookback-minutes <n>", "Lookback in minutes", intArg).action(async function() {
|
|
5176
|
+
const o = this.optsWithGlobals();
|
|
5177
|
+
await runDiagnoseAnomalies({
|
|
5178
|
+
...backendFlagsFromOpts(o),
|
|
5179
|
+
service: o.service,
|
|
5180
|
+
operation: o.operation,
|
|
5181
|
+
lookbackMinutes: o.lookbackMinutes
|
|
5182
|
+
});
|
|
5183
|
+
});
|
|
5184
|
+
const rootCauseCmd = new Command5("root-cause").description("Walk a trace tree to identify the bottleneck span").argument("<traceId>", "Trace ID").action(async function(traceId) {
|
|
5185
|
+
await runDiagnoseRootCause({
|
|
5186
|
+
...backendFlagsFromOpts(this.optsWithGlobals()),
|
|
5187
|
+
traceId
|
|
5188
|
+
});
|
|
5189
|
+
});
|
|
5190
|
+
const errorsCmd = new Command5("errors").description("Aggregate error spans by service/operation").option("--service <name>", "Service filter").option("--lookback-minutes <n>", "Lookback in minutes", intArg).option("--limit <n>", "Max traces to scan", intArg).action(async function() {
|
|
5191
|
+
const o = this.optsWithGlobals();
|
|
5192
|
+
await runDiagnoseErrors({
|
|
5193
|
+
...backendFlagsFromOpts(o),
|
|
5194
|
+
service: o.service,
|
|
5195
|
+
lookbackMinutes: o.lookbackMinutes,
|
|
5196
|
+
limit: o.limit
|
|
5197
|
+
});
|
|
5198
|
+
});
|
|
5199
|
+
const slosCmd = new Command5("slos").description("Report SLO violations for a service").requiredOption("--service <name>", "Service to check").option("--p99-latency-ms <n>", "p99 latency target", floatArg).option("--max-error-rate <n>", "Error-rate target (0..1)", floatArg).option("--lookback-minutes <n>", "Lookback in minutes", intArg).action(async function() {
|
|
5200
|
+
const o = this.optsWithGlobals();
|
|
5201
|
+
await runDiagnoseSlos({
|
|
5202
|
+
...backendFlagsFromOpts(o),
|
|
5203
|
+
service: o.service,
|
|
5204
|
+
p99LatencyMs: o.p99LatencyMs,
|
|
5205
|
+
maxErrorRate: o.maxErrorRate,
|
|
5206
|
+
lookbackMinutes: o.lookbackMinutes
|
|
5207
|
+
});
|
|
5208
|
+
});
|
|
5209
|
+
addBackendFlags(diagnoseCmd);
|
|
5210
|
+
diagnoseCmd.addCommand(anomaliesCmd);
|
|
5211
|
+
diagnoseCmd.addCommand(rootCauseCmd);
|
|
5212
|
+
diagnoseCmd.addCommand(errorsCmd);
|
|
5213
|
+
diagnoseCmd.addCommand(slosCmd);
|
|
5214
|
+
program.addCommand(diagnoseCmd);
|
|
5215
|
+
}
|
|
5216
|
+
|
|
5217
|
+
// src/commands/investigate/correlation.ts
|
|
5218
|
+
import { Command as Command6 } from "commander";
|
|
5219
|
+
import { detectAnomalies as detectAnomalies2, findRootCause as findRootCause2 } from "autotel-mcp";
|
|
5220
|
+
async function runCorrelate(flags) {
|
|
5221
|
+
await runInvestigate(
|
|
5222
|
+
"correlate trace",
|
|
5223
|
+
flags,
|
|
5224
|
+
async (backend) => backend.getCorrelatedSignals(flags.traceId)
|
|
5225
|
+
);
|
|
5226
|
+
}
|
|
5227
|
+
async function runExplainSlowdown(flags) {
|
|
5228
|
+
await runInvestigate("correlate explain-slowdown", flags, async (backend) => {
|
|
5229
|
+
const lookback = flags.lookbackMinutes ?? 60;
|
|
5230
|
+
const nowMs = Date.now();
|
|
5231
|
+
const result = await backend.searchTraces({
|
|
5232
|
+
service: flags.service,
|
|
5233
|
+
startTimeUnixMs: nowMs - lookback * 60 * 1e3,
|
|
5234
|
+
endTimeUnixMs: nowMs,
|
|
5235
|
+
limit: 100
|
|
5236
|
+
});
|
|
5237
|
+
const anomalies = detectAnomalies2(result.items, { service: flags.service });
|
|
5238
|
+
const findings = await Promise.all(
|
|
5239
|
+
anomalies.map(async (anomaly) => {
|
|
5240
|
+
const sampleTraceId = anomaly.affectedTraceIds[0];
|
|
5241
|
+
if (!sampleTraceId) {
|
|
5242
|
+
return { anomaly, rootCause: null, correlatedSignals: null };
|
|
5243
|
+
}
|
|
5244
|
+
const trace = await backend.getTrace(sampleTraceId);
|
|
5245
|
+
const rootCause = trace ? findRootCause2(trace) : null;
|
|
5246
|
+
const correlatedSignals = await backend.getCorrelatedSignals(sampleTraceId);
|
|
5247
|
+
return { anomaly, rootCause, correlatedSignals };
|
|
5248
|
+
})
|
|
5249
|
+
);
|
|
5250
|
+
return {
|
|
5251
|
+
service: flags.service,
|
|
5252
|
+
lookbackMinutes: lookback,
|
|
5253
|
+
anomalyCount: anomalies.length,
|
|
5254
|
+
findings
|
|
5255
|
+
};
|
|
5256
|
+
});
|
|
5257
|
+
}
|
|
5258
|
+
function registerCorrelateCommands(program) {
|
|
5259
|
+
const correlateCmd = new Command6("correlate").description(
|
|
5260
|
+
"Cross-signal correlation (JSON)"
|
|
5261
|
+
);
|
|
5262
|
+
const traceCmd = new Command6("trace").description("Trace + metrics + correlated logs for a trace ID").argument("<traceId>", "Trace ID").action(async function(traceId) {
|
|
5263
|
+
await runCorrelate({
|
|
5264
|
+
...backendFlagsFromOpts(this.optsWithGlobals()),
|
|
5265
|
+
traceId
|
|
5266
|
+
});
|
|
5267
|
+
});
|
|
5268
|
+
const slowdownCmd = new Command6("explain-slowdown").description("Identify when/why a service degraded").requiredOption("--service <name>", "Service name").option("--lookback-minutes <n>", "Lookback in minutes", intArg).action(async function() {
|
|
5269
|
+
const o = this.optsWithGlobals();
|
|
5270
|
+
await runExplainSlowdown({
|
|
5271
|
+
...backendFlagsFromOpts(o),
|
|
5272
|
+
service: o.service,
|
|
5273
|
+
lookbackMinutes: o.lookbackMinutes
|
|
5274
|
+
});
|
|
5275
|
+
});
|
|
5276
|
+
addBackendFlags(correlateCmd);
|
|
5277
|
+
correlateCmd.addCommand(traceCmd);
|
|
5278
|
+
correlateCmd.addCommand(slowdownCmd);
|
|
5279
|
+
program.addCommand(correlateCmd);
|
|
5280
|
+
}
|
|
5281
|
+
|
|
5282
|
+
// src/commands/investigate/llm.ts
|
|
5283
|
+
import {
|
|
5284
|
+
collectUsage,
|
|
5285
|
+
listModels,
|
|
5286
|
+
getModelStats,
|
|
5287
|
+
rankExpensiveTraces,
|
|
5288
|
+
rankSlowTraces,
|
|
5289
|
+
listToolUsage,
|
|
5290
|
+
toTraceSearchQuery as toTraceSearchQuery2
|
|
5291
|
+
} from "autotel-mcp";
|
|
5292
|
+
import { Command as Command7 } from "commander";
|
|
5293
|
+
function parseDateToUnixMs(value) {
|
|
5294
|
+
if (!value) return void 0;
|
|
5295
|
+
const parsed = Date.parse(value);
|
|
5296
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
5297
|
+
}
|
|
5298
|
+
async function collectTracesForAnalytics(backend, input2) {
|
|
5299
|
+
const result = await backend.searchTraces(
|
|
5300
|
+
toTraceSearchQuery2({
|
|
5301
|
+
serviceName: input2.serviceName,
|
|
5302
|
+
genAiSystem: input2.genAiSystem,
|
|
5303
|
+
genAiRequestModel: input2.genAiRequestModel,
|
|
5304
|
+
genAiResponseModel: input2.genAiResponseModel,
|
|
5305
|
+
limit: input2.limit ?? 1e3
|
|
5306
|
+
})
|
|
5307
|
+
);
|
|
5308
|
+
let filtered = result.items;
|
|
5309
|
+
const startUnixMs = parseDateToUnixMs(input2.startTime);
|
|
5310
|
+
const endUnixMs = parseDateToUnixMs(input2.endTime);
|
|
5311
|
+
if (startUnixMs !== void 0) {
|
|
5312
|
+
filtered = filtered.filter(
|
|
5313
|
+
(trace) => trace.spans.some((span) => span.startTimeUnixMs >= startUnixMs)
|
|
5314
|
+
);
|
|
5315
|
+
}
|
|
5316
|
+
if (endUnixMs !== void 0) {
|
|
5317
|
+
filtered = filtered.filter(
|
|
5318
|
+
(trace) => trace.spans.some((span) => span.startTimeUnixMs <= endUnixMs)
|
|
5319
|
+
);
|
|
5320
|
+
}
|
|
5321
|
+
return filtered;
|
|
5322
|
+
}
|
|
5323
|
+
async function runLlmUsage(flags) {
|
|
5324
|
+
await runInvestigate("llm usage", flags, async (backend) => {
|
|
5325
|
+
const traces = await collectTracesForAnalytics(backend, flags);
|
|
5326
|
+
const report = collectUsage(traces);
|
|
5327
|
+
return {
|
|
5328
|
+
period: {
|
|
5329
|
+
startTime: flags.startTime ?? null,
|
|
5330
|
+
endTime: flags.endTime ?? null
|
|
5331
|
+
},
|
|
5332
|
+
filters: {
|
|
5333
|
+
serviceName: flags.serviceName ?? null,
|
|
5334
|
+
genAiSystem: flags.genAiSystem ?? null,
|
|
5335
|
+
genAiRequestModel: flags.genAiRequestModel ?? null,
|
|
5336
|
+
genAiResponseModel: flags.genAiResponseModel ?? null
|
|
5337
|
+
},
|
|
5338
|
+
summary: {
|
|
5339
|
+
totalRequests: report.totalRequests,
|
|
5340
|
+
totalPromptTokens: report.totalPromptTokens,
|
|
5341
|
+
totalCompletionTokens: report.totalCompletionTokens,
|
|
5342
|
+
totalTokens: report.totalTokens,
|
|
5343
|
+
totalCostUsd: report.totalCostUsd,
|
|
5344
|
+
unpricedRequests: report.unpricedRequests
|
|
5345
|
+
},
|
|
5346
|
+
byModel: report.byModel,
|
|
5347
|
+
byService: report.byService
|
|
5348
|
+
};
|
|
5349
|
+
});
|
|
5350
|
+
}
|
|
5351
|
+
async function runLlmModels(flags) {
|
|
5352
|
+
await runInvestigate("llm models", flags, async (backend) => {
|
|
5353
|
+
const traces = await collectTracesForAnalytics(backend, flags);
|
|
5354
|
+
const models = listModels(traces).slice(0, flags.limit ?? 1e3);
|
|
5355
|
+
return { count: models.length, models };
|
|
5356
|
+
});
|
|
5357
|
+
}
|
|
5358
|
+
async function runLlmModelStats(flags) {
|
|
5359
|
+
await runInvestigate("llm model-stats", flags, async (backend) => {
|
|
5360
|
+
const traces = await collectTracesForAnalytics(backend, flags);
|
|
5361
|
+
const stats = getModelStats(traces, flags.modelName);
|
|
5362
|
+
if (!stats) {
|
|
5363
|
+
return {
|
|
5364
|
+
error: `No traces found for model '${flags.modelName}' in the specified time range`
|
|
5365
|
+
};
|
|
5366
|
+
}
|
|
5367
|
+
return stats;
|
|
5368
|
+
});
|
|
5369
|
+
}
|
|
5370
|
+
async function runLlmExpensive(flags) {
|
|
5371
|
+
await runInvestigate("llm expensive", flags, async (backend) => {
|
|
5372
|
+
const traces = await collectTracesForAnalytics(backend, flags);
|
|
5373
|
+
let ranked = rankExpensiveTraces(traces);
|
|
5374
|
+
if (flags.minTokens !== void 0) {
|
|
5375
|
+
ranked = ranked.filter((t) => t.tokens.total >= flags.minTokens);
|
|
5376
|
+
}
|
|
5377
|
+
ranked = ranked.slice(0, flags.limit ?? 10);
|
|
5378
|
+
return { count: ranked.length, traces: ranked };
|
|
5379
|
+
});
|
|
5380
|
+
}
|
|
5381
|
+
async function runLlmSlow(flags) {
|
|
5382
|
+
await runInvestigate("llm slow", flags, async (backend) => {
|
|
5383
|
+
const traces = await collectTracesForAnalytics(backend, flags);
|
|
5384
|
+
let ranked = rankSlowTraces(traces);
|
|
5385
|
+
if (flags.minDurationMs !== void 0) {
|
|
5386
|
+
ranked = ranked.filter((t) => t.durationMs >= flags.minDurationMs);
|
|
5387
|
+
}
|
|
5388
|
+
ranked = ranked.slice(0, flags.limit ?? 10);
|
|
5389
|
+
return { count: ranked.length, traces: ranked };
|
|
5390
|
+
});
|
|
5391
|
+
}
|
|
5392
|
+
async function runLlmTools(flags) {
|
|
5393
|
+
await runInvestigate("llm tools", flags, async (backend) => {
|
|
5394
|
+
const traces = await collectTracesForAnalytics(backend, flags);
|
|
5395
|
+
const tools = listToolUsage(traces).slice(0, flags.limit ?? 1e3);
|
|
5396
|
+
return {
|
|
5397
|
+
count: tools.length,
|
|
5398
|
+
totalCalls: tools.reduce((sum, t) => sum + t.usageCount, 0),
|
|
5399
|
+
tools
|
|
5400
|
+
};
|
|
5401
|
+
});
|
|
5402
|
+
}
|
|
5403
|
+
function registerLlmCommands(program) {
|
|
5404
|
+
const llmCmd = new Command7("llm").description(
|
|
5405
|
+
"LLM analytics (cost, models, expensive/slow traces, tools)"
|
|
5406
|
+
);
|
|
5407
|
+
const commonOpts = (cmd) => cmd.option("--start-time <iso>", "Start of window (ISO 8601)").option("--end-time <iso>", "End of window (ISO 8601)").option("--service-name <name>", "Service filter").option("--gen-ai-system <name>", "gen_ai.system filter").option("--gen-ai-request-model <name>", "gen_ai.request.model filter").option("--gen-ai-response-model <name>", "gen_ai.response.model filter").option("--limit <n>", "Max results", intArg);
|
|
5408
|
+
const usageCmd = commonOpts(new Command7("usage")).description("Aggregate token usage by model and service").action(async function() {
|
|
5409
|
+
const o = this.optsWithGlobals();
|
|
5410
|
+
await runLlmUsage({
|
|
5411
|
+
...backendFlagsFromOpts(o),
|
|
5412
|
+
startTime: o.startTime,
|
|
5413
|
+
endTime: o.endTime,
|
|
5414
|
+
serviceName: o.serviceName,
|
|
5415
|
+
genAiSystem: o.genAiSystem,
|
|
5416
|
+
genAiRequestModel: o.genAiRequestModel,
|
|
5417
|
+
genAiResponseModel: o.genAiResponseModel,
|
|
5418
|
+
limit: o.limit
|
|
5419
|
+
});
|
|
5420
|
+
});
|
|
5421
|
+
const modelsCmd = commonOpts(new Command7("models")).description("Discover LLM models in use").action(async function() {
|
|
5422
|
+
const o = this.optsWithGlobals();
|
|
5423
|
+
await runLlmModels({
|
|
5424
|
+
...backendFlagsFromOpts(o),
|
|
5425
|
+
startTime: o.startTime,
|
|
5426
|
+
endTime: o.endTime,
|
|
5427
|
+
serviceName: o.serviceName,
|
|
5428
|
+
genAiSystem: o.genAiSystem,
|
|
5429
|
+
limit: o.limit
|
|
5430
|
+
});
|
|
5431
|
+
});
|
|
5432
|
+
const modelStatsCmd = commonOpts(new Command7("model-stats")).description("Latency/token/error stats for one LLM model").requiredOption("--model-name <name>", "Model to inspect").action(async function() {
|
|
5433
|
+
const o = this.optsWithGlobals();
|
|
5434
|
+
await runLlmModelStats({
|
|
5435
|
+
...backendFlagsFromOpts(o),
|
|
5436
|
+
modelName: o.modelName,
|
|
5437
|
+
startTime: o.startTime,
|
|
5438
|
+
endTime: o.endTime,
|
|
5439
|
+
serviceName: o.serviceName
|
|
5440
|
+
});
|
|
5441
|
+
});
|
|
5442
|
+
const expensiveCmd = commonOpts(new Command7("expensive")).description("Traces with highest total LLM token usage").option("--min-tokens <n>", "Minimum token threshold", intArg).action(async function() {
|
|
5443
|
+
const o = this.optsWithGlobals();
|
|
5444
|
+
await runLlmExpensive({
|
|
5445
|
+
...backendFlagsFromOpts(o),
|
|
5446
|
+
startTime: o.startTime,
|
|
5447
|
+
endTime: o.endTime,
|
|
5448
|
+
serviceName: o.serviceName,
|
|
5449
|
+
genAiRequestModel: o.genAiRequestModel,
|
|
5450
|
+
genAiResponseModel: o.genAiResponseModel,
|
|
5451
|
+
minTokens: o.minTokens,
|
|
5452
|
+
limit: o.limit
|
|
5453
|
+
});
|
|
5454
|
+
});
|
|
5455
|
+
const slowCmd = commonOpts(new Command7("slow")).description("Slowest traces that include LLM spans").option("--min-duration-ms <n>", "Minimum duration", intArg).action(async function() {
|
|
5456
|
+
const o = this.optsWithGlobals();
|
|
5457
|
+
await runLlmSlow({
|
|
5458
|
+
...backendFlagsFromOpts(o),
|
|
5459
|
+
startTime: o.startTime,
|
|
5460
|
+
endTime: o.endTime,
|
|
5461
|
+
serviceName: o.serviceName,
|
|
5462
|
+
genAiRequestModel: o.genAiRequestModel,
|
|
5463
|
+
genAiResponseModel: o.genAiResponseModel,
|
|
5464
|
+
minDurationMs: o.minDurationMs,
|
|
5465
|
+
limit: o.limit
|
|
5466
|
+
});
|
|
5467
|
+
});
|
|
5468
|
+
const toolsCmd = commonOpts(new Command7("tools")).description("Discover tool/function spans grouped by tool name").action(async function() {
|
|
5469
|
+
const o = this.optsWithGlobals();
|
|
5470
|
+
await runLlmTools({
|
|
5471
|
+
...backendFlagsFromOpts(o),
|
|
5472
|
+
startTime: o.startTime,
|
|
5473
|
+
endTime: o.endTime,
|
|
5474
|
+
serviceName: o.serviceName,
|
|
5475
|
+
genAiSystem: o.genAiSystem,
|
|
5476
|
+
limit: o.limit
|
|
5477
|
+
});
|
|
5478
|
+
});
|
|
5479
|
+
addBackendFlags(llmCmd);
|
|
5480
|
+
llmCmd.addCommand(usageCmd);
|
|
5481
|
+
llmCmd.addCommand(modelsCmd);
|
|
5482
|
+
llmCmd.addCommand(modelStatsCmd);
|
|
5483
|
+
llmCmd.addCommand(expensiveCmd);
|
|
5484
|
+
llmCmd.addCommand(slowCmd);
|
|
5485
|
+
llmCmd.addCommand(toolsCmd);
|
|
5486
|
+
program.addCommand(llmCmd);
|
|
5487
|
+
}
|
|
5488
|
+
|
|
5489
|
+
// src/commands/investigate/semconv.ts
|
|
5490
|
+
import {
|
|
5491
|
+
clearSemanticConventionCache,
|
|
5492
|
+
getSemanticConventionNamespace,
|
|
5493
|
+
listSemanticConventionNamespaces
|
|
5494
|
+
} from "autotel-mcp";
|
|
5495
|
+
import { Command as Command8 } from "commander";
|
|
5496
|
+
async function runSemconvList(flags) {
|
|
5497
|
+
await runStatic("semconv list", flags, async () => ({
|
|
5498
|
+
namespaces: await listSemanticConventionNamespaces()
|
|
5499
|
+
}));
|
|
5500
|
+
}
|
|
5501
|
+
async function runSemconvGet(flags) {
|
|
5502
|
+
await runStatic(
|
|
5503
|
+
"semconv get",
|
|
5504
|
+
flags,
|
|
5505
|
+
async () => getSemanticConventionNamespace(flags.namespace)
|
|
5506
|
+
);
|
|
5507
|
+
}
|
|
5508
|
+
async function runSemconvRefresh(flags) {
|
|
5509
|
+
await runStatic("semconv refresh", flags, async () => {
|
|
5510
|
+
clearSemanticConventionCache();
|
|
5511
|
+
return { cleared: true };
|
|
5512
|
+
});
|
|
5513
|
+
}
|
|
5514
|
+
function registerSemconvCommands(program) {
|
|
5515
|
+
const semconvCmd = new Command8("semconv").description(
|
|
5516
|
+
"OpenTelemetry semantic conventions lookup (JSON)"
|
|
5517
|
+
);
|
|
5518
|
+
const listCmd = addStaticFlags(new Command8("list")).description("List semconv namespaces").action(async function() {
|
|
5519
|
+
await runSemconvList(staticFlagsFromOpts(this.optsWithGlobals()));
|
|
5520
|
+
});
|
|
5521
|
+
const getCmd = addStaticFlags(new Command8("get")).description("Get groups for one namespace").argument("<namespace>", "Namespace (e.g. http, rpc, database)").action(async function(namespace) {
|
|
5522
|
+
await runSemconvGet({
|
|
5523
|
+
...staticFlagsFromOpts(this.optsWithGlobals()),
|
|
5524
|
+
namespace
|
|
5525
|
+
});
|
|
5526
|
+
});
|
|
5527
|
+
const refreshCmd = addStaticFlags(new Command8("refresh")).description("Clear semconv cache").action(async function() {
|
|
5528
|
+
await runSemconvRefresh(staticFlagsFromOpts(this.optsWithGlobals()));
|
|
5529
|
+
});
|
|
5530
|
+
semconvCmd.addCommand(listCmd);
|
|
5531
|
+
semconvCmd.addCommand(getCmd);
|
|
5532
|
+
semconvCmd.addCommand(refreshCmd);
|
|
5533
|
+
program.addCommand(semconvCmd);
|
|
5534
|
+
}
|
|
5535
|
+
|
|
5536
|
+
// src/commands/investigate/instrumentation.ts
|
|
5537
|
+
import {
|
|
5538
|
+
scoreSpan,
|
|
5539
|
+
suggestInstrumentationFixes,
|
|
5540
|
+
buildInstrumentationGuide
|
|
5541
|
+
} from "autotel-mcp";
|
|
5542
|
+
import { Command as Command9 } from "commander";
|
|
5543
|
+
import * as fs7 from "fs";
|
|
5544
|
+
function readSpanFromStdinOrFile(spanFile) {
|
|
5545
|
+
let raw;
|
|
5546
|
+
if (spanFile) {
|
|
5547
|
+
raw = fs7.readFileSync(spanFile, "utf8");
|
|
5548
|
+
} else {
|
|
5549
|
+
raw = fs7.readFileSync(0, "utf8");
|
|
5550
|
+
}
|
|
5551
|
+
const parsed = JSON.parse(raw);
|
|
5552
|
+
if (typeof parsed.operationName !== "string" || typeof parsed.serviceName !== "string" || typeof parsed.tags !== "object" || typeof parsed.hasError !== "boolean") {
|
|
5553
|
+
throw new AutotelError({
|
|
5554
|
+
type: "validation",
|
|
5555
|
+
code: "AUTOTEL_E_INVALID_INPUT",
|
|
5556
|
+
message: "score expects JSON with operationName, serviceName, tags, hasError",
|
|
5557
|
+
retryable: false,
|
|
5558
|
+
expected: {
|
|
5559
|
+
shape: {
|
|
5560
|
+
operationName: "string",
|
|
5561
|
+
serviceName: "string",
|
|
5562
|
+
tags: "Record<string, string | number | boolean>",
|
|
5563
|
+
hasError: "boolean"
|
|
5564
|
+
}
|
|
5565
|
+
}
|
|
5566
|
+
});
|
|
5567
|
+
}
|
|
5568
|
+
return parsed;
|
|
5569
|
+
}
|
|
5570
|
+
async function runScoreSpan(flags) {
|
|
5571
|
+
await runStatic("score", flags, async () => {
|
|
5572
|
+
const span = readSpanFromStdinOrFile(flags.spanFile);
|
|
5573
|
+
const result = scoreSpan(span);
|
|
5574
|
+
const suggestions = suggestInstrumentationFixes(span);
|
|
5575
|
+
return { ...result, suggestions };
|
|
5576
|
+
});
|
|
5577
|
+
}
|
|
5578
|
+
async function runScoreExplain(flags) {
|
|
5579
|
+
await runStatic("score explain", flags, async () => ({
|
|
5580
|
+
guide: buildInstrumentationGuide()
|
|
5581
|
+
}));
|
|
5582
|
+
}
|
|
5583
|
+
function registerScoreCommands(program) {
|
|
5584
|
+
const scoreCmd = addStaticFlags(new Command9("score")).description("Score a span for instrumentation quality (JSON)").option("--span-file <path>", "Read span JSON from file (default: stdin)").action(async function() {
|
|
5585
|
+
const o = this.optsWithGlobals();
|
|
5586
|
+
await runScoreSpan({
|
|
5587
|
+
...staticFlagsFromOpts(o),
|
|
5588
|
+
spanFile: o.spanFile
|
|
5589
|
+
});
|
|
5590
|
+
});
|
|
5591
|
+
const explainCmd = addStaticFlags(new Command9("explain")).description("Explain the instrumentation scoring rubric").action(async function() {
|
|
5592
|
+
await runScoreExplain(staticFlagsFromOpts(this.optsWithGlobals()));
|
|
5593
|
+
});
|
|
5594
|
+
scoreCmd.addCommand(explainCmd);
|
|
5595
|
+
program.addCommand(scoreCmd);
|
|
5596
|
+
}
|
|
5597
|
+
|
|
5598
|
+
// src/commands/investigate/collector.ts
|
|
5599
|
+
import {
|
|
5600
|
+
validateOtlpReceiverConfig,
|
|
5601
|
+
suggestCollectorConfig,
|
|
5602
|
+
buildCollectorGuide,
|
|
5603
|
+
listCollectorVersions,
|
|
5604
|
+
listCollectorComponents,
|
|
5605
|
+
getCollectorComponentSchema,
|
|
5606
|
+
getCollectorComponentReadme,
|
|
5607
|
+
validateCollectorComponentConfig,
|
|
5608
|
+
refreshCollectorCatalog,
|
|
5609
|
+
resolveCollectorVersion
|
|
5610
|
+
} from "autotel-mcp";
|
|
5611
|
+
import { Command as Command10 } from "commander";
|
|
5612
|
+
import * as fs8 from "fs";
|
|
5613
|
+
function readJsonFromStdinOrFile(file) {
|
|
5614
|
+
const raw = file ? fs8.readFileSync(file, "utf8") : fs8.readFileSync(0, "utf8");
|
|
5615
|
+
return JSON.parse(raw);
|
|
5616
|
+
}
|
|
5617
|
+
function assertVersion(version) {
|
|
5618
|
+
if (version === void 0) return;
|
|
5619
|
+
if (!/^\d+\.\d+\.\d+$/.test(version)) {
|
|
5620
|
+
throw new AutotelError({
|
|
5621
|
+
type: "validation",
|
|
5622
|
+
code: "AUTOTEL_E_INVALID_INPUT",
|
|
5623
|
+
message: `--version must be semver (got "${version}")`,
|
|
5624
|
+
retryable: false
|
|
5625
|
+
});
|
|
5626
|
+
}
|
|
5627
|
+
}
|
|
5628
|
+
async function runCollectorValidate(flags) {
|
|
5629
|
+
await runStatic("collector validate", flags, async () => {
|
|
5630
|
+
const config = readJsonFromStdinOrFile(flags.configFile);
|
|
5631
|
+
return validateOtlpReceiverConfig(config);
|
|
5632
|
+
});
|
|
5633
|
+
}
|
|
5634
|
+
async function runCollectorSuggest(flags) {
|
|
5635
|
+
await runStatic("collector suggest", flags, async () => ({
|
|
5636
|
+
suggestion: suggestCollectorConfig()
|
|
5637
|
+
}));
|
|
5638
|
+
}
|
|
5639
|
+
async function runCollectorExplain(flags) {
|
|
5640
|
+
await runStatic("collector explain", flags, async () => ({
|
|
5641
|
+
guide: buildCollectorGuide()
|
|
5642
|
+
}));
|
|
5643
|
+
}
|
|
5644
|
+
async function runCollectorVersions(flags) {
|
|
5645
|
+
await runStatic("collector versions", flags, async () => ({
|
|
5646
|
+
versions: await listCollectorVersions()
|
|
5647
|
+
}));
|
|
5648
|
+
}
|
|
5649
|
+
async function runCollectorComponents(flags) {
|
|
5650
|
+
await runStatic("collector components", flags, async () => {
|
|
5651
|
+
assertVersion(flags.version);
|
|
5652
|
+
const resolvedVersion = await resolveCollectorVersion(flags.version);
|
|
5653
|
+
const components = await listCollectorComponents(resolvedVersion);
|
|
5654
|
+
if (flags.kind) {
|
|
5655
|
+
return {
|
|
5656
|
+
version: resolvedVersion,
|
|
5657
|
+
kind: flags.kind,
|
|
5658
|
+
components: components[flags.kind]
|
|
5659
|
+
};
|
|
5660
|
+
}
|
|
5661
|
+
return { version: resolvedVersion, components };
|
|
5662
|
+
});
|
|
5663
|
+
}
|
|
5664
|
+
async function runCollectorSchema(flags) {
|
|
5665
|
+
await runStatic("collector schema", flags, async () => {
|
|
5666
|
+
assertVersion(flags.version);
|
|
5667
|
+
const resolvedVersion = await resolveCollectorVersion(flags.version);
|
|
5668
|
+
const schema = await getCollectorComponentSchema(
|
|
5669
|
+
flags.kind,
|
|
5670
|
+
flags.name,
|
|
5671
|
+
resolvedVersion
|
|
5672
|
+
);
|
|
5673
|
+
return { version: resolvedVersion, kind: flags.kind, name: flags.name, schema };
|
|
5674
|
+
});
|
|
5675
|
+
}
|
|
5676
|
+
async function runCollectorReadme(flags) {
|
|
5677
|
+
await runStatic("collector readme", flags, async () => {
|
|
5678
|
+
assertVersion(flags.version);
|
|
5679
|
+
const resolvedVersion = await resolveCollectorVersion(flags.version);
|
|
5680
|
+
const readme = await getCollectorComponentReadme(
|
|
5681
|
+
flags.kind,
|
|
5682
|
+
flags.name,
|
|
5683
|
+
resolvedVersion
|
|
5684
|
+
);
|
|
5685
|
+
return { version: resolvedVersion, kind: flags.kind, name: flags.name, readme };
|
|
5686
|
+
});
|
|
5687
|
+
}
|
|
5688
|
+
async function runCollectorValidateComponent(flags) {
|
|
5689
|
+
await runStatic("collector validate-component", flags, async () => {
|
|
5690
|
+
assertVersion(flags.version);
|
|
5691
|
+
const resolvedVersion = await resolveCollectorVersion(flags.version);
|
|
5692
|
+
const config = readJsonFromStdinOrFile(flags.configFile);
|
|
5693
|
+
const result = await validateCollectorComponentConfig({
|
|
5694
|
+
kind: flags.kind,
|
|
5695
|
+
name: flags.name,
|
|
5696
|
+
version: resolvedVersion,
|
|
5697
|
+
config
|
|
5698
|
+
});
|
|
5699
|
+
return { version: resolvedVersion, kind: flags.kind, name: flags.name, ...result };
|
|
5700
|
+
});
|
|
5701
|
+
}
|
|
5702
|
+
async function runCollectorRefresh(flags) {
|
|
5703
|
+
await runStatic(
|
|
5704
|
+
"collector refresh",
|
|
5705
|
+
flags,
|
|
5706
|
+
async () => refreshCollectorCatalog()
|
|
5707
|
+
);
|
|
5708
|
+
}
|
|
5709
|
+
function registerCollectorCommands(program) {
|
|
5710
|
+
const collectorCmd = new Command10("collector").description(
|
|
5711
|
+
"OpenTelemetry Collector config + schema commands (JSON)"
|
|
5712
|
+
);
|
|
5713
|
+
const validateCmd = addStaticFlags(new Command10("validate")).description("Validate an OTLP receiver config fragment").option("--config-file <path>", "Read JSON config (default: stdin)").action(async function() {
|
|
5714
|
+
const o = this.optsWithGlobals();
|
|
5715
|
+
await runCollectorValidate({
|
|
5716
|
+
...staticFlagsFromOpts(o),
|
|
5717
|
+
configFile: o.configFile
|
|
5718
|
+
});
|
|
5719
|
+
});
|
|
5720
|
+
const suggestCmd = addStaticFlags(new Command10("suggest")).description("Print a minimal OTLP receiver config").action(async function() {
|
|
5721
|
+
await runCollectorSuggest(staticFlagsFromOpts(this.optsWithGlobals()));
|
|
5722
|
+
});
|
|
5723
|
+
const explainCmd = addStaticFlags(new Command10("explain")).description("Explain OTLP receiver config shape + defaults").action(async function() {
|
|
5724
|
+
await runCollectorExplain(staticFlagsFromOpts(this.optsWithGlobals()));
|
|
5725
|
+
});
|
|
5726
|
+
const versionsCmd = addStaticFlags(new Command10("versions")).description("List supported collector schema versions").action(async function() {
|
|
5727
|
+
await runCollectorVersions(staticFlagsFromOpts(this.optsWithGlobals()));
|
|
5728
|
+
});
|
|
5729
|
+
const componentsCmd = addStaticFlags(new Command10("components")).description("List components for a version (optionally filter by kind)").option("--version <semver>", "Collector version (e.g. 0.110.0)").option(
|
|
5730
|
+
"--kind <kind>",
|
|
5731
|
+
"receiver | processor | exporter | connector | extension"
|
|
5732
|
+
).action(async function() {
|
|
5733
|
+
const o = this.optsWithGlobals();
|
|
5734
|
+
await runCollectorComponents({
|
|
5735
|
+
...staticFlagsFromOpts(o),
|
|
5736
|
+
version: o.version,
|
|
5737
|
+
kind: o.kind
|
|
5738
|
+
});
|
|
5739
|
+
});
|
|
5740
|
+
const schemaCmd = addStaticFlags(new Command10("schema")).description("Get JSON Schema for a collector component").requiredOption("--kind <kind>", "Component kind").requiredOption("--name <name>", "Component name").option("--version <semver>", "Collector version").action(async function() {
|
|
5741
|
+
const o = this.optsWithGlobals();
|
|
5742
|
+
await runCollectorSchema({
|
|
5743
|
+
...staticFlagsFromOpts(o),
|
|
5744
|
+
kind: o.kind,
|
|
5745
|
+
name: o.name,
|
|
5746
|
+
version: o.version
|
|
5747
|
+
});
|
|
5748
|
+
});
|
|
5749
|
+
const readmeCmd = addStaticFlags(new Command10("readme")).description("Get README for a collector component").requiredOption("--kind <kind>", "Component kind").requiredOption("--name <name>", "Component name").option("--version <semver>", "Collector version").action(async function() {
|
|
5750
|
+
const o = this.optsWithGlobals();
|
|
5751
|
+
await runCollectorReadme({
|
|
5752
|
+
...staticFlagsFromOpts(o),
|
|
5753
|
+
kind: o.kind,
|
|
5754
|
+
name: o.name,
|
|
5755
|
+
version: o.version
|
|
5756
|
+
});
|
|
5757
|
+
});
|
|
5758
|
+
const validateComponentCmd = addStaticFlags(
|
|
5759
|
+
new Command10("validate-component")
|
|
5760
|
+
).description("Validate component config against upstream schema").requiredOption("--kind <kind>", "Component kind").requiredOption("--name <name>", "Component name").option("--version <semver>", "Collector version").option("--config-file <path>", "Read JSON config (default: stdin)").action(async function() {
|
|
5761
|
+
const o = this.optsWithGlobals();
|
|
5762
|
+
await runCollectorValidateComponent({
|
|
5763
|
+
...staticFlagsFromOpts(o),
|
|
5764
|
+
kind: o.kind,
|
|
5765
|
+
name: o.name,
|
|
5766
|
+
version: o.version,
|
|
5767
|
+
configFile: o.configFile
|
|
5768
|
+
});
|
|
5769
|
+
});
|
|
5770
|
+
const refreshCmd = addStaticFlags(new Command10("refresh")).description("Refresh in-memory collector metadata cache").action(async function() {
|
|
5771
|
+
await runCollectorRefresh(staticFlagsFromOpts(this.optsWithGlobals()));
|
|
5772
|
+
});
|
|
5773
|
+
collectorCmd.addCommand(validateCmd);
|
|
5774
|
+
collectorCmd.addCommand(suggestCmd);
|
|
5775
|
+
collectorCmd.addCommand(explainCmd);
|
|
5776
|
+
collectorCmd.addCommand(versionsCmd);
|
|
5777
|
+
collectorCmd.addCommand(componentsCmd);
|
|
5778
|
+
collectorCmd.addCommand(schemaCmd);
|
|
5779
|
+
collectorCmd.addCommand(readmeCmd);
|
|
5780
|
+
collectorCmd.addCommand(validateComponentCmd);
|
|
5781
|
+
collectorCmd.addCommand(refreshCmd);
|
|
5782
|
+
program.addCommand(collectorCmd);
|
|
5783
|
+
}
|
|
5784
|
+
|
|
4464
5785
|
// src/cli.ts
|
|
4465
5786
|
function createProgram() {
|
|
4466
|
-
const program = new
|
|
5787
|
+
const program = new Command11();
|
|
4467
5788
|
program.name("autotel").description("CLI for autotel - setup wizard, diagnostics, and incremental features").version("0.1.0");
|
|
4468
5789
|
const addGlobalOptions = (cmd) => {
|
|
4469
5790
|
return cmd.option("--cwd <path>", "Target directory", process.cwd()).option("--verbose", "Show detailed output").option("--quiet", "Only show warnings and errors");
|
|
4470
5791
|
};
|
|
4471
|
-
const initCmd = new
|
|
5792
|
+
const initCmd = new Command11("init").description("Initialize autotel in your project").option("--dry-run", "Skip installation and print what would be done").option("--no-install", "Generate files only, skip package installation").option("--print-install-cmd", "Output the install command without running it").option("-y, --yes", "Accept defaults, non-interactive").option("--preset <name>", "Use a quick preset (e.g., node-datadog-pino)").option("--force", "Overwrite existing config (creates backup first)").option("--workspace-root", "Install at workspace root instead of package root").option("--no-detect", "Skip auto-detection of installed deps").option("--detect-only", "Run detection, print the plan, write nothing").option("--plan <path>", "Apply a pre-built InitPlan JSON file").option("--input <path>", "Read InitPlan JSON from stdin (-) or a file").option("--scan-env", "Consent to reading .env / .env.local for backend detection").option("--json", "Emit machine-readable JSON").option("--output-file <path>", "Persist JSON output to this file").option("--no-secrets-in-output", "Redact secret-shaped values").option("--no-interactive", "Never prompt; fail if input would be required").action(async (opts) => {
|
|
4472
5793
|
const options = {
|
|
4473
5794
|
cwd: opts.cwd ?? process.cwd(),
|
|
4474
5795
|
dryRun: opts.dryRun ?? false,
|
|
@@ -4498,7 +5819,7 @@ function createProgram() {
|
|
|
4498
5819
|
});
|
|
4499
5820
|
addGlobalOptions(initCmd);
|
|
4500
5821
|
program.addCommand(initCmd);
|
|
4501
|
-
const doctorCmd = new
|
|
5822
|
+
const doctorCmd = new Command11("doctor").description("Run diagnostics on your autotel setup").option("--json", "Output machine-readable JSON").option("--fix", "Auto-fix resolvable issues").option("--list-checks", "List all available checks").option("--env-file <path>", "Specify env file to check").action(async (opts) => {
|
|
4502
5823
|
const options = {
|
|
4503
5824
|
cwd: opts.cwd ?? process.cwd(),
|
|
4504
5825
|
dryRun: false,
|
|
@@ -4516,7 +5837,7 @@ function createProgram() {
|
|
|
4516
5837
|
});
|
|
4517
5838
|
addGlobalOptions(doctorCmd);
|
|
4518
5839
|
program.addCommand(doctorCmd);
|
|
4519
|
-
const addCmd = new
|
|
5840
|
+
const addCmd = new Command11("add").description("Add a backend, subscriber, plugin, or platform").argument("[type]", "Preset type (backend, subscriber, plugin, platform)").argument("[name]", "Preset name (e.g., datadog, posthog, mongoose)").option("--list", "List available presets for the given type").option("--dry-run", "Skip installation and print what would be done").option("--no-install", "Generate files only, skip package installation").option("--print-install-cmd", "Output the install command without running it").option("-y, --yes", "Accept defaults, non-interactive").option("--force", "Overwrite non-CLI-owned config (creates backup first)").option("--json", "Output machine-readable JSON (for --list)").option("--workspace-root", "Install at workspace root instead of package root").action(async (type, name, opts) => {
|
|
4520
5841
|
const options = {
|
|
4521
5842
|
cwd: opts.cwd ?? process.cwd(),
|
|
4522
5843
|
dryRun: opts.dryRun ?? false,
|
|
@@ -4538,8 +5859,8 @@ function createProgram() {
|
|
|
4538
5859
|
});
|
|
4539
5860
|
addGlobalOptions(addCmd);
|
|
4540
5861
|
program.addCommand(addCmd);
|
|
4541
|
-
const codemodCmd = new
|
|
4542
|
-
const traceCmd = new
|
|
5862
|
+
const codemodCmd = new Command11("codemod").description("Codemod commands for adopting autotel");
|
|
5863
|
+
const traceCmd = new Command11("trace").description("Wrap functions in trace() with span name from function/variable/method name").argument("<path>", "File path or glob (e.g. src/index.ts, src/**/*.ts)").option("--dry-run", "Print changes without writing files").option("--name-pattern <pattern>", "Span name template: {name}, {file}, {path}").option("--skip <regex>...", "Skip functions whose name matches (repeatable)").option("--print-files", "Print per-file summary (wrapped count, skipped)").action(async (pathArg, opts) => {
|
|
4543
5864
|
const options = {
|
|
4544
5865
|
cwd: opts.cwd ?? process.cwd(),
|
|
4545
5866
|
dryRun: opts.dryRun ?? false,
|
|
@@ -4559,45 +5880,93 @@ function createProgram() {
|
|
|
4559
5880
|
codemodCmd.addCommand(traceCmd);
|
|
4560
5881
|
addGlobalOptions(codemodCmd);
|
|
4561
5882
|
program.addCommand(codemodCmd);
|
|
4562
|
-
const schemaCmd = new
|
|
5883
|
+
const schemaCmd = new Command11("schema").description("Print the CLI manifest as JSON (agent discovery)").option("--output-file <path>", "Persist JSON to a file").option("--no-secrets-in-output", "Redact secret-shaped values").action((opts) => {
|
|
4563
5884
|
runSchema({ outputFile: opts.outputFile, noSecrets: opts.secretsInOutput === false });
|
|
4564
5885
|
});
|
|
4565
|
-
const schemaErrorsCmd = new
|
|
5886
|
+
const schemaErrorsCmd = new Command11("errors").description("Print error envelope shape + AUTOTEL_E_* codes").option("--output-file <path>", "Persist JSON to a file").action((opts) => {
|
|
4566
5887
|
runSchemaErrors({ outputFile: opts.outputFile });
|
|
4567
5888
|
});
|
|
4568
|
-
const schemaOutputsCmd = new
|
|
5889
|
+
const schemaOutputsCmd = new Command11("outputs").description("Print JSON output shapes per command").option("--output-file <path>", "Persist JSON to a file").action((opts) => {
|
|
4569
5890
|
runSchemaOutputs({ outputFile: opts.outputFile });
|
|
4570
5891
|
});
|
|
4571
5892
|
schemaCmd.addCommand(schemaErrorsCmd);
|
|
4572
5893
|
schemaCmd.addCommand(schemaOutputsCmd);
|
|
4573
5894
|
program.addCommand(schemaCmd);
|
|
4574
|
-
const commandsCmd = new
|
|
5895
|
+
const commandsCmd = new Command11("commands").description("Print compact tool-style listing of commands").option("--output-file <path>", "Persist JSON to a file").action((opts) => {
|
|
4575
5896
|
runCommandsListing({ outputFile: opts.outputFile });
|
|
4576
5897
|
});
|
|
4577
5898
|
program.addCommand(commandsCmd);
|
|
4578
|
-
const examplesCmd = new
|
|
5899
|
+
const examplesCmd = new Command11("examples").description("Print copy-pasteable examples for a command").argument("[command]", "Command name (omit for all)").option("--output-file <path>", "Persist JSON to a file").action((name, opts) => {
|
|
4579
5900
|
runExamples(name, { outputFile: opts.outputFile });
|
|
4580
5901
|
});
|
|
4581
5902
|
program.addCommand(examplesCmd);
|
|
4582
|
-
const versionCmd = new
|
|
5903
|
+
const versionCmd = new Command11("version").description("Print version info as JSON").option("--output-file <path>", "Persist JSON to a file").action((opts) => {
|
|
4583
5904
|
runVersion({ outputFile: opts.outputFile });
|
|
4584
5905
|
});
|
|
4585
5906
|
program.addCommand(versionCmd);
|
|
5907
|
+
registerHealthCommands(program);
|
|
5908
|
+
registerDiscoveryCommands(program);
|
|
5909
|
+
registerQueryCommands(program);
|
|
5910
|
+
registerTraceCommands(program);
|
|
5911
|
+
registerTopologyCommands(program);
|
|
5912
|
+
registerDiagnoseCommands(program);
|
|
5913
|
+
registerCorrelateCommands(program);
|
|
5914
|
+
registerLlmCommands(program);
|
|
5915
|
+
registerSemconvCommands(program);
|
|
5916
|
+
registerScoreCommands(program);
|
|
5917
|
+
registerCollectorCommands(program);
|
|
4586
5918
|
return program;
|
|
4587
5919
|
}
|
|
4588
5920
|
async function run() {
|
|
4589
5921
|
const program = createProgram();
|
|
5922
|
+
program.exitOverride();
|
|
5923
|
+
const argvJoined = process.argv.slice(2).join(" ");
|
|
5924
|
+
const isJsonOnly = process.argv.includes("--json") || /^(schema|commands|examples|version|health|capabilities|discover|query|trace|diagnose|topology|correlate|llm|semconv|score|collector)\b/.test(
|
|
5925
|
+
argvJoined
|
|
5926
|
+
);
|
|
5927
|
+
if (isJsonOnly) {
|
|
5928
|
+
program.configureOutput({ writeErr: () => {
|
|
5929
|
+
} });
|
|
5930
|
+
}
|
|
5931
|
+
const stack = [...program.commands];
|
|
5932
|
+
while (stack.length > 0) {
|
|
5933
|
+
const cmd = stack.pop();
|
|
5934
|
+
cmd.exitOverride();
|
|
5935
|
+
if (isJsonOnly) {
|
|
5936
|
+
cmd.configureOutput({ writeErr: () => {
|
|
5937
|
+
} });
|
|
5938
|
+
}
|
|
5939
|
+
stack.push(...cmd.commands);
|
|
5940
|
+
}
|
|
4590
5941
|
await program.parseAsync(process.argv);
|
|
4591
5942
|
}
|
|
4592
5943
|
|
|
5944
|
+
// src/lib/commander-error.ts
|
|
5945
|
+
function commanderErrorToAutotel(error2) {
|
|
5946
|
+
if (error2 === null || typeof error2 !== "object" || !("code" in error2) || typeof error2.code !== "string" || !error2.code.startsWith("commander.")) {
|
|
5947
|
+
return null;
|
|
5948
|
+
}
|
|
5949
|
+
const ce = error2;
|
|
5950
|
+
if (ce.code === "commander.help" || ce.code === "commander.helpDisplayed" || ce.code === "commander.version") {
|
|
5951
|
+
process.exit(ce.exitCode ?? 0);
|
|
5952
|
+
}
|
|
5953
|
+
return new AutotelError({
|
|
5954
|
+
type: "validation",
|
|
5955
|
+
code: "AUTOTEL_E_INVALID_FLAG",
|
|
5956
|
+
message: ce.message,
|
|
5957
|
+
retryable: false,
|
|
5958
|
+
expected: { commanderCode: ce.code }
|
|
5959
|
+
});
|
|
5960
|
+
}
|
|
5961
|
+
|
|
4593
5962
|
// src/index.ts
|
|
4594
5963
|
function jsonModeRequested() {
|
|
4595
5964
|
return process.argv.includes("--json");
|
|
4596
5965
|
}
|
|
4597
5966
|
run().catch((error2) => {
|
|
4598
|
-
const err = toAutotelError(error2);
|
|
4599
|
-
const isJson = jsonModeRequested() || // schema/commands/examples/version are JSON-only
|
|
4600
|
-
/^(schema|commands|examples|version)\b/.test(
|
|
5967
|
+
const err = commanderErrorToAutotel(error2) ?? toAutotelError(error2);
|
|
5968
|
+
const isJson = jsonModeRequested() || // schema/commands/examples/version + investigate commands are JSON-only
|
|
5969
|
+
/^(schema|commands|examples|version|health|capabilities|discover|query|trace|diagnose|topology|correlate|llm|semconv|score|collector)\b/.test(
|
|
4601
5970
|
process.argv.slice(2).join(" ")
|
|
4602
5971
|
);
|
|
4603
5972
|
if (isJson) {
|