Investigate and root-cause production issues from live OpenTelemetry telemetry (traces, logs, metrics) with the Kopai SDK in TypeScript code mode. Use this skill to debug errors and error-rate spikes, investigate latency/slowness and timeouts, trace requests across services, find failing or cascading services, and correlate logs to a trace — including vague symptom reports like 'why is my API slow', 'getting 500 errors', 'service is down', 'requests are timing out', or 'something is failing in prod', even when the user never says 'traces' or 'observability'. This analyzes existing telemetry to find a cause. Do NOT use it to add instrumentation (use otel-instrumentation), to build dashboards or visualizations (use create-dashboard), or to fix non-telemetry problems like failing CI, lint errors, unit tests, or SQL query tuning.
77
97%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
| title | impact | tags |
|---|---|---|
| Step 4: Check Metrics | CRITICAL | workflow, metrics, step4 |
Impact: CRITICAL
This is the upgrade the CLI cannot do: bucket measures over time to see when the
regression started and how wide the blast radius is. .timeSeries(granularity)
turns a flat "it's broken" into a precise onset and a per-service spread.
Traces already carry error rate and latency — no metric discovery needed. Bucket over a
window wide enough to bracket the onset (3h), at a granularity fine enough to spot the
inflection (5m). errorRate counts errors server-side; result rows still report
Duration in nanoseconds, so avg/max come back in ns.
import { kq, KopaiQueryBuildError } from "@kopai/sdk";
// const client = clientFromConfig();
try {
const q = kq.traces
.aggregate()
.measure((m) => m.errorRate("error_rate"))
.measure((m) => m.avg("Duration", "avg_ns"))
.measure((m) => m.max("Duration", "max_ns"))
.dimension("service.name")
.timeRelative("3h")
.timeSeries("5m")
.orderByMeasure("error_rate", "desc")
.build();
const { data } = await client.query(q);
console.log(JSON.stringify(data, null, 2)); // each row: bucket + service + measures
} catch (e) {
if (e instanceof KopaiQueryBuildError) console.error(e.issues);
else throw e;
}Read the buckets: the first one where error_rate (or avg_ns/max_ns) jumps is the
onset; the set of service.name values that move together is the blast radius.
ClickHouse upgrade — add p95/p99 for tail latency. Percentiles are ClickHouse-only
and hard-fail at query time on SQLite, so isolate them in their own try/catch and keep
avg/max as the portable baseline:
try {
const q = kq.traces
.aggregate()
.measure((m) => m.p95("Duration", "p95_ns"))
.measure((m) => m.p99("Duration", "p99_ns"))
.dimension("service.name")
.timeRelative("3h")
.timeSeries("5m")
.build();
const { data } = await client.query(q);
console.log(JSON.stringify(data, null, 2));
} catch (e) {
console.error("percentiles need ClickHouse; fall back to avg/max:", e);
}When the symptom is a system resource (CPU, memory, queue depth) rather than request
errors, switch to the metrics pipeline — but discover first to get the exact name
and type:
const { metrics } = await client.discoverMetrics();
// each entry: { name, type, unit, description, attributes, resourceAttributes }
console.log(metrics.map((m) => `${m.name} (${m.type}, ${m.unit})`));The MetricType is a builder argument: kq.metrics("<Type>") auto-pins it, where the
type is one of "Gauge"/"Sum"/"Histogram"/"ExponentialHistogram"/"Summary". Value
columns are typed per type: "Value" for Gauge/Sum; Count/Sum/Min/Max for
Histogram/ExponentialHistogram; Count/Sum for Summary. Bucket with
.timeSeries() to align the metric's onset against the trace onset from (a).
const NAME = "http.server.errors"; // from discoverMetrics()
try {
const q = kq
.metrics("Sum")
.aggregate()
.measure((m) => m.avg("Value", "v"))
.where((f) => f.eq("MetricName", NAME))
.timeRelative("1h")
.timeSeries("1m")
.build();
const { data } = await client.query(q);
console.log(JSON.stringify(data, null, 2));
} catch (e) {
if (e instanceof KopaiQueryBuildError) console.error(e.issues);
else throw e;
}errorRate measure (a), or a Sum metric like
http.server.errors (c). Use kq.metrics("Sum"), aggregate Value.avg/max of Duration (portable); p95/p99 only on
ClickHouse. For a metric-side view use kq.metrics("Histogram") and aggregate
Count/Sum/Max.Gauge. Use
kq.metrics("Gauge"), aggregate Value (avg/max), .dimension("host.name") for
per-host spread.Whatever the signal, .timeSeries(granularity) is what reveals onset — a .summary()
only confirms the issue exists.
See references/metric-filters.md (MetricType builder arg, value columns, aggregations) and references/trace-filters.md (Duration units, measure ops).
The CLI has no aggregation or time-series bucketing — use it only to discover a metric or
eyeball one type. Aggregate (--aggregate) is Gauge/Sum-only.
npx @kopai/cli metrics discover --json
npx @kopai/cli metrics search --type Sum --name http.server.errors --json
npx @kopai/cli metrics search --type Histogram --name http.server.duration --json
npx @kopai/cli metrics search --type Gauge --service payment-api --json