From 47fe82cd16e67ca50ff48782d7d5c8d9fba42e2c Mon Sep 17 00:00:00 2001 From: Vladimir Adamic Date: Mon, 9 Mar 2026 20:42:41 +0100 Subject: [PATCH 1/8] feat(skills): optimize skills --- skills/create-dashboard/SKILL.md | 2 +- skills/create-dashboard/rules/workflow.md | 21 ++++++++ skills/otel-instrumentation/SKILL.md | 2 +- .../otel-instrumentation/rules/_sections.md | 12 ++--- .../rules/setup-environment.md | 20 ++++++++ skills/root-cause-analysis/SKILL.md | 3 +- skills/root-cause-analysis/rules/_sections.md | 8 +-- .../rules/workflow-identify-cause.md | 49 +++++++++++++++++++ 8 files changed, 104 insertions(+), 13 deletions(-) create mode 100644 skills/root-cause-analysis/rules/workflow-identify-cause.md diff --git a/skills/create-dashboard/SKILL.md b/skills/create-dashboard/SKILL.md index dfa3133..94d3cd8 100644 --- a/skills/create-dashboard/SKILL.md +++ b/skills/create-dashboard/SKILL.md @@ -1,6 +1,6 @@ --- name: create-dashboard -description: Create a dashboard with valid uiTree using the component schema. Use when building observability dashboards or metric visualizations. +description: Create observability dashboards from OTEL metrics, logs, and traces using Kopai. Use when building metric visualizations, monitoring views, KPI panels, or when the user wants to see their telemetry data in a dashboard — even if they don't say "dashboard" explicitly. Also use when other skills or workflows need to present telemetry data visually (e.g. after root cause analysis). license: Apache-2.0 metadata: author: kopai diff --git a/skills/create-dashboard/rules/workflow.md b/skills/create-dashboard/rules/workflow.md index 1b5fb76..a56b20f 100644 --- a/skills/create-dashboard/rules/workflow.md +++ b/skills/create-dashboard/rules/workflow.md @@ -57,6 +57,27 @@ priority: critical echo '{"uiTree":{"root":"stack-1","elements":{"stack-1":{"key":"stack-1","type":"Stack","props":{"direction":"vertical","gap":"md","align":null},"children":["card-1"],"parentKey":""},"card-1":{"key":"card-1","type":"Card","props":{"title":"CPU Usage","description":null,"padding":null},"children":["ts-1"],"parentKey":"stack-1"},"ts-1":{"key":"ts-1","type":"MetricTimeSeries","props":{"height":300,"showBrush":null,"yAxisLabel":null,"unit":"1"},"children":[],"parentKey":"card-1","dataSource":{"method":"searchMetricsPage","params":{"metricType":"Gauge","metricName":"system.cpu.utilization"}}}}},"metadata":{}}' | npx @kopai/cli dashboards create --name "CPU Dashboard" --tree-version "0.5.0" --json ``` +## Error Handling + +### No metrics discovered + +If `metrics discover` returns an empty array, telemetry data hasn't reached Kopai yet. + +1. Check Kopai is running: `npx @kopai/cli metrics discover --json` — if this returns data, Kopai is up and receiving telemetry +2. Verify the instrumented app is sending data — check app logs for OTLP export errors +3. Wait 10-30 seconds and retry — metrics may take time to appear after app starts + +### Dashboard creation fails validation + +The CLI returns a JSON error with a `message` field describing what's wrong. Common issues: + +- **"Invalid metric type"** — `metricType` doesn't match the actual type from `metrics discover`. Re-run discover and use the exact `type` value +- **"Unknown element type"** — component type not in schema. Re-check `dashboards schema` output +- **"Orphan element"** — an element's `parentKey` references a non-existent key. Verify all parent-child relationships +- **"Root key not found"** — `root` value doesn't match any key in `elements` + +When creation fails, read the error message, fix the tree, and retry. Do not guess — always validate against the schema and discover output. + ## Post-Creation After the dashboard is created, display the URL to the user: diff --git a/skills/otel-instrumentation/SKILL.md b/skills/otel-instrumentation/SKILL.md index fc37b87..6d10707 100644 --- a/skills/otel-instrumentation/SKILL.md +++ b/skills/otel-instrumentation/SKILL.md @@ -1,6 +1,6 @@ --- name: otel-instrumentation -description: Instrument applications with OpenTelemetry SDK and validate telemetry using Kopai. Use when setting up observability, adding tracing/logging/metrics, testing instrumentation, or debugging missing telemetry data. +description: Instrument applications with OpenTelemetry SDK and validate telemetry using Kopai. Use when setting up observability, adding tracing/logging/metrics, testing instrumentation, debugging missing telemetry data, or when traces/logs/metrics aren't appearing after setup. Also use when users say things like "my traces aren't showing up", "I don't see any data", or "how do I add observability to my app". license: Apache-2.0 metadata: author: kopai diff --git a/skills/otel-instrumentation/rules/_sections.md b/skills/otel-instrumentation/rules/_sections.md index 5fd9a1b..30340ef 100644 --- a/skills/otel-instrumentation/rules/_sections.md +++ b/skills/otel-instrumentation/rules/_sections.md @@ -10,9 +10,9 @@ Section definitions for OpenTelemetry instrumentation rules. ### Sections -| Section | Impact | Files | -| ------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| setup | CRITICAL | setup-backend.md, setup-environment.md | -| lang | HIGH | lang-nodejs.md, lang-python.md, lang-go.md, lang-java.md, lang-dotnet.md, lang-ruby.md, lang-php.md, lang-rust.md, lang-swift.md, lang-erlang.md, lang-cpp.md | -| validate | HIGH | validate-traces.md, validate-logs.md, validate-metrics.md | -| troubleshoot | MEDIUM | troubleshoot-no-data.md, troubleshoot-missing-spans.md, troubleshoot-missing-attrs.md, troubleshoot-wrong-port.md | +| Section | Impact | Files | +| ------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| setup | CRITICAL | setup-backend.md, setup-environment.md | +| lang | HIGH | lang-nodejs.md, lang-python.md, lang-go.md, lang-java.md, lang-dotnet.md, lang-ruby.md, lang-php.md, lang-rust.md, lang-erlang.md, lang-cpp.md | +| validate | HIGH | validate-traces.md, validate-logs.md, validate-metrics.md | +| troubleshoot | MEDIUM | troubleshoot-no-data.md, troubleshoot-missing-spans.md, troubleshoot-missing-attrs.md, troubleshoot-wrong-port.md | diff --git a/skills/otel-instrumentation/rules/setup-environment.md b/skills/otel-instrumentation/rules/setup-environment.md index 3d8ca93..1c01eaa 100644 --- a/skills/otel-instrumentation/rules/setup-environment.md +++ b/skills/otel-instrumentation/rules/setup-environment.md @@ -22,6 +22,26 @@ export OTEL_SERVICE_NAME=my-service | OTEL_EXPORTER_OTLP_ENDPOINT | http://localhost:4318 | Kopai collector endpoint | | OTEL_SERVICE_NAME | your-service | Identifies service in telemetry | +### Protocol: HTTP only + +Kopai accepts OTLP over **HTTP only** (port 4318). gRPC (port 4317) is not supported. + +Some SDKs default to gRPC — if you see connection errors, check the protocol: + +```bash +# Force HTTP protocol (needed for Go, Java, and other SDKs that default to gRPC) +export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf +``` + +| SDK | Default Protocol | Action Needed | +| ------- | ---------------- | ----------------------------------------------- | +| Node.js | HTTP | None | +| Python | HTTP | None | +| Go | gRPC | Set `OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf` | +| Java | gRPC | Set `OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf` | +| .NET | HTTP | None | +| Rust | HTTP | None | + ### Reference https://opentelemetry.io/docs/concepts/sdk-configuration/otlp-exporter-configuration/ diff --git a/skills/root-cause-analysis/SKILL.md b/skills/root-cause-analysis/SKILL.md index c9d8ce7..39bc9c0 100644 --- a/skills/root-cause-analysis/SKILL.md +++ b/skills/root-cause-analysis/SKILL.md @@ -1,6 +1,6 @@ --- name: root-cause-analysis -description: Analyze telemetry data for root cause analysis using Kopai CLI. Use when debugging errors, investigating latency issues, tracing request flows across services, or correlating logs with traces. +description: Analyze telemetry data for root cause analysis using Kopai CLI. Use when debugging errors, investigating latency issues, tracing request flows across services, or correlating logs with traces. Also use when users report production issues like "why is my API slow", "getting 500 errors", "service is down", "requests are timing out", or any symptom that needs telemetry-based investigation — even if they don't mention traces or observability explicitly. license: Apache-2.0 metadata: author: kopai @@ -33,6 +33,7 @@ See otel-instrumentation skill for setup. - `workflow-get-context` - Get Full Trace Context - `workflow-correlate-logs` - Correlate Logs with Trace - `workflow-check-metrics` - Check Related Metrics +- `workflow-identify-cause` - Identify Root Cause & Present Findings ### 2. Patterns (HIGH) diff --git a/skills/root-cause-analysis/rules/_sections.md b/skills/root-cause-analysis/rules/_sections.md index 6baf273..5fd3a11 100644 --- a/skills/root-cause-analysis/rules/_sections.md +++ b/skills/root-cause-analysis/rules/_sections.md @@ -10,7 +10,7 @@ Section definitions for root cause analysis rules. ### Sections -| Section | Impact | Files | -| -------- | -------- | ------------------------------------------------------------------------------------------------------- | -| workflow | CRITICAL | workflow-find-errors.md, workflow-get-context.md, workflow-correlate-logs.md, workflow-check-metrics.md | -| pattern | HIGH | pattern-http-errors.md, pattern-slow-requests.md, pattern-distributed.md, pattern-log-driven.md | +| Section | Impact | Files | +| -------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| workflow | CRITICAL | workflow-find-errors.md, workflow-get-context.md, workflow-correlate-logs.md, workflow-check-metrics.md, workflow-identify-cause.md | +| pattern | HIGH | pattern-http-errors.md, pattern-slow-requests.md, pattern-distributed.md, pattern-log-driven.md | diff --git a/skills/root-cause-analysis/rules/workflow-identify-cause.md b/skills/root-cause-analysis/rules/workflow-identify-cause.md new file mode 100644 index 0000000..54037b2 --- /dev/null +++ b/skills/root-cause-analysis/rules/workflow-identify-cause.md @@ -0,0 +1,49 @@ +| title | impact | tags | +| ---------------------------------------------- | -------- | ------------------------------------- | +| Step 5: Identify Root Cause & Present Findings | CRITICAL | workflow, synthesis, dashboard, step5 | + +## Step 5: Identify Root Cause & Present Findings + +**Impact:** CRITICAL + +Final step in RCA workflow — synthesize findings from steps 1-4, present the analysis, and create a visual dashboard for the user to review. + +### 5a. Synthesize Findings + +Combine evidence from the previous steps into a coherent narrative: + +1. **Timeline** — establish the sequence of events using timestamps from traces and logs +2. **Blast radius** — which services are affected? Use service grouping from error traces +3. **Root vs symptoms** — distinguish the originating failure from cascading effects. The earliest error in the trace chain is usually closest to root cause +4. **Evidence chain** — link specific TraceIds, SpanIds, log entries, and metric anomalies that support the conclusion + +### 5b. Present Analysis + +Present the root cause analysis to the user with: + +- **Summary** — one-sentence root cause statement +- **Evidence** — the specific traces, logs, and metrics that support it +- **Impact** — which services/endpoints are affected and how +- **Suggested fix** — actionable next steps based on the findings + +### 5c. Create Incident Dashboard + +Use the **create-dashboard** skill to build a dashboard that visualizes the evidence from the analysis. This lets the user visually verify the hypothesis and explore the data themselves. + +The dashboard should include: + +1. **Relevant metrics** — MetricTimeSeries or MetricStat components for metrics that showed anomalies during the incident (e.g., error rates, latency spikes, resource exhaustion) +2. **Logs** — LogTimeline component filtered to the affected service(s) during the incident timeframe (dataSource method: `searchLogsPage` with `serviceName` param) +3. **Traces** — TraceDetail component showing a representative error trace + +After dashboard creation, present the link to the user: + +``` +/?tab=metrics&dashboardId= +``` + +Where `` is from the CLI JSON response and `` is the `--url` flag value or `http://localhost:8000` if omitted. + +### Why create a dashboard? + +The raw CLI output gives you the data to analyze, but the user needs to visually review and validate the findings. A dashboard with the relevant signals side-by-side makes it easy to spot patterns, confirm the timeline, and decide on next actions. It also serves as a persistent artifact of the investigation that can be shared with the team. From 0eed9d801ca2c552c80d851efe5b4dc94a93fffa Mon Sep 17 00:00:00 2001 From: Vladimir Adamic Date: Tue, 10 Mar 2026 12:19:55 +0100 Subject: [PATCH 2/8] fix(ui): set height for LogTimeline --- .../renderers/OtelLogTimeline.tsx | 14 ++++++++----- skills/create-dashboard/rules/workflow.md | 21 +++++++++++++++++-- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/packages/ui/src/components/observability/renderers/OtelLogTimeline.tsx b/packages/ui/src/components/observability/renderers/OtelLogTimeline.tsx index 12e3f7e..6820e70 100644 --- a/packages/ui/src/components/observability/renderers/OtelLogTimeline.tsx +++ b/packages/ui/src/components/observability/renderers/OtelLogTimeline.tsx @@ -18,11 +18,15 @@ export function OtelLogTimeline(props: Props) { const response = props.data as { data?: OtelLogsRow[] } | null; + const height = props.element.props.height ?? 600; + return ( - +
+ +
); } diff --git a/skills/create-dashboard/rules/workflow.md b/skills/create-dashboard/rules/workflow.md index a56b20f..d6aaa18 100644 --- a/skills/create-dashboard/rules/workflow.md +++ b/skills/create-dashboard/rules/workflow.md @@ -12,7 +12,7 @@ priority: critical 2. **Design tree** — build a uiTree matching the component schema 3. **Create dashboard** — pipe JSON to CLI: ```bash - echo '{"uiTree":,"metadata":{}}' | npx @kopai/cli dashboards create --name "" --tree-version "0.5.0" --json + echo '{"uiTree":,"metadata":{}}' | npx @kopai/cli dashboards create --name "" --tree-version "0.7.0" --json ``` ## Tree Structure Rules @@ -50,11 +50,24 @@ priority: critical - Wrap data components in `Card` with descriptive `title` - Use `MetricStat` for KPI overview, `MetricTimeSeries` for trends - Use `MetricHistogram` only for Histogram/ExponentialHistogram metric types +- Set `height: 600` on LogTimeline — smaller values collapse the log content and only show a count badge +- Set `height: 300` on MetricTimeSeries and MetricHistogram +- MetricStat does not need a height prop + +## Component Compatibility + +- **MetricStat** — works with **Sum** and **Gauge** only. Does NOT work with Histogram (shows "--") +- **MetricTimeSeries** — works with **Sum** and **Gauge** only. Histogram causes internal server errors +- **MetricHistogram** — works with **Histogram** and **ExponentialHistogram** only + +When choosing components, always check the metric's `type` from `metrics discover` output. Mismatched types render empty or show "--". + +**For Histogram metrics**: use `MetricHistogram` for distribution views, or `MetricStat` is NOT compatible. If you need a time-series trend for a Histogram metric, use `MetricHistogram` — do NOT use `MetricTimeSeries`. ## Example Creation ```bash -echo '{"uiTree":{"root":"stack-1","elements":{"stack-1":{"key":"stack-1","type":"Stack","props":{"direction":"vertical","gap":"md","align":null},"children":["card-1"],"parentKey":""},"card-1":{"key":"card-1","type":"Card","props":{"title":"CPU Usage","description":null,"padding":null},"children":["ts-1"],"parentKey":"stack-1"},"ts-1":{"key":"ts-1","type":"MetricTimeSeries","props":{"height":300,"showBrush":null,"yAxisLabel":null,"unit":"1"},"children":[],"parentKey":"card-1","dataSource":{"method":"searchMetricsPage","params":{"metricType":"Gauge","metricName":"system.cpu.utilization"}}}}},"metadata":{}}' | npx @kopai/cli dashboards create --name "CPU Dashboard" --tree-version "0.5.0" --json +echo '{"uiTree":{"root":"stack-1","elements":{"stack-1":{"key":"stack-1","type":"Stack","props":{"direction":"vertical","gap":"md","align":null},"children":["card-1"],"parentKey":""},"card-1":{"key":"card-1","type":"Card","props":{"title":"CPU Usage","description":null,"padding":null},"children":["ts-1"],"parentKey":"stack-1"},"ts-1":{"key":"ts-1","type":"MetricTimeSeries","props":{"height":300,"showBrush":null,"yAxisLabel":null,"unit":"1"},"children":[],"parentKey":"card-1","dataSource":{"method":"searchMetricsPage","params":{"metricType":"Gauge","metricName":"system.cpu.utilization"}}}}},"metadata":{}}' | npx @kopai/cli dashboards create --name "CPU Dashboard" --tree-version "0.7.0" --json ``` ## Error Handling @@ -88,3 +101,7 @@ After the dashboard is created, display the URL to the user: - `` — the `id` field from the CLI JSON response - `` — the URL used for the CLI command: the `--url` flag value, or `http://localhost:8000` if omitted + +Common pitfalls: + +- **LogTimeline with severity filter** — avoid `severityNumberMin` unless the user explicitly asks for error logs. Many services only emit info-level logs, so filtering to ERROR+ returns empty results. Default to showing all logs. From 480e75b96e95d34ebafd7470e044d656b803b879 Mon Sep 17 00:00:00 2001 From: Vladimir Adamic Date: Tue, 10 Mar 2026 15:28:35 +0100 Subject: [PATCH 3/8] feat(skills): improve create-dashboard skill --- skills/create-dashboard/rules/workflow.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skills/create-dashboard/rules/workflow.md b/skills/create-dashboard/rules/workflow.md index d6aaa18..4c25d96 100644 --- a/skills/create-dashboard/rules/workflow.md +++ b/skills/create-dashboard/rules/workflow.md @@ -57,12 +57,12 @@ priority: critical ## Component Compatibility - **MetricStat** — works with **Sum** and **Gauge** only. Does NOT work with Histogram (shows "--") -- **MetricTimeSeries** — works with **Sum** and **Gauge** only. Histogram causes internal server errors +- **MetricTimeSeries** — works with **Sum**, **Gauge**, and **Histogram** (renders mean duration over time) - **MetricHistogram** — works with **Histogram** and **ExponentialHistogram** only When choosing components, always check the metric's `type` from `metrics discover` output. Mismatched types render empty or show "--". -**For Histogram metrics**: use `MetricHistogram` for distribution views, or `MetricStat` is NOT compatible. If you need a time-series trend for a Histogram metric, use `MetricHistogram` — do NOT use `MetricTimeSeries`. +**For Histogram metrics**: use `MetricHistogram` for distribution views, or `MetricTimeSeries` for trends over time (renders mean = Sum/Count). `MetricStat` is NOT compatible with Histogram. ## Example Creation From 03f02b7d775f9c26c58f3e19b4ec2c62796067ba Mon Sep 17 00:00:00 2001 From: Vladimir Adamic Date: Tue, 10 Mar 2026 15:29:01 +0100 Subject: [PATCH 4/8] fix(ui,sqlite) --- .../src/datasource-write.test.ts | 2 +- .../sqlite-datasource/src/db-datasource.ts | 29 ++++++++++++------- .../observability/MetricTimeSeries/index.tsx | 24 +++++++++++---- 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/packages/sqlite-datasource/src/datasource-write.test.ts b/packages/sqlite-datasource/src/datasource-write.test.ts index d037a68..ff0ff7d 100644 --- a/packages/sqlite-datasource/src/datasource-write.test.ts +++ b/packages/sqlite-datasource/src/datasource-write.test.ts @@ -881,7 +881,7 @@ describe("OptimizedDatasource", () => { TraceFlags: 0n, SeverityText: "", SeverityNumber: 0n, - Body: "null", + Body: "", LogAttributes: "{}", ResourceAttributes: "{}", ResourceSchemaUrl: "", diff --git a/packages/sqlite-datasource/src/db-datasource.ts b/packages/sqlite-datasource/src/db-datasource.ts index a4cf796..b1caa6b 100644 --- a/packages/sqlite-datasource/src/db-datasource.ts +++ b/packages/sqlite-datasource/src/db-datasource.ts @@ -1246,7 +1246,7 @@ function aggTemporalityToString( } function anyValueToSimple(value: otlp.AnyValue | undefined): unknown { - if (!value) return null; + if (!value) return undefined; if (value.stringValue !== undefined) return value.stringValue; if (value.boolValue !== undefined) return value.boolValue; if (value.intValue !== undefined) return value.intValue; @@ -1262,12 +1262,13 @@ function anyValueToSimple(value: otlp.AnyValue | undefined): unknown { } return obj; } - return null; + return undefined; } function anyValueToBodyString(value: otlp.AnyValue | undefined): string { const simple = anyValueToSimple(value); if (typeof simple === "string") return simple; + if (simple === undefined || simple === null) return ""; return JSON.stringify(simple); } @@ -1345,7 +1346,15 @@ function parseJsonField( ): Record | undefined { if (typeof value !== "string") return undefined; try { - return JSON.parse(value); + const parsed = JSON.parse(value); + if (typeof parsed !== "object" || parsed === null) return undefined; + // Strip null values — OTLP attributes with unrecognized AnyValue types + // were previously stored as null, which fails Zod attributeValue validation + const result: Record = {}; + for (const [k, v] of Object.entries(parsed)) { + if (v != null) result[k] = v as AttributeValue; + } + return result; } catch { return undefined; } @@ -1474,9 +1483,9 @@ function mapRowToOtelMetrics( ...base, MetricType: "Histogram" as const, Count: toNumber(row.Count), - Sum: row.Sum as number | undefined, - Min: row.Min as number | null | undefined, - Max: row.Max as number | null | undefined, + Sum: toNumber(row.Sum), + Min: row.Min == null ? (row.Min as null | undefined) : toNumber(row.Min), + Max: row.Max == null ? (row.Max as null | undefined) : toNumber(row.Max), BucketCounts: parseNumberArrayField(row.BucketCounts), ExplicitBounds: parseNumberArrayField(row.ExplicitBounds), AggregationTemporality: row.AggregationTemporality as string | undefined, @@ -1488,9 +1497,9 @@ function mapRowToOtelMetrics( ...base, MetricType: "ExponentialHistogram" as const, Count: toNumber(row.Count), - Sum: row.Sum as number | undefined, - Min: row.Min as number | null | undefined, - Max: row.Max as number | null | undefined, + Sum: toNumber(row.Sum), + Min: row.Min == null ? (row.Min as null | undefined) : toNumber(row.Min), + Max: row.Max == null ? (row.Max as null | undefined) : toNumber(row.Max), Scale: toNumber(row.Scale), ZeroCount: toNumber(row.ZeroCount), PositiveOffset: toNumber(row.PositiveOffset), @@ -1506,7 +1515,7 @@ function mapRowToOtelMetrics( ...base, MetricType: "Summary" as const, Count: toNumber(row.Count), - Sum: row.Sum as number | undefined, + Sum: toNumber(row.Sum), "ValueAtQuantiles.Quantile": parseNumberArrayField( row["ValueAtQuantiles.Quantile"] ), diff --git a/packages/ui/src/components/observability/MetricTimeSeries/index.tsx b/packages/ui/src/components/observability/MetricTimeSeries/index.tsx index 272dfbb..1d2c358 100644 --- a/packages/ui/src/components/observability/MetricTimeSeries/index.tsx +++ b/packages/ui/src/components/observability/MetricTimeSeries/index.tsx @@ -96,12 +96,28 @@ function buildMetrics(rows: OtelMetricsRow[]): ParsedMetricGroup[] { for (const row of rows) { const name = row.MetricName ?? "unknown"; const type = row.MetricType; - if ( + + // Extract scalar value depending on metric type + let value: number | undefined; + if (type === "Gauge" || type === "Sum") { + value = "Value" in row ? row.Value : undefined; + } else if ( type === "Histogram" || type === "ExponentialHistogram" || type === "Summary" - ) - continue; // TimeSeries only handles Gauge/Sum + ) { + // Use mean (Sum/Count) for distribution metrics + const sum = "Sum" in row ? (row as { Sum?: number }).Sum : undefined; + const count = + "Count" in row ? (row as { Count?: number }).Count : undefined; + if (sum != null && count != null && count > 0) { + value = sum / count; + } else if (count != null) { + value = count; + } + } + + if (value === undefined) continue; if (!metricMap.has(name)) metricMap.set(name, new Map()); if (!metricMeta.has(name)) @@ -136,8 +152,6 @@ function buildMetrics(rows: OtelMetricsRow[]): ParsedMetricGroup[] { }); } - if (!("Value" in row)) continue; - const value = row.Value; const timestamp = parseInt(row.TimeUnix, 10) / 1e6; seriesMap.get(seriesKey)!.dataPoints.push({ timestamp, value }); } From a3754df3c231174bb2d2045af8a83780df1ca19b Mon Sep 17 00:00:00 2001 From: Vladimir Adamic Date: Tue, 10 Mar 2026 16:56:10 +0100 Subject: [PATCH 5/8] fix(ui): use kopai SDK to get dashboards --- packages/ui/src/hooks/use-kopai-data.test.ts | 1 + packages/ui/src/hooks/use-live-logs.test.ts | 1 + packages/ui/src/pages/observability.test.tsx | 14 ++------------ packages/ui/src/pages/observability.tsx | 19 +++++++++---------- packages/ui/src/providers/kopai-provider.tsx | 1 + 5 files changed, 14 insertions(+), 22 deletions(-) diff --git a/packages/ui/src/hooks/use-kopai-data.test.ts b/packages/ui/src/hooks/use-kopai-data.test.ts index 9dbefa6..3407f02 100644 --- a/packages/ui/src/hooks/use-kopai-data.test.ts +++ b/packages/ui/src/hooks/use-kopai-data.test.ts @@ -18,6 +18,7 @@ const createMockClient = () => ({ searchMetricsPage: vi.fn(), getTrace: vi.fn(), discoverMetrics: vi.fn(), + getDashboard: vi.fn(), }); type MockClient = ReturnType; diff --git a/packages/ui/src/hooks/use-live-logs.test.ts b/packages/ui/src/hooks/use-live-logs.test.ts index f27eefe..6e5ff32 100644 --- a/packages/ui/src/hooks/use-live-logs.test.ts +++ b/packages/ui/src/hooks/use-live-logs.test.ts @@ -21,6 +21,7 @@ const createMockClient = () => ({ searchMetricsPage: vi.fn(), getTrace: vi.fn(), discoverMetrics: vi.fn(), + getDashboard: vi.fn(), }); function wrapper(client: KopaiClient) { diff --git a/packages/ui/src/pages/observability.test.tsx b/packages/ui/src/pages/observability.test.tsx index 9dd749a..b99b863 100644 --- a/packages/ui/src/pages/observability.test.tsx +++ b/packages/ui/src/pages/observability.test.tsx @@ -77,12 +77,7 @@ describe("useDashboardTree validation", () => { } it("renders DynamicDashboard when API returns a valid uiTree", async () => { - vi.spyOn(globalThis, "fetch").mockResolvedValueOnce( - new Response(JSON.stringify({ uiTree: VALID_TREE }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }) - ); + mockClient.getDashboard.mockResolvedValueOnce({ uiTree: VALID_TREE }); setURL("?tab=metrics&dashboardId=abc"); @@ -107,12 +102,7 @@ describe("useDashboardTree validation", () => { }, }; - vi.spyOn(globalThis, "fetch").mockResolvedValueOnce( - new Response(JSON.stringify({ uiTree: invalidTree }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }) - ); + mockClient.getDashboard.mockResolvedValueOnce({ uiTree: invalidTree }); setURL("?tab=metrics&dashboardId=def"); diff --git a/packages/ui/src/pages/observability.tsx b/packages/ui/src/pages/observability.tsx index 086082e..740c7ae 100644 --- a/packages/ui/src/pages/observability.tsx +++ b/packages/ui/src/pages/observability.tsx @@ -663,8 +663,6 @@ function ServicesTab({ // Metrics tab — DynamicDashboard // --------------------------------------------------------------------------- -const DASHBOARDS_API_BASE = "/dashboards"; - const METRICS_TREE = { root: "root", elements: { @@ -715,16 +713,17 @@ const METRICS_TREE = { }, }; -function useDashboardTree(dashboardId: string | null) { +function useDashboardTree( + client: Pick, + dashboardId: string | null +) { const { data, isFetching, error } = useQuery({ queryKey: ["dashboard-tree", dashboardId], queryFn: async ({ signal }) => { - const res = await fetch(`${DASHBOARDS_API_BASE}/${dashboardId}`, { - signal, - }); - if (!res.ok) throw new Error(`Failed to load dashboard: ${res.status}`); - const json = await res.json(); - const parsed = observabilityCatalog.uiTreeSchema.safeParse(json.uiTree); + const dashboard = await client.getDashboard(dashboardId!, { signal }); + const parsed = observabilityCatalog.uiTreeSchema.safeParse( + dashboard.uiTree + ); if (!parsed.success) { const issue = parsed.error.issues[0]; const path = issue?.path.length ? issue.path.join(".") + ": " : ""; @@ -747,7 +746,7 @@ function useDashboardTree(dashboardId: string | null) { function MetricsTab() { const kopaiClient = useKopaiSDK(); const { dashboardId } = useURLState(); - const { loading, error, tree } = useDashboardTree(dashboardId); + const { loading, error, tree } = useDashboardTree(kopaiClient, dashboardId); if (loading) return ( diff --git a/packages/ui/src/providers/kopai-provider.tsx b/packages/ui/src/providers/kopai-provider.tsx index f181197..e5f9a9b 100644 --- a/packages/ui/src/providers/kopai-provider.tsx +++ b/packages/ui/src/providers/kopai-provider.tsx @@ -9,6 +9,7 @@ export type KopaiClient = Pick< | "searchMetricsPage" | "getTrace" | "discoverMetrics" + | "getDashboard" >; interface KopaiSDKContextValue { From 113c541f6a5082d5fe069fd2219706129c630b38 Mon Sep 17 00:00:00 2001 From: Vladimir Adamic Date: Tue, 10 Mar 2026 17:02:58 +0100 Subject: [PATCH 6/8] chore: add changeset --- .changeset/cute-pandas-cheer.md | 6 ++++++ .changeset/fine-eyes-own.md | 6 ++++++ 2 files changed, 12 insertions(+) create mode 100644 .changeset/cute-pandas-cheer.md create mode 100644 .changeset/fine-eyes-own.md diff --git a/.changeset/cute-pandas-cheer.md b/.changeset/cute-pandas-cheer.md new file mode 100644 index 0000000..903c76f --- /dev/null +++ b/.changeset/cute-pandas-cheer.md @@ -0,0 +1,6 @@ +--- +"@kopai/ui": minor +"@kopai/sqlite-datasource": patch +--- + +Use kopai SDK to get dashboard diff --git a/.changeset/fine-eyes-own.md b/.changeset/fine-eyes-own.md new file mode 100644 index 0000000..7399b6f --- /dev/null +++ b/.changeset/fine-eyes-own.md @@ -0,0 +1,6 @@ +--- +"@kopai/sqlite-datasource": minor +"@kopai/ui": minor +--- + +Fix metrics rendering and sqlite data parsing From 2180858e312bc654647a259d6158491161eeb3f4 Mon Sep 17 00:00:00 2001 From: Vladimir Adamic Date: Tue, 10 Mar 2026 18:15:51 +0100 Subject: [PATCH 7/8] fix: PR comments --- .../src/datasource-write.test.ts | 15 +++++++++++++++ packages/sqlite-datasource/src/db-datasource.ts | 6 +++--- packages/sqlite-datasource/src/db-types.ts | 6 +++--- .../observability/MetricTimeSeries/index.tsx | 2 -- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/sqlite-datasource/src/datasource-write.test.ts b/packages/sqlite-datasource/src/datasource-write.test.ts index ff0ff7d..29e1912 100644 --- a/packages/sqlite-datasource/src/datasource-write.test.ts +++ b/packages/sqlite-datasource/src/datasource-write.test.ts @@ -891,6 +891,21 @@ describe("OptimizedDatasource", () => { ScopeAttributes: "{}", ScopeSchemaUrl: "", }); + + const readDs = ds as unknown as datasource.ReadTelemetryDatasource; + const readResult = await readDs.getLogs({ limit: 10 }); + expect(readResult.data).toHaveLength(1); + const log = readResult.data[0]; + expect(log).toMatchObject({ + Timestamp: "1000000000", + TraceId: "", + SpanId: "", + SeverityText: "", + SeverityNumber: 0, + Body: "", + ServiceName: "", + ScopeName: "minimal-scope", + }); }); }); }); diff --git a/packages/sqlite-datasource/src/db-datasource.ts b/packages/sqlite-datasource/src/db-datasource.ts index b1caa6b..3844c2f 100644 --- a/packages/sqlite-datasource/src/db-datasource.ts +++ b/packages/sqlite-datasource/src/db-datasource.ts @@ -1116,7 +1116,7 @@ function toHistogramRow( StartTimeUnix: nanosToSqlite(dataPoint.startTimeUnixNano), TimeUnix: nanosToSqlite(dataPoint.timeUnixNano), Count: Number(dataPoint.count ?? 0), - Sum: dataPoint.sum ?? 0, + Sum: dataPoint.sum ?? null, BucketCounts: JSON.stringify(dataPoint.bucketCounts ?? []), ExplicitBounds: JSON.stringify(dataPoint.explicitBounds ?? []), Min: dataPoint.min ?? null, @@ -1166,7 +1166,7 @@ function toExpHistogramRow( StartTimeUnix: nanosToSqlite(dataPoint.startTimeUnixNano), TimeUnix: nanosToSqlite(dataPoint.timeUnixNano), Count: Number(dataPoint.count ?? 0), - Sum: dataPoint.sum ?? 0, + Sum: dataPoint.sum ?? null, Scale: dataPoint.scale ?? 0, ZeroCount: Number(dataPoint.zeroCount ?? 0), PositiveOffset: dataPoint.positive?.offset ?? 0, @@ -1224,7 +1224,7 @@ function toSummaryRow( StartTimeUnix: nanosToSqlite(dataPoint.startTimeUnixNano), TimeUnix: nanosToSqlite(dataPoint.timeUnixNano), Count: Number(dataPoint.count ?? 0), - Sum: dataPoint.sum ?? 0, + Sum: dataPoint.sum ?? null, "ValueAtQuantiles.Quantile": JSON.stringify( quantileValues.map( (q: otlpMetrics.SummaryDataPoint_ValueAtQuantile) => q.quantile ?? 0 diff --git a/packages/sqlite-datasource/src/db-types.ts b/packages/sqlite-datasource/src/db-types.ts index f9b0098..fa17497 100644 --- a/packages/sqlite-datasource/src/db-types.ts +++ b/packages/sqlite-datasource/src/db-types.ts @@ -56,7 +56,7 @@ export interface OtelMetricsExponentialHistogram { ScopeVersion: Generated; ServiceName: Generated; StartTimeUnix: bigint; - Sum: Generated; + Sum: number | null; TimeUnix: bigint; ZeroCount: Generated; ZeroThreshold: Generated; @@ -111,7 +111,7 @@ export interface OtelMetricsHistogram { ScopeVersion: Generated; ServiceName: Generated; StartTimeUnix: bigint; - Sum: Generated; + Sum: number | null; TimeUnix: bigint; } @@ -156,7 +156,7 @@ export interface OtelMetricsSummary { ScopeVersion: Generated; ServiceName: Generated; StartTimeUnix: bigint; - Sum: Generated; + Sum: number | null; TimeUnix: bigint; "ValueAtQuantiles.Quantile": Generated; "ValueAtQuantiles.Value": Generated; diff --git a/packages/ui/src/components/observability/MetricTimeSeries/index.tsx b/packages/ui/src/components/observability/MetricTimeSeries/index.tsx index 1d2c358..909b5c4 100644 --- a/packages/ui/src/components/observability/MetricTimeSeries/index.tsx +++ b/packages/ui/src/components/observability/MetricTimeSeries/index.tsx @@ -112,8 +112,6 @@ function buildMetrics(rows: OtelMetricsRow[]): ParsedMetricGroup[] { "Count" in row ? (row as { Count?: number }).Count : undefined; if (sum != null && count != null && count > 0) { value = sum / count; - } else if (count != null) { - value = count; } } From f8d2f9f4db1b2a681e038ac090b796618c48176a Mon Sep 17 00:00:00 2001 From: Vladimir Adamic Date: Tue, 10 Mar 2026 18:42:46 +0100 Subject: [PATCH 8/8] fix: PR comments --- .changeset/cute-pandas-cheer.md | 2 +- packages/sqlite-datasource/src/datasource-write.test.ts | 6 +++--- packages/ui/src/pages/observability.test.tsx | 9 +++++++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.changeset/cute-pandas-cheer.md b/.changeset/cute-pandas-cheer.md index 903c76f..bbc4f39 100644 --- a/.changeset/cute-pandas-cheer.md +++ b/.changeset/cute-pandas-cheer.md @@ -3,4 +3,4 @@ "@kopai/sqlite-datasource": patch --- -Use kopai SDK to get dashboard +Use kopai SDK to get dashboards; preserve NULL histogram/summary Sum on write path diff --git a/packages/sqlite-datasource/src/datasource-write.test.ts b/packages/sqlite-datasource/src/datasource-write.test.ts index 29e1912..c17c355 100644 --- a/packages/sqlite-datasource/src/datasource-write.test.ts +++ b/packages/sqlite-datasource/src/datasource-write.test.ts @@ -748,7 +748,8 @@ describe("OptimizedDatasource", () => { describe("writeLogs", () => { let testConnection: DatabaseSync; - let ds: datasource.WriteTelemetryDatasource; + let ds: datasource.WriteTelemetryDatasource & + datasource.ReadTelemetryDatasource; beforeEach(async () => { testConnection = initializeDatabase(":memory:"); @@ -892,8 +893,7 @@ describe("OptimizedDatasource", () => { ScopeSchemaUrl: "", }); - const readDs = ds as unknown as datasource.ReadTelemetryDatasource; - const readResult = await readDs.getLogs({ limit: 10 }); + const readResult = await ds.getLogs({ limit: 10 }); expect(readResult.data).toHaveLength(1); const log = readResult.data[0]; expect(log).toMatchObject({ diff --git a/packages/ui/src/pages/observability.test.tsx b/packages/ui/src/pages/observability.test.tsx index b99b863..9df1a26 100644 --- a/packages/ui/src/pages/observability.test.tsx +++ b/packages/ui/src/pages/observability.test.tsx @@ -91,6 +91,10 @@ describe("useDashboardTree validation", () => { expect(screen.getByText("Test Dashboard")).toBeTruthy(); }); + expect(mockClient.getDashboard).toHaveBeenCalledWith( + "abc", + expect.anything() + ); expect(screen.queryByText(/invalid layout/i)).toBeNull(); }); @@ -115,5 +119,10 @@ describe("useDashboardTree validation", () => { await waitFor(() => { expect(screen.getByText(/invalid layout/i)).toBeTruthy(); }); + + expect(mockClient.getDashboard).toHaveBeenCalledWith( + "def", + expect.anything() + ); }); });