autotel-cli 0.15.3 → 0.16.1
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 +440 -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,56 @@ 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
|
+
"Docs: https://jagreehal.github.io/autotel/integrations/posthog/",
|
|
1415
|
+
"Backend init({ baggage: \"\" }) so the checkout span carries session.id from W3C baggage",
|
|
1416
|
+
"Spans now carry session.id and user.id from PostHog; failed spans carry session.replay.url",
|
|
1417
|
+
"For a clickable link back: joinPostHog(posthog, { traceUrl: ({ traceId }) => `...${traceId}` })",
|
|
1418
|
+
"Name any feature flags worth slicing by: joinPostHog(posthog, { featureFlags: [\"my-flag\"] })"
|
|
1419
|
+
]
|
|
1420
|
+
};
|
|
1421
|
+
//#endregion
|
|
1356
1422
|
//#region src/presets/subscribers/mixpanel.ts
|
|
1357
1423
|
/**
|
|
1358
1424
|
* Mixpanel subscriber preset
|
|
@@ -2053,7 +2119,8 @@ const plugins = /* @__PURE__ */ new Map([
|
|
|
2053
2119
|
["nestjs", nestjs],
|
|
2054
2120
|
["sveltekit", sveltekit],
|
|
2055
2121
|
["elysia", elysia],
|
|
2056
|
-
["nuxt", nuxt]
|
|
2122
|
+
["nuxt", nuxt],
|
|
2123
|
+
["posthog-web", posthogWeb]
|
|
2057
2124
|
]);
|
|
2058
2125
|
/**
|
|
2059
2126
|
* Platform presets registry
|
|
@@ -2529,14 +2596,14 @@ const REDACTED = "[REDACTED]";
|
|
|
2529
2596
|
function redact(value) {
|
|
2530
2597
|
if (value === null || value === void 0) return value;
|
|
2531
2598
|
if (Array.isArray(value)) return value.map((v) => redact(v));
|
|
2532
|
-
|
|
2599
|
+
const record = asRecord(value);
|
|
2600
|
+
if (record) {
|
|
2533
2601
|
const out = {};
|
|
2534
|
-
for (const [
|
|
2535
|
-
else out[k] = redact(v);
|
|
2602
|
+
for (const [key, field] of Object.entries(record)) out[key] = asString(field) !== void 0 && SECRET_KEY_PATTERN.test(key) ? REDACTED : redact(field);
|
|
2536
2603
|
return out;
|
|
2537
2604
|
}
|
|
2538
|
-
|
|
2539
|
-
return value;
|
|
2605
|
+
const text = asString(value);
|
|
2606
|
+
return text !== void 0 && SECRET_VALUE_PATTERN.test(text) ? REDACTED : value;
|
|
2540
2607
|
}
|
|
2541
2608
|
/**
|
|
2542
2609
|
* Print a JSON payload to stdout. If `--output-file` was configured, also
|
|
@@ -2812,7 +2879,7 @@ function readPlanFromFile(filePath) {
|
|
|
2812
2879
|
throw new AutotelError({
|
|
2813
2880
|
type: "validation",
|
|
2814
2881
|
code: AutotelErrorCodes.E_INVALID_PLAN,
|
|
2815
|
-
message: `Plan file is not valid JSON: ${error.message}`
|
|
2882
|
+
message: `Plan file is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
2816
2883
|
});
|
|
2817
2884
|
}
|
|
2818
2885
|
}
|
|
@@ -2828,7 +2895,7 @@ async function readPlanFromInput(input) {
|
|
|
2828
2895
|
throw new AutotelError({
|
|
2829
2896
|
type: "validation",
|
|
2830
2897
|
code: AutotelErrorCodes.E_INVALID_INPUT,
|
|
2831
|
-
message: `stdin did not contain valid JSON: ${error.message}`
|
|
2898
|
+
message: `stdin did not contain valid JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
2832
2899
|
});
|
|
2833
2900
|
}
|
|
2834
2901
|
}
|
|
@@ -4880,7 +4947,7 @@ const traceRule = {
|
|
|
4880
4947
|
question: "Does this entry point produce a span?",
|
|
4881
4948
|
weight: 40,
|
|
4882
4949
|
appliesTo: { kinds: HANDLER_KINDS },
|
|
4883
|
-
fix: ({ target }) => `export const handler =
|
|
4950
|
+
fix: ({ target }) => `export const handler = instrument({ key: '${target.method?.toLowerCase() ?? "handle"} ${target.path}', fn: async () => { /* … */ } });`,
|
|
4884
4951
|
check(context) {
|
|
4885
4952
|
if (usesAutotelApi(context, SPAN_WRAPPERS)) return null;
|
|
4886
4953
|
if (context.project.ambientTracing) return null;
|
|
@@ -5057,7 +5124,7 @@ const RULES = [
|
|
|
5057
5124
|
kinds: HANDLER_KINDS,
|
|
5058
5125
|
when: ({ project }) => project.hasLlmDependency
|
|
5059
5126
|
},
|
|
5060
|
-
fix: () => "traceGenAI({ operation: 'chat', model }
|
|
5127
|
+
fix: () => "traceGenAI({ operation: 'chat', model })((ctx) => async () => client.chat.completions.create(…));",
|
|
5061
5128
|
check(context) {
|
|
5062
5129
|
const llmCall = LLM_CALLS.flatMap((name) => context.facts.memberCalls(name)).find((call) => call.root !== null && call.root !== "JSON");
|
|
5063
5130
|
if (!llmCall) return null;
|
|
@@ -6703,6 +6770,70 @@ const COMMANDS = [
|
|
|
6703
6770
|
}
|
|
6704
6771
|
]
|
|
6705
6772
|
},
|
|
6773
|
+
{
|
|
6774
|
+
name: "estimate",
|
|
6775
|
+
description: "Estimate what a month of telemetry costs, before and after canonical log lines",
|
|
6776
|
+
args: [],
|
|
6777
|
+
flags: [
|
|
6778
|
+
...GLOBAL_FLAGS,
|
|
6779
|
+
...AGENT_FLAGS,
|
|
6780
|
+
{
|
|
6781
|
+
name: "--requests-per-month",
|
|
6782
|
+
takesValue: true,
|
|
6783
|
+
description: "Requests the application serves per month (required)"
|
|
6784
|
+
},
|
|
6785
|
+
{
|
|
6786
|
+
name: "--per-gb",
|
|
6787
|
+
takesValue: true,
|
|
6788
|
+
description: "USD per gigabyte ingested by your provider (required — no rate is assumed)"
|
|
6789
|
+
},
|
|
6790
|
+
{
|
|
6791
|
+
name: "--log-lines-per-request",
|
|
6792
|
+
takesValue: true,
|
|
6793
|
+
description: "Log lines written per request today (default 4)"
|
|
6794
|
+
},
|
|
6795
|
+
{
|
|
6796
|
+
name: "--spans-per-request",
|
|
6797
|
+
takesValue: true,
|
|
6798
|
+
description: "Spans exported per request after instrumenting (default 0)"
|
|
6799
|
+
},
|
|
6800
|
+
{
|
|
6801
|
+
name: "--per-million-events",
|
|
6802
|
+
takesValue: true,
|
|
6803
|
+
description: "USD per million events indexed, if metered separately"
|
|
6804
|
+
},
|
|
6805
|
+
{
|
|
6806
|
+
name: "--keep-percent",
|
|
6807
|
+
takesValue: true,
|
|
6808
|
+
description: "Traffic kept after sampling, applied to both shapes (default 100)"
|
|
6809
|
+
}
|
|
6810
|
+
],
|
|
6811
|
+
mutating: false,
|
|
6812
|
+
network: false,
|
|
6813
|
+
writesFiles: false,
|
|
6814
|
+
supportsDryRun: false,
|
|
6815
|
+
requiresPackageJson: false,
|
|
6816
|
+
mayReadEnv: false,
|
|
6817
|
+
supportsJson: true,
|
|
6818
|
+
examples: [
|
|
6819
|
+
{
|
|
6820
|
+
description: "Ten million requests a month at four log lines each",
|
|
6821
|
+
command: "autotel estimate --requests-per-month 10000000 --per-gb 0.10"
|
|
6822
|
+
},
|
|
6823
|
+
{
|
|
6824
|
+
description: "Include the spans instrumenting will add",
|
|
6825
|
+
command: "autotel estimate --requests-per-month 10000000 --per-gb 0.10 --spans-per-request 6 --json"
|
|
6826
|
+
},
|
|
6827
|
+
{
|
|
6828
|
+
description: "A provider that meters indexed events as well as bytes",
|
|
6829
|
+
command: "autotel estimate --requests-per-month 300000000 --per-gb 0.50 --per-million-events 2.50"
|
|
6830
|
+
},
|
|
6831
|
+
{
|
|
6832
|
+
description: "What head sampling at 10% does to the bill",
|
|
6833
|
+
command: "autotel estimate --requests-per-month 10000000 --per-gb 0.10 --keep-percent 10"
|
|
6834
|
+
}
|
|
6835
|
+
]
|
|
6836
|
+
},
|
|
6706
6837
|
{
|
|
6707
6838
|
name: "codemod trace",
|
|
6708
6839
|
description: "Wrap functions in trace() with span name from function/variable/method name",
|
|
@@ -7289,26 +7420,40 @@ function runSchemaOutputs(opts) {
|
|
|
7289
7420
|
printJson({
|
|
7290
7421
|
ok: true,
|
|
7291
7422
|
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[]"
|
|
7423
|
+
outputs: {
|
|
7424
|
+
"autotel init --json": {
|
|
7425
|
+
ok: "boolean",
|
|
7426
|
+
command: "string",
|
|
7427
|
+
detected: {
|
|
7428
|
+
packages: "array of { name, version, resolution: \"target\" | \"workspace-root\" }",
|
|
7429
|
+
logger: "\"pino\" | \"winston\" | \"bunyan\" | null",
|
|
7430
|
+
backend: "{ source: \"env\" | \"wrangler\" | \"deps\" | \"prompt\" | \"default\", value: string }",
|
|
7431
|
+
platform: "\"cloudflare\" | \"aws-lambda\" | \"edge\" | null"
|
|
7306
7432
|
},
|
|
7307
|
-
|
|
7308
|
-
|
|
7433
|
+
plan: {
|
|
7434
|
+
presets: "string[] (slugs)",
|
|
7435
|
+
packagesToInstall: {
|
|
7436
|
+
prod: "string[]",
|
|
7437
|
+
dev: "string[]"
|
|
7438
|
+
},
|
|
7439
|
+
filesToWrite: "array of { path, action: \"create\" | \"merge\" | \"skip\" }",
|
|
7440
|
+
envVars: "array of { name, sensitive, action }"
|
|
7441
|
+
},
|
|
7442
|
+
nextSteps: "string[]"
|
|
7309
7443
|
},
|
|
7310
|
-
|
|
7311
|
-
|
|
7444
|
+
"autotel estimate --json": {
|
|
7445
|
+
ok: "boolean",
|
|
7446
|
+
command: "string",
|
|
7447
|
+
estimate: {
|
|
7448
|
+
currency: "\"USD\"",
|
|
7449
|
+
before: "{ events: number, gb: number, cost: number }",
|
|
7450
|
+
after: "{ events: number, gb: number, cost: number }",
|
|
7451
|
+
saved: "number (USD/month, negative when instrumenting costs more)",
|
|
7452
|
+
savedPercent: "number",
|
|
7453
|
+
basis: "{ logLineBytes, canonicalLineBytes, spanBytes, bytesFrom: \"measured\" | \"caller\", perGb, perMillionEvents, keepPercent, bytesPerGb }"
|
|
7454
|
+
}
|
|
7455
|
+
}
|
|
7456
|
+
}
|
|
7312
7457
|
});
|
|
7313
7458
|
}
|
|
7314
7459
|
/** `autotel commands [--json]` — compact listing. */
|
|
@@ -7476,6 +7621,23 @@ function toInvestigateError(command, error) {
|
|
|
7476
7621
|
});
|
|
7477
7622
|
}
|
|
7478
7623
|
//#endregion
|
|
7624
|
+
//#region src/lib/opts.ts
|
|
7625
|
+
/** A flag declared without an argument parser: Commander yields its string. */
|
|
7626
|
+
function stringOpt(o, key) {
|
|
7627
|
+
const value = o[key];
|
|
7628
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
7629
|
+
}
|
|
7630
|
+
/** A flag declared with `intArg` or another numeric parser. */
|
|
7631
|
+
function numberOpt(o, key) {
|
|
7632
|
+
const value = o[key];
|
|
7633
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
7634
|
+
}
|
|
7635
|
+
/** A boolean flag: present or absent. */
|
|
7636
|
+
function boolOpt(o, key) {
|
|
7637
|
+
const value = o[key];
|
|
7638
|
+
return typeof value === "boolean" ? value : void 0;
|
|
7639
|
+
}
|
|
7640
|
+
//#endregion
|
|
7479
7641
|
//#region src/commands/investigate/cli-helpers.ts
|
|
7480
7642
|
/**
|
|
7481
7643
|
* Shared commander wiring for investigate commands. Each command group's
|
|
@@ -7501,22 +7663,22 @@ function addStaticFlags(cmd) {
|
|
|
7501
7663
|
function backendFlagsFromOpts(opts) {
|
|
7502
7664
|
return {
|
|
7503
7665
|
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
|
|
7666
|
+
jaegerBaseUrl: stringOpt(opts, "jaegerBaseUrl"),
|
|
7667
|
+
tempoBaseUrl: stringOpt(opts, "tempoBaseUrl"),
|
|
7668
|
+
prometheusBaseUrl: stringOpt(opts, "prometheusBaseUrl"),
|
|
7669
|
+
lokiBaseUrl: stringOpt(opts, "lokiBaseUrl"),
|
|
7670
|
+
collectorPort: numberOpt(opts, "collectorPort"),
|
|
7671
|
+
fixturePath: stringOpt(opts, "fixturePath"),
|
|
7672
|
+
logfireBaseUrl: stringOpt(opts, "logfireBaseUrl"),
|
|
7673
|
+
datadogSite: stringOpt(opts, "datadogSite"),
|
|
7674
|
+
signozBaseUrl: stringOpt(opts, "signozBaseUrl"),
|
|
7675
|
+
outputFile: stringOpt(opts, "outputFile"),
|
|
7514
7676
|
noSecrets: opts.secretsInOutput === false
|
|
7515
7677
|
};
|
|
7516
7678
|
}
|
|
7517
7679
|
function staticFlagsFromOpts(opts) {
|
|
7518
7680
|
return {
|
|
7519
|
-
outputFile: opts
|
|
7681
|
+
outputFile: stringOpt(opts, "outputFile"),
|
|
7520
7682
|
noSecrets: opts.secretsInOutput === false
|
|
7521
7683
|
};
|
|
7522
7684
|
}
|
|
@@ -7528,12 +7690,12 @@ function addTimeWindowFlags(cmd) {
|
|
|
7528
7690
|
}
|
|
7529
7691
|
function windowFlagsFromOpts(opts) {
|
|
7530
7692
|
return {
|
|
7531
|
-
serviceName: opts
|
|
7532
|
-
operationName: opts
|
|
7533
|
-
lookbackMinutes: opts
|
|
7534
|
-
from: opts
|
|
7535
|
-
to: opts
|
|
7536
|
-
limit: opts
|
|
7693
|
+
serviceName: stringOpt(opts, "serviceName"),
|
|
7694
|
+
operationName: stringOpt(opts, "operationName"),
|
|
7695
|
+
lookbackMinutes: numberOpt(opts, "lookbackMinutes"),
|
|
7696
|
+
from: stringOpt(opts, "from"),
|
|
7697
|
+
to: stringOpt(opts, "to"),
|
|
7698
|
+
limit: numberOpt(opts, "limit")
|
|
7537
7699
|
};
|
|
7538
7700
|
}
|
|
7539
7701
|
//#endregion
|
|
@@ -7727,8 +7889,8 @@ function registerHealthCommands(program) {
|
|
|
7727
7889
|
const opts = this.opts();
|
|
7728
7890
|
await runHealth({
|
|
7729
7891
|
...backendFlagsFromOpts(opts),
|
|
7730
|
-
otlpEndpoint: opts
|
|
7731
|
-
freshnessTimeoutMs: opts
|
|
7892
|
+
otlpEndpoint: stringOpt(opts, "otlpEndpoint"),
|
|
7893
|
+
freshnessTimeoutMs: numberOpt(opts, "freshnessTimeoutMs"),
|
|
7732
7894
|
otlpEncoding: opts.otlpEncoding
|
|
7733
7895
|
});
|
|
7734
7896
|
});
|
|
@@ -7800,26 +7962,26 @@ function registerDiscoveryCommands(program) {
|
|
|
7800
7962
|
const o = this.optsWithGlobals();
|
|
7801
7963
|
await runDiscoverServices({
|
|
7802
7964
|
...backendFlagsFromOpts(o),
|
|
7803
|
-
limitServices: o
|
|
7804
|
-
traceSample: o
|
|
7805
|
-
logSample: o
|
|
7806
|
-
metricSample: o
|
|
7965
|
+
limitServices: numberOpt(o, "limitServices"),
|
|
7966
|
+
traceSample: numberOpt(o, "traceSample"),
|
|
7967
|
+
logSample: numberOpt(o, "logSample"),
|
|
7968
|
+
metricSample: numberOpt(o, "metricSample")
|
|
7807
7969
|
});
|
|
7808
7970
|
});
|
|
7809
7971
|
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
7972
|
const o = this.optsWithGlobals();
|
|
7811
7973
|
await runDiscoverTraceFields({
|
|
7812
7974
|
...backendFlagsFromOpts(o),
|
|
7813
|
-
search: o
|
|
7814
|
-
sampleSize: o
|
|
7975
|
+
search: stringOpt(o, "search"),
|
|
7976
|
+
sampleSize: numberOpt(o, "sampleSize")
|
|
7815
7977
|
});
|
|
7816
7978
|
});
|
|
7817
7979
|
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
7980
|
const o = this.optsWithGlobals();
|
|
7819
7981
|
await runDiscoverLogFields({
|
|
7820
7982
|
...backendFlagsFromOpts(o),
|
|
7821
|
-
search: o
|
|
7822
|
-
sampleSize: o
|
|
7983
|
+
search: stringOpt(o, "search"),
|
|
7984
|
+
sampleSize: numberOpt(o, "sampleSize")
|
|
7823
7985
|
});
|
|
7824
7986
|
});
|
|
7825
7987
|
addBackendFlags(discoverCmd);
|
|
@@ -7856,62 +8018,62 @@ function registerQueryCommands(program) {
|
|
|
7856
8018
|
const o = this.optsWithGlobals();
|
|
7857
8019
|
await runQueryTraces({
|
|
7858
8020
|
...backendFlagsFromOpts(o),
|
|
7859
|
-
serviceName: o
|
|
7860
|
-
operationName: o
|
|
7861
|
-
lookbackMinutes: o
|
|
7862
|
-
from: o
|
|
7863
|
-
to: o
|
|
7864
|
-
limit: o
|
|
7865
|
-
errorOnly: o
|
|
8021
|
+
serviceName: stringOpt(o, "serviceName"),
|
|
8022
|
+
operationName: stringOpt(o, "operationName"),
|
|
8023
|
+
lookbackMinutes: numberOpt(o, "lookbackMinutes"),
|
|
8024
|
+
from: stringOpt(o, "from"),
|
|
8025
|
+
to: stringOpt(o, "to"),
|
|
8026
|
+
limit: numberOpt(o, "limit"),
|
|
8027
|
+
errorOnly: boolOpt(o, "errorOnly"),
|
|
7866
8028
|
statusCode: o.statusCode,
|
|
7867
|
-
minDurationMs: o
|
|
7868
|
-
maxDurationMs: o
|
|
7869
|
-
genAiSystem: o
|
|
7870
|
-
genAiRequestModel: o
|
|
7871
|
-
genAiResponseModel: o
|
|
8029
|
+
minDurationMs: numberOpt(o, "minDurationMs"),
|
|
8030
|
+
maxDurationMs: numberOpt(o, "maxDurationMs"),
|
|
8031
|
+
genAiSystem: stringOpt(o, "genAiSystem"),
|
|
8032
|
+
genAiRequestModel: stringOpt(o, "genAiRequestModel"),
|
|
8033
|
+
genAiResponseModel: stringOpt(o, "genAiResponseModel")
|
|
7872
8034
|
});
|
|
7873
8035
|
});
|
|
7874
8036
|
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
8037
|
const o = this.optsWithGlobals();
|
|
7876
8038
|
await runQuerySpans({
|
|
7877
8039
|
...backendFlagsFromOpts(o),
|
|
7878
|
-
serviceName: o
|
|
7879
|
-
operationName: o
|
|
7880
|
-
lookbackMinutes: o
|
|
7881
|
-
from: o
|
|
7882
|
-
to: o
|
|
7883
|
-
limit: o
|
|
7884
|
-
errorOnly: o
|
|
8040
|
+
serviceName: stringOpt(o, "serviceName"),
|
|
8041
|
+
operationName: stringOpt(o, "operationName"),
|
|
8042
|
+
lookbackMinutes: numberOpt(o, "lookbackMinutes"),
|
|
8043
|
+
from: stringOpt(o, "from"),
|
|
8044
|
+
to: stringOpt(o, "to"),
|
|
8045
|
+
limit: numberOpt(o, "limit"),
|
|
8046
|
+
errorOnly: boolOpt(o, "errorOnly"),
|
|
7885
8047
|
statusCode: o.statusCode,
|
|
7886
|
-
minDurationMs: o
|
|
7887
|
-
maxDurationMs: o
|
|
8048
|
+
minDurationMs: numberOpt(o, "minDurationMs"),
|
|
8049
|
+
maxDurationMs: numberOpt(o, "maxDurationMs")
|
|
7888
8050
|
});
|
|
7889
8051
|
});
|
|
7890
8052
|
const metricsCmd = addTimeWindowFlags(new Command("metrics")).description("List metric series").option("--metric-name <name>", "Filter by metric name").action(async function() {
|
|
7891
8053
|
const o = this.optsWithGlobals();
|
|
7892
8054
|
await runQueryMetrics({
|
|
7893
8055
|
...backendFlagsFromOpts(o),
|
|
7894
|
-
metricName: o
|
|
7895
|
-
serviceName: o
|
|
7896
|
-
lookbackMinutes: o
|
|
7897
|
-
from: o
|
|
7898
|
-
to: o
|
|
7899
|
-
limit: o
|
|
8056
|
+
metricName: stringOpt(o, "metricName"),
|
|
8057
|
+
serviceName: stringOpt(o, "serviceName"),
|
|
8058
|
+
lookbackMinutes: numberOpt(o, "lookbackMinutes"),
|
|
8059
|
+
from: stringOpt(o, "from"),
|
|
8060
|
+
to: stringOpt(o, "to"),
|
|
8061
|
+
limit: numberOpt(o, "limit")
|
|
7900
8062
|
});
|
|
7901
8063
|
});
|
|
7902
8064
|
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
8065
|
const o = this.optsWithGlobals();
|
|
7904
8066
|
await runQueryLogs({
|
|
7905
8067
|
...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
|
|
8068
|
+
serviceName: stringOpt(o, "serviceName"),
|
|
8069
|
+
traceId: stringOpt(o, "traceId"),
|
|
8070
|
+
spanId: stringOpt(o, "spanId"),
|
|
8071
|
+
severityText: stringOpt(o, "severityText"),
|
|
8072
|
+
text: stringOpt(o, "text"),
|
|
8073
|
+
lookbackMinutes: numberOpt(o, "lookbackMinutes"),
|
|
8074
|
+
from: stringOpt(o, "from"),
|
|
8075
|
+
to: stringOpt(o, "to"),
|
|
8076
|
+
limit: numberOpt(o, "limit")
|
|
7915
8077
|
});
|
|
7916
8078
|
});
|
|
7917
8079
|
addBackendFlags(queryCmd);
|
|
@@ -7966,8 +8128,8 @@ function registerTopologyCommands(program) {
|
|
|
7966
8128
|
const o = this.optsWithGlobals();
|
|
7967
8129
|
await runServiceMap({
|
|
7968
8130
|
...backendFlagsFromOpts(o),
|
|
7969
|
-
lookbackMinutes: o
|
|
7970
|
-
limit: o
|
|
8131
|
+
lookbackMinutes: numberOpt(o, "lookbackMinutes"),
|
|
8132
|
+
limit: numberOpt(o, "limit")
|
|
7971
8133
|
});
|
|
7972
8134
|
});
|
|
7973
8135
|
addBackendFlags(topologyCmd);
|
|
@@ -8084,9 +8246,9 @@ function registerDiagnoseCommands(program) {
|
|
|
8084
8246
|
const o = this.optsWithGlobals();
|
|
8085
8247
|
await runDiagnoseAnomalies({
|
|
8086
8248
|
...backendFlagsFromOpts(o),
|
|
8087
|
-
service: o
|
|
8088
|
-
operation: o
|
|
8089
|
-
lookbackMinutes: o
|
|
8249
|
+
service: stringOpt(o, "service"),
|
|
8250
|
+
operation: stringOpt(o, "operation"),
|
|
8251
|
+
lookbackMinutes: numberOpt(o, "lookbackMinutes")
|
|
8090
8252
|
});
|
|
8091
8253
|
});
|
|
8092
8254
|
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 +8261,19 @@ function registerDiagnoseCommands(program) {
|
|
|
8099
8261
|
const o = this.optsWithGlobals();
|
|
8100
8262
|
await runDiagnoseErrors({
|
|
8101
8263
|
...backendFlagsFromOpts(o),
|
|
8102
|
-
service: o
|
|
8103
|
-
lookbackMinutes: o
|
|
8104
|
-
limit: o
|
|
8264
|
+
service: stringOpt(o, "service"),
|
|
8265
|
+
lookbackMinutes: numberOpt(o, "lookbackMinutes"),
|
|
8266
|
+
limit: numberOpt(o, "limit")
|
|
8105
8267
|
});
|
|
8106
8268
|
});
|
|
8107
8269
|
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
8270
|
const o = this.optsWithGlobals();
|
|
8109
8271
|
await runDiagnoseSlos({
|
|
8110
8272
|
...backendFlagsFromOpts(o),
|
|
8111
|
-
service: o
|
|
8112
|
-
p99LatencyMs: o
|
|
8113
|
-
maxErrorRate: o
|
|
8114
|
-
lookbackMinutes: o
|
|
8273
|
+
service: stringOpt(o, "service") ?? "",
|
|
8274
|
+
p99LatencyMs: numberOpt(o, "p99LatencyMs"),
|
|
8275
|
+
maxErrorRate: numberOpt(o, "maxErrorRate"),
|
|
8276
|
+
lookbackMinutes: numberOpt(o, "lookbackMinutes")
|
|
8115
8277
|
});
|
|
8116
8278
|
});
|
|
8117
8279
|
addBackendFlags(diagnoseCmd);
|
|
@@ -8170,8 +8332,8 @@ function registerCorrelateCommands(program) {
|
|
|
8170
8332
|
const o = this.optsWithGlobals();
|
|
8171
8333
|
await runExplainSlowdown({
|
|
8172
8334
|
...backendFlagsFromOpts(o),
|
|
8173
|
-
service: o
|
|
8174
|
-
lookbackMinutes: o
|
|
8335
|
+
service: stringOpt(o, "service") ?? "",
|
|
8336
|
+
lookbackMinutes: numberOpt(o, "lookbackMinutes")
|
|
8175
8337
|
});
|
|
8176
8338
|
});
|
|
8177
8339
|
addBackendFlags(correlateCmd);
|
|
@@ -8282,71 +8444,71 @@ function registerLlmCommands(program) {
|
|
|
8282
8444
|
const o = this.optsWithGlobals();
|
|
8283
8445
|
await runLlmUsage({
|
|
8284
8446
|
...backendFlagsFromOpts(o),
|
|
8285
|
-
startTime: o
|
|
8286
|
-
endTime: o
|
|
8287
|
-
serviceName: o
|
|
8288
|
-
genAiSystem: o
|
|
8289
|
-
genAiRequestModel: o
|
|
8290
|
-
genAiResponseModel: o
|
|
8291
|
-
limit: o
|
|
8447
|
+
startTime: stringOpt(o, "startTime"),
|
|
8448
|
+
endTime: stringOpt(o, "endTime"),
|
|
8449
|
+
serviceName: stringOpt(o, "serviceName"),
|
|
8450
|
+
genAiSystem: stringOpt(o, "genAiSystem"),
|
|
8451
|
+
genAiRequestModel: stringOpt(o, "genAiRequestModel"),
|
|
8452
|
+
genAiResponseModel: stringOpt(o, "genAiResponseModel"),
|
|
8453
|
+
limit: numberOpt(o, "limit")
|
|
8292
8454
|
});
|
|
8293
8455
|
});
|
|
8294
8456
|
const modelsCmd = commonOpts(new Command("models")).description("Discover LLM models in use").action(async function() {
|
|
8295
8457
|
const o = this.optsWithGlobals();
|
|
8296
8458
|
await runLlmModels({
|
|
8297
8459
|
...backendFlagsFromOpts(o),
|
|
8298
|
-
startTime: o
|
|
8299
|
-
endTime: o
|
|
8300
|
-
serviceName: o
|
|
8301
|
-
genAiSystem: o
|
|
8302
|
-
limit: o
|
|
8460
|
+
startTime: stringOpt(o, "startTime"),
|
|
8461
|
+
endTime: stringOpt(o, "endTime"),
|
|
8462
|
+
serviceName: stringOpt(o, "serviceName"),
|
|
8463
|
+
genAiSystem: stringOpt(o, "genAiSystem"),
|
|
8464
|
+
limit: numberOpt(o, "limit")
|
|
8303
8465
|
});
|
|
8304
8466
|
});
|
|
8305
8467
|
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
8468
|
const o = this.optsWithGlobals();
|
|
8307
8469
|
await runLlmModelStats({
|
|
8308
8470
|
...backendFlagsFromOpts(o),
|
|
8309
|
-
modelName: o
|
|
8310
|
-
startTime: o
|
|
8311
|
-
endTime: o
|
|
8312
|
-
serviceName: o
|
|
8471
|
+
modelName: stringOpt(o, "modelName") ?? "",
|
|
8472
|
+
startTime: stringOpt(o, "startTime"),
|
|
8473
|
+
endTime: stringOpt(o, "endTime"),
|
|
8474
|
+
serviceName: stringOpt(o, "serviceName")
|
|
8313
8475
|
});
|
|
8314
8476
|
});
|
|
8315
8477
|
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
8478
|
const o = this.optsWithGlobals();
|
|
8317
8479
|
await runLlmExpensive({
|
|
8318
8480
|
...backendFlagsFromOpts(o),
|
|
8319
|
-
startTime: o
|
|
8320
|
-
endTime: o
|
|
8321
|
-
serviceName: o
|
|
8322
|
-
genAiRequestModel: o
|
|
8323
|
-
genAiResponseModel: o
|
|
8324
|
-
minTokens: o
|
|
8325
|
-
limit: o
|
|
8481
|
+
startTime: stringOpt(o, "startTime"),
|
|
8482
|
+
endTime: stringOpt(o, "endTime"),
|
|
8483
|
+
serviceName: stringOpt(o, "serviceName"),
|
|
8484
|
+
genAiRequestModel: stringOpt(o, "genAiRequestModel"),
|
|
8485
|
+
genAiResponseModel: stringOpt(o, "genAiResponseModel"),
|
|
8486
|
+
minTokens: numberOpt(o, "minTokens"),
|
|
8487
|
+
limit: numberOpt(o, "limit")
|
|
8326
8488
|
});
|
|
8327
8489
|
});
|
|
8328
8490
|
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
8491
|
const o = this.optsWithGlobals();
|
|
8330
8492
|
await runLlmSlow({
|
|
8331
8493
|
...backendFlagsFromOpts(o),
|
|
8332
|
-
startTime: o
|
|
8333
|
-
endTime: o
|
|
8334
|
-
serviceName: o
|
|
8335
|
-
genAiRequestModel: o
|
|
8336
|
-
genAiResponseModel: o
|
|
8337
|
-
minDurationMs: o
|
|
8338
|
-
limit: o
|
|
8494
|
+
startTime: stringOpt(o, "startTime"),
|
|
8495
|
+
endTime: stringOpt(o, "endTime"),
|
|
8496
|
+
serviceName: stringOpt(o, "serviceName"),
|
|
8497
|
+
genAiRequestModel: stringOpt(o, "genAiRequestModel"),
|
|
8498
|
+
genAiResponseModel: stringOpt(o, "genAiResponseModel"),
|
|
8499
|
+
minDurationMs: numberOpt(o, "minDurationMs"),
|
|
8500
|
+
limit: numberOpt(o, "limit")
|
|
8339
8501
|
});
|
|
8340
8502
|
});
|
|
8341
8503
|
const toolsCmd = commonOpts(new Command("tools")).description("Discover tool/function spans grouped by tool name").action(async function() {
|
|
8342
8504
|
const o = this.optsWithGlobals();
|
|
8343
8505
|
await runLlmTools({
|
|
8344
8506
|
...backendFlagsFromOpts(o),
|
|
8345
|
-
startTime: o
|
|
8346
|
-
endTime: o
|
|
8347
|
-
serviceName: o
|
|
8348
|
-
genAiSystem: o
|
|
8349
|
-
limit: o
|
|
8507
|
+
startTime: stringOpt(o, "startTime"),
|
|
8508
|
+
endTime: stringOpt(o, "endTime"),
|
|
8509
|
+
serviceName: stringOpt(o, "serviceName"),
|
|
8510
|
+
genAiSystem: stringOpt(o, "genAiSystem"),
|
|
8511
|
+
limit: numberOpt(o, "limit")
|
|
8350
8512
|
});
|
|
8351
8513
|
});
|
|
8352
8514
|
addBackendFlags(llmCmd);
|
|
@@ -8431,7 +8593,7 @@ function registerScoreCommands(program) {
|
|
|
8431
8593
|
const o = this.optsWithGlobals();
|
|
8432
8594
|
await runScoreSpan({
|
|
8433
8595
|
...staticFlagsFromOpts(o),
|
|
8434
|
-
spanFile: o
|
|
8596
|
+
spanFile: stringOpt(o, "spanFile")
|
|
8435
8597
|
});
|
|
8436
8598
|
});
|
|
8437
8599
|
const explainCmd = addStaticFlags(new Command("explain")).description("Explain the instrumentation scoring rubric").action(async function() {
|
|
@@ -8539,7 +8701,7 @@ function registerCollectorCommands(program) {
|
|
|
8539
8701
|
const o = this.optsWithGlobals();
|
|
8540
8702
|
await runCollectorValidate({
|
|
8541
8703
|
...staticFlagsFromOpts(o),
|
|
8542
|
-
configFile: o
|
|
8704
|
+
configFile: stringOpt(o, "configFile")
|
|
8543
8705
|
});
|
|
8544
8706
|
});
|
|
8545
8707
|
const suggestCmd = addStaticFlags(new Command("suggest")).description("Print a minimal OTLP receiver config").action(async function() {
|
|
@@ -8555,7 +8717,7 @@ function registerCollectorCommands(program) {
|
|
|
8555
8717
|
const o = this.optsWithGlobals();
|
|
8556
8718
|
await runCollectorComponents({
|
|
8557
8719
|
...staticFlagsFromOpts(o),
|
|
8558
|
-
version: o
|
|
8720
|
+
version: stringOpt(o, "version"),
|
|
8559
8721
|
kind: o.kind
|
|
8560
8722
|
});
|
|
8561
8723
|
});
|
|
@@ -8564,8 +8726,8 @@ function registerCollectorCommands(program) {
|
|
|
8564
8726
|
await runCollectorSchema({
|
|
8565
8727
|
...staticFlagsFromOpts(o),
|
|
8566
8728
|
kind: o.kind,
|
|
8567
|
-
name: o
|
|
8568
|
-
version: o
|
|
8729
|
+
name: stringOpt(o, "name") ?? "",
|
|
8730
|
+
version: stringOpt(o, "version")
|
|
8569
8731
|
});
|
|
8570
8732
|
});
|
|
8571
8733
|
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 +8735,8 @@ function registerCollectorCommands(program) {
|
|
|
8573
8735
|
await runCollectorReadme({
|
|
8574
8736
|
...staticFlagsFromOpts(o),
|
|
8575
8737
|
kind: o.kind,
|
|
8576
|
-
name: o
|
|
8577
|
-
version: o
|
|
8738
|
+
name: stringOpt(o, "name") ?? "",
|
|
8739
|
+
version: stringOpt(o, "version")
|
|
8578
8740
|
});
|
|
8579
8741
|
});
|
|
8580
8742
|
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 +8744,9 @@ function registerCollectorCommands(program) {
|
|
|
8582
8744
|
await runCollectorValidateComponent({
|
|
8583
8745
|
...staticFlagsFromOpts(o),
|
|
8584
8746
|
kind: o.kind,
|
|
8585
|
-
name: o
|
|
8586
|
-
version: o
|
|
8587
|
-
configFile: o
|
|
8747
|
+
name: stringOpt(o, "name") ?? "",
|
|
8748
|
+
version: stringOpt(o, "version"),
|
|
8749
|
+
configFile: stringOpt(o, "configFile")
|
|
8588
8750
|
});
|
|
8589
8751
|
});
|
|
8590
8752
|
const refreshCmd = addStaticFlags(new Command("refresh")).description("Refresh in-memory collector metadata cache").action(async function() {
|
|
@@ -8880,8 +9042,8 @@ function registerSecurityCommands(program) {
|
|
|
8880
9042
|
await runSecurityEvents({
|
|
8881
9043
|
...backendFlagsFromOpts(o),
|
|
8882
9044
|
...windowFlagsFromOpts(o),
|
|
8883
|
-
category: o
|
|
8884
|
-
severity: o
|
|
9045
|
+
category: stringOpt(o, "category"),
|
|
9046
|
+
severity: stringOpt(o, "severity")
|
|
8885
9047
|
});
|
|
8886
9048
|
});
|
|
8887
9049
|
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 +9148,47 @@ function composeDrains(...drains) {
|
|
|
8986
9148
|
function resolveEndpoint(explicit) {
|
|
8987
9149
|
return explicit ?? process.env.AUTOTEL_TELEMETRY_ENDPOINT;
|
|
8988
9150
|
}
|
|
9151
|
+
/** A finite number, as opposed to the strings and markers around it. */
|
|
9152
|
+
function isNumber(value) {
|
|
9153
|
+
return Number.isFinite(value);
|
|
9154
|
+
}
|
|
8989
9155
|
const SENSITIVE_PATTERN = /(?:token|secret|password|key|auth|credential|api[_-]?key|private)/i;
|
|
8990
9156
|
function sanitizeFlags(argv, allowlistedStringFlags = []) {
|
|
8991
|
-
const
|
|
9157
|
+
const entries = [];
|
|
9158
|
+
/** What a flag's value is worth reporting as, once the key is known safe. */
|
|
9159
|
+
const valueOf = (key, rawValue) => {
|
|
9160
|
+
if (/^\d+$/.test(rawValue)) return Number(rawValue);
|
|
9161
|
+
if (allowlistedStringFlags.includes(key)) return rawValue;
|
|
9162
|
+
return { present: true };
|
|
9163
|
+
};
|
|
8992
9164
|
for (let i = 0; i < argv.length; i++) {
|
|
8993
9165
|
const arg = argv[i];
|
|
8994
9166
|
if (arg === void 0 || !arg.startsWith("-")) continue;
|
|
8995
9167
|
const key = arg.replace(/^-+/, "").split("=")[0] ?? arg;
|
|
8996
9168
|
if (SENSITIVE_PATTERN.test(key)) {
|
|
8997
|
-
|
|
9169
|
+
entries.push([key, { present: true }]);
|
|
8998
9170
|
continue;
|
|
8999
9171
|
}
|
|
9000
9172
|
if (arg.includes("=")) {
|
|
9001
9173
|
const [, rawValue] = arg.split("=");
|
|
9002
9174
|
if (rawValue === void 0) continue;
|
|
9003
|
-
|
|
9004
|
-
else if (allowlistedStringFlags.includes(key)) out[key] = rawValue;
|
|
9005
|
-
else out[key] = { present: true };
|
|
9175
|
+
entries.push([key, valueOf(key, rawValue)]);
|
|
9006
9176
|
continue;
|
|
9007
9177
|
}
|
|
9008
9178
|
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 };
|
|
9179
|
+
if (next && !next.startsWith("-")) {
|
|
9180
|
+
entries.push([key, valueOf(key, next)]);
|
|
9017
9181
|
i++;
|
|
9018
|
-
}
|
|
9019
|
-
else out[key] = true;
|
|
9182
|
+
} else entries.push([key, true]);
|
|
9020
9183
|
}
|
|
9021
|
-
return
|
|
9184
|
+
return Object.fromEntries(entries);
|
|
9022
9185
|
}
|
|
9023
9186
|
function sanitizeCustom(fields) {
|
|
9024
|
-
|
|
9025
|
-
|
|
9026
|
-
|
|
9027
|
-
|
|
9187
|
+
return Object.fromEntries(Object.entries(fields).filter(([, value]) => value !== null && value !== void 0).map(([key, value]) => {
|
|
9188
|
+
if (value === true || value === false) return [key, value];
|
|
9189
|
+
if (isNumber(value)) return [key, value];
|
|
9190
|
+
return [key, { present: true }];
|
|
9191
|
+
}));
|
|
9028
9192
|
}
|
|
9029
9193
|
const runStorage = new AsyncLocalStorage();
|
|
9030
9194
|
let activeHandle = null;
|
|
@@ -9036,7 +9200,7 @@ var InternalTelemetry = class {
|
|
|
9036
9200
|
constructor(options, enabled) {
|
|
9037
9201
|
this.options = options;
|
|
9038
9202
|
this.enabled = enabled;
|
|
9039
|
-
this.outbox = new TelemetryOutbox({ toolName: options.name });
|
|
9203
|
+
this.outbox = options.outbox ?? new TelemetryOutbox({ toolName: options.name });
|
|
9040
9204
|
const endpoint = resolveEndpoint(options.endpoint);
|
|
9041
9205
|
const drains = [createDebugDrain(), endpoint ? createHttpDrain(endpoint) : createNoopDrain()];
|
|
9042
9206
|
this.drain = composeDrains(...drains);
|
|
@@ -9190,6 +9354,62 @@ function runTelemetryDisclosure(toolName, version) {
|
|
|
9190
9354
|
console.log(disclosure.markdown);
|
|
9191
9355
|
}
|
|
9192
9356
|
//#endregion
|
|
9357
|
+
//#region src/commands/estimate.ts
|
|
9358
|
+
/** `keepPercent` → `--keep-percent`, so the fix hint names a real flag. */
|
|
9359
|
+
function flagFor(field) {
|
|
9360
|
+
return `--${field.replaceAll(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`;
|
|
9361
|
+
}
|
|
9362
|
+
/**
|
|
9363
|
+
* Answer "what will this telemetry cost me a month" as a JSON envelope.
|
|
9364
|
+
*
|
|
9365
|
+
* Throws `AutotelError` rather than exiting, so the top-level handler picks
|
|
9366
|
+
* pretty or JSON output and maps the code — bad input is the caller's to fix
|
|
9367
|
+
* (exit 2), not a broken run (exit 1).
|
|
9368
|
+
*/
|
|
9369
|
+
function runEstimate(input) {
|
|
9370
|
+
try {
|
|
9371
|
+
return {
|
|
9372
|
+
ok: true,
|
|
9373
|
+
command: "estimate",
|
|
9374
|
+
estimate: estimateCost(input)
|
|
9375
|
+
};
|
|
9376
|
+
} catch (error) {
|
|
9377
|
+
if (error instanceof EstimateInputError) throw new AutotelError({
|
|
9378
|
+
type: "validation",
|
|
9379
|
+
code: AutotelErrorCodes.E_INVALID_INPUT,
|
|
9380
|
+
message: error.message,
|
|
9381
|
+
fix: `Pass ${flagFor(error.field)} with a value in ${error.expected}.`,
|
|
9382
|
+
expected: { [error.field]: error.expected }
|
|
9383
|
+
});
|
|
9384
|
+
throw error;
|
|
9385
|
+
}
|
|
9386
|
+
}
|
|
9387
|
+
//#endregion
|
|
9388
|
+
//#region src/ui/estimate.ts
|
|
9389
|
+
const usd = (value) => `$${value.toFixed(2)}`;
|
|
9390
|
+
/**
|
|
9391
|
+
* The human view. Every figure the estimate rests on is printed with it —
|
|
9392
|
+
* a cost number without its basis is a number nobody can argue with, which is
|
|
9393
|
+
* the wrong property for a number that will end up in a budget conversation.
|
|
9394
|
+
*/
|
|
9395
|
+
function printEstimate(estimate) {
|
|
9396
|
+
const { before, after, basis } = estimate;
|
|
9397
|
+
const lines = [
|
|
9398
|
+
"",
|
|
9399
|
+
chalk.bold(" Telemetry cost per month"),
|
|
9400
|
+
"",
|
|
9401
|
+
` ${chalk.dim("today")} ${before.events.toLocaleString()} events ${before.gb} GB ${chalk.bold(usd(before.cost))}`,
|
|
9402
|
+
` ${chalk.dim("canonical")} ${after.events.toLocaleString()} events ${after.gb} GB ${chalk.bold(usd(after.cost))}`,
|
|
9403
|
+
"",
|
|
9404
|
+
estimate.saved > 0 ? ` ${chalk.green(`saves ${usd(estimate.saved)}/month (${estimate.savedPercent}%)`)}` : ` ${chalk.yellow(`costs ${usd(Math.abs(estimate.saved))}/month more`)}`,
|
|
9405
|
+
"",
|
|
9406
|
+
chalk.dim(` basis: ${basis.logLineBytes} B per log line, ${basis.canonicalLineBytes} B per canonical line, ${basis.spanBytes} B per span (${basis.bytesFrom})`),
|
|
9407
|
+
chalk.dim(` ${usd(basis.perGb)}/GB, ${usd(basis.perMillionEvents)}/million events, ${basis.keepPercent}% of traffic kept`),
|
|
9408
|
+
""
|
|
9409
|
+
];
|
|
9410
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
9411
|
+
}
|
|
9412
|
+
//#endregion
|
|
9193
9413
|
//#region src/cli.ts
|
|
9194
9414
|
/**
|
|
9195
9415
|
* Create the CLI program
|
|
@@ -9310,6 +9530,27 @@ function createProgram() {
|
|
|
9310
9530
|
codemodCmd.addCommand(traceCmd);
|
|
9311
9531
|
addGlobalOptions(codemodCmd);
|
|
9312
9532
|
program.addCommand(codemodCmd);
|
|
9533
|
+
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) => {
|
|
9534
|
+
configureJsonOutput({
|
|
9535
|
+
outputFile: opts.outputFile,
|
|
9536
|
+
noSecrets: opts.secretsInOutput === false
|
|
9537
|
+
});
|
|
9538
|
+
const envelope = runEstimate({
|
|
9539
|
+
requestsPerMonth: Number(opts.requestsPerMonth),
|
|
9540
|
+
perGb: Number(opts.perGb),
|
|
9541
|
+
...opts.logLinesPerRequest !== void 0 && { logLinesPerRequest: Number(opts.logLinesPerRequest) },
|
|
9542
|
+
...opts.spansPerRequest !== void 0 && { spansPerRequest: Number(opts.spansPerRequest) },
|
|
9543
|
+
...opts.perMillionEvents !== void 0 && { perMillionEvents: Number(opts.perMillionEvents) },
|
|
9544
|
+
...opts.keepPercent !== void 0 && { keepPercent: Number(opts.keepPercent) }
|
|
9545
|
+
});
|
|
9546
|
+
if (opts.json) {
|
|
9547
|
+
printJson(envelope);
|
|
9548
|
+
return;
|
|
9549
|
+
}
|
|
9550
|
+
printEstimate(envelope.estimate);
|
|
9551
|
+
});
|
|
9552
|
+
addGlobalOptions(estimateCmd);
|
|
9553
|
+
program.addCommand(estimateCmd);
|
|
9313
9554
|
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
9555
|
runSchema({
|
|
9315
9556
|
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.1",
|
|
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.1"
|
|
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"
|