UNPKG

@controlplane/cli

Version:

Control Plane Corporation CLI

284 lines 12.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MetricsQuery = exports.MetricsCmd = void 0; exports.resolveExpr = resolveExpr; exports.resolveQueryPlan = resolveQueryPlan; exports.flattenPrometheusResult = flattenPrometheusResult; const command_1 = require("../cli/command"); const functions_1 = require("../util/functions"); const time_1 = require("../util/time"); const parse_duration_1 = require("../util/parse-duration"); const resultFetcher_1 = require("../rest/resultFetcher"); const api_1 = require("../rest/api"); const client_1 = require("../session/client"); const options_1 = require("./options"); // ANCHOR - Constants const DEFAULT_SINCE = '1h'; const DEFAULT_STEP = '60s'; const MAX_RANGE_SAMPLES = 11000; // ANCHOR - Options /** * Adds the `cpln metrics query` positional argument and time-range options. * * @param {Argv} yargs - The yargs instance to configure. * @returns {Argv} The configured yargs instance. */ function withMetricsQueryOptions(yargs) { const group = 'Query options:'; return yargs.options({ expr: { group, requiresArg: true, describe: 'PromQL expression (alternative to the positional argument)', }, from: { group, requiresArg: true, describe: 'Range start (ISO 8601, Unix timestamp, or relative duration, e.g., 2025-10-23T07:00:00Z, 7d, now-1h)', }, to: { group, requiresArg: true, describe: 'Range end (ISO 8601, Unix timestamp, or relative duration; defaults to now)', }, since: { group, requiresArg: true, describe: `Lookback window before --to (or now); mutually exclusive with --from (default ${DEFAULT_SINCE} when a range is requested)`, }, step: { group, requiresArg: true, describe: `Range query resolution step, e.g., 15s, 1m, 5m (default ${DEFAULT_STEP})`, }, time: { group, requiresArg: true, describe: 'Evaluation time for an instant query (ISO 8601, Unix timestamp, or relative like now-5m); mutually exclusive with --from/--to/--since', }, }); } // ANCHOR - Command class MetricsCmd extends command_1.Command { constructor() { super(...arguments); this.command = 'metrics'; this.describe = 'Query metrics'; } builder(yargs) { return yargs.demandCommand().version(false).help().command(new MetricsQuery().toYargs()); } handle() { } } exports.MetricsCmd = MetricsCmd; class MetricsQuery extends command_1.Command { constructor() { super(...arguments); this.command = 'query [expr]'; this.describe = 'Execute a PromQL query'; } builder(yargs) { return (0, functions_1.pipe)( // withMetricsQueryOptions, options_1.withProfileOptions, options_1.withOrgOptions, options_1.withFormatOptions, options_1.withRequestOptions, options_1.withDebugOptions)(yargs); } async handle(args) { var _a, _b, _c, _d, _e, _f; // Metrics are org-scoped this.requireOrg(); // Resolve the PromQL expression from the positional argument or --expr const expr = resolveExpr(args); if (!expr) { this.session.abort({ message: 'ERROR: A PromQL expression is required. Provide it as an argument or with --expr.' }); } // Resolve instant vs. range and validate the time flags const result = resolveQueryPlan(expr, args, Date.now()); if (!result.ok) { this.session.abort({ message: `ERROR: ${result.error}` }); } const plan = result.plan; // Resolve the metrics host from service discovery const metricsEndpoint = await (0, api_1.getDiscoveryEndpoint)(this.session.request.endpoint, 'metrics'); if (!metricsEndpoint) { this.session.abort({ message: 'ERROR: The metrics endpoint is not available for this environment.' }); } // Build a client bound to the metrics host, reusing the session credentials const metricsClient = (0, client_1.makeSessionClient)(this.session, metricsEndpoint); // The Prometheus API is org-scoped in the path const verb = plan.instant ? 'query' : 'query_range'; const path = `/metrics/org/${this.session.context.org}/api/v1/${verb}`; // Query the metrics API (Prometheus returns the full result set in one response — no pagination) let body; try { body = await metricsClient.get(path, { params: plan.params }); } catch (e) { // Surface the Prometheus error text when present const message = (_d = (_c = (_b = (_a = e.response) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.error) !== null && _c !== void 0 ? _c : e.message) !== null && _d !== void 0 ? _d : 'Unknown error'; this.session.abort({ message: `ERROR: Metrics query failed: ${message}`, error: e }); } // A 2xx response may still carry an error status if (body.status === 'error') { this.session.abort({ message: `ERROR: Metrics query failed: ${(_f = (_e = body.error) !== null && _e !== void 0 ? _e : body.errorType) !== null && _f !== void 0 ? _f : 'Unknown error'}` }); } // Flatten the Prometheus result into rows and cap them by --max const items = (0, resultFetcher_1.applyMaxAndWarn)(this.session, flattenPrometheusResult(body.data)); // Wrap as a list; the metricSample itemKind selects the registered table model for text output const list = { kind: 'list', itemKind: 'metricSample', items }; await this.session.outFormat(list); } } exports.MetricsQuery = MetricsQuery; // ANCHOR - Functions /** * Resolves the PromQL expression from the positional argument and/or --expr. * * @param {Arguments<MetricsQueryOptions>} args - The parsed command-line arguments. * @returns {string | null} The trimmed expression, or null when none was provided. */ function resolveExpr(args) { const raw = args.expr; // The positional and --expr share one key; the parser collapses them (and repeated --expr) // to the last scalar value, so an array only appears defensively — take the last entry const value = Array.isArray(raw) ? raw[raw.length - 1] : raw; if (typeof value !== 'string' || value.trim() === '') { return null; } return value.trim(); } /** * Resolves the query into an instant or range plan and validates the time flags. * Instant is the default; the presence of --from/--to/--since/--step selects range; * --time forces an instant query at a specific moment. * * @param {string} query - The PromQL expression to run. * @param {Arguments<MetricsQueryOptions>} args - The parsed command-line arguments. * @param {number} nowMillis - The current time in milliseconds. * @returns {QueryPlanResult} A valid plan, or an error describing the invalid flag combination. */ function resolveQueryPlan(query, args, nowMillis) { var _a, _b; const nowSeconds = Math.round(nowMillis / 1000); const hasRangeFlag = args.from !== undefined || args.to !== undefined || args.since !== undefined || args.step !== undefined; // --time is only meaningful for an instant query if (args.time !== undefined && hasRangeFlag) { return { ok: false, error: '--time cannot be combined with --from, --to, --since, or --step.' }; } // --since and --from both set the start boundary if (args.since !== undefined && args.from !== undefined) { return { ok: false, error: '--since and --from are mutually exclusive.' }; } // Instant query: at --time when given, otherwise at now if (!hasRangeFlag) { let time = nowSeconds; if (args.time !== undefined) { const parsed = (0, time_1.parseTimeToUnixSeconds)(args.time, nowMillis); if (parsed === null) { return { ok: false, error: invalidTimeMessage('--time', args.time) }; } time = parsed; } return { ok: true, plan: { instant: true, params: { query, time } } }; } // Range query: resolve the end boundary (--to or now) let end = nowSeconds; if (args.to !== undefined) { const parsed = (0, time_1.parseTimeToUnixSeconds)(args.to, nowMillis); if (parsed === null) { return { ok: false, error: invalidTimeMessage('--to', args.to) }; } end = parsed; } // Resolve the start boundary from --from, or from the --since lookback before end let start; if (args.from !== undefined) { const parsed = (0, time_1.parseTimeToUnixSeconds)(args.from, nowMillis); if (parsed === null) { return { ok: false, error: invalidTimeMessage('--from', args.from) }; } start = parsed; } else { const sinceValue = (_a = args.since) !== null && _a !== void 0 ? _a : DEFAULT_SINCE; const sinceMs = (0, parse_duration_1.parseDuration)(sinceValue); if (sinceMs === null || sinceMs <= 0) { return { ok: false, error: `Invalid --since duration: ${sinceValue}. Use a relative duration, e.g., 30m, 6h, 7d.` }; } start = end - Math.round(sinceMs / 1000); } if (end <= start) { return { ok: false, error: 'The range end must be after the range start.' }; } // Resolve and validate the step const stepValue = (_b = args.step) !== null && _b !== void 0 ? _b : DEFAULT_STEP; const stepMs = (0, parse_duration_1.parseDuration)(stepValue); if (stepMs === null || stepMs <= 0) { return { ok: false, error: `Invalid --step: ${stepValue}. Use a duration, e.g., 15s, 1m, 5m.` }; } const stepSeconds = stepMs / 1000; // Guard against exceeding Prometheus's per-series sample cap const sampleCount = (end - start) / stepSeconds; if (sampleCount > MAX_RANGE_SAMPLES) { return { ok: false, error: `The requested range needs ${Math.ceil(sampleCount)} samples per series, exceeding the ${MAX_RANGE_SAMPLES} limit. Use a larger --step or a narrower range.`, }; } return { ok: true, plan: { instant: false, params: { query, start, end, step: stepValue } } }; } /** * Builds the standard "invalid time" error message for a given flag. * * @param {string} flag - The originating flag name. * @param {string} value - The rejected value. * @returns {string} A human-readable error message. */ function invalidTimeMessage(flag, value) { return `Invalid ${flag} format: ${value}. Use ISO 8601 (e.g., 2025-10-23T07:00:00Z), a Unix timestamp, or a relative duration (e.g., 7d, 1h, now-30m).`; } /** * Flattens a Prometheus response `data` object into a flat list of samples. * Vector series yield one row each; matrix series yield one row per data point. * * @param {PrometheusData | undefined} data - The `data` field of a Prometheus response. * @returns {MetricSample[]} The flattened samples, empty when there is no data. */ function flattenPrometheusResult(data) { var _a; if (!data || !data.result) { return []; } const samples = []; // scalar / string results are a single [ts, value] pair, not an array of series if (data.resultType === 'scalar' || data.resultType === 'string') { const point = data.result; samples.push({ metric: {}, timestamp: toIso(point[0]), value: point[1] }); return samples; } const series = data.result; for (const one of series) { const metric = (_a = one.metric) !== null && _a !== void 0 ? _a : {}; // matrix: many points per series if (one.values) { for (const [ts, value] of one.values) { samples.push({ metric, timestamp: toIso(ts), value }); } continue; } // vector: a single point per series if (one.value) { samples.push({ metric, timestamp: toIso(one.value[0]), value: one.value[1] }); } } return samples; } /** * Converts Unix seconds (possibly fractional) into an ISO 8601 timestamp string. * * @param {number} seconds - The time in Unix seconds. * @returns {string} The ISO 8601 representation. */ function toIso(seconds) { return new Date(seconds * 1000).toISOString(); } //# sourceMappingURL=metrics.js.map