mcpField
R
root-cause-analysis

root-cause-analysis

github.com/kopai-appCloud & DevOps1044 installsUpdated 1d ago
Repository

About root-cause-analysis

root-cause-analysis is a Kopai-focused skill for investigating production issues using OpenTelemetry traces, logs, and metrics. It guides TypeScript SDK queries to find errors, correlate evidence, quantify impact, and report likely causes.

Key features

  1. 01Typed TypeScript SDK queries
  2. 02Trace, log, and metric analysis
  3. 03Error-rate and throughput ranking
  4. 04Time-series impact analysis
  5. 05Trace and log correlation
  6. 06SQLite query caveat guidance

Use cases

  1. 01Ranking services during incident triage
  2. 02Tracing an error through related logs
  3. 03Measuring when and how widely an issue began
  4. 04Investigating latency and metric changes

MCP support

This is a skill for using Kopai's SDK and CLI, not an MCP server or client. The README does not describe MCP integration.

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/kopai-app/kopai-mono --skill root-cause-analysis

After installing, type/root-cause-analysisin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

tile.json
references/
log-filters.md
metric-filters.md
trace-filters.md
rules/
_sections.md
pattern-distributed.md
pattern-http-errors.md
pattern-log-driven.md
pattern-slow-requests.md
workflow-check-metrics.md
workflow-correlate-logs.md
workflow-find-errors.md
workflow-get-context.md
workflow-identify-cause.md

Details

From the project README

Root Cause Analysis with Kopai

Debug production issues from telemetry (traces, logs, metrics). Kopai's analytical power — error rate, throughput, latency, group-by, time-series bucketing — lives in the SDK query API, which the CLI does not expose. So lead every investigation by writing a short TypeScript script; the CLI stays as a quick-lookup fallback.

Prerequisites

  • Services are sending OpenTelemetry data to Kopai (see the otel-instrumentation skill).
  • @kopai/sdk is installed in the project: npm i @kopai/sdk (it ships the query API and the .kopairc reader).

Code mode (recommended)

Connect — clientFromConfig() reads .kopairc exactly like the CLI:

import { clientFromConfig } from "@kopai/sdk/node";
// Reads ./.kopairc then ~/.kopairc; defaults to http://localhost:8000.
// Override any field: clientFromConfig({ url, token, configPath, timeout }).
const client = clientFromConfig();

Run — npx tsx rca.mts. Use the .mts extension: it is always an ES module, so top-level await works without depending on the host project's package.json "type". (If tsx is missing: node --experimental-strip-types rca.mts.)

Build a query with kq, run it with client.query(q). client.query(q) is the single clean way to run any built query and is fully typed — rows autocomplete and typos are compile errors. Aggregate queries return { data }; raw queries return { data, nextCursor }. Result rows are fully typed — iterate data directly and never cast to any/any[]. Aggregate measure values are number (so row.error_rate * 100 just works — no String(...)/cast), grouped dimensions are present (row["service.name"]), and .timeSeries() rows add bucket_start:

const { data } = await client.query(q); // typed rows — do NOT write `data as any[]`
for (const row of data) {
  console.log(
    row["service.name"],
    `${(row.error_rate * 100).toFixed(1)}%`,
    row.rps
  );
}

Output — stdout is your result. End with console.log(JSON.stringify(data, null, 2)), and wrap .build() + the query call in try/catch:

import { kq, KopaiQueryBuildError } from "@kopai/sdk";
try {
  const q = kq.traces
    .aggregate() /* … */
    .build();
  const { data } = await client.query(q);
  console.log(JSON.stringify(data, null, 2));
} catch (e) {
  if (e instanceof KopaiQueryBuildError)
    console.error(e.issues); // {path,message}[]
  else throw e;
}

Backend caveats & gotchas (read before querying)

  • Every query needs a time window — .timeRelative("1h") or .timeAbsolute(startISO, endISO). Lookback/granularity match ^[1-9]\d*[smhdw]$ ("30s", "15m", "2h", "7d").
  • StatusCode is exactly "Unset" | "Ok" | "Error" (title case). Successful spans are usually "Unset", not "Ok". This enum is type-checked: a wrong value like "ERROR" is a compile error, so the casing trap is caught for you. Prefer the errorRate measure (counts errors server-side) over filtering the raw value anyway.
  • Use service.name (dotted), never ServiceName. ServiceName is not a queryable column — filtering on it silently matches nothing. (In result rows the value comes back keyed as you grouped it, e.g. row["service.name"].)
  • Empty result on a busy system ≈ a wrong column/value, not real absence. Before concluding "no errors / no data", re-check the column name and value casing — silent empties are the #1 way to reach a false "all healthy" conclusion.
  • Percentiles (p50–p999) are ClickHouse-only. On SQLite they fail at query time (KopaiError: Percentile measures … not yet supported on the sqlite backend). Lead latency with avg/max of Duration; treat percentiles as a ClickHouse upgrade in try/catch.
  • Duration filters accept duration strings with units s/m/h/d/w — f.gt("Duration", "1s"), f.lte("Duration", "2h") (also gte/lt). No sub-second units; for sub-second thresholds pass a nanosecond number. (Result rows still report Duration in nanoseconds — avg/max of Duration come back in ns: 1ms = 1e6, 1s = 1e9.)
  • Metric queries choose the type up front: kq.metrics("Gauge")…, kq.metrics("Sum")… (type ∈ "Gauge" | "Sum" | "Histogram" | "ExponentialHistogram" | "Summary"). The builder arg auto-pins the MetricType, so no manual .where(f => f.eq("MetricType", …)). Value columns are typed per type: Gauge/Sum → "Value"; Histogram/ExponentialHistogram → "Count" | "Sum" | "Min" | "Max"; Summary → "Count" | "Sum".
  • searchTraces/searchLogs/searchMetrics are async iterables (auto-paginate) — consume with for await (const row of client.searchLogs({ … })) { … }, do not await them as an array. For a single page use searchTracesPage/searchLogsPage/searchMetricsPage → { data, nextCursor }. Limits: kq .limit() caps at 10000; search* filters cap at 1000.

RCA Workflow

  1. Find the failing work — rank services by error rate and throughput. errorRate handles StatusCode server-side, so you never guess the value:

    const q = kq.traces
      .aggregate()
      .measure((m) => m.errorRate("error_rate"))
      .measure((m) => m.throughput("rps"))
      .measure((m) => m.count("spans"))
      .dimension("service.name")
      .timeRelative("1h")
      .summary()
      .orderByMeasure("error_rate", "desc")
      .build();
    const { data } = await client.query(q);
    

    For log-first triage, pull error-level logs by SeverityNumber >= 17 (catches ERROR/FATAL regardless of text casing): client.query(kq.logs.raw().where(f => f.gte("SeverityNumber", 17)).timeRelative("1h").limit(50).build()). See workflow-find-errors.

  2. Get full trace context — const spans = await client.getTrace(traceId). Inspect Duration (bottlenecks), ParentSpanId (call chain), StatusMessage, SpanKind. See workflow-get-context.

  3. Correlate logs to the trace — client.query(kq.logs.raw().where(f => f.eq("TraceId", traceId)).timeRelative("1h").build()) (or iterate client.searchLogs({ traceId })). Look for the earliest error and its stack trace. See workflow-correlate-logs.

  4. Quantify impact / pinpoint onset — the upgrade the CLI can't do. Bucket the metric over time to see when it started and how wide the blast radius is. Watch the retention floor: if the earliest bucket is already elevated, the true onset is at or before it — say so rather than calling the floor a "step change":

    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);
    

    When the signal is a metric rather than a trace, choose the type up front and let it auto-pin MetricType — e.g. bucket a Gauge over time:

    const q = kq
      .metrics("Gauge")
      .aggregate()
      .measure((m) => m.avg("Value", "avg_value"))
      .where((f) => f.eq("MetricName", "process.cpu.utilization"))
      .timeRelative("3h")
      .timeSeries("5m")
      .build();
    const { data } = await client.query(q);
    

    See workflow-check-metrics.

  5. Present findings — root cause with evidence (specific TraceIds, log entries, metric deltas), blast radius, and a suggested fix. Offer to build an incident dashboard (see the create-dashboard skill). See workflow-identify-cause.

Quick Example

// rca-triage.mts — run: npx tsx rca-triage.mts
import { kq, KopaiQueryBuildError } from "@kopai/sdk";
import { clientFromConfig } from "@kopai/sdk/node";
const client = clientFromConfig();

try {
  const q = kq.traces
    .aggregate()
    .measure((m) => m.errorRate("error_rate"))
    .measure((m) => m.throughput("rps"))
    .measure((m) => m.avg("Duration", "avg_ns"))
    .measure((m) => m.max("Duration", "max_ns"))
    .measure((m) => m.count("spans"))
    .dimension("service.name")
    .timeRelative("1h")
    .summary()
    .orderByMeasure("error_rate", "desc")
    .build();

  const { data } = await client.query(q); // typed rows
  console.log(JSON.stringify(data, null, 2));
} catch (e) {
  if (e instanceof KopaiQueryBuildError) console.error(e.issues);
  else throw e;
}

Rules

1. Workflow (CRITICAL)
  • workflow-find-errors - Find error traces and error-level logs
  • workflow-get-context - Get full trace context
  • workflow-correlate-logs - Correlate logs with a trace
  • workflow-check-metrics - Quantify impact with aggregate/time-series queries
  • workflow-identify-cause - Identify root cause & present findings
2. Patterns (HIGH)
  • pattern-http-errors - HTTP error debugging
  • pattern-slow-requests - Slow request analysis
  • pattern-distributed - Distributed failure tracing
  • pattern-log-driven - Log-driven investigation

Read rules/<rule-name>.md for details.

Tips

  1. Start from the aggregate (error rate / latency by service), then drill into one trace.
  2. Prefer the errorRate measure over filtering StatusCode (counted server-side).
  3. Run any built query with client.query(q) — it is fully typed (rows autocomplete, typos compile-error).
  4. Filter Duration with strings (f.gt("Duration", "1s")); result rows still report Duration in nanoseconds. The earliest error in a trace chain is usually closest to root cause.
  5. Use .timeSeries(granularity) to find when a regression started — and don't mistake the retention floor for the onset.
  6. For metrics, pick the type up front — kq.metrics("Gauge")… (auto-pins MetricType, types the value columns).
  7. On SQLite, latency = avg/max of Duration; switch to p95/p99 on ClickHouse.

CLI fallback (quick one-offs)

The CLI is fine for a single lookup, but has no query/aggregation command. Note the StatusCode value is the literal "Error".

npx @kopai/cli traces search --status-code Error --limit 20 --json
npx @kopai/cli traces get <traceId> --json
npx @kopai/cli logs search --trace-id <traceId> --severity-min 17 --json
npx @kopai/cli metrics discover --json

References

Comments

Markdown supported