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.
91%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Low
Low-risk findings worth noting
Columns, filters, and measures for kq.metrics("<Type>").aggregate() / kq.metrics("<Type>").raw()
(and the client.searchMetrics(...) / client.discoverMetrics() shapes). Run a built query with
client.query(q) (fully typed: returns { data } for aggregate, { data, nextCursor } for raw).
Dashboard tiles render raw rows: when feeding a metric tile via
dataSource:{method:"query"}, build it withkq.metrics("<Type>").raw()(not.aggregate()) — see thecreate-dashboardskill.
const { metrics } = await client.discoverMetrics(); — each entry is
{ name, type, unit, description, attributes, resourceAttributes }. Use it to pick a
metric's exact name and type before querying.
Choose the MetricType as the builder argument: kq.metrics("Gauge")… auto-pins it (type
∈ "Gauge" | "Sum" | "Histogram" | "ExponentialHistogram" | "Summary"). No manual
MetricType filter is needed. Value columns are typed per type, since each type stores its
value in different structural columns.
kq.metrics("Gauge") // auto-pins MetricType: "Gauge"
.aggregate()
.where((f) => f.eq("MetricName", "system.cpu.utilization"));| Type | Value column(s) | Use case |
|---|---|---|
Gauge | Value | Point-in-time: CPU, memory, connections |
Sum | Value | Cumulative counters: request/error counts |
Histogram | Count, Sum, Min, Max, BucketCounts, ExplicitBounds | Latency distributions |
ExponentialHistogram | Count, Sum, Positive/NegativeBucketCounts, … | Distributions |
Summary | Count, Sum, ValueAtQuantiles.* | Pre-computed quantiles |
For Gauge, aggregate over Value (avg/max). For a counter (Sum) or a Histogram, use the
distribution and counter measures below: avg/p95 of a counter's Value or a histogram's
Sum/Count mean nothing. On a histogram's Sum/Count they are refused; on a counter's
Value they are accepted, so do not reach for them.
MetricName, MetricType, MetricUnit, MetricDescription, TimeUnix,
StartTimeUnix, Value, Count, Sum, Min, Max, BucketCounts, ExplicitBounds,
ScopeName, ScopeVersion. Containers for metric attributes: Attributes
(data-point), ResourceAttributes, ScopeAttributes.
// avg CPU by host over the last hour, bucketed every minute
kq.metrics("Gauge")
.aggregate()
.measure((m) => m.avg("Value", "avg_cpu"))
.where((f) => f.eq("MetricName", "system.cpu.utilization"))
.dimension("host.name")
.timeRelative("1h")
.timeSeries("1m")
.build();Percentiles (p50–p999) are ClickHouse-only; on SQLite use avg/max.
Each method exists only on the builder for its type, and each handles cumulative and delta data, restarts and staleness markers. All are ClickHouse-only.
| Builder | Method | Gives |
|---|---|---|
kq.metrics("Sum") | rate("Value", a), increase(…) | Per-second rate / total growth of a counter |
kq.metrics("Histogram") | histogramQuantile(0.95, a) | p95 from merged buckets |
kq.metrics("Histogram") | histogramAvg(a) | Mean observation (ΔSum ÷ ΔCount) |
kq.metrics("Histogram") | rate("Count", a), increase(…) | Requests per second / requests in the window |
kq.metrics("Summary") | summaryQuantile(0.99, a) | Highest per-instance pre-computed quantile |
// error rate from a monotonic counter: the IsMonotonic filter is required
kq.metrics("Sum")
.aggregate()
.where((f) => f.eq("MetricName", "http.server.errors"))
.where((f) => f.eq("IsMonotonic", true))
.measure((m) => m.rate("Value", "errors_per_s"))
.timeRelative("1h")
.timeSeries("1m")
.build();
// p95 latency from a histogram, per service
kq.metrics("Histogram")
.aggregate()
.where((f) => f.eq("MetricName", "http.server.request.duration"))
.measure((m) => m.histogramQuantile(0.95, "p95"))
.dimension("service.name")
.timeRelative("1h")
.timeSeries("5m")
.build();number | null, and null is never zero. What a null means depends on the
measure:
histogramQuantile/histogramAvg: read <alias>_error first; a non-null code means
the group has no honest value (below). With no error, nothing was observed.summaryQuantile (no _error key): no instance's latest point reports that quantile;
try another quantile before concluding there is no data.increase/rate (no _error key): nothing was observed.histogramQuantile rows also carry <alias>_low/_high (the bucket the true value lies
in), _coarsened and _error. Report only the value to people ("p95 ≈ 430 ms"); use
the range yourself as the uncertainty around the value: the value is interpolated inside
the bucket, so a move that stays inside one range is weak evidence. Confirm it elsewhere
(e.g. trace latency) before calling it a regression, but don't discard it. When _high is
null the value is only the lower bound of the open-ended last bucket: say "p95 > 430 ms".
When _low is null, say "p95 ≤ 430 ms"._error "mixed_units": filter to one MetricUnit. "no_shared_bounds": the series'
bucket layouts cannot be merged; group by a series attribute. "malformed_buckets": an
exporter sent bucket counts that do not fit the bounds; find and filter out that series.MetricName, MetricUnit,
ScopeName, ScopeVersion, IsMonotonic, attributes), and group by time with
.timeSeries(), not by TimeUnix.| Filter | Flag | Example |
|---|---|---|
| Type (required) | --type | --type Gauge |
| Name | --name | --name system.cpu.utilization |
| Service | --service | --service payment |
| Attribute | --attr | --attr "host.name=web-1" |
| Aggregate (Gauge/Sum only) | --aggregate | --aggregate sum |
| Group by | --group-by | --group-by host.name (repeatable) |
npx @kopai/cli metrics discover --json
npx @kopai/cli metrics search --type Gauge --name system.cpu.utilization --jsonclient.searchMetrics({ metricType, metricName, serviceName, attributes, aggregate, groupBy, limit })
mirrors these (metricType required; aggregate is Gauge/Sum-only and groupBy requires aggregate; limit caps at 1000).