Skip to content

Metrics and dashboard data

Kumiko uses the word “metric” for two different things:

  • Dashboard metrics come from the bundled metrics and metrics-system features. A metric declaration becomes a query that aggregates a table and returns a value, a trend and rows for dashboard panels. This guide covers them.
  • Prometheus metrics come from r.metric. They are counters and histograms that a monitoring system scrapes. They do not show up on a dashboard screen.
  • An app with the admin-shell feature, or your own dashboard screen.
  • The features that own the tables your metrics read, for example jobs or delivery.

Pass the same metric list to createMetricsFeature, createSystemMetricsFeature and createAdminShellFeature. The admin console sample mounts seven of the default metrics:

// No sessions feature in this app, so no active-users metric.
const CONSOLE_METRICS = [
jobRunsByStatusMetric,
failedJobRunsMetric,
tenantJobFailuresMetric,
deliveriesByChannelMetric,
failedDeliveriesMetric,
activeTenantsMetric,
auditWritesMetric,
] as const;
export const APP_FEATURES = [
localeDe(),
createSecretsFeature(),
createAuditFeature(),
createDeliveryFeature(),
createJobsFeature(),
createTierEngineFeature(),
createMetricsFeature({ metrics: CONSOLE_METRICS }),
createSystemMetricsFeature({ metrics: CONSOLE_METRICS }),
createAdminShellFeature({ metrics: CONSOLE_METRICS }),
homeFeature,
] as const;

createMetricsFeature registers the tenant metrics (feature metrics), and createSystemMetricsFeature registers the cross-tenant ones (feature metrics-system). admin-shell requires both and builds its overview pages from the metric ids it knows.

Each metric names the feature that owns its table in requires. Boot fails when that feature is not mounted, so drop the metrics you cannot serve. The show-pony app has no delivery feature and filters the delivery metrics out of DEFAULT_METRICS:

import { DEFAULT_TIER, SHOWPONY_TIER_MAP } from "./features/show-pony/tier-map";
import { showPonyLegalLayout } from "./marketing/layouts/legal";
import { resolveSubdomainPageTenant } from "./tenant-routing";
/** Overview screens + nav only — app-shell owns workspaces host/platform. */
const adminShellFeature = createAdminShellFeature({
registerWorkspaces: false,
includeTierAdmin: true,
metrics: DEFAULT_METRICS,
});

defineMetric describes one aggregation over a table. This example counts the open orders of the caller’s tenant per day:

import { defineMetric, metricQueryName } from "@cosmicdrift/kumiko-bundled-features/metrics";
import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
import { orderEntity } from "./order-entity";
const orderTable = buildEntityTable("order", orderEntity);
export const openOrdersMetric = defineMetric({
id: "shop-open-orders",
description: "Counts open orders created in the selected range, per day.",
source: orderTable,
requires: "shop",
measure: { fn: "count" },
where: { status: "open" },
timeField: "createdAt",
bucket: "day",
scopes: ["tenant"],
});
export const openOrdersQuery = metricQueryName("tenant", openOrdersMetric.id);

Add it to the shared list next to the defaults, for example const APP_METRICS = [...DEFAULT_METRICS, openOrdersMetric], and pass that list to all three factories. Each metrics feature only registers the metrics whose scopes include its own scope, so metrics-system skips a tenant-only metric.

The main options:

OptionMeaning
measure{ fn: "count" }, or countDistinct, sum, avg with a field
whereFixed filter on the source table
timeFieldTimestamp column. Without it the metric is a snapshot with no window
windowFixed range (24h, 7d, 14d, 30d). Without it the caller picks the range
buckethour, day or auto (hour for 24h, otherwise day)
groupBy, stackBy, groupLabelsSplit the result into rows and segments, with i18n keys for the labels
filtersPayload keys that narrow the result, for example a folder filter
comparePreviousDefault true. Set false when the previous period cannot be measured
scopestenant, system or both

Every metric becomes one query per scope, named metrics:query:<id> for the tenant scope and metrics-system:query:<id> for the system scope. Use metricQueryName(scope, id) instead of writing the name by hand. Tenant queries are open to tenant admins and always aggregate the caller’s own tenant. System queries are open to system admins only.

The result carries value, delta and deltaDirection against the previous period, points for a sparkline, and series and rows for charts and ranked lists.

admin-shell only places the metrics it knows. Show an app metric on your own dashboard screen:

r.screen({
id: "shop-dashboard",
type: "dashboard",
panels: [
{
kind: "stat-group",
id: "kpis",
stats: [
{
kind: "stat",
id: "open-orders",
label: "shop:dashboard.openOrders",
query: openOrdersQuery,
valueField: "value",
deltaField: "delta",
deltaDirectionField: "deltaDirection",
sparklineField: "points",
},
],
},
],
});

A metric query is built on ctx.db.aggregate(table, spec, where). Call it directly when a query handler needs a sum or a count that does not fit a metric declaration. Like the other ctx.db reads, it only sees rows of the caller’s tenant and shared rows of the system tenant:

const rows = await ctx.db.aggregate(
orderTable,
{
measure: { fn: "sum", field: "total" },
groupBy: [{ field: "status" }],
orderByValue: "desc",
limit: 10,
},
{ archived: false },
);

measure takes the same functions as a metric. Each groupBy entry is a field, or a time field with bucket (hour or day) and a timeZone. orderByValue, orderByKeys and limit sort and cut the result. Each row carries one key per groupBy entry and the measured value.

  • Boot fails with a missing feature. A metric’s requires names a feature the app does not mount. Remove the metric from the list.
  • The admin overview does not show your metric. It only renders known default ids. Add your own dashboard screen.
  • The lists differ. Pass one shared constant to all three factories, as the samples do.