autotel-cli 0.15.3 → 0.16.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/dist/index.js +438 -199
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ import * as semver from "semver";
|
|
|
14
14
|
import { Project, SyntaxKind } from "ts-morph";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
import { JsonTraceSerializer, ProtobufTraceSerializer } from "@opentelemetry/otlp-transformer";
|
|
17
|
-
import { buildCollectorGuide, buildInstrumentationGuide, clearSemanticConventionCache, collectUsage, detectAnomalies, discoverLogFields, discoverServices, discoverTraceFields, findRootCause, getCollectorComponentReadme, getCollectorComponentSchema, getModelStats, getSemanticConventionNamespace, listCollectorComponents, listCollectorVersions, listModels, listSemanticConventionNamespaces, listToolUsage, pickErrorMessage, rankExpensiveTraces, rankSlowTraces, refreshCollectorCatalog, resolveCollectorVersion, scoreSpan, suggestCollectorConfig, suggestInstrumentationFixes, toLogSearchQuery, toMetricSearchQuery, toSpanSearchQuery, toTraceSearchQuery, validateCollectorComponentConfig, validateOtlpReceiverConfig } from "autotel-mcp";
|
|
17
|
+
import { EstimateInputError, buildCollectorGuide, buildInstrumentationGuide, clearSemanticConventionCache, collectUsage, detectAnomalies, discoverLogFields, discoverServices, discoverTraceFields, estimateCost, findRootCause, getCollectorComponentReadme, getCollectorComponentSchema, getModelStats, getSemanticConventionNamespace, listCollectorComponents, listCollectorVersions, listModels, listSemanticConventionNamespaces, listToolUsage, pickErrorMessage, rankExpensiveTraces, rankSlowTraces, refreshCollectorCatalog, resolveCollectorVersion, scoreSpan, suggestCollectorConfig, suggestInstrumentationFixes, toLogSearchQuery, toMetricSearchQuery, toSpanSearchQuery, toTraceSearchQuery, validateCollectorComponentConfig, validateOtlpReceiverConfig } from "autotel-mcp";
|
|
18
18
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
19
19
|
import { appendFile, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
20
20
|
import { homedir } from "node:os";
|
|
@@ -143,16 +143,30 @@ function findAllUpward(startDir, filenames) {
|
|
|
143
143
|
return found;
|
|
144
144
|
}
|
|
145
145
|
//#endregion
|
|
146
|
+
//#region src/lib/values.ts
|
|
147
|
+
/** The value as an object, or undefined when it is anything else. */
|
|
148
|
+
function asRecord(value) {
|
|
149
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
150
|
+
}
|
|
151
|
+
/** The string the value carries, or undefined when it carries anything else. */
|
|
152
|
+
function asString(value) {
|
|
153
|
+
return typeof value === "string" ? value : void 0;
|
|
154
|
+
}
|
|
155
|
+
/** One field of a value, when the value is an object at all. */
|
|
156
|
+
function readProperty(source, key) {
|
|
157
|
+
return asRecord(source)?.[key];
|
|
158
|
+
}
|
|
159
|
+
//#endregion
|
|
146
160
|
//#region src/lib/package-manager.ts
|
|
147
161
|
/**
|
|
148
162
|
* Lockfile to package manager mapping
|
|
149
163
|
*/
|
|
150
|
-
const LOCKFILE_MAP =
|
|
151
|
-
"pnpm-lock.yaml"
|
|
152
|
-
"bun.lockb"
|
|
153
|
-
"yarn.lock"
|
|
154
|
-
"package-lock.json"
|
|
155
|
-
|
|
164
|
+
const LOCKFILE_MAP = /* @__PURE__ */ new Map([
|
|
165
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
166
|
+
["bun.lockb", "bun"],
|
|
167
|
+
["yarn.lock", "yarn"],
|
|
168
|
+
["package-lock.json", "npm"]
|
|
169
|
+
]);
|
|
156
170
|
/**
|
|
157
171
|
* All lockfile names in priority order
|
|
158
172
|
*/
|
|
@@ -171,10 +185,6 @@ const PM_PRIORITY = [
|
|
|
171
185
|
"yarn",
|
|
172
186
|
"npm"
|
|
173
187
|
];
|
|
174
|
-
/**
|
|
175
|
-
* Detect package manager from nearest lockfile
|
|
176
|
-
* Algorithm: Find closest lockfile to cwd, working upward
|
|
177
|
-
*/
|
|
178
188
|
function detectPackageManager(startDir) {
|
|
179
189
|
const foundLockfiles = findAllUpward(startDir, LOCKFILES);
|
|
180
190
|
if (foundLockfiles.size === 0) return {
|
|
@@ -186,7 +196,7 @@ function detectPackageManager(startDir) {
|
|
|
186
196
|
let closestPM = "npm";
|
|
187
197
|
for (const [lockfileName, lockfilePath] of foundLockfiles) {
|
|
188
198
|
const depth = lockfilePath.split(path$1.sep).length;
|
|
189
|
-
const pm = LOCKFILE_MAP
|
|
199
|
+
const pm = LOCKFILE_MAP.get(lockfileName);
|
|
190
200
|
if (pm === void 0) continue;
|
|
191
201
|
if (depth > closestDepth || depth === closestDepth && PM_PRIORITY.indexOf(pm) < PM_PRIORITY.indexOf(closestPM)) {
|
|
192
202
|
closestDepth = depth;
|
|
@@ -212,9 +222,6 @@ function getInstallCommand(pm, packages, options = {}) {
|
|
|
212
222
|
default: return `npm install${dev ? " --save-dev" : ""} ${pkgList}`;
|
|
213
223
|
}
|
|
214
224
|
}
|
|
215
|
-
/**
|
|
216
|
-
* Detect workspace root markers
|
|
217
|
-
*/
|
|
218
225
|
function detectWorkspaceRoot(startDir) {
|
|
219
226
|
let currentDir = path$1.resolve(startDir);
|
|
220
227
|
const root = path$1.parse(currentDir).root;
|
|
@@ -229,7 +236,7 @@ function detectWorkspaceRoot(startDir) {
|
|
|
229
236
|
};
|
|
230
237
|
const pkgJsonContent = readFileSafe(path$1.join(currentDir, "package.json"));
|
|
231
238
|
if (pkgJsonContent) try {
|
|
232
|
-
if (JSON.parse(pkgJsonContent)
|
|
239
|
+
if (readProperty(JSON.parse(pkgJsonContent), "workspaces")) {
|
|
233
240
|
const hasYarnLock = fileExists(path$1.join(currentDir, "yarn.lock"));
|
|
234
241
|
return {
|
|
235
242
|
workspaceRoot: currentDir,
|
|
@@ -739,7 +746,7 @@ async function checkEnvVarsPresent(packageRoot, varNames, specificFile) {
|
|
|
739
746
|
*
|
|
740
747
|
* Loggers handled separately (see `detectLoggers`).
|
|
741
748
|
*/
|
|
742
|
-
const DEP_TO_PRESET = {
|
|
749
|
+
const DEP_TO_PRESET = new Map(Object.entries({
|
|
743
750
|
"@sentry/node": "sentry",
|
|
744
751
|
"@sentry/bun": "sentry",
|
|
745
752
|
hono: "hono",
|
|
@@ -753,6 +760,7 @@ const DEP_TO_PRESET = {
|
|
|
753
760
|
nuxt: "nuxt",
|
|
754
761
|
"posthog-node": "posthog",
|
|
755
762
|
"@posthog/node": "posthog",
|
|
763
|
+
"posthog-js": "posthog-web",
|
|
756
764
|
mixpanel: "mixpanel",
|
|
757
765
|
"@amplitude/analytics-node": "amplitude",
|
|
758
766
|
"@segment/analytics-node": "segment",
|
|
@@ -770,12 +778,12 @@ const DEP_TO_PRESET = {
|
|
|
770
778
|
ioredis: "auto-instr",
|
|
771
779
|
"@aws-sdk/client-s3": "auto-instr",
|
|
772
780
|
graphql: "auto-instr"
|
|
773
|
-
};
|
|
774
|
-
const LOGGER_DEPS = {
|
|
781
|
+
}));
|
|
782
|
+
const LOGGER_DEPS = new Map(Object.entries({
|
|
775
783
|
pino: "pino",
|
|
776
784
|
winston: "winston",
|
|
777
785
|
bunyan: "bunyan"
|
|
778
|
-
};
|
|
786
|
+
}));
|
|
779
787
|
/** Backends inferable from env-var keys (values not used — only key presence). */
|
|
780
788
|
const ENV_KEY_TO_BACKEND = [
|
|
781
789
|
{
|
|
@@ -828,7 +836,7 @@ function mergeDeps(target, root) {
|
|
|
828
836
|
*/
|
|
829
837
|
function detectLoggers(deps) {
|
|
830
838
|
const present = [];
|
|
831
|
-
for (const [name, kind] of
|
|
839
|
+
for (const [name, kind] of LOGGER_DEPS) if (deps.has(name)) present.push(kind);
|
|
832
840
|
if (present.length === 0) return {
|
|
833
841
|
primary: null,
|
|
834
842
|
autoInstrument: []
|
|
@@ -916,8 +924,8 @@ function detectInProject(opts) {
|
|
|
916
924
|
const presetSet = /* @__PURE__ */ new Set();
|
|
917
925
|
const autoInstrDeps = [];
|
|
918
926
|
for (const [name, info] of deps.entries()) {
|
|
919
|
-
const mapping = DEP_TO_PRESET
|
|
920
|
-
if (mapping === void 0 && LOGGER_DEPS
|
|
927
|
+
const mapping = DEP_TO_PRESET.get(name);
|
|
928
|
+
if (mapping === void 0 && !LOGGER_DEPS.has(name)) continue;
|
|
921
929
|
detectedPackages.push({
|
|
922
930
|
name,
|
|
923
931
|
version: info.version,
|
|
@@ -1312,7 +1320,11 @@ const local = {
|
|
|
1312
1320
|
//#endregion
|
|
1313
1321
|
//#region src/presets/subscribers/posthog.ts
|
|
1314
1322
|
/**
|
|
1315
|
-
* PostHog subscriber preset
|
|
1323
|
+
* PostHog subscriber preset — product events posted from the server.
|
|
1324
|
+
*
|
|
1325
|
+
* The browser counterpart is the `posthog-web` plugin preset, which joins
|
|
1326
|
+
* traces to the session PostHog is recording. Both live in `autotel-posthog`;
|
|
1327
|
+
* this one needs `autotel-subscribers` too, for the base class it extends.
|
|
1316
1328
|
*/
|
|
1317
1329
|
const posthog = {
|
|
1318
1330
|
name: "PostHog",
|
|
@@ -1320,7 +1332,11 @@ const posthog = {
|
|
|
1320
1332
|
type: "subscriber",
|
|
1321
1333
|
description: "Send events to PostHog for product analytics",
|
|
1322
1334
|
packages: {
|
|
1323
|
-
required: [
|
|
1335
|
+
required: [
|
|
1336
|
+
"autotel-posthog",
|
|
1337
|
+
"autotel-subscribers",
|
|
1338
|
+
"posthog-node"
|
|
1339
|
+
],
|
|
1324
1340
|
optional: [],
|
|
1325
1341
|
devOnly: []
|
|
1326
1342
|
},
|
|
@@ -1339,7 +1355,7 @@ const posthog = {
|
|
|
1339
1355
|
}]
|
|
1340
1356
|
},
|
|
1341
1357
|
imports: [{
|
|
1342
|
-
source: "autotel-
|
|
1358
|
+
source: "autotel-posthog/subscriber",
|
|
1343
1359
|
specifiers: ["PostHogSubscriber"]
|
|
1344
1360
|
}],
|
|
1345
1361
|
configBlock: {
|
|
@@ -1353,6 +1369,54 @@ const posthog = {
|
|
|
1353
1369
|
nextSteps: ["Set POSTHOG_API_KEY from PostHog Project Settings", "Events will be sent to PostHog as custom events"]
|
|
1354
1370
|
};
|
|
1355
1371
|
//#endregion
|
|
1372
|
+
//#region src/presets/plugins/posthog-web.ts
|
|
1373
|
+
/**
|
|
1374
|
+
* PostHog browser preset — joins the trace to the session PostHog is recording.
|
|
1375
|
+
*
|
|
1376
|
+
* Distinct from the `posthog` subscriber preset, which posts events from a
|
|
1377
|
+
* server. A front end running `posthog-js` already has a session id, a person,
|
|
1378
|
+
* and often a replay; what it lacks is any link between those and its traces.
|
|
1379
|
+
* This wires both directions: spans carry the session, person and (on failures)
|
|
1380
|
+
* a replay link, and PostHog events carry the trace id that explains them.
|
|
1381
|
+
*/
|
|
1382
|
+
const posthogWeb = {
|
|
1383
|
+
name: "PostHog (browser)",
|
|
1384
|
+
slug: "posthog-web",
|
|
1385
|
+
type: "plugin",
|
|
1386
|
+
description: "Join browser traces to PostHog sessions, replays and events (both directions)",
|
|
1387
|
+
packages: {
|
|
1388
|
+
required: [
|
|
1389
|
+
"autotel-web",
|
|
1390
|
+
"autotel-posthog",
|
|
1391
|
+
"posthog-js"
|
|
1392
|
+
],
|
|
1393
|
+
optional: [],
|
|
1394
|
+
devOnly: []
|
|
1395
|
+
},
|
|
1396
|
+
env: {
|
|
1397
|
+
required: [],
|
|
1398
|
+
optional: []
|
|
1399
|
+
},
|
|
1400
|
+
imports: [],
|
|
1401
|
+
configBlock: {
|
|
1402
|
+
type: "plugin",
|
|
1403
|
+
code: `// PostHog join: wire this in your browser entry, not here —
|
|
1404
|
+
// import posthog from 'posthog-js';
|
|
1405
|
+
// import { initFull } from 'autotel-web/full';
|
|
1406
|
+
// import { joinPostHog } from 'autotel-posthog';
|
|
1407
|
+
//
|
|
1408
|
+
// posthog.init('<key>');
|
|
1409
|
+
// initFull({ service: '<app>', endpoint, spanEnrichers: [joinPostHog(posthog)] });`,
|
|
1410
|
+
section: "PLUGIN_INIT"
|
|
1411
|
+
},
|
|
1412
|
+
nextSteps: [
|
|
1413
|
+
"Browser entry: initFull({ ..., spanEnrichers: [joinPostHog(posthog)] }) — one call wires both directions",
|
|
1414
|
+
"Spans now carry session.id and user.id from PostHog; failed spans carry session.replay.url",
|
|
1415
|
+
"For a clickable link back: joinPostHog(posthog, { traceUrl: ({ traceId }) => `...${traceId}` })",
|
|
1416
|
+
"Name any feature flags worth slicing by: joinPostHog(posthog, { featureFlags: [\"my-flag\"] })"
|
|
1417
|
+
]
|
|
1418
|
+
};
|
|
1419
|
+
//#endregion
|
|
1356
1420
|
//#region src/presets/subscribers/mixpanel.ts
|
|
1357
1421
|
/**
|
|
1358
1422
|
* Mixpanel subscriber preset
|
|
@@ -2053,7 +2117,8 @@ const plugins = /* @__PURE__ */ new Map([
|
|
|
2053
2117
|
["nestjs", nestjs],
|
|
2054
2118
|
["sveltekit", sveltekit],
|
|
2055
2119
|
["elysia", elysia],
|
|
2056
|
-
["nuxt", nuxt]
|
|
2120
|
+
["nuxt", nuxt],
|
|
2121
|
+
["posthog-web", posthogWeb]
|
|
2057
2122
|
]);
|
|
2058
2123
|
/**
|
|
2059
2124
|
* Platform presets registry
|
|
@@ -2529,14 +2594,14 @@ const REDACTED = "[REDACTED]";
|
|
|
2529
2594
|
function redact(value) {
|
|
2530
2595
|
if (value === null || value === void 0) return value;
|
|
2531
2596
|
if (Array.isArray(value)) return value.map((v) => redact(v));
|
|
2532
|
-
|
|
2597
|
+
const record = asRecord(value);
|
|
2598
|
+
if (record) {
|
|
2533
2599
|
const out = {};
|
|
2534
|
-
for (const [
|
|
2535
|
-
else out[k] = redact(v);
|
|
2600
|
+
for (const [key, field] of Object.entries(record)) out[key] = asString(field) !== void 0 && SECRET_KEY_PATTERN.test(key) ? REDACTED : redact(field);
|
|
2536
2601
|
return out;
|
|
2537
2602
|
}
|
|
2538
|
-
|
|
2539
|
-
return value;
|
|
2603
|
+
const text = asString(value);
|
|
2604
|
+
return text !== void 0 && SECRET_VALUE_PATTERN.test(text) ? REDACTED : value;
|
|
2540
2605
|
}
|
|
2541
2606
|
/**
|
|
2542
2607
|
* Print a JSON payload to stdout. If `--output-file` was configured, also
|
|
@@ -2812,7 +2877,7 @@ function readPlanFromFile(filePath) {
|
|
|
2812
2877
|
throw new AutotelError({
|
|
2813
2878
|
type: "validation",
|
|
2814
2879
|
code: AutotelErrorCodes.E_INVALID_PLAN,
|
|
2815
|
-
message: `Plan file is not valid JSON: ${error.message}`
|
|
2880
|
+
message: `Plan file is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
2816
2881
|
});
|
|
2817
2882
|
}
|
|
2818
2883
|
}
|
|
@@ -2828,7 +2893,7 @@ async function readPlanFromInput(input) {
|
|
|
2828
2893
|
throw new AutotelError({
|
|
2829
2894
|
type: "validation",
|
|
2830
2895
|
code: AutotelErrorCodes.E_INVALID_INPUT,
|
|
2831
|
-
message: `stdin did not contain valid JSON: ${error.message}`
|
|
2896
|
+
message: `stdin did not contain valid JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
2832
2897
|
});
|
|
2833
2898
|
}
|
|
2834
2899
|
}
|
|
@@ -4880,7 +4945,7 @@ const traceRule = {
|
|
|
4880
4945
|
question: "Does this entry point produce a span?",
|
|
4881
4946
|
weight: 40,
|
|
4882
4947
|
appliesTo: { kinds: HANDLER_KINDS },
|
|
4883
|
-
fix: ({ target }) => `export const handler =
|
|
4948
|
+
fix: ({ target }) => `export const handler = instrument({ key: '${target.method?.toLowerCase() ?? "handle"} ${target.path}', fn: async () => { /* … */ } });`,
|
|
4884
4949
|
check(context) {
|
|
4885
4950
|
if (usesAutotelApi(context, SPAN_WRAPPERS)) return null;
|
|
4886
4951
|
if (context.project.ambientTracing) return null;
|
|
@@ -5057,7 +5122,7 @@ const RULES = [
|
|
|
5057
5122
|
kinds: HANDLER_KINDS,
|
|
5058
5123
|
when: ({ project }) => project.hasLlmDependency
|
|
5059
5124
|
},
|
|
5060
|
-
fix: () => "traceGenAI({ operation: 'chat', model }
|
|
5125
|
+
fix: () => "traceGenAI({ operation: 'chat', model })((ctx) => async () => client.chat.completions.create(…));",
|
|
5061
5126
|
check(context) {
|
|
5062
5127
|
const llmCall = LLM_CALLS.flatMap((name) => context.facts.memberCalls(name)).find((call) => call.root !== null && call.root !== "JSON");
|
|
5063
5128
|
if (!llmCall) return null;
|
|
@@ -6703,6 +6768,70 @@ const COMMANDS = [
|
|
|
6703
6768
|
}
|
|
6704
6769
|
]
|
|
6705
6770
|
},
|
|
6771
|
+
{
|
|
6772
|
+
name: "estimate",
|
|
6773
|
+
description: "Estimate what a month of telemetry costs, before and after canonical log lines",
|
|
6774
|
+
args: [],
|
|
6775
|
+
flags: [
|
|
6776
|
+
...GLOBAL_FLAGS,
|
|
6777
|
+
...AGENT_FLAGS,
|
|
6778
|
+
{
|
|
6779
|
+
name: "--requests-per-month",
|
|
6780
|
+
takesValue: true,
|
|
6781
|
+
description: "Requests the application serves per month (required)"
|
|
6782
|
+
},
|
|
6783
|
+
{
|
|
6784
|
+
name: "--per-gb",
|
|
6785
|
+
takesValue: true,
|
|
6786
|
+
description: "USD per gigabyte ingested by your provider (required — no rate is assumed)"
|
|
6787
|
+
},
|
|
6788
|
+
{
|
|
6789
|
+
name: "--log-lines-per-request",
|
|
6790
|
+
takesValue: true,
|
|
6791
|
+
description: "Log lines written per request today (default 4)"
|
|
6792
|
+
},
|
|
6793
|
+
{
|
|
6794
|
+
name: "--spans-per-request",
|
|
6795
|
+
takesValue: true,
|
|
6796
|
+
description: "Spans exported per request after instrumenting (default 0)"
|
|
6797
|
+
},
|
|
6798
|
+
{
|
|
6799
|
+
name: "--per-million-events",
|
|
6800
|
+
takesValue: true,
|
|
6801
|
+
description: "USD per million events indexed, if metered separately"
|
|
6802
|
+
},
|
|
6803
|
+
{
|
|
6804
|
+
name: "--keep-percent",
|
|
6805
|
+
takesValue: true,
|
|
6806
|
+
description: "Traffic kept after sampling, applied to both shapes (default 100)"
|
|
6807
|
+
}
|
|
6808
|
+
],
|
|
6809
|
+
mutating: false,
|
|
6810
|
+
network: false,
|
|
6811
|
+
writesFiles: false,
|
|
6812
|
+
supportsDryRun: false,
|
|
6813
|
+
requiresPackageJson: false,
|
|
6814
|
+
mayReadEnv: false,
|
|
6815
|
+
supportsJson: true,
|
|
6816
|
+
examples: [
|
|
6817
|
+
{
|
|
6818
|
+
description: "Ten million requests a month at four log lines each",
|
|
6819
|
+
command: "autotel estimate --requests-per-month 10000000 --per-gb 0.10"
|
|
6820
|
+
},
|
|
6821
|
+
{
|
|
6822
|
+
description: "Include the spans instrumenting will add",
|
|
6823
|
+
command: "autotel estimate --requests-per-month 10000000 --per-gb 0.10 --spans-per-request 6 --json"
|
|
6824
|
+
},
|
|
6825
|
+
{
|
|
6826
|
+
description: "A provider that meters indexed events as well as bytes",
|
|
6827
|
+
command: "autotel estimate --requests-per-month 300000000 --per-gb 0.50 --per-million-events 2.50"
|
|
6828
|
+
},
|
|
6829
|
+
{
|
|
6830
|
+
description: "What head sampling at 10% does to the bill",
|
|
6831
|
+
command: "autotel estimate --requests-per-month 10000000 --per-gb 0.10 --keep-percent 10"
|
|
6832
|
+
}
|
|
6833
|
+
]
|
|
6834
|
+
},
|
|
6706
6835
|
{
|
|
6707
6836
|
name: "codemod trace",
|
|
6708
6837
|
description: "Wrap functions in trace() with span name from function/variable/method name",
|
|
@@ -7289,26 +7418,40 @@ function runSchemaOutputs(opts) {
|
|
|
7289
7418
|
printJson({
|
|
7290
7419
|
ok: true,
|
|
7291
7420
|
command: "autotel schema outputs",
|
|
7292
|
-
outputs: {
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
|
|
7297
|
-
|
|
7298
|
-
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
plan: {
|
|
7302
|
-
presets: "string[] (slugs)",
|
|
7303
|
-
packagesToInstall: {
|
|
7304
|
-
prod: "string[]",
|
|
7305
|
-
dev: "string[]"
|
|
7421
|
+
outputs: {
|
|
7422
|
+
"autotel init --json": {
|
|
7423
|
+
ok: "boolean",
|
|
7424
|
+
command: "string",
|
|
7425
|
+
detected: {
|
|
7426
|
+
packages: "array of { name, version, resolution: \"target\" | \"workspace-root\" }",
|
|
7427
|
+
logger: "\"pino\" | \"winston\" | \"bunyan\" | null",
|
|
7428
|
+
backend: "{ source: \"env\" | \"wrangler\" | \"deps\" | \"prompt\" | \"default\", value: string }",
|
|
7429
|
+
platform: "\"cloudflare\" | \"aws-lambda\" | \"edge\" | null"
|
|
7306
7430
|
},
|
|
7307
|
-
|
|
7308
|
-
|
|
7431
|
+
plan: {
|
|
7432
|
+
presets: "string[] (slugs)",
|
|
7433
|
+
packagesToInstall: {
|
|
7434
|
+
prod: "string[]",
|
|
7435
|
+
dev: "string[]"
|
|
7436
|
+
},
|
|
7437
|
+
filesToWrite: "array of { path, action: \"create\" | \"merge\" | \"skip\" }",
|
|
7438
|
+
envVars: "array of { name, sensitive, action }"
|
|
7439
|
+
},
|
|
7440
|
+
nextSteps: "string[]"
|
|
7309
7441
|
},
|
|
7310
|
-
|
|
7311
|
-
|
|
7442
|
+
"autotel estimate --json": {
|
|
7443
|
+
ok: "boolean",
|
|
7444
|
+
command: "string",
|
|
7445
|
+
estimate: {
|
|
7446
|
+
currency: "\"USD\"",
|
|
7447
|
+
before: "{ events: number, gb: number, cost: number }",
|
|
7448
|
+
after: "{ events: number, gb: number, cost: number }",
|
|
7449
|
+
saved: "number (USD/month, negative when instrumenting costs more)",
|
|
7450
|
+
savedPercent: "number",
|
|
7451
|
+
basis: "{ logLineBytes, canonicalLineBytes, spanBytes, bytesFrom: \"measured\" | \"caller\", perGb, perMillionEvents, keepPercent, bytesPerGb }"
|
|
7452
|
+
}
|
|
7453
|
+
}
|
|
7454
|
+
}
|
|
7312
7455
|
});
|
|
7313
7456
|
}
|
|
7314
7457
|
/** `autotel commands [--json]` — compact listing. */
|
|
@@ -7476,6 +7619,23 @@ function toInvestigateError(command, error) {
|
|
|
7476
7619
|
});
|
|
7477
7620
|
}
|
|
7478
7621
|
//#endregion
|
|
7622
|
+
//#region src/lib/opts.ts
|
|
7623
|
+
/** A flag declared without an argument parser: Commander yields its string. */
|
|
7624
|
+
function stringOpt(o, key) {
|
|
7625
|
+
const value = o[key];
|
|
7626
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
7627
|
+
}
|
|
7628
|
+
/** A flag declared with `intArg` or another numeric parser. */
|
|
7629
|
+
function numberOpt(o, key) {
|
|
7630
|
+
const value = o[key];
|
|
7631
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
7632
|
+
}
|
|
7633
|
+
/** A boolean flag: present or absent. */
|
|
7634
|
+
function boolOpt(o, key) {
|
|
7635
|
+
const value = o[key];
|
|
7636
|
+
return typeof value === "boolean" ? value : void 0;
|
|
7637
|
+
}
|
|
7638
|
+
//#endregion
|
|
7479
7639
|
//#region src/commands/investigate/cli-helpers.ts
|
|
7480
7640
|
/**
|
|
7481
7641
|
* Shared commander wiring for investigate commands. Each command group's
|
|
@@ -7501,22 +7661,22 @@ function addStaticFlags(cmd) {
|
|
|
7501
7661
|
function backendFlagsFromOpts(opts) {
|
|
7502
7662
|
return {
|
|
7503
7663
|
backend: opts.backend,
|
|
7504
|
-
jaegerBaseUrl: opts
|
|
7505
|
-
tempoBaseUrl: opts
|
|
7506
|
-
prometheusBaseUrl: opts
|
|
7507
|
-
lokiBaseUrl: opts
|
|
7508
|
-
collectorPort: opts
|
|
7509
|
-
fixturePath: opts
|
|
7510
|
-
logfireBaseUrl: opts
|
|
7511
|
-
datadogSite: opts
|
|
7512
|
-
signozBaseUrl: opts
|
|
7513
|
-
outputFile: opts
|
|
7664
|
+
jaegerBaseUrl: stringOpt(opts, "jaegerBaseUrl"),
|
|
7665
|
+
tempoBaseUrl: stringOpt(opts, "tempoBaseUrl"),
|
|
7666
|
+
prometheusBaseUrl: stringOpt(opts, "prometheusBaseUrl"),
|
|
7667
|
+
lokiBaseUrl: stringOpt(opts, "lokiBaseUrl"),
|
|
7668
|
+
collectorPort: numberOpt(opts, "collectorPort"),
|
|
7669
|
+
fixturePath: stringOpt(opts, "fixturePath"),
|
|
7670
|
+
logfireBaseUrl: stringOpt(opts, "logfireBaseUrl"),
|
|
7671
|
+
datadogSite: stringOpt(opts, "datadogSite"),
|
|
7672
|
+
signozBaseUrl: stringOpt(opts, "signozBaseUrl"),
|
|
7673
|
+
outputFile: stringOpt(opts, "outputFile"),
|
|
7514
7674
|
noSecrets: opts.secretsInOutput === false
|
|
7515
7675
|
};
|
|
7516
7676
|
}
|
|
7517
7677
|
function staticFlagsFromOpts(opts) {
|
|
7518
7678
|
return {
|
|
7519
|
-
outputFile: opts
|
|
7679
|
+
outputFile: stringOpt(opts, "outputFile"),
|
|
7520
7680
|
noSecrets: opts.secretsInOutput === false
|
|
7521
7681
|
};
|
|
7522
7682
|
}
|
|
@@ -7528,12 +7688,12 @@ function addTimeWindowFlags(cmd) {
|
|
|
7528
7688
|
}
|
|
7529
7689
|
function windowFlagsFromOpts(opts) {
|
|
7530
7690
|
return {
|
|
7531
|
-
serviceName: opts
|
|
7532
|
-
operationName: opts
|
|
7533
|
-
lookbackMinutes: opts
|
|
7534
|
-
from: opts
|
|
7535
|
-
to: opts
|
|
7536
|
-
limit: opts
|
|
7691
|
+
serviceName: stringOpt(opts, "serviceName"),
|
|
7692
|
+
operationName: stringOpt(opts, "operationName"),
|
|
7693
|
+
lookbackMinutes: numberOpt(opts, "lookbackMinutes"),
|
|
7694
|
+
from: stringOpt(opts, "from"),
|
|
7695
|
+
to: stringOpt(opts, "to"),
|
|
7696
|
+
limit: numberOpt(opts, "limit")
|
|
7537
7697
|
};
|
|
7538
7698
|
}
|
|
7539
7699
|
//#endregion
|
|
@@ -7727,8 +7887,8 @@ function registerHealthCommands(program) {
|
|
|
7727
7887
|
const opts = this.opts();
|
|
7728
7888
|
await runHealth({
|
|
7729
7889
|
...backendFlagsFromOpts(opts),
|
|
7730
|
-
otlpEndpoint: opts
|
|
7731
|
-
freshnessTimeoutMs: opts
|
|
7890
|
+
otlpEndpoint: stringOpt(opts, "otlpEndpoint"),
|
|
7891
|
+
freshnessTimeoutMs: numberOpt(opts, "freshnessTimeoutMs"),
|
|
7732
7892
|
otlpEncoding: opts.otlpEncoding
|
|
7733
7893
|
});
|
|
7734
7894
|
});
|
|
@@ -7800,26 +7960,26 @@ function registerDiscoveryCommands(program) {
|
|
|
7800
7960
|
const o = this.optsWithGlobals();
|
|
7801
7961
|
await runDiscoverServices({
|
|
7802
7962
|
...backendFlagsFromOpts(o),
|
|
7803
|
-
limitServices: o
|
|
7804
|
-
traceSample: o
|
|
7805
|
-
logSample: o
|
|
7806
|
-
metricSample: o
|
|
7963
|
+
limitServices: numberOpt(o, "limitServices"),
|
|
7964
|
+
traceSample: numberOpt(o, "traceSample"),
|
|
7965
|
+
logSample: numberOpt(o, "logSample"),
|
|
7966
|
+
metricSample: numberOpt(o, "metricSample")
|
|
7807
7967
|
});
|
|
7808
7968
|
});
|
|
7809
7969
|
const traceFieldsCmd = new Command("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() {
|
|
7810
7970
|
const o = this.optsWithGlobals();
|
|
7811
7971
|
await runDiscoverTraceFields({
|
|
7812
7972
|
...backendFlagsFromOpts(o),
|
|
7813
|
-
search: o
|
|
7814
|
-
sampleSize: o
|
|
7973
|
+
search: stringOpt(o, "search"),
|
|
7974
|
+
sampleSize: numberOpt(o, "sampleSize")
|
|
7815
7975
|
});
|
|
7816
7976
|
});
|
|
7817
7977
|
const logFieldsCmd = new Command("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() {
|
|
7818
7978
|
const o = this.optsWithGlobals();
|
|
7819
7979
|
await runDiscoverLogFields({
|
|
7820
7980
|
...backendFlagsFromOpts(o),
|
|
7821
|
-
search: o
|
|
7822
|
-
sampleSize: o
|
|
7981
|
+
search: stringOpt(o, "search"),
|
|
7982
|
+
sampleSize: numberOpt(o, "sampleSize")
|
|
7823
7983
|
});
|
|
7824
7984
|
});
|
|
7825
7985
|
addBackendFlags(discoverCmd);
|
|
@@ -7856,62 +8016,62 @@ function registerQueryCommands(program) {
|
|
|
7856
8016
|
const o = this.optsWithGlobals();
|
|
7857
8017
|
await runQueryTraces({
|
|
7858
8018
|
...backendFlagsFromOpts(o),
|
|
7859
|
-
serviceName: o
|
|
7860
|
-
operationName: o
|
|
7861
|
-
lookbackMinutes: o
|
|
7862
|
-
from: o
|
|
7863
|
-
to: o
|
|
7864
|
-
limit: o
|
|
7865
|
-
errorOnly: o
|
|
8019
|
+
serviceName: stringOpt(o, "serviceName"),
|
|
8020
|
+
operationName: stringOpt(o, "operationName"),
|
|
8021
|
+
lookbackMinutes: numberOpt(o, "lookbackMinutes"),
|
|
8022
|
+
from: stringOpt(o, "from"),
|
|
8023
|
+
to: stringOpt(o, "to"),
|
|
8024
|
+
limit: numberOpt(o, "limit"),
|
|
8025
|
+
errorOnly: boolOpt(o, "errorOnly"),
|
|
7866
8026
|
statusCode: o.statusCode,
|
|
7867
|
-
minDurationMs: o
|
|
7868
|
-
maxDurationMs: o
|
|
7869
|
-
genAiSystem: o
|
|
7870
|
-
genAiRequestModel: o
|
|
7871
|
-
genAiResponseModel: o
|
|
8027
|
+
minDurationMs: numberOpt(o, "minDurationMs"),
|
|
8028
|
+
maxDurationMs: numberOpt(o, "maxDurationMs"),
|
|
8029
|
+
genAiSystem: stringOpt(o, "genAiSystem"),
|
|
8030
|
+
genAiRequestModel: stringOpt(o, "genAiRequestModel"),
|
|
8031
|
+
genAiResponseModel: stringOpt(o, "genAiResponseModel")
|
|
7872
8032
|
});
|
|
7873
8033
|
});
|
|
7874
8034
|
const spansCmd = addTimeWindowFlags(new Command("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() {
|
|
7875
8035
|
const o = this.optsWithGlobals();
|
|
7876
8036
|
await runQuerySpans({
|
|
7877
8037
|
...backendFlagsFromOpts(o),
|
|
7878
|
-
serviceName: o
|
|
7879
|
-
operationName: o
|
|
7880
|
-
lookbackMinutes: o
|
|
7881
|
-
from: o
|
|
7882
|
-
to: o
|
|
7883
|
-
limit: o
|
|
7884
|
-
errorOnly: o
|
|
8038
|
+
serviceName: stringOpt(o, "serviceName"),
|
|
8039
|
+
operationName: stringOpt(o, "operationName"),
|
|
8040
|
+
lookbackMinutes: numberOpt(o, "lookbackMinutes"),
|
|
8041
|
+
from: stringOpt(o, "from"),
|
|
8042
|
+
to: stringOpt(o, "to"),
|
|
8043
|
+
limit: numberOpt(o, "limit"),
|
|
8044
|
+
errorOnly: boolOpt(o, "errorOnly"),
|
|
7885
8045
|
statusCode: o.statusCode,
|
|
7886
|
-
minDurationMs: o
|
|
7887
|
-
maxDurationMs: o
|
|
8046
|
+
minDurationMs: numberOpt(o, "minDurationMs"),
|
|
8047
|
+
maxDurationMs: numberOpt(o, "maxDurationMs")
|
|
7888
8048
|
});
|
|
7889
8049
|
});
|
|
7890
8050
|
const metricsCmd = addTimeWindowFlags(new Command("metrics")).description("List metric series").option("--metric-name <name>", "Filter by metric name").action(async function() {
|
|
7891
8051
|
const o = this.optsWithGlobals();
|
|
7892
8052
|
await runQueryMetrics({
|
|
7893
8053
|
...backendFlagsFromOpts(o),
|
|
7894
|
-
metricName: o
|
|
7895
|
-
serviceName: o
|
|
7896
|
-
lookbackMinutes: o
|
|
7897
|
-
from: o
|
|
7898
|
-
to: o
|
|
7899
|
-
limit: o
|
|
8054
|
+
metricName: stringOpt(o, "metricName"),
|
|
8055
|
+
serviceName: stringOpt(o, "serviceName"),
|
|
8056
|
+
lookbackMinutes: numberOpt(o, "lookbackMinutes"),
|
|
8057
|
+
from: stringOpt(o, "from"),
|
|
8058
|
+
to: stringOpt(o, "to"),
|
|
8059
|
+
limit: numberOpt(o, "limit")
|
|
7900
8060
|
});
|
|
7901
8061
|
});
|
|
7902
8062
|
const logsCmd = addTimeWindowFlags(new Command("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() {
|
|
7903
8063
|
const o = this.optsWithGlobals();
|
|
7904
8064
|
await runQueryLogs({
|
|
7905
8065
|
...backendFlagsFromOpts(o),
|
|
7906
|
-
serviceName: o
|
|
7907
|
-
traceId: o
|
|
7908
|
-
spanId: o
|
|
7909
|
-
severityText: o
|
|
7910
|
-
text: o
|
|
7911
|
-
lookbackMinutes: o
|
|
7912
|
-
from: o
|
|
7913
|
-
to: o
|
|
7914
|
-
limit: o
|
|
8066
|
+
serviceName: stringOpt(o, "serviceName"),
|
|
8067
|
+
traceId: stringOpt(o, "traceId"),
|
|
8068
|
+
spanId: stringOpt(o, "spanId"),
|
|
8069
|
+
severityText: stringOpt(o, "severityText"),
|
|
8070
|
+
text: stringOpt(o, "text"),
|
|
8071
|
+
lookbackMinutes: numberOpt(o, "lookbackMinutes"),
|
|
8072
|
+
from: stringOpt(o, "from"),
|
|
8073
|
+
to: stringOpt(o, "to"),
|
|
8074
|
+
limit: numberOpt(o, "limit")
|
|
7915
8075
|
});
|
|
7916
8076
|
});
|
|
7917
8077
|
addBackendFlags(queryCmd);
|
|
@@ -7966,8 +8126,8 @@ function registerTopologyCommands(program) {
|
|
|
7966
8126
|
const o = this.optsWithGlobals();
|
|
7967
8127
|
await runServiceMap({
|
|
7968
8128
|
...backendFlagsFromOpts(o),
|
|
7969
|
-
lookbackMinutes: o
|
|
7970
|
-
limit: o
|
|
8129
|
+
lookbackMinutes: numberOpt(o, "lookbackMinutes"),
|
|
8130
|
+
limit: numberOpt(o, "limit")
|
|
7971
8131
|
});
|
|
7972
8132
|
});
|
|
7973
8133
|
addBackendFlags(topologyCmd);
|
|
@@ -8084,9 +8244,9 @@ function registerDiagnoseCommands(program) {
|
|
|
8084
8244
|
const o = this.optsWithGlobals();
|
|
8085
8245
|
await runDiagnoseAnomalies({
|
|
8086
8246
|
...backendFlagsFromOpts(o),
|
|
8087
|
-
service: o
|
|
8088
|
-
operation: o
|
|
8089
|
-
lookbackMinutes: o
|
|
8247
|
+
service: stringOpt(o, "service"),
|
|
8248
|
+
operation: stringOpt(o, "operation"),
|
|
8249
|
+
lookbackMinutes: numberOpt(o, "lookbackMinutes")
|
|
8090
8250
|
});
|
|
8091
8251
|
});
|
|
8092
8252
|
const rootCauseCmd = new Command("root-cause").description("Walk a trace tree to identify the bottleneck span").argument("<traceId>", "Trace ID").action(async function(traceId) {
|
|
@@ -8099,19 +8259,19 @@ function registerDiagnoseCommands(program) {
|
|
|
8099
8259
|
const o = this.optsWithGlobals();
|
|
8100
8260
|
await runDiagnoseErrors({
|
|
8101
8261
|
...backendFlagsFromOpts(o),
|
|
8102
|
-
service: o
|
|
8103
|
-
lookbackMinutes: o
|
|
8104
|
-
limit: o
|
|
8262
|
+
service: stringOpt(o, "service"),
|
|
8263
|
+
lookbackMinutes: numberOpt(o, "lookbackMinutes"),
|
|
8264
|
+
limit: numberOpt(o, "limit")
|
|
8105
8265
|
});
|
|
8106
8266
|
});
|
|
8107
8267
|
const slosCmd = new Command("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() {
|
|
8108
8268
|
const o = this.optsWithGlobals();
|
|
8109
8269
|
await runDiagnoseSlos({
|
|
8110
8270
|
...backendFlagsFromOpts(o),
|
|
8111
|
-
service: o
|
|
8112
|
-
p99LatencyMs: o
|
|
8113
|
-
maxErrorRate: o
|
|
8114
|
-
lookbackMinutes: o
|
|
8271
|
+
service: stringOpt(o, "service") ?? "",
|
|
8272
|
+
p99LatencyMs: numberOpt(o, "p99LatencyMs"),
|
|
8273
|
+
maxErrorRate: numberOpt(o, "maxErrorRate"),
|
|
8274
|
+
lookbackMinutes: numberOpt(o, "lookbackMinutes")
|
|
8115
8275
|
});
|
|
8116
8276
|
});
|
|
8117
8277
|
addBackendFlags(diagnoseCmd);
|
|
@@ -8170,8 +8330,8 @@ function registerCorrelateCommands(program) {
|
|
|
8170
8330
|
const o = this.optsWithGlobals();
|
|
8171
8331
|
await runExplainSlowdown({
|
|
8172
8332
|
...backendFlagsFromOpts(o),
|
|
8173
|
-
service: o
|
|
8174
|
-
lookbackMinutes: o
|
|
8333
|
+
service: stringOpt(o, "service") ?? "",
|
|
8334
|
+
lookbackMinutes: numberOpt(o, "lookbackMinutes")
|
|
8175
8335
|
});
|
|
8176
8336
|
});
|
|
8177
8337
|
addBackendFlags(correlateCmd);
|
|
@@ -8282,71 +8442,71 @@ function registerLlmCommands(program) {
|
|
|
8282
8442
|
const o = this.optsWithGlobals();
|
|
8283
8443
|
await runLlmUsage({
|
|
8284
8444
|
...backendFlagsFromOpts(o),
|
|
8285
|
-
startTime: o
|
|
8286
|
-
endTime: o
|
|
8287
|
-
serviceName: o
|
|
8288
|
-
genAiSystem: o
|
|
8289
|
-
genAiRequestModel: o
|
|
8290
|
-
genAiResponseModel: o
|
|
8291
|
-
limit: o
|
|
8445
|
+
startTime: stringOpt(o, "startTime"),
|
|
8446
|
+
endTime: stringOpt(o, "endTime"),
|
|
8447
|
+
serviceName: stringOpt(o, "serviceName"),
|
|
8448
|
+
genAiSystem: stringOpt(o, "genAiSystem"),
|
|
8449
|
+
genAiRequestModel: stringOpt(o, "genAiRequestModel"),
|
|
8450
|
+
genAiResponseModel: stringOpt(o, "genAiResponseModel"),
|
|
8451
|
+
limit: numberOpt(o, "limit")
|
|
8292
8452
|
});
|
|
8293
8453
|
});
|
|
8294
8454
|
const modelsCmd = commonOpts(new Command("models")).description("Discover LLM models in use").action(async function() {
|
|
8295
8455
|
const o = this.optsWithGlobals();
|
|
8296
8456
|
await runLlmModels({
|
|
8297
8457
|
...backendFlagsFromOpts(o),
|
|
8298
|
-
startTime: o
|
|
8299
|
-
endTime: o
|
|
8300
|
-
serviceName: o
|
|
8301
|
-
genAiSystem: o
|
|
8302
|
-
limit: o
|
|
8458
|
+
startTime: stringOpt(o, "startTime"),
|
|
8459
|
+
endTime: stringOpt(o, "endTime"),
|
|
8460
|
+
serviceName: stringOpt(o, "serviceName"),
|
|
8461
|
+
genAiSystem: stringOpt(o, "genAiSystem"),
|
|
8462
|
+
limit: numberOpt(o, "limit")
|
|
8303
8463
|
});
|
|
8304
8464
|
});
|
|
8305
8465
|
const modelStatsCmd = commonOpts(new Command("model-stats")).description("Latency/token/error stats for one LLM model").requiredOption("--model-name <name>", "Model to inspect").action(async function() {
|
|
8306
8466
|
const o = this.optsWithGlobals();
|
|
8307
8467
|
await runLlmModelStats({
|
|
8308
8468
|
...backendFlagsFromOpts(o),
|
|
8309
|
-
modelName: o
|
|
8310
|
-
startTime: o
|
|
8311
|
-
endTime: o
|
|
8312
|
-
serviceName: o
|
|
8469
|
+
modelName: stringOpt(o, "modelName") ?? "",
|
|
8470
|
+
startTime: stringOpt(o, "startTime"),
|
|
8471
|
+
endTime: stringOpt(o, "endTime"),
|
|
8472
|
+
serviceName: stringOpt(o, "serviceName")
|
|
8313
8473
|
});
|
|
8314
8474
|
});
|
|
8315
8475
|
const expensiveCmd = commonOpts(new Command("expensive")).description("Traces with highest total LLM token usage").option("--min-tokens <n>", "Minimum token threshold", intArg).action(async function() {
|
|
8316
8476
|
const o = this.optsWithGlobals();
|
|
8317
8477
|
await runLlmExpensive({
|
|
8318
8478
|
...backendFlagsFromOpts(o),
|
|
8319
|
-
startTime: o
|
|
8320
|
-
endTime: o
|
|
8321
|
-
serviceName: o
|
|
8322
|
-
genAiRequestModel: o
|
|
8323
|
-
genAiResponseModel: o
|
|
8324
|
-
minTokens: o
|
|
8325
|
-
limit: o
|
|
8479
|
+
startTime: stringOpt(o, "startTime"),
|
|
8480
|
+
endTime: stringOpt(o, "endTime"),
|
|
8481
|
+
serviceName: stringOpt(o, "serviceName"),
|
|
8482
|
+
genAiRequestModel: stringOpt(o, "genAiRequestModel"),
|
|
8483
|
+
genAiResponseModel: stringOpt(o, "genAiResponseModel"),
|
|
8484
|
+
minTokens: numberOpt(o, "minTokens"),
|
|
8485
|
+
limit: numberOpt(o, "limit")
|
|
8326
8486
|
});
|
|
8327
8487
|
});
|
|
8328
8488
|
const slowCmd = commonOpts(new Command("slow")).description("Slowest traces that include LLM spans").option("--min-duration-ms <n>", "Minimum duration", intArg).action(async function() {
|
|
8329
8489
|
const o = this.optsWithGlobals();
|
|
8330
8490
|
await runLlmSlow({
|
|
8331
8491
|
...backendFlagsFromOpts(o),
|
|
8332
|
-
startTime: o
|
|
8333
|
-
endTime: o
|
|
8334
|
-
serviceName: o
|
|
8335
|
-
genAiRequestModel: o
|
|
8336
|
-
genAiResponseModel: o
|
|
8337
|
-
minDurationMs: o
|
|
8338
|
-
limit: o
|
|
8492
|
+
startTime: stringOpt(o, "startTime"),
|
|
8493
|
+
endTime: stringOpt(o, "endTime"),
|
|
8494
|
+
serviceName: stringOpt(o, "serviceName"),
|
|
8495
|
+
genAiRequestModel: stringOpt(o, "genAiRequestModel"),
|
|
8496
|
+
genAiResponseModel: stringOpt(o, "genAiResponseModel"),
|
|
8497
|
+
minDurationMs: numberOpt(o, "minDurationMs"),
|
|
8498
|
+
limit: numberOpt(o, "limit")
|
|
8339
8499
|
});
|
|
8340
8500
|
});
|
|
8341
8501
|
const toolsCmd = commonOpts(new Command("tools")).description("Discover tool/function spans grouped by tool name").action(async function() {
|
|
8342
8502
|
const o = this.optsWithGlobals();
|
|
8343
8503
|
await runLlmTools({
|
|
8344
8504
|
...backendFlagsFromOpts(o),
|
|
8345
|
-
startTime: o
|
|
8346
|
-
endTime: o
|
|
8347
|
-
serviceName: o
|
|
8348
|
-
genAiSystem: o
|
|
8349
|
-
limit: o
|
|
8505
|
+
startTime: stringOpt(o, "startTime"),
|
|
8506
|
+
endTime: stringOpt(o, "endTime"),
|
|
8507
|
+
serviceName: stringOpt(o, "serviceName"),
|
|
8508
|
+
genAiSystem: stringOpt(o, "genAiSystem"),
|
|
8509
|
+
limit: numberOpt(o, "limit")
|
|
8350
8510
|
});
|
|
8351
8511
|
});
|
|
8352
8512
|
addBackendFlags(llmCmd);
|
|
@@ -8431,7 +8591,7 @@ function registerScoreCommands(program) {
|
|
|
8431
8591
|
const o = this.optsWithGlobals();
|
|
8432
8592
|
await runScoreSpan({
|
|
8433
8593
|
...staticFlagsFromOpts(o),
|
|
8434
|
-
spanFile: o
|
|
8594
|
+
spanFile: stringOpt(o, "spanFile")
|
|
8435
8595
|
});
|
|
8436
8596
|
});
|
|
8437
8597
|
const explainCmd = addStaticFlags(new Command("explain")).description("Explain the instrumentation scoring rubric").action(async function() {
|
|
@@ -8539,7 +8699,7 @@ function registerCollectorCommands(program) {
|
|
|
8539
8699
|
const o = this.optsWithGlobals();
|
|
8540
8700
|
await runCollectorValidate({
|
|
8541
8701
|
...staticFlagsFromOpts(o),
|
|
8542
|
-
configFile: o
|
|
8702
|
+
configFile: stringOpt(o, "configFile")
|
|
8543
8703
|
});
|
|
8544
8704
|
});
|
|
8545
8705
|
const suggestCmd = addStaticFlags(new Command("suggest")).description("Print a minimal OTLP receiver config").action(async function() {
|
|
@@ -8555,7 +8715,7 @@ function registerCollectorCommands(program) {
|
|
|
8555
8715
|
const o = this.optsWithGlobals();
|
|
8556
8716
|
await runCollectorComponents({
|
|
8557
8717
|
...staticFlagsFromOpts(o),
|
|
8558
|
-
version: o
|
|
8718
|
+
version: stringOpt(o, "version"),
|
|
8559
8719
|
kind: o.kind
|
|
8560
8720
|
});
|
|
8561
8721
|
});
|
|
@@ -8564,8 +8724,8 @@ function registerCollectorCommands(program) {
|
|
|
8564
8724
|
await runCollectorSchema({
|
|
8565
8725
|
...staticFlagsFromOpts(o),
|
|
8566
8726
|
kind: o.kind,
|
|
8567
|
-
name: o
|
|
8568
|
-
version: o
|
|
8727
|
+
name: stringOpt(o, "name") ?? "",
|
|
8728
|
+
version: stringOpt(o, "version")
|
|
8569
8729
|
});
|
|
8570
8730
|
});
|
|
8571
8731
|
const readmeCmd = addStaticFlags(new Command("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() {
|
|
@@ -8573,8 +8733,8 @@ function registerCollectorCommands(program) {
|
|
|
8573
8733
|
await runCollectorReadme({
|
|
8574
8734
|
...staticFlagsFromOpts(o),
|
|
8575
8735
|
kind: o.kind,
|
|
8576
|
-
name: o
|
|
8577
|
-
version: o
|
|
8736
|
+
name: stringOpt(o, "name") ?? "",
|
|
8737
|
+
version: stringOpt(o, "version")
|
|
8578
8738
|
});
|
|
8579
8739
|
});
|
|
8580
8740
|
const validateComponentCmd = addStaticFlags(new Command("validate-component")).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() {
|
|
@@ -8582,9 +8742,9 @@ function registerCollectorCommands(program) {
|
|
|
8582
8742
|
await runCollectorValidateComponent({
|
|
8583
8743
|
...staticFlagsFromOpts(o),
|
|
8584
8744
|
kind: o.kind,
|
|
8585
|
-
name: o
|
|
8586
|
-
version: o
|
|
8587
|
-
configFile: o
|
|
8745
|
+
name: stringOpt(o, "name") ?? "",
|
|
8746
|
+
version: stringOpt(o, "version"),
|
|
8747
|
+
configFile: stringOpt(o, "configFile")
|
|
8588
8748
|
});
|
|
8589
8749
|
});
|
|
8590
8750
|
const refreshCmd = addStaticFlags(new Command("refresh")).description("Refresh in-memory collector metadata cache").action(async function() {
|
|
@@ -8880,8 +9040,8 @@ function registerSecurityCommands(program) {
|
|
|
8880
9040
|
await runSecurityEvents({
|
|
8881
9041
|
...backendFlagsFromOpts(o),
|
|
8882
9042
|
...windowFlagsFromOpts(o),
|
|
8883
|
-
category: o
|
|
8884
|
-
severity: o
|
|
9043
|
+
category: stringOpt(o, "category"),
|
|
9044
|
+
severity: stringOpt(o, "severity")
|
|
8885
9045
|
});
|
|
8886
9046
|
});
|
|
8887
9047
|
const mcpCmd = addTimeWindowFlags(new Command("mcp")).description("MCP protocol-boundary security: prompt-injection verdicts, output-budget breaches, untrusted-content tool calls").action(async function() {
|
|
@@ -8986,45 +9146,47 @@ function composeDrains(...drains) {
|
|
|
8986
9146
|
function resolveEndpoint(explicit) {
|
|
8987
9147
|
return explicit ?? process.env.AUTOTEL_TELEMETRY_ENDPOINT;
|
|
8988
9148
|
}
|
|
9149
|
+
/** A finite number, as opposed to the strings and markers around it. */
|
|
9150
|
+
function isNumber(value) {
|
|
9151
|
+
return Number.isFinite(value);
|
|
9152
|
+
}
|
|
8989
9153
|
const SENSITIVE_PATTERN = /(?:token|secret|password|key|auth|credential|api[_-]?key|private)/i;
|
|
8990
9154
|
function sanitizeFlags(argv, allowlistedStringFlags = []) {
|
|
8991
|
-
const
|
|
9155
|
+
const entries = [];
|
|
9156
|
+
/** What a flag's value is worth reporting as, once the key is known safe. */
|
|
9157
|
+
const valueOf = (key, rawValue) => {
|
|
9158
|
+
if (/^\d+$/.test(rawValue)) return Number(rawValue);
|
|
9159
|
+
if (allowlistedStringFlags.includes(key)) return rawValue;
|
|
9160
|
+
return { present: true };
|
|
9161
|
+
};
|
|
8992
9162
|
for (let i = 0; i < argv.length; i++) {
|
|
8993
9163
|
const arg = argv[i];
|
|
8994
9164
|
if (arg === void 0 || !arg.startsWith("-")) continue;
|
|
8995
9165
|
const key = arg.replace(/^-+/, "").split("=")[0] ?? arg;
|
|
8996
9166
|
if (SENSITIVE_PATTERN.test(key)) {
|
|
8997
|
-
|
|
9167
|
+
entries.push([key, { present: true }]);
|
|
8998
9168
|
continue;
|
|
8999
9169
|
}
|
|
9000
9170
|
if (arg.includes("=")) {
|
|
9001
9171
|
const [, rawValue] = arg.split("=");
|
|
9002
9172
|
if (rawValue === void 0) continue;
|
|
9003
|
-
|
|
9004
|
-
else if (allowlistedStringFlags.includes(key)) out[key] = rawValue;
|
|
9005
|
-
else out[key] = { present: true };
|
|
9173
|
+
entries.push([key, valueOf(key, rawValue)]);
|
|
9006
9174
|
continue;
|
|
9007
9175
|
}
|
|
9008
9176
|
const next = argv[i + 1];
|
|
9009
|
-
if (next && !next.startsWith("-"))
|
|
9010
|
-
|
|
9011
|
-
i++;
|
|
9012
|
-
} else if (allowlistedStringFlags.includes(key)) {
|
|
9013
|
-
out[key] = next;
|
|
9014
|
-
i++;
|
|
9015
|
-
} else {
|
|
9016
|
-
out[key] = { present: true };
|
|
9177
|
+
if (next && !next.startsWith("-")) {
|
|
9178
|
+
entries.push([key, valueOf(key, next)]);
|
|
9017
9179
|
i++;
|
|
9018
|
-
}
|
|
9019
|
-
else out[key] = true;
|
|
9180
|
+
} else entries.push([key, true]);
|
|
9020
9181
|
}
|
|
9021
|
-
return
|
|
9182
|
+
return Object.fromEntries(entries);
|
|
9022
9183
|
}
|
|
9023
9184
|
function sanitizeCustom(fields) {
|
|
9024
|
-
|
|
9025
|
-
|
|
9026
|
-
|
|
9027
|
-
|
|
9185
|
+
return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== null && value !== void 0).map(([key, value]) => {
|
|
9186
|
+
if (value === true || value === false) return [key, value];
|
|
9187
|
+
if (isNumber(value)) return [key, value];
|
|
9188
|
+
return [key, { present: true }];
|
|
9189
|
+
}));
|
|
9028
9190
|
}
|
|
9029
9191
|
const runStorage = new AsyncLocalStorage();
|
|
9030
9192
|
let activeHandle = null;
|
|
@@ -9036,7 +9198,7 @@ var InternalTelemetry = class {
|
|
|
9036
9198
|
constructor(options, enabled) {
|
|
9037
9199
|
this.options = options;
|
|
9038
9200
|
this.enabled = enabled;
|
|
9039
|
-
this.outbox = new TelemetryOutbox({ toolName: options.name });
|
|
9201
|
+
this.outbox = options.outbox ?? new TelemetryOutbox({ toolName: options.name });
|
|
9040
9202
|
const endpoint = resolveEndpoint(options.endpoint);
|
|
9041
9203
|
const drains = [createDebugDrain(), endpoint ? createHttpDrain(endpoint) : createNoopDrain()];
|
|
9042
9204
|
this.drain = composeDrains(...drains);
|
|
@@ -9190,6 +9352,62 @@ function runTelemetryDisclosure(toolName, version) {
|
|
|
9190
9352
|
console.log(disclosure.markdown);
|
|
9191
9353
|
}
|
|
9192
9354
|
//#endregion
|
|
9355
|
+
//#region src/commands/estimate.ts
|
|
9356
|
+
/** `keepPercent` → `--keep-percent`, so the fix hint names a real flag. */
|
|
9357
|
+
function flagFor(field) {
|
|
9358
|
+
return `--${field.replaceAll(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`;
|
|
9359
|
+
}
|
|
9360
|
+
/**
|
|
9361
|
+
* Answer "what will this telemetry cost me a month" as a JSON envelope.
|
|
9362
|
+
*
|
|
9363
|
+
* Throws `AutotelError` rather than exiting, so the top-level handler picks
|
|
9364
|
+
* pretty or JSON output and maps the code — bad input is the caller's to fix
|
|
9365
|
+
* (exit 2), not a broken run (exit 1).
|
|
9366
|
+
*/
|
|
9367
|
+
function runEstimate(input) {
|
|
9368
|
+
try {
|
|
9369
|
+
return {
|
|
9370
|
+
ok: true,
|
|
9371
|
+
command: "estimate",
|
|
9372
|
+
estimate: estimateCost(input)
|
|
9373
|
+
};
|
|
9374
|
+
} catch (error) {
|
|
9375
|
+
if (error instanceof EstimateInputError) throw new AutotelError({
|
|
9376
|
+
type: "validation",
|
|
9377
|
+
code: AutotelErrorCodes.E_INVALID_INPUT,
|
|
9378
|
+
message: error.message,
|
|
9379
|
+
fix: `Pass ${flagFor(error.field)} with a value in ${error.expected}.`,
|
|
9380
|
+
expected: { [error.field]: error.expected }
|
|
9381
|
+
});
|
|
9382
|
+
throw error;
|
|
9383
|
+
}
|
|
9384
|
+
}
|
|
9385
|
+
//#endregion
|
|
9386
|
+
//#region src/ui/estimate.ts
|
|
9387
|
+
const usd = (value) => `$${value.toFixed(2)}`;
|
|
9388
|
+
/**
|
|
9389
|
+
* The human view. Every figure the estimate rests on is printed with it —
|
|
9390
|
+
* a cost number without its basis is a number nobody can argue with, which is
|
|
9391
|
+
* the wrong property for a number that will end up in a budget conversation.
|
|
9392
|
+
*/
|
|
9393
|
+
function printEstimate(estimate) {
|
|
9394
|
+
const { before, after, basis } = estimate;
|
|
9395
|
+
const lines = [
|
|
9396
|
+
"",
|
|
9397
|
+
chalk.bold(" Telemetry cost per month"),
|
|
9398
|
+
"",
|
|
9399
|
+
` ${chalk.dim("today")} ${before.events.toLocaleString()} events ${before.gb} GB ${chalk.bold(usd(before.cost))}`,
|
|
9400
|
+
` ${chalk.dim("canonical")} ${after.events.toLocaleString()} events ${after.gb} GB ${chalk.bold(usd(after.cost))}`,
|
|
9401
|
+
"",
|
|
9402
|
+
estimate.saved > 0 ? ` ${chalk.green(`saves ${usd(estimate.saved)}/month (${estimate.savedPercent}%)`)}` : ` ${chalk.yellow(`costs ${usd(Math.abs(estimate.saved))}/month more`)}`,
|
|
9403
|
+
"",
|
|
9404
|
+
chalk.dim(` basis: ${basis.logLineBytes} B per log line, ${basis.canonicalLineBytes} B per canonical line, ${basis.spanBytes} B per span (${basis.bytesFrom})`),
|
|
9405
|
+
chalk.dim(` ${usd(basis.perGb)}/GB, ${usd(basis.perMillionEvents)}/million events, ${basis.keepPercent}% of traffic kept`),
|
|
9406
|
+
""
|
|
9407
|
+
];
|
|
9408
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
9409
|
+
}
|
|
9410
|
+
//#endregion
|
|
9193
9411
|
//#region src/cli.ts
|
|
9194
9412
|
/**
|
|
9195
9413
|
* Create the CLI program
|
|
@@ -9310,6 +9528,27 @@ function createProgram() {
|
|
|
9310
9528
|
codemodCmd.addCommand(traceCmd);
|
|
9311
9529
|
addGlobalOptions(codemodCmd);
|
|
9312
9530
|
program.addCommand(codemodCmd);
|
|
9531
|
+
const estimateCmd = new Command("estimate").description("Estimate what a month of telemetry costs, before and after").requiredOption("--requests-per-month <n>", "Requests the application serves per month").requiredOption("--per-gb <usd>", "USD per gigabyte ingested by your provider").option("--log-lines-per-request <n>", "Log lines written per request today (default 4)").option("--spans-per-request <n>", "Spans exported per request after instrumenting (default 0)").option("--per-million-events <usd>", "USD per million events indexed, if your provider meters events too").option("--keep-percent <n>", "Traffic kept after sampling, applied to both shapes (default 100)").option("--json", "Output machine-readable JSON").option("--output-file <path>", "Persist JSON output to this path").action((opts) => {
|
|
9532
|
+
configureJsonOutput({
|
|
9533
|
+
outputFile: opts.outputFile,
|
|
9534
|
+
noSecrets: opts.secretsInOutput === false
|
|
9535
|
+
});
|
|
9536
|
+
const envelope = runEstimate({
|
|
9537
|
+
requestsPerMonth: Number(opts.requestsPerMonth),
|
|
9538
|
+
perGb: Number(opts.perGb),
|
|
9539
|
+
...opts.logLinesPerRequest !== void 0 && { logLinesPerRequest: Number(opts.logLinesPerRequest) },
|
|
9540
|
+
...opts.spansPerRequest !== void 0 && { spansPerRequest: Number(opts.spansPerRequest) },
|
|
9541
|
+
...opts.perMillionEvents !== void 0 && { perMillionEvents: Number(opts.perMillionEvents) },
|
|
9542
|
+
...opts.keepPercent !== void 0 && { keepPercent: Number(opts.keepPercent) }
|
|
9543
|
+
});
|
|
9544
|
+
if (opts.json) {
|
|
9545
|
+
printJson(envelope);
|
|
9546
|
+
return;
|
|
9547
|
+
}
|
|
9548
|
+
printEstimate(envelope.estimate);
|
|
9549
|
+
});
|
|
9550
|
+
addGlobalOptions(estimateCmd);
|
|
9551
|
+
program.addCommand(estimateCmd);
|
|
9313
9552
|
const schemaCmd = new Command("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) => {
|
|
9314
9553
|
runSchema({
|
|
9315
9554
|
outputFile: opts.outputFile,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autotel-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "CLI for autotel - setup wizard, diagnostics, and incremental features",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"semver": "^7.8.5",
|
|
26
26
|
"ts-morph": "^28.0.0",
|
|
27
27
|
"yaml": "^2.9.0",
|
|
28
|
-
"autotel-mcp": "0.
|
|
28
|
+
"autotel-mcp": "0.5.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@eslint/js": "^10.0.1",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"typescript": "^6.0.3",
|
|
41
41
|
"typescript-eslint": "^8.65.0",
|
|
42
42
|
"vitest": "^4.1.10",
|
|
43
|
-
"autotel-telemetry": "0.
|
|
43
|
+
"autotel-telemetry": "0.2.0"
|
|
44
44
|
},
|
|
45
45
|
"engines": {
|
|
46
46
|
"node": ">=22.0.0"
|