Skip to content
Oday Bakkour
Back to Knowledge Hub

A Release Command Center in Claude Dashboards: SLOs, Burn Rates and DORA

Oday Bakkour profile photo
Oday Bakkour
6 min read
Share
A Release Command Center in Claude Dashboards: SLOs, Burn Rates and DORA

Executive Summary

  • What it is: Claude Dashboards, in beta since October 8, builds a live dashboard from a plain-language request on top of a connected data platform such as BigQuery, Snowflake, Databricks, Redshift or ClickHouse.
  • What I built with it: a release command center for three services. SLO error budgets with 1h and 6h burn rates, the 5xx rate with every deploy marked on it, a ranked list of the deploys most likely behind a spike, and the five DORA metrics.
  • The data: three feeds into one BigQuery dataset: deploys from GitHub Actions, request logs from the load balancer, incidents from PagerDuty.
  • The thresholds: burn-rate alerts follow Google's SRE Workbook: page at 14.4x over 1 hour or 6x over 6 hours, ticket at 1x over 3 days, for a 99.9% SLO.
  • The rule I follow: click a number and read its query before anyone acts on it.

I made a short video of this on X; this post has every command and query so you can build it yourself.

A release command center, built from one prompt

A release command center, built from one prompt

What goes on the dashboard

  • SLO cards: per service, the error budget left over 30 days and two burn rates. A burn rate of 1 means you are using the budget exactly as fast as the SLO allows. Higher means faster.
  • 5xx rate with deploy markers: the last 6 hours for one service, with a vertical line for every deploy, so a spike right after a deploy is obvious.
  • Suspect deploys: every recent deploy, ranked by how much the error rate changed in the 15 minutes after it compared with the hour before.
  • DORA strip: deployment frequency, change lead time, change fail rate, failed deployment recovery time and deployment rework rate (dora.dev).
Your release data lives in three places; route it into one BigQuery dataset

Your release data lives in three places; route it into one BigQuery dataset

Step 1: route the data into BigQuery

Request logs. Turn on logging for each backend service of your external Application Load Balancer, then route those logs to BigQuery with a log sink. With partitioned tables, the load balancer's requests log lands in one table, ops.requests, and keeps field names like httpRequest.status and resource.labels.backend_service_name (Cloud Logging docs).

setup.sh
# 1. Log every request on the backend service (repeat per service)
gcloud compute backend-services update api \
  --enable-logging --logging-sample-rate=1.0 --global

# 2. Route load balancer logs to the ops dataset
gcloud logging sinks create lb-to-bq \
  bigquery.googleapis.com/projects/acme/datasets/ops \
  --log-filter='resource.type="http_load_balancer"' \
  --use-partitioned-tables

The sink create command prints a writer identity. Give that service account the BigQuery Data Editor role on the ops dataset, or nothing arrives. If you log a sample of requests instead of all of them, counts shrink but error ratios stay right.

Deploys and incidents. Create the two small tables once:

schema.sql
CREATE TABLE ops.deploys (
  service STRING,
  sha STRING,
  status STRING,          -- success | failure | cancelled
  committed_at TIMESTAMP,
  deployed_at TIMESTAMP
)
PARTITION BY DATE(deployed_at);

CREATE TABLE ops.incidents (
  service STRING,
  sha STRING,             -- the deploy that caused it
  fix_sha STRING,         -- the deploy that fixed it (hotfix or rollback)
  opened_at TIMESTAMP,
  resolved_at TIMESTAMP
)
PARTITION BY DATE(opened_at);

Then one step at the end of the deploy job writes a row for every deploy. if: always() runs it for failed deploys too:

.github/workflows/deploy.yml
name: deploy
on:
  push:
    branches: [main]

permissions:
  contents: read
  id-token: write

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
          service_account: ${{ secrets.DEPLOY_SA }}
      - uses: google-github-actions/setup-gcloud@v2
      - run: ./deploy.sh
      - name: Record deploy
        if: always()
        run: |
          bq query --nouse_legacy_sql "INSERT ops.deploys
            (service, sha, status, committed_at, deployed_at)
            VALUES ('api', '${{ github.sha }}', '${{ job.status }}',
            TIMESTAMP('$(git log -1 --format=%cI)'), CURRENT_TIMESTAMP())"

For incidents, write one row per incident from your PagerDuty (or other incident tool) webhook, or by hand while you start: sha is the deploy that caused it and fix_sha the deploy that fixed it. The queries below assume each backend service is named like the service in ops.deploys (api, web, worker).

Step 1: wire the data in once

Step 1: wire the data in once

Step 2: the prompt

Connect BigQuery in Claude and ask for the whole thing at once. This is my prompt:

prompt.txt
Build a live release command center from the ops dataset in BigQuery for the
services api, web and worker.

1. SLO cards: SLO 99.9% for api and worker, 99.5% for web, over 30 days.
   Show error budget left, and the burn rate over the last 1 hour and 6 hours.
   A 5xx response counts as an error. Mark PAGE when the 1h burn rate is
   at least 14.4 or the 6h burn rate is at least 6.
2. A chart of the 5xx rate in 5-minute buckets over the last 6 hours, per
   service, with every deploy from ops.deploys as a vertical marker.
3. Suspect deploys: for every deploy in the last 6 hours, the 5xx rate in the
   15 minutes after it divided by the rate in the hour before, highest first.
4. A strip with the five DORA metrics over 90 days, as defined on dora.dev.

Aggregate in SQL so each dataset stays small, and show me the query behind
every number.

Claude writes the queries and builds the dashboard, and it keeps updating as new data lands. Each chart shows when it was last refreshed (Anthropic).

Illustration: one prompt builds the command center

Illustration: one prompt builds the command center

The burn-rate rule, and why two windows

A burn rate is how fast you use the error budget relative to the SLO: the error ratio divided by the allowed error ratio (0.1% for a 99.9% SLO). Google's SRE Workbook recommends these thresholds for a 99.9% SLO over 30 days:

  • Page: 14.4x over 1 hour (2% of the budget gone), or 6x over 6 hours (5% gone).
  • Ticket: 1x over 3 days (10% gone).

The Workbook also pairs each long window with a short one (5 minutes, 30 minutes, 6 hours) so an alert stops soon after the problem stops. That belongs in your alerting tool. The dashboard is where you look when the page arrives.

Google's SRE Workbook thresholds for a 99.9% SLO

Google's SRE Workbook thresholds for a 99.9% SLO

The SQL behind each panel

Claude writes its own queries, so these are the reference I compare against. All are BigQuery standard SQL.

Error budget left (30 days, 99.9% SLO):

error_budget.sql
SELECT
  1 - SAFE_DIVIDE(COUNTIF(httpRequest.status >= 500), COUNT(*)) / (1 - 0.999)
    AS budget_left
FROM ops.requests
WHERE resource.labels.backend_service_name = 'api'
  AND timestamp > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY);

Burn rates over 1 hour and 6 hours (the query in the video):

burn_rate.sql
WITH m AS (
  SELECT
    httpRequest.status >= 500 AS err,
    timestamp > TIMESTAMP_SUB(
      CURRENT_TIMESTAMP(), INTERVAL 1 HOUR) AS last_1h
  FROM ops.requests
  WHERE resource.labels.backend_service_name = 'api'
    AND timestamp > TIMESTAMP_SUB(
      CURRENT_TIMESTAMP(), INTERVAL 6 HOUR)
)
SELECT
  COUNTIF(err AND last_1h) / COUNTIF(last_1h)
    / (1 - 0.999) AS burn_1h,
  COUNTIF(err) / COUNT(*) / (1 - 0.999) AS burn_6h
FROM m;

5xx rate in 5-minute buckets (the chart):

error_rate_chart.sql
SELECT
  TIMESTAMP_SECONDS(DIV(UNIX_SECONDS(timestamp), 300) * 300) AS bucket,
  COUNTIF(httpRequest.status >= 500) / COUNT(*) AS error_rate
FROM ops.requests
WHERE resource.labels.backend_service_name = 'api'
  AND timestamp > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 6 HOUR)
GROUP BY bucket
ORDER BY bucket;

Suspect deploys (error rate 15 minutes after each deploy compared with the hour before):

suspect_deploys.sql
WITH d AS (
  SELECT service, sha, deployed_at
  FROM ops.deploys
  WHERE status = 'success'
    AND deployed_at > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 6 HOUR)
),
r AS (
  SELECT
    resource.labels.backend_service_name AS service,
    timestamp,
    httpRequest.status >= 500 AS err
  FROM ops.requests
  WHERE timestamp > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 8 HOUR)
),
w AS (
  SELECT d.service, d.sha, d.deployed_at,
    SAFE_DIVIDE(COUNTIF(r.err AND r.timestamp >= d.deployed_at),
                COUNTIF(r.timestamp >= d.deployed_at)) AS err_after,
    SAFE_DIVIDE(COUNTIF(r.err AND r.timestamp < d.deployed_at),
                COUNTIF(r.timestamp < d.deployed_at)) AS err_before
  FROM d
  JOIN r
    ON r.service = d.service
   AND r.timestamp BETWEEN TIMESTAMP_SUB(d.deployed_at, INTERVAL 60 MINUTE)
                       AND TIMESTAMP_ADD(d.deployed_at, INTERVAL 15 MINUTE)
  GROUP BY d.service, d.sha, d.deployed_at
)
SELECT *, SAFE_DIVIDE(err_after, err_before) AS ratio
FROM w
ORDER BY ratio DESC;

This is a signal, not proof: a deploy can line up with a spike that a dependency caused. It tells you where to look first.

DORA (90 days, successful deploys only):

dora.sql
-- Deployment frequency per week
SELECT service, DATE_TRUNC(DATE(deployed_at), WEEK) AS week, COUNT(*) AS deploys
FROM ops.deploys
WHERE status = 'success'
  AND deployed_at > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 90 DAY)
GROUP BY service, week;

-- Median change lead time (hours)
SELECT service,
  APPROX_QUANTILES(TIMESTAMP_DIFF(deployed_at, committed_at, MINUTE), 100)[OFFSET(50)] / 60
    AS median_lead_time_h
FROM ops.deploys
WHERE status = 'success'
  AND deployed_at > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 90 DAY)
GROUP BY service;

-- Change fail rate
SELECT service, COUNTIF(i.sha IS NOT NULL) / COUNT(*) AS change_fail_rate
FROM ops.deploys AS d
LEFT JOIN (SELECT DISTINCT sha FROM ops.incidents) AS i USING (sha)
WHERE d.status = 'success'
  AND d.deployed_at > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 90 DAY)
GROUP BY service;

-- Median failed deployment recovery time (minutes)
SELECT service,
  APPROX_QUANTILES(TIMESTAMP_DIFF(resolved_at, opened_at, MINUTE), 100)[OFFSET(50)]
    AS median_recovery_min
FROM ops.incidents
WHERE resolved_at IS NOT NULL
  AND opened_at > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 90 DAY)
GROUP BY service;

-- Deployment rework rate
SELECT d.service, COUNTIF(f.fix_sha IS NOT NULL) / COUNT(*) AS rework_rate
FROM ops.deploys AS d
LEFT JOIN (SELECT DISTINCT fix_sha FROM ops.incidents) AS f ON d.sha = f.fix_sha
WHERE d.status = 'success'
  AND d.deployed_at > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 90 DAY)
GROUP BY d.service;
Illustration: click a burn rate to see its query

Illustration: click a burn rate to see its query

Step 3: check it, then watch it

Click the number you are about to act on. Claude shows the query, and you can ask it to explain the calculation. Compare it with the reference above. The mistakes I look for: counting 4xx as errors, the wrong time window, and joining incidents twice.

Then let it run. When the rollback deploy lands, the marker appears, the 5xx rate drops back to normal, the 1h burn rate starts to fall and the recovery time updates. Nobody rebuilds anything.

Illustration: the rollback lands and the numbers update live

Illustration: the rollback lands and the numbers update live

Before you trust it with on-call

  • Permissions: with a live connection, queries run with each viewer's own identity and permissions (Tabular Editor's hands-on review). Someone without access to ops sees no data.
  • Live or extract: only live queries get a refresh button. Check the Sources pane to see which one each chart uses.
  • Keep it small: each dataset is capped at 8 MB. For busy services, schedule a query that rolls ops.requests up per minute into a small table, and point the dashboard there.
  • Cost: the request log table is the big one. Filter on timestamp so BigQuery only reads the partitions you need.
  • Sharing: your team can edit the dashboard with Claude, and send it on to tools like Grafana, Hex and PostHog (Anthropic).
  • Plans and data: Dashboards is in beta on paid plans. On Enterprise, an admin turns it on under Organization settings, Artifacts. The queried data is processed by Anthropic, so check zero data retention and data residency needs first.
Before you trust it: permissions, size and sharing

Before you trust it: permissions, size and sharing

Sources

Add Oday Bakkour as a preferred source on Google

Comments

Share your thoughts and join the conversation

Leave a Comment

Loading comments...
RELATED