mcpField
K
kubesense-slo

kubesense-slo

github.com/kubesense-aiCloud & DevOps342 installsUpdated 2d ago
Repository

About kubesense-slo

kubesense-slo is a workflow skill for creating, previewing, and diagnosing KubeSense service-level objectives. It manages SLOs through the REST API and emphasizes checking actual evaluation data rather than relying on previews alone.

Key features

  1. 01Discover metrics, fields, and alert channels
  2. 02Construct and preview SLO configurations
  3. 03Create, read, update, and delete SLOs
  4. 04Inspect evaluation buckets and budget burn
  5. 05Configure and verify budget-burn alerts

Use cases

  1. 01Create service-level objectives for telemetry
  2. 02Estimate compliance before creating an SLO
  3. 03Diagnose missing data or budget burn
  4. 04Migrate or maintain existing SLOs

MCP support

The skill states that SLOs have no MCP tools and must be managed through HTTP calls to the `/api/slo` REST API. MCP query tools can help discover metrics, fields, and related information.

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/kubesense-ai/kubesense-mcp-skills --skill kubesense-slo

After installing, type/kubesense-sloin Claude Code to confirm it loaded.

Bundled filesreferences/api-payloads.mdreferences/datadog-migration.md

Details

From the project README

KubeSense SLOs

An SLO here is a stored definition plus a background evaluator. Every evaluation_interval_seconds the evaluator scores one bucket, writes it to ClickHouse, and emits Prometheus series (kubesense_slo_*). Compliance is then a rolling aggregate over evaluation_window_days.

Important

There are no SLO MCP tools. Logs, traces, metrics and alerts have them; SLOs do not. Everything here is the REST API at /api/slo, authenticated exactly like the MCP server (x-api-key, or Authorization: Bearer <token>). Use the MCP query tools to discover metric and field names, then build the SLO body.

So this skill needs two things an MCP connection does not give you: a way to make HTTP calls (a shell with curl, or an HTTP tool), and the API credentials as a value you can put in a header — the MCP server's own credentials are not readable from inside a tool call. An agent with MCP alone can still build and explain the payload, and should say plainly that the user has to send it.

The Lifecycle

Follow it in order. Steps 5 and 6 are the ones people skip, and they are the ones that catch an SLO that stored cleanly and evaluates nothing.

  1. Discover. Real metric names (get-available-metrics, get-metric-labels) and real field names (get-trace-or-log-fields). Channel ids for the burn-rate alerts (list-notification-channels). Alert rule uuids if it is an alert SLO.
  2. Construct the body — the type, the two queries, the window, alert_types.
  3. Preview with POST /api/slo/preview, knowing what preview does and does not check. A wildly wrong number is usually a filter bug. no_data is not proof of one — a genuinely quiet service produces empty windows too; tell the two apart before changing anything (step 6). For an alert SLO, pass from_time/to_time explicitly: the default window reaches back before the rules existed, where the absence of firing rows reads as perfect uptime.
  4. Create with POST /api/slo and keep the returned id. No id, no SLO.
  5. Read back with GET /api/slo/{id}?current_time=… and check the stored config is what you sent — particularly slo_type, operation, and both groupBy lists.
  6. Verify real buckets after one or two evaluation intervals: GET /api/slo/{id}/metrics?from_time=…&to_time=…, plus the SLO read-back.
    • last_evaluated advancing between two reads is what proves the evaluator picked the SLO up. Null, or frozen, means it never did — check status, the interval, and the group cap.
    • A non-zero total_events (or total slices/seconds) additionally proves the query resolves and matches.
    • Zero is not automatically a bug. A quiet service legitimately produces empty buckets — no_data for by_count, uptime for time_slice. Before touching the SLO, query the same scope and window directly with analyze-traces / analyze-logs / analyze-metrics: no rows there either means the telemetry is absent, which is a different problem to fix somewhere else.
    • Never widen the user's filter to make numbers appear. That changes what is being measured into something nobody asked for. Report the empty result and what you checked.
  7. Verify the generated alerts if you set alert_types — they should exist as rules named "<SLO name> Fast Burn Rate" and so on, with your channel ids.
  8. Update or delete with PUT (full body, id inside) or DELETE /api/slo/{id}, then repeat steps 5–7. An update rebuilds the generated rules.

The Three SLI Types

slo_typeNumerator / denominatorWhat you configureCount unit
by_countgood events / total eventstwo queries: good_events_filter and total_events_filterevents
time_slicegood slices / total slicesone query (total_events_filter) + operation + threshold_valueslices
alertnon-firing seconds / total secondsalert rule ids in total_events_filter.alert_idsseconds

slo_type defaults to by_count when omitted. The UI calls them "By Count", "By Time Slices" and "By Uptime".

Routing

The user is asking…Do this
"create an SLO for X"build the POST /api/slo body — references/api-payloads.md
"will this SLO pass?" / "what would compliance have been?"POST /api/slo/preview before creating — it costs nothing and stores nothing, but it is an estimate, not a replay (limits)
"what SLOs do we have / which are breached?"POST /api/slo/list?status=active&current_time=…, or POST /api/slo/stats
"why is this SLO burning budget?"GET /api/slo/{id}/burn-rate/timeseries, then kubesense-traces / kubesense-logs on the same filter
"alert me when the budget burns"alert_types on the SLO — do not hand-write burn-rate alert rules
"port our Datadog SLOs"references/datadog-migration.md
"put SLOs on a dashboard"kubesense-dashboards — panelType: "slo"

Endpoints

All under /api/slo, RBAC module slo. Create/update/delete additionally need write access on that module.

MethodPathPurpose
POST/api/slocreate — returns the new SLO id in data
PUT/api/sloupdate — the id goes in the body as id, not the path
DELETE/api/slo/{id}soft delete (sets removed) and drops its burn-rate alert rules
GET/api/slo/{id}?current_time=<RFC3339>one SLO with its rollups
POST/api/slo/list?status=active&current_time=<RFC3339>list; filters in the JSON body
GET/api/slo/search?search=<substr>&limit=50{id, name} only — for pickers
POST/api/slo/statscounts by status: normal / warning / breached / no_data
POST/api/slo/previewdry-run compliance over a window, stores nothing (status is normal/breached/no_data — never warning)
GET/api/slo/{id}/metrics?from_time=&to_time=good/bad/total + budget for a window
GET/api/slo/{id}/groups?windows=last_24_hour,last_7_daysper-group figures, several windows in one scan
GET/api/slo/{id}/timeseriescompliance over time
GET/api/slo/{id}/burn-rate/timeseriesburn rate over time
POST/api/slo/filters, /api/slo/filter/searchfilter values for the list page

Warning

current_time is required on /list and GET /{id}, and status is required on /list. Both are query parameters, not body fields. Omit current_time and the call 500s ("currentTime must not be zero"); omit status and the WHERE status = ? matches nothing and you get an empty list that reads like "no SLOs exist". Send status=active.

The Create Payload

Minimum viable by_count SLO over traces:

{
  "name": "Checkout availability",
  "description": "Successful checkout spans / all checkout spans",
  "slo_type": "by_count",
  "metric": "requests",
  "slo_target_percentage": 99.9,
  "evaluation_window_days": 30,
  "evaluation_time_interval_seconds": 60,
  "evaluate_from": "2026-09-01T00:00:00Z",
  "evaluation_start_time": "2026-09-01T00:00:00Z",
  "status": "active",
  "cluster": "", "namespace": "", "workload": "", "customer_identifier": "",
  "operation": "", "threshold_value": 0, "value": 0,
  "good_events_filter":  { "selectedMode": "traces", "value_operation": "row_count", "groupBy": [], "unified_filter": { "type": "common", "common_filter": [ {"field": "app_service", "operation": "IN", "values": ["checkout"]}, {"field": "status", "operation": "IN", "values": ["ok"]} ], "adv_filters": {} } },
  "total_events_filter": { "selectedMode": "traces", "value_operation": "row_count", "groupBy": [], "unified_filter": { "type": "common", "common_filter": [ {"field": "app_service", "operation": "IN", "values": ["checkout"]} ], "adv_filters": {} } },
  "alert_types": ["burn_rate_fast", "error_budget_low"],
  "notification_channels": [1]
}

Every per-type shape, the metrics and logs variants, and the response bodies are in references/api-payloads.md.

Rules of the body:

  • evaluation_time_interval_seconds must be > 0 — the server rejects 0 outright, and the evaluator skips an SLO whose interval is 0 rather than looping forever.
  • status: "active" or the evaluator never picks it up.
  • slo_target_percentage is a percentage (99.9), not a fraction.
  • warning_target_percentage, when set, should be higher than the target — it is where you want to be warned before breaching. The UI enforces >= target + 0.1; the API does not validate it at all, and a value below the target is stored and never reached, because the breached check runs first.
  • metric is a label, not a query. "requests" for a traces SLI, the PromQL metric name for a metrics SLI, "alert_uptime" for an alert SLI. Nothing is evaluated from it — it just lands on the list page and in filters.
  • operation / threshold_value are the time_slice condition only. by_count ignores them; fold a latency cutoff into the good query as a duration filter instead.
  • Update is a full replace: PUT /api/slo writes every column from the body, so send the whole object (read it back with GET /api/slo/{id} first), and put the id in the body as id.

Good and Total Are Alert Query Configs

good_events_filter and total_events_filter are the same query_config object an alert rule uses — same JSON keys, same executors, same field allow-lists. That is the single most useful thing to know: anything you know about building an alert rule's query applies here.

KeyMeaning
selectedModetraces | logs | metrics. Defaults to traces when empty.
unified_filtertraces/logs filter tree: {type, common_filter[], adv_filters{}}
promqlmetrics only — the whole metrics query. (An alert rule's queryMode has no field here and is dropped.)
value_operationrow_count (the normal choice), unique_count, avg, sum, min, max, percentiles
fieldsrequired for any non-count value_operation
groupBy[{field, type, is_attribute}] — see Grouping
alert_idsalert-type SLOs only

Important

Field names follow the ALERT engine's vocabulary, not the query tools'. Logs and traces SLI queries run through the same rule-engine executors alert rules use, so app_service (not service), level (not type), pod_name/pod, node_name/node, return_code/status_code. The full allow-lists are in kubesense-alerts — they are the authority for SLOs too.

There is one divergence to know about: POST /api/slo/preview runs the query through the API's own explore engine, while the evaluator runs it through the rule-engine. Their field maps overlap but are not identical, so a name that previews fine can still error on every evaluation. Preview also hardcodes row_count for traces and logs (ignoring value_operation and fields, so a latency SLI previews request counts) and samples ~300 points for metrics rather than evaluating every bucket. Preview catches an empty or wrong filter; it does not validate the number. Confirm on the first real buckets after creating.

Formulas and Ratios

A Datadog monitor or SLO built from several queries and an expression like (A/B)*100 does not port as one thing. Where it lands decides the shape:

What you are portingWhere it goesHow
An SLO whose SLI is numerator / denominatorby_count SLOTwo queries, no formula. A → good_events_filter, B → total_events_filter. The evaluator divides them itself.
A monitor that thresholds a ratioAlert ruleAlerts have a real formula mode — keep A, B and expression as separate queries and point metric_query_label at the formula's label. See kubesense-alerts.
An SLI that is a ratio compared to a threshold each intervaltime_slice SLOOne inlined PromQL, with operation + threshold_value.
Ad-hoc analysis, no SLO or ruleanalyze-telemetry (MCP)An array of labelled queries plus {"selectedMode": "formula", "expression": "(A/B)*100"} — it composes across logs, traces and metrics.

Warning

selectedMode: "formula" does not work in an SLO. The evaluator resolves its query through an executor factory that knows logs, metrics and traces and returns unsupported query type: formula for anything else — on every evaluation, so the SLO sits at no_data forever. Alerts are the surface with formula support; SLOs are not.

Inline it into one PromQL string instead. That is what the product does: the SLO editor's formula builder substitutes each label with its parenthesised query before it posts, and stores one flat promql. So (A/B)*100 is stored as

(sum(increase(http_requests_total{status=~"5.."}[5m])) / sum(increase(http_requests_total[5m]))) * 100

Important

Do not inline a ratio into a by_count SLO. There the ratio is the SLO — the evaluator computes good/total and builds the error budget from it. Feed it a percentage as total_events_filter and it counts percentages as events, and every number downstream is meaningless. Ratios belong in time_slice (one value per slice, compared to a threshold) or in an alert rule.

When an inlined ratio is grouped, keep by (…) on both sides of the division and a non-empty groupBy on the query — by (…) alone leaves the SLO on the single-series path (see Grouping).

Grouping

Put groupBy on total_events_filter (and the matching one on good_events_filter) and the SLO splits into one compliance figure per group, joined on group identity.

  • Groups are emitted as Prometheus labels on every kubesense_slo_* series, plus a pooled overall series labelled slo_group="*".
  • slo_id, slo_group, window, target_percentage, error_budget and __name__ are reserved — a groupBy on a field that renders to one of those names is dropped. Grouping by workload, namespace or cluster is fine and is exactly how you get them onto the series.
  • Field names are sanitised to valid Prometheus label names: anything outside [a-zA-Z0-9_] becomes _, so service.name → service_name.
  • Hard cap of 100 groups. Past that the evaluator skips the whole bucket and logs it — the SLO silently stops producing data. Never group by pod, instance, trace_id or any unbounded field.
  • Groups on the good side that have no matching total group are clamped away, with a warning. Keep the two groupBy lists identical.
  • A metrics SLI needs a non-empty groupBy too. by (…) in the PromQL does not switch grouping on by itself: the evaluator reads total_events_filter.groupBy, and with an empty list a multi-series result is reduced to its first series, the rest discarded silently. (time_slice over metrics is the exception — it always runs multi-series.) See references/api-payloads.md.

Burn-Rate Alerting Comes From alert_types

Set alert_types and the API creates alert rules for you, inside the same transaction as the SLO:

alert_types valueRuleSeverity
burn_rate_fastkubesense_slo_burn_rate{window="1h"} over (days × 24 × 0.02) / 1critical
burn_rate_slowkubesense_slo_burn_rate{window="6h"} over (days × 24 × 0.05) / 6warning
error_budget_criticalkubesense_slo_error_budget_balance < 5critical
error_budget_lowkubesense_slo_error_budget_balance < 10warning

Thresholds are derived from evaluation_window_days, so a 30-day window gives the familiar 14.4× / 6× multi-window burn rates. They are recomputed on every update.

Warning

Never hand-write these rules. PUT /api/slo deletes every alert rule attached to the SLO and rebuilds them from alert_types; DELETE removes them. An edit to a generated rule is silently reverted on the next SLO update.

"Attached" means the rule's own labels.slo_id, not its PromQL. The delete is WHERE JSON_EXTRACT(labels, '$.slo_id') = <id>, and it does not care who created the rule. So a hand-written rule that merely selects kubesense_slo_burn_rate{slo_id="…"} in its query survives, while one that carries slo_id in its own labels — the obvious thing to do when copying a generated rule — is deleted along with them. Keep slo_id out of the labels of any rule you want to keep.

notification_channels on the SLO is what those generated rules page. Call list-notification-channels (MCP) or GET /api/alerts/notification-channels first — a non-existent id fails rule creation and takes the whole create transaction down with it.

Window, Cadence and Backfill

  • evaluation_window_days is the rolling compliance window (7, 30, 90 …).
  • evaluation_time_interval_seconds is the bucket size — and, for time_slice, the slice size. The UI offers 60 and 300 for slices; they are the same two values Datadog offers.
  • evaluate_from is where evaluation starts, and the evaluator backfills from there to now in batches of 20 buckets per pass. Setting it months back on a 60s interval means tens of thousands of buckets before it catches up to live — set it to the start of the window you actually care about.
  • evaluation_start_time is clamped forward to evaluate_from if it is earlier.
  • Rollups the UI and /groups expose: last_2_hour, last_24_hour, last_7_days, last_28_days.

Traps

TrapConsequence
evaluation_time_interval_seconds: 0500 on create; an existing 0 makes the evaluator skip the SLO
status not "active"Stored, listed, and never evaluated
/list without status=activeEmpty list that looks like "no SLOs"
/list or GET /{id} without current_time500
selectedMode omittedSilently treated as traces — a metrics SLI with only promql set queries traces and finds nothing
Query-tool field names (service, type) in unified_filterErrors on every evaluation; the SLO sits at no_data
groupBy resolving to > 100 groupsEvery bucket skipped — no data, no error surfaced in the UI
groupBy lists differing between good and totalOrphaned good groups are clamped; compliance reads as 100% for them
Alert SLO with no alert_idsEvaluator skips it (it would otherwise report a flawless 100% forever)
warning_target_percentage below the targetNever warns — warning must be above target
time_slice operation as a long form (less_than) or lowercase (lt)Passes API validation, unreadable to the evaluator — every slice scores as downtime, 0% forever. Uppercase GT/GTE/LT/LTE/EQ only
time_slice operation: "NEQ"Rejected at create, despite being offered in the UI's dropdown
Metrics by_count with by (…) but an empty groupByOnly the first series is counted; the rest vanish silently
A hand-written rule carrying slo_id in its own labelsDeleted on the next PUT /api/slo along with the generated ones
Alert SLO with evaluate_from before its rules existedBackfills invented perfect uptime — it is not recorded as no-data
Editing a generated burn-rate ruleReverted on the next PUT /api/slo
PUT with a partial bodyFull replace — unsent fields are written as their zero values
evaluation_window_days not a multiple of 7 and > 31 (e.g. 90)Works over the API, but the UI's period input (1–31 × day/week) cannot represent it, so a later edit in the UI fails validation

Missing-Data Semantics Differ By Type — Deliberately

  • by_count: an empty denominator is no_data. Compliance is not computed, the bucket is not scored.
  • time_slice: missing data counts as uptime, matching Datadog. A count query returning 0 is treated as "no data", so a zero-traffic minute is a good slice. This does mean a telemetry outage reads as a healthy SLO.
  • alert: downtime is the union of firing intervals of the named rules; a rule still firing at the window edge is counted as firing up to that edge. A bucket with no firing rows at all scores as fully healthy, not as no-data — so an evaluate_from earlier than the rules themselves backfills invented perfect uptime into the budget. Set evaluate_from to when the rules started existing.

Say which one applies when you hand an SLO over. It is the single most common source of "this number looks wrong".

Error Budget and Burn Rate

  • error_budget_percentage = 100 - slo_target_percentage.
  • error_budget_balance = (1 - error% / budget%) × 100, unclamped — a blown budget goes negative (99.52% actual against a 99.9% target is −380%), matching Datadog. Do not "fix" a negative number; it is the burndown.
  • Burn rate is normalised: 1.0 consumes exactly the budget over the window, whatever the window's length. The list page's indicator uses the 2h window, critical above 6 and elevated above 1 — the same thresholds Datadog shows.
  • Series emitted: kubesense_slo_evaluated_events_total / _good, kubesense_slo_evaluated_events_bad_percent, kubesense_slo_error_budget_balance, kubesense_slo_burn_rate (windows 1h, 2h, 6h), kubesense_slo_breached, kubesense_slo_status, kubesense_slo_burn_rate_status. Query them with kubesense-metrics.

Rules

  1. Discover real metric and field names first (get-available-metrics, get-trace-or-log-fields). Never invent one.
  2. Use the alert engine's field vocabulary in unified_filter — app_service, not service; level, not type.
  3. Always POST /api/slo/preview before creating, and show the user the compliance and budget numbers it returns. Preview is free and stores nothing.
  4. Set selectedMode explicitly on both filters. The default is traces.
  5. evaluation_time_interval_seconds > 0 and status: "active", always.
  6. Keep groupBy identical on good and total, and bounded well under 100 groups — and set it even on a metrics SLI, where by (…) alone leaves you with one series.
  7. Never hand-write burn-rate alert rules — use alert_types, and verify the channel ids exist first.
  8. PUT sends the whole object; read the SLO back before editing it.
  9. time_slice operators are uppercase GT, GTE, LT, LTE, EQ. Nothing else works end to end.
  10. State the missing-data semantics for the type you built — and for an alert SLO, set evaluate_from to when its rules started existing.
  11. Never claim an SLO was created unless POST /api/slo returned an id — and never call it working until a real bucket came back with events (step 6).

Comments

Markdown supported