skilldrop-cli 0.16.34 → 0.16.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/agents/README.md +4 -1
- package/agents/docs-reviewer.md +47 -0
- package/agents/finding-adjudicator.md +49 -0
- package/agents/test-reviewer.md +52 -0
- package/catalogue.json +17 -7
- package/model-routing.json +40 -0
- package/package.json +1 -1
- package/packs/ai-engineering/skills/agent-config-review/SKILL.md +116 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/eval_queries.json +12 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/evals.json +75 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/1/.claude/agents/db-migrator.md +8 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/1/.claude/agents/reviewer.md +9 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/1/.claude/settings.json +48 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/1/.claude/settings.local.json +5 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/1/.mcp.json +22 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/1/package.json +12 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/2/.cursor/mcp.json +17 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/2/config.toml +21 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/4/my-user-settings.json +11 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/4/quillstack/.claude/agents/release-notes.md +13 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/4/quillstack/.claude/settings.json +39 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/4/quillstack/.mcp.json +18 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/4/quillstack/package.json +14 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/5/.github/workflows/codex-triage.yml +29 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/5/AGENTS.md +16 -0
- package/packs/ai-engineering/skills/agent-config-review/evals/files/5/codex/config.toml +34 -0
- package/packs/ai-engineering/skills/agent-config-review/examples/inputs/tidewater-ingest/.claude/settings.json +22 -0
- package/packs/ai-engineering/skills/agent-config-review/examples/inputs/tidewater-ingest/.mcp.json +9 -0
- package/packs/ai-engineering/skills/agent-config-review/examples/python-repo-tighten.md +81 -0
- package/packs/ai-engineering/skills/agent-config-review/manifest.json +60 -0
- package/packs/ai-engineering/skills/agent-config-review/reference.md +205 -0
- package/packs/ai-engineering/skills/agent-config-review/scripts/agent_config_scan.py +989 -0
- package/packs/ai-engineering/skills/agent-incident-response/SKILL.md +175 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/eval_queries.json +14 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/evals.json +107 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/1/agent-session-excerpt.txt +11 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/1/ci-build-log.txt +24 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/1/incident-notes.md +19 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/2/mcp-config.json +20 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/2/session-transcript.txt +8 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/4/agent-session.log +70 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/4/infra/staging-db.tf +25 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/5/jobs/nightly-triage/job.yaml +19 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/5/jobs/nightly-triage/run.log +13 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/5/usage-export-2026-10-20.csv +13 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/6/.mcp.json +20 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/6/git-log.txt +12 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/6/notesync-excerpt.txt +30 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/6/notesync.log +15 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/6/ssh-config.txt +16 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/7/.github/workflows/ci.yml +28 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/7/SECURITY.md +8 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/7/issue-479.md +35 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/7/package.json +15 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/7/pr-482.diff +20 -0
- package/packs/ai-engineering/skills/agent-incident-response/evals/files/7/run-log.txt +30 -0
- package/packs/ai-engineering/skills/agent-incident-response/examples/injected-issue-comment.md +57 -0
- package/packs/ai-engineering/skills/agent-incident-response/manifest.json +55 -0
- package/packs/ai-engineering/skills/agent-incident-response/reference.md +152 -0
- package/packs/ai-engineering/skills/agent-incident-response/scripts/find_secret_exposure.py +239 -0
- package/packs/ai-engineering/skills/agent-incident-response/scripts/usage_split.py +194 -0
- package/packs/converters/skills/file-to-markdown/SKILL.md +3 -2
- package/packs/converters/skills/file-to-markdown/evals/eval_queries.json +4 -0
- package/packs/converters/skills/file-to-markdown/manifest.json +2 -2
- package/packs/converters/skills/file-to-markdown/scripts/to_markdown.py +340 -7
- package/packs/converters/skills/md-to-confluence/SKILL.md +139 -0
- package/packs/converters/skills/md-to-confluence/evals/eval_queries.json +42 -0
- package/packs/converters/skills/md-to-confluence/evals/evals.json +89 -0
- package/packs/converters/skills/md-to-confluence/evals/files/1/img/latency-dashboard.png +0 -0
- package/packs/converters/skills/md-to-confluence/evals/files/1/payments-runbook.md +60 -0
- package/packs/converters/skills/md-to-confluence/evals/files/2/deploy-checklist.md +33 -0
- package/packs/converters/skills/md-to-confluence/evals/files/3/img/after/screen.png +0 -0
- package/packs/converters/skills/md-to-confluence/evals/files/3/img/before/screen.png +0 -0
- package/packs/converters/skills/md-to-confluence/evals/files/3/release-notes-4.2.md +49 -0
- package/packs/converters/skills/md-to-confluence/evals/files/4/docs/img/topology.png +0 -0
- package/packs/converters/skills/md-to-confluence/evals/files/4/docs/ingest-pipeline-design.md +65 -0
- package/packs/converters/skills/md-to-confluence/evals/files/5/support/troubleshooting-sso.md +57 -0
- package/packs/converters/skills/md-to-confluence/examples/inputs/img/queue-depth.png +0 -0
- package/packs/converters/skills/md-to-confluence/examples/inputs/queue-runbook.fixed.md +21 -0
- package/packs/converters/skills/md-to-confluence/examples/inputs/queue-runbook.md +21 -0
- package/packs/converters/skills/md-to-confluence/examples/queue-runbook.md +57 -0
- package/packs/converters/skills/md-to-confluence/manifest.json +52 -0
- package/packs/converters/skills/md-to-confluence/reference.md +92 -0
- package/packs/converters/skills/md-to-confluence/scripts/md_to_confluence.py +821 -0
- package/packs/converters/skills/md-to-pptx/SKILL.md +131 -0
- package/packs/converters/skills/md-to-pptx/evals/eval_queries.json +12 -0
- package/packs/converters/skills/md-to-pptx/evals/evals.json +84 -0
- package/packs/converters/skills/md-to-pptx/evals/files/1/img/defect-burndown.png +0 -0
- package/packs/converters/skills/md-to-pptx/evals/files/1/status-report.md +75 -0
- package/packs/converters/skills/md-to-pptx/evals/files/3/brand.json +12 -0
- package/packs/converters/skills/md-to-pptx/evals/files/3/night-shift-induction.md +53 -0
- package/packs/converters/skills/md-to-pptx/evals/files/4/charts/ontime.png +0 -0
- package/packs/converters/skills/md-to-pptx/evals/files/4/qbr-q3.md +85 -0
- package/packs/converters/skills/md-to-pptx/evals/files/5/vendor-sync-notes.md +28 -0
- package/packs/converters/skills/md-to-pptx/examples/support-weekly-input.md +48 -0
- package/packs/converters/skills/md-to-pptx/examples/support-weekly.md +75 -0
- package/packs/converters/skills/md-to-pptx/examples/tickets-by-week.png +0 -0
- package/packs/converters/skills/md-to-pptx/manifest.json +49 -0
- package/packs/converters/skills/md-to-pptx/reference.md +139 -0
- package/packs/converters/skills/md-to-pptx/scripts/md_to_pptx.py +1841 -0
- package/packs/dev-team/loops/modernize/LOOP.md +124 -0
- package/packs/dev-team/loops/modernize/evals/eval_queries.json +9 -0
- package/packs/dev-team/loops/modernize/evals/evals.json +27 -0
- package/packs/dev-team/loops/modernize/loop.json +60 -0
- package/packs/dev-team/skills/behaviour-equivalence/SKILL.md +155 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/eval_queries.json +11 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/evals.json +76 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/cases.jsonl +12 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/new.jsonl +12 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/notes.md +15 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/old.jsonl +12 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/old_rerun.jsonl +12 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/pricing_v1.py +45 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/pricing_v2.py +65 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/1/rules.json +10 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/3/diff-summary.txt +16 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/4/diff.txt +13 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/4/fee_calc_py2.py +16 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/4/fee_calc_py3.py +24 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/4/input.csv +13 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/4/new_output.csv +13 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/4/old_output.csv +13 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/5/TEAM_NOTES.md +19 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/5/normalisation.yaml +12 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/5/recordings/new_run.jsonl +8 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/5/recordings/old_run1.jsonl +8 -0
- package/packs/dev-team/skills/behaviour-equivalence/evals/files/5/recordings/old_run2.jsonl +8 -0
- package/packs/dev-team/skills/behaviour-equivalence/examples/delivery-date.md +78 -0
- package/packs/dev-team/skills/behaviour-equivalence/manifest.json +33 -0
- package/packs/dev-team/skills/behaviour-equivalence/reference.md +97 -0
- package/packs/dev-team/skills/behaviour-equivalence/scripts/golden_diff.py +524 -0
- package/packs/dev-team/skills/behaviour-equivalence/templates/accepted.json +10 -0
- package/packs/dev-team/skills/behaviour-equivalence/templates/rules.json +7 -0
- package/packs/frontend/pack.json +55 -0
- package/packs/frontend/skills/rendering-strategy/SKILL.md +149 -0
- package/packs/frontend/skills/rendering-strategy/evals/eval_queries.json +38 -0
- package/packs/frontend/skills/rendering-strategy/evals/evals.json +77 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/1/fernhill-app.md +40 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/3/routes.md +33 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/4/build-notes.txt +5 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/4/nuxt.config.ts +19 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/4/pages/recipes/[slug].vue +37 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/4/routes.md +15 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/5/routes.txt +18 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/5/src/hooks.server.ts +14 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/5/src/routes/(marketing)/pricing/+page.server.ts +10 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/5/src/routes/+layout.server.ts +10 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/5/svelte.config.js +10 -0
- package/packs/frontend/skills/rendering-strategy/evals/files/5/traffic.md +18 -0
- package/packs/frontend/skills/rendering-strategy/examples/saas-site.md +42 -0
- package/packs/frontend/skills/rendering-strategy/manifest.json +50 -0
- package/packs/frontend/skills/rendering-strategy/reference.md +116 -0
- package/packs/frontend/skills/rendering-strategy/scripts/render_load.py +181 -0
- package/packs/frontend/skills/web-performance-review/SKILL.md +106 -0
- package/packs/frontend/skills/web-performance-review/evals/eval_queries.json +38 -0
- package/packs/frontend/skills/web-performance-review/evals/evals.json +76 -0
- package/packs/frontend/skills/web-performance-review/evals/files/1/psi.json +1065 -0
- package/packs/frontend/skills/web-performance-review/evals/files/2/after.json +359 -0
- package/packs/frontend/skills/web-performance-review/evals/files/2/before.json +375 -0
- package/packs/frontend/skills/web-performance-review/evals/files/4/psi-pdp-mobile-2026-10-19.json +171 -0
- package/packs/frontend/skills/web-performance-review/evals/files/4/rum-inp-attribution-oct14-19.csv +6 -0
- package/packs/frontend/skills/web-performance-review/evals/files/4/rum-inp-daily.csv +15 -0
- package/packs/frontend/skills/web-performance-review/evals/files/5/crux-home-phone.json +35 -0
- package/packs/frontend/skills/web-performance-review/evals/files/5/lighthouse-home-mobile.json +173 -0
- package/packs/frontend/skills/web-performance-review/examples/quayside-article-lhr.json +480 -0
- package/packs/frontend/skills/web-performance-review/examples/quayside-article.md +103 -0
- package/packs/frontend/skills/web-performance-review/manifest.json +48 -0
- package/packs/frontend/skills/web-performance-review/reference.md +105 -0
- package/packs/frontend/skills/web-performance-review/scripts/lh_summary.py +743 -0
- package/packs/product-manager/skills/lean-canvas/SKILL.md +162 -0
- package/packs/product-manager/skills/lean-canvas/evals/eval_queries.json +42 -0
- package/packs/product-manager/skills/lean-canvas/evals/evals.json +82 -0
- package/packs/product-manager/skills/lean-canvas/evals/files/1/pawpass-notes.md +54 -0
- package/packs/product-manager/skills/lean-canvas/evals/files/3/lunchloop-pitch.md +30 -0
- package/packs/product-manager/skills/lean-canvas/evals/files/4/clubkit-notes.md +44 -0
- package/packs/product-manager/skills/lean-canvas/evals/files/5/cost-model.csv +5 -0
- package/packs/product-manager/skills/lean-canvas/evals/files/5/proposal.md +40 -0
- package/packs/product-manager/skills/lean-canvas/evals/files/5/stakeholder-emails.txt +18 -0
- package/packs/product-manager/skills/lean-canvas/examples/bike-repair.md +72 -0
- package/packs/product-manager/skills/lean-canvas/manifest.json +34 -0
- package/packs/product-manager/skills/lean-canvas/reference.md +102 -0
- package/packs/product-manager/skills/lean-canvas/scripts/unit_economics.py +211 -0
- package/packs/solution-architect/skills/api-contract-draft/SKILL.md +2 -2
- package/packs/solution-architect/skills/api-contract-draft/manifest.json +28 -6
- package/packs/solution-architect/skills/event-contract-draft/SKILL.md +167 -0
- package/packs/solution-architect/skills/event-contract-draft/evals/eval_queries.json +38 -0
- package/packs/solution-architect/skills/event-contract-draft/evals/evals.json +76 -0
- package/packs/solution-architect/skills/event-contract-draft/evals/files/1/order-events-brief.md +45 -0
- package/packs/solution-architect/skills/event-contract-draft/evals/files/4/NOTES.md +23 -0
- package/packs/solution-architect/skills/event-contract-draft/evals/files/4/schemas/shipment-update.json +40 -0
- package/packs/solution-architect/skills/event-contract-draft/evals/files/5/invoice.example.json +25 -0
- package/packs/solution-architect/skills/event-contract-draft/examples/order-service.asyncapi.json +246 -0
- package/packs/solution-architect/skills/event-contract-draft/examples/order-service.md +92 -0
- package/packs/solution-architect/skills/event-contract-draft/manifest.json +25 -0
- package/packs/solution-architect/skills/event-contract-draft/reference.md +113 -0
- package/packs/solution-architect/skills/event-contract-draft/scripts/check_asyncapi.py +502 -0
- package/packs/solution-architect/skills/event-contract-draft/templates/asyncapi-skeleton.json +144 -0
- package/packs/sre-oncall/skills/slo-spec/SKILL.md +161 -0
- package/packs/sre-oncall/skills/slo-spec/evals/eval_queries.json +38 -0
- package/packs/sre-oncall/skills/slo-spec/evals/evals.json +75 -0
- package/packs/sre-oncall/skills/slo-spec/evals/files/1/order-service.md +51 -0
- package/packs/sre-oncall/skills/slo-spec/evals/files/3/search-slo-status.md +30 -0
- package/packs/sre-oncall/skills/slo-spec/evals/files/4/alerts/payments-api.rules.yaml +16 -0
- package/packs/sre-oncall/skills/slo-spec/evals/files/4/docs/dependencies.md +8 -0
- package/packs/sre-oncall/skills/slo-spec/evals/files/4/metrics/summary-90d.md +51 -0
- package/packs/sre-oncall/skills/slo-spec/evals/files/5/ops-notes.md +34 -0
- package/packs/sre-oncall/skills/slo-spec/examples/ticket-booking.md +88 -0
- package/packs/sre-oncall/skills/slo-spec/manifest.json +34 -0
- package/packs/sre-oncall/skills/slo-spec/reference.md +113 -0
- package/packs/sre-oncall/skills/slo-spec/scripts/slo_math.py +449 -0
- package/packs/sre-oncall/skills/slo-spec/templates/slo-spec.md +35 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Error-budget and burn-rate arithmetic for one or more SLOs.
|
|
3
|
+
|
|
4
|
+
Stdlib only; runs on Python 3.9+.
|
|
5
|
+
|
|
6
|
+
For each SLO it prints:
|
|
7
|
+
* the error budget as a fraction, as minutes of full outage per window, and
|
|
8
|
+
(when traffic is given) as bad requests per window at that traffic;
|
|
9
|
+
* a multi-window, multi-burn-rate alert table: burn rate, long and short
|
|
10
|
+
windows, the error rate that trips it, the budget consumed when it fires,
|
|
11
|
+
the time from a full budget to exhaustion at that burn, and the expected
|
|
12
|
+
number of bad events in each window at the threshold;
|
|
13
|
+
* the share of the budget a full outage of a given length spends (--outage);
|
|
14
|
+
* flags: windows with fewer than --min-events expected bad events (too noisy
|
|
15
|
+
to alert on), thresholds above 100% (the alert can never fire), a long
|
|
16
|
+
window longer than the SLO window, and a target above what the hard
|
|
17
|
+
dependencies in series allow.
|
|
18
|
+
|
|
19
|
+
Usage (one SLO):
|
|
20
|
+
python3 slo_math.py --target 99.9 --window 30 --rpd 1200000
|
|
21
|
+
python3 slo_math.py --target 99.5% --window 28 --rph 40 \
|
|
22
|
+
--alerts "14.4:1h:5m:page,6:6h:30m:page,1:3d:6h:ticket" --dep 99.9 --json
|
|
23
|
+
|
|
24
|
+
Usage (several SLOs from a JSON file, or "-" for stdin):
|
|
25
|
+
python3 slo_math.py --batch slos.json [--json] [-o out.txt]
|
|
26
|
+
|
|
27
|
+
slos.json is a list of objects:
|
|
28
|
+
{"name": "checkout availability", "target": 99.9, "window_days": 30,
|
|
29
|
+
"requests_per_day": 1200000, # or "requests_per_hour"
|
|
30
|
+
"alerts": "standard", # optional; or the --alerts string
|
|
31
|
+
"dependencies": [99.95, 99.9], # optional; hard deps in series
|
|
32
|
+
"outages_minutes": [40, 50]} # optional; full outages to price
|
|
33
|
+
|
|
34
|
+
Standard alert set (Google SRE workbook, chapter 5, tuned for a 30-day window):
|
|
35
|
+
14.4x long 1h short 5m page
|
|
36
|
+
6x long 6h short 30m page
|
|
37
|
+
1x long 3d short 6h ticket
|
|
38
|
+
|
|
39
|
+
Traffic is treated as an even average. Minutes of full outage are
|
|
40
|
+
time-based; for a request-based SLI they equal bad requests only if traffic
|
|
41
|
+
is uniform, so peak hours burn budget faster than the minutes suggest.
|
|
42
|
+
|
|
43
|
+
Exit codes: 0 ok, 2 bad input.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
import argparse
|
|
49
|
+
import json
|
|
50
|
+
import math
|
|
51
|
+
import re
|
|
52
|
+
import sys
|
|
53
|
+
from typing import Any, Dict, List, Optional
|
|
54
|
+
|
|
55
|
+
STANDARD_ALERTS = "14.4:1h:5m:page,6:6h:30m:page,1:3d:6h:ticket"
|
|
56
|
+
DEFAULT_MIN_EVENTS = 10.0
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class InputError(Exception):
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------- parsing
|
|
64
|
+
|
|
65
|
+
def parse_target(value: Any) -> float:
|
|
66
|
+
"""Return the target as a fraction in (0, 1). Accepts 99.9, '99.9%', 0.999."""
|
|
67
|
+
text = str(value).strip().rstrip("%").strip()
|
|
68
|
+
try:
|
|
69
|
+
num = float(text)
|
|
70
|
+
except ValueError:
|
|
71
|
+
raise InputError("target must be a number like 99.9 or 99.9%%, got %r" % value)
|
|
72
|
+
if math.isnan(num) or math.isinf(num):
|
|
73
|
+
raise InputError("target must be a finite number, got %r" % value)
|
|
74
|
+
frac = num / 100.0 if num > 1.0 else num
|
|
75
|
+
if frac >= 1.0:
|
|
76
|
+
raise InputError("a 100% target leaves no error budget: every failure is a breach "
|
|
77
|
+
"and nothing can ever ship safely. Pick a target below 100%.")
|
|
78
|
+
if frac <= 0.0:
|
|
79
|
+
raise InputError("target must be above 0%%, got %r" % value)
|
|
80
|
+
return frac
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def parse_duration_hours(text: str) -> float:
|
|
84
|
+
m = re.fullmatch(r"\s*(\d+(?:\.\d+)?)\s*([smhdw])\s*", str(text).lower())
|
|
85
|
+
if not m:
|
|
86
|
+
raise InputError("duration must look like 5m, 1h, 6h, 3d or 1w, got %r" % text)
|
|
87
|
+
n, unit = float(m.group(1)), m.group(2)
|
|
88
|
+
if n <= 0:
|
|
89
|
+
raise InputError("duration must be positive, got %r" % text)
|
|
90
|
+
return n * {"s": 1 / 3600.0, "m": 1 / 60.0, "h": 1.0, "d": 24.0, "w": 168.0}[unit]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def parse_alerts(spec: Optional[str]) -> List[Dict[str, Any]]:
|
|
94
|
+
if spec is None or str(spec).strip().lower() in ("", "standard"):
|
|
95
|
+
spec = STANDARD_ALERTS
|
|
96
|
+
rows = []
|
|
97
|
+
for chunk in str(spec).split(","):
|
|
98
|
+
chunk = chunk.strip()
|
|
99
|
+
if not chunk:
|
|
100
|
+
continue
|
|
101
|
+
parts = chunk.split(":")
|
|
102
|
+
if len(parts) not in (3, 4):
|
|
103
|
+
raise InputError("alert must be burn:long:short[:severity], e.g. 14.4:1h:5m:page, "
|
|
104
|
+
"got %r" % chunk)
|
|
105
|
+
try:
|
|
106
|
+
burn = float(parts[0].rstrip("x"))
|
|
107
|
+
except ValueError:
|
|
108
|
+
raise InputError("burn rate must be a number, got %r" % parts[0])
|
|
109
|
+
if burn <= 0 or math.isinf(burn) or math.isnan(burn):
|
|
110
|
+
raise InputError("burn rate must be a positive number, got %r" % parts[0])
|
|
111
|
+
long_h = parse_duration_hours(parts[1])
|
|
112
|
+
short_h = parse_duration_hours(parts[2])
|
|
113
|
+
if short_h >= long_h:
|
|
114
|
+
raise InputError("short window (%s) must be shorter than long window (%s)"
|
|
115
|
+
% (parts[2], parts[1]))
|
|
116
|
+
severity = parts[3].strip() if len(parts) == 4 and parts[3].strip() else "page"
|
|
117
|
+
rows.append({"burn": burn, "long": parts[1].strip(), "short": parts[2].strip(),
|
|
118
|
+
"long_hours": long_h, "short_hours": short_h, "severity": severity})
|
|
119
|
+
if not rows:
|
|
120
|
+
raise InputError("no alerts given")
|
|
121
|
+
return rows
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def positive(value: Any, name: str, allow_zero: bool = False) -> float:
|
|
125
|
+
try:
|
|
126
|
+
num = float(value)
|
|
127
|
+
except (TypeError, ValueError):
|
|
128
|
+
raise InputError("%s must be a number, got %r" % (name, value))
|
|
129
|
+
if math.isnan(num) or math.isinf(num) or num < 0 or (num == 0 and not allow_zero):
|
|
130
|
+
raise InputError("%s must be %s, got %r" % (name, "zero or more" if allow_zero
|
|
131
|
+
else "greater than zero", value))
|
|
132
|
+
return num
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ---------------------------------------------------------------- maths
|
|
136
|
+
|
|
137
|
+
def compute(name: str, target: Any, window_days: Any, rpd: Optional[float],
|
|
138
|
+
alerts_spec: Optional[str], deps: List[Any], min_events: float,
|
|
139
|
+
outages: Optional[List[Any]] = None) -> Dict[str, Any]:
|
|
140
|
+
frac = parse_target(target)
|
|
141
|
+
days = positive(window_days, "window (days)")
|
|
142
|
+
budget = 1.0 - frac
|
|
143
|
+
window_h = days * 24.0
|
|
144
|
+
window_min = days * 1440.0
|
|
145
|
+
rph = rpd / 24.0 if rpd is not None else None
|
|
146
|
+
flags: List[str] = []
|
|
147
|
+
|
|
148
|
+
result: Dict[str, Any] = {
|
|
149
|
+
"name": name,
|
|
150
|
+
"target_pct": round(frac * 100.0, 6),
|
|
151
|
+
"window_days": days,
|
|
152
|
+
"budget_fraction": round(budget, 12),
|
|
153
|
+
"budget_pct": round(budget * 100.0, 6),
|
|
154
|
+
"budget_minutes_full_outage": round(budget * window_min, 4),
|
|
155
|
+
"requests_per_day": rpd,
|
|
156
|
+
"requests_in_window": round(rpd * days, 2) if rpd is not None else None,
|
|
157
|
+
"budget_bad_requests": round(budget * rpd * days, 2) if rpd is not None else None,
|
|
158
|
+
"alerts": [],
|
|
159
|
+
"dependencies": None,
|
|
160
|
+
"outages": [],
|
|
161
|
+
"flags": flags,
|
|
162
|
+
}
|
|
163
|
+
if rpd is None:
|
|
164
|
+
flags.append("No traffic given: the budget in bad requests and the noise checks "
|
|
165
|
+
"are not computed.")
|
|
166
|
+
elif result["budget_bad_requests"] < min_events:
|
|
167
|
+
flags.append("The whole window's budget is %s bad requests: one or two failures "
|
|
168
|
+
"spend it. Use a longer window or synthetic probes."
|
|
169
|
+
% fmt_count(result["budget_bad_requests"]))
|
|
170
|
+
|
|
171
|
+
for a in parse_alerts(alerts_spec):
|
|
172
|
+
threshold = a["burn"] * budget
|
|
173
|
+
row = {
|
|
174
|
+
"burn_rate": a["burn"],
|
|
175
|
+
"long_window": a["long"],
|
|
176
|
+
"short_window": a["short"],
|
|
177
|
+
"severity": a["severity"],
|
|
178
|
+
"error_rate_threshold_pct": round(threshold * 100.0, 6),
|
|
179
|
+
"budget_consumed_at_fire_pct": round(a["burn"] * a["long_hours"] / window_h * 100.0, 4),
|
|
180
|
+
"hours_to_exhaustion": round(window_h / a["burn"], 4),
|
|
181
|
+
"can_fire": threshold <= 1.0,
|
|
182
|
+
"expected_bad_long": None,
|
|
183
|
+
"expected_bad_short": None,
|
|
184
|
+
"min_requests_per_hour_for_long": None,
|
|
185
|
+
"min_requests_per_hour_for_short": None,
|
|
186
|
+
"noisy": [],
|
|
187
|
+
}
|
|
188
|
+
label = "%gx %s/%s" % (a["burn"], a["long"], a["short"])
|
|
189
|
+
if threshold > 1.0:
|
|
190
|
+
flags.append("%s: the error rate that trips it is %.4g%%, above 100%%, so it can "
|
|
191
|
+
"never fire. Lower the burn rate for this target."
|
|
192
|
+
% (label, threshold * 100.0))
|
|
193
|
+
if a["long_hours"] > window_h:
|
|
194
|
+
flags.append("%s: the long window is longer than the %g-day SLO window."
|
|
195
|
+
% (label, days))
|
|
196
|
+
if rph is not None:
|
|
197
|
+
bad_long = threshold * rph * a["long_hours"]
|
|
198
|
+
bad_short = threshold * rph * a["short_hours"]
|
|
199
|
+
row["expected_bad_long"] = round(bad_long, 3)
|
|
200
|
+
row["expected_bad_short"] = round(bad_short, 3)
|
|
201
|
+
row["min_requests_per_hour_for_long"] = round(
|
|
202
|
+
min_events / (threshold * a["long_hours"]), 2)
|
|
203
|
+
row["min_requests_per_hour_for_short"] = round(
|
|
204
|
+
min_events / (threshold * a["short_hours"]), 2)
|
|
205
|
+
for which, n in (("long", bad_long), ("short", bad_short)):
|
|
206
|
+
if n < min_events:
|
|
207
|
+
row["noisy"].append(which)
|
|
208
|
+
if row["can_fire"] and "long" in row["noisy"]:
|
|
209
|
+
flags.append("%s: only %s bad events in the long window at the threshold "
|
|
210
|
+
"(under %g) at %s requests/hour - a handful of failures fires it, "
|
|
211
|
+
"so it is too noisy to alert on. Needs %s requests/hour, a longer "
|
|
212
|
+
"window, or synthetic probes."
|
|
213
|
+
% (label, fmt_count(bad_long), min_events, fmt_count(rph),
|
|
214
|
+
fmt_count(row["min_requests_per_hour_for_long"])))
|
|
215
|
+
elif row["can_fire"] and "short" in row["noisy"]:
|
|
216
|
+
flags.append("%s: only %s bad events in the %s short window at the threshold "
|
|
217
|
+
"(under %g) - the short window will flap. Lengthen it (needs %s "
|
|
218
|
+
"requests/hour for %s)."
|
|
219
|
+
% (label, fmt_count(bad_short), a["short"], min_events,
|
|
220
|
+
fmt_count(row["min_requests_per_hour_for_short"]), a["short"]))
|
|
221
|
+
result["alerts"].append(row)
|
|
222
|
+
|
|
223
|
+
for o in outages or []:
|
|
224
|
+
mins = positive(o, "outage minutes")
|
|
225
|
+
share = mins / (budget * window_min) * 100.0
|
|
226
|
+
result["outages"].append({"minutes": mins, "budget_spent_pct": round(share, 2)})
|
|
227
|
+
if share >= 100.0:
|
|
228
|
+
flags.append("A %g-minute full outage spends %.0f%% of the budget on its own."
|
|
229
|
+
% (mins, share))
|
|
230
|
+
|
|
231
|
+
if deps:
|
|
232
|
+
dep_fracs = [parse_target(d) for d in deps]
|
|
233
|
+
ceiling = 1.0
|
|
234
|
+
for d in dep_fracs:
|
|
235
|
+
ceiling *= d
|
|
236
|
+
dep_unavail = 1.0 - ceiling
|
|
237
|
+
share = dep_unavail / budget * 100.0
|
|
238
|
+
result["dependencies"] = {
|
|
239
|
+
"slos_pct": [round(d * 100.0, 6) for d in dep_fracs],
|
|
240
|
+
"serial_ceiling_pct": round(ceiling * 100.0, 6),
|
|
241
|
+
"budget_used_by_dependencies_pct": round(share, 2),
|
|
242
|
+
"achievable_without_redundancy": frac <= ceiling,
|
|
243
|
+
}
|
|
244
|
+
if frac > ceiling:
|
|
245
|
+
flags.append("Target %s%% is above %s%%, what the hard dependencies allow in series "
|
|
246
|
+
"(they alone would spend %.0f%% of the budget at their own SLOs). "
|
|
247
|
+
"Not achievable without redundancy, caching or a fallback path."
|
|
248
|
+
% (fmt_pct(frac), fmt_pct(ceiling), share))
|
|
249
|
+
elif share >= 50.0:
|
|
250
|
+
flags.append("The hard dependencies at their own SLOs would spend %.0f%% of the "
|
|
251
|
+
"budget, leaving %.0f%% for this service's own failures."
|
|
252
|
+
% (share, 100.0 - share))
|
|
253
|
+
return result
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
# ---------------------------------------------------------------- output
|
|
257
|
+
|
|
258
|
+
def fmt_pct(frac: float) -> str:
|
|
259
|
+
return ("%.6f" % (frac * 100.0)).rstrip("0").rstrip(".")
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def fmt_count(n: Optional[float]) -> str:
|
|
263
|
+
if n is None:
|
|
264
|
+
return "-"
|
|
265
|
+
if abs(n - round(n)) < 1e-9 and abs(n) >= 1:
|
|
266
|
+
return "{:,}".format(int(round(n)))
|
|
267
|
+
if abs(n) >= 100:
|
|
268
|
+
return "{:,.0f}".format(n)
|
|
269
|
+
return ("%.2f" % n).rstrip("0").rstrip(".")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def fmt_hours(h: float) -> str:
|
|
273
|
+
total_min = int(round(h * 60))
|
|
274
|
+
d, rem = divmod(total_min, 1440)
|
|
275
|
+
hh, mm = divmod(rem, 60)
|
|
276
|
+
parts = []
|
|
277
|
+
if d:
|
|
278
|
+
parts.append("%dd" % d)
|
|
279
|
+
if hh:
|
|
280
|
+
parts.append("%dh" % hh)
|
|
281
|
+
if mm or not parts:
|
|
282
|
+
parts.append("%dm" % mm)
|
|
283
|
+
return " ".join(parts)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def fmt_minutes(m: float) -> str:
|
|
287
|
+
whole = int(m)
|
|
288
|
+
secs = int(round((m - whole) * 60))
|
|
289
|
+
if secs == 60:
|
|
290
|
+
whole, secs = whole + 1, 0
|
|
291
|
+
human = fmt_hours(whole / 60.0) if whole else "0m"
|
|
292
|
+
if secs:
|
|
293
|
+
human += " %ds" % secs
|
|
294
|
+
return human
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def render_text(r: Dict[str, Any]) -> str:
|
|
298
|
+
out = []
|
|
299
|
+
title = r["name"] or "SLO"
|
|
300
|
+
out.append("%s: %s%% over %g days" % (title, fmt_pct(r["target_pct"] / 100.0), r["window_days"]))
|
|
301
|
+
out.append(" Error budget: %s%% of valid events" % fmt_pct(r["budget_fraction"]))
|
|
302
|
+
out.append(" = %s minutes of full outage per window (%s)"
|
|
303
|
+
% (("%.2f" % r["budget_minutes_full_outage"]).rstrip("0").rstrip("."),
|
|
304
|
+
fmt_minutes(r["budget_minutes_full_outage"])))
|
|
305
|
+
if r["requests_per_day"] is not None:
|
|
306
|
+
out.append(" = %s bad requests per window at %s requests/day (%s requests in window)"
|
|
307
|
+
% (fmt_count(r["budget_bad_requests"]), fmt_count(r["requests_per_day"]),
|
|
308
|
+
fmt_count(r["requests_in_window"])))
|
|
309
|
+
out.append("")
|
|
310
|
+
head = ["burn", "long", "short", "severity", "trips at err%", "budget at fire",
|
|
311
|
+
"exhaust in", "bad(long)", "bad(short)"]
|
|
312
|
+
rows = [head]
|
|
313
|
+
for a in r["alerts"]:
|
|
314
|
+
rows.append([
|
|
315
|
+
"%gx" % a["burn_rate"], a["long_window"], a["short_window"], a["severity"],
|
|
316
|
+
("%.4f" % a["error_rate_threshold_pct"]).rstrip("0").rstrip(".") + "%",
|
|
317
|
+
("%.2f" % a["budget_consumed_at_fire_pct"]).rstrip("0").rstrip(".") + "%",
|
|
318
|
+
fmt_hours(a["hours_to_exhaustion"]),
|
|
319
|
+
fmt_count(a["expected_bad_long"]) + (" *" if "long" in a["noisy"] else ""),
|
|
320
|
+
fmt_count(a["expected_bad_short"]) + (" *" if "short" in a["noisy"] else ""),
|
|
321
|
+
])
|
|
322
|
+
widths = [max(len(str(row[i])) for row in rows) for i in range(len(head))]
|
|
323
|
+
for i, row in enumerate(rows):
|
|
324
|
+
out.append(" " + " ".join(str(c).ljust(widths[j]) for j, c in enumerate(row)).rstrip())
|
|
325
|
+
if i == 0:
|
|
326
|
+
out.append(" " + " ".join("-" * w for w in widths))
|
|
327
|
+
if any(a["noisy"] for a in r["alerts"]):
|
|
328
|
+
out.append(" * fewer than the minimum expected bad events in that window (see flags)")
|
|
329
|
+
if r["outages"]:
|
|
330
|
+
out.append("")
|
|
331
|
+
for o in r["outages"]:
|
|
332
|
+
out.append(" A %g-minute full outage spends %s%% of this budget"
|
|
333
|
+
% (o["minutes"], ("%.2f" % o["budget_spent_pct"]).rstrip("0").rstrip(".")))
|
|
334
|
+
if r["dependencies"]:
|
|
335
|
+
d = r["dependencies"]
|
|
336
|
+
out.append("")
|
|
337
|
+
out.append(" Hard dependencies in series: %s -> ceiling %s%%; at their SLOs they "
|
|
338
|
+
"spend %s%% of this budget"
|
|
339
|
+
% (", ".join(fmt_pct(x / 100.0) + "%" for x in d["slos_pct"]),
|
|
340
|
+
fmt_pct(d["serial_ceiling_pct"] / 100.0),
|
|
341
|
+
("%.2f" % d["budget_used_by_dependencies_pct"]).rstrip("0").rstrip(".")))
|
|
342
|
+
if r["flags"]:
|
|
343
|
+
out.append("")
|
|
344
|
+
out.append(" Flags:")
|
|
345
|
+
for f in r["flags"]:
|
|
346
|
+
out.append(" - " + f)
|
|
347
|
+
return "\n".join(out)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
# ---------------------------------------------------------------- CLI
|
|
351
|
+
|
|
352
|
+
def load_batch(path: str) -> List[Dict[str, Any]]:
|
|
353
|
+
try:
|
|
354
|
+
if path == "-":
|
|
355
|
+
data = json.load(sys.stdin)
|
|
356
|
+
else:
|
|
357
|
+
with open(path, encoding="utf-8") as fh:
|
|
358
|
+
data = json.load(fh)
|
|
359
|
+
except OSError as exc:
|
|
360
|
+
raise InputError("cannot read %s: %s" % (path, exc))
|
|
361
|
+
except json.JSONDecodeError as exc:
|
|
362
|
+
raise InputError("%s is not valid JSON: %s" % (path, exc))
|
|
363
|
+
if isinstance(data, dict):
|
|
364
|
+
data = [data]
|
|
365
|
+
if not isinstance(data, list) or not data:
|
|
366
|
+
raise InputError("batch file must hold a non-empty list of SLO objects")
|
|
367
|
+
return data
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def traffic_from(obj: Dict[str, Any]) -> Optional[float]:
|
|
371
|
+
rpd, rph = obj.get("requests_per_day"), obj.get("requests_per_hour")
|
|
372
|
+
if rpd is not None and rph is not None:
|
|
373
|
+
raise InputError("give requests_per_day or requests_per_hour, not both")
|
|
374
|
+
if rpd is not None:
|
|
375
|
+
return positive(rpd, "requests_per_day", allow_zero=True)
|
|
376
|
+
if rph is not None:
|
|
377
|
+
return positive(rph, "requests_per_hour", allow_zero=True) * 24.0
|
|
378
|
+
return None
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
382
|
+
p = argparse.ArgumentParser(description="Error budget and burn-rate alert arithmetic.")
|
|
383
|
+
p.add_argument("--target", help="SLO target, e.g. 99.9 or 99.9%% or 0.999")
|
|
384
|
+
p.add_argument("--window", help="SLO window in days, e.g. 30 or 28")
|
|
385
|
+
g = p.add_mutually_exclusive_group()
|
|
386
|
+
g.add_argument("--rpd", help="valid requests (events) per day")
|
|
387
|
+
g.add_argument("--rph", help="valid requests (events) per hour")
|
|
388
|
+
p.add_argument("--alerts", default="standard",
|
|
389
|
+
help='"standard" or "burn:long:short[:severity],..." e.g. "14.4:1h:5m:page"')
|
|
390
|
+
p.add_argument("--outage", action="append", default=[],
|
|
391
|
+
help="minutes of a full outage to price against the budget (repeatable)")
|
|
392
|
+
p.add_argument("--dep", action="append", default=[],
|
|
393
|
+
help="SLO of a hard dependency called in series (repeatable), e.g. 99.9")
|
|
394
|
+
p.add_argument("--name", default="", help="label for the SLO")
|
|
395
|
+
p.add_argument("--batch", help="JSON file (or - for stdin) with a list of SLOs")
|
|
396
|
+
p.add_argument("--min-events", default=str(DEFAULT_MIN_EVENTS),
|
|
397
|
+
help="expected bad events below which a window is too noisy (default 10)")
|
|
398
|
+
p.add_argument("--json", action="store_true", help="print JSON instead of text")
|
|
399
|
+
p.add_argument("-o", "--out", help="write output to this path instead of stdout")
|
|
400
|
+
args = p.parse_args(argv)
|
|
401
|
+
|
|
402
|
+
try:
|
|
403
|
+
min_events = positive(args.min_events, "--min-events")
|
|
404
|
+
results = []
|
|
405
|
+
if args.batch:
|
|
406
|
+
for i, obj in enumerate(load_batch(args.batch), 1):
|
|
407
|
+
if not isinstance(obj, dict):
|
|
408
|
+
raise InputError("batch entry %d is not an object" % i)
|
|
409
|
+
for key in ("target", "window_days"):
|
|
410
|
+
if key not in obj:
|
|
411
|
+
raise InputError("batch entry %d is missing %r" % (i, key))
|
|
412
|
+
deps = obj.get("dependencies") or []
|
|
413
|
+
if not isinstance(deps, list):
|
|
414
|
+
raise InputError("batch entry %d: dependencies must be a list" % i)
|
|
415
|
+
outages = obj.get("outages_minutes") or []
|
|
416
|
+
if not isinstance(outages, list):
|
|
417
|
+
raise InputError("batch entry %d: outages_minutes must be a list" % i)
|
|
418
|
+
results.append(compute(str(obj.get("name", "SLO %d" % i)), obj["target"],
|
|
419
|
+
obj["window_days"], traffic_from(obj),
|
|
420
|
+
obj.get("alerts"), deps, min_events,
|
|
421
|
+
outages))
|
|
422
|
+
else:
|
|
423
|
+
if args.target is None or args.window is None:
|
|
424
|
+
raise InputError("give --target and --window, or --batch FILE")
|
|
425
|
+
rpd = None
|
|
426
|
+
if args.rpd is not None:
|
|
427
|
+
rpd = positive(args.rpd, "--rpd", allow_zero=True)
|
|
428
|
+
elif args.rph is not None:
|
|
429
|
+
rpd = positive(args.rph, "--rph", allow_zero=True) * 24.0
|
|
430
|
+
results.append(compute(args.name, args.target, args.window, rpd,
|
|
431
|
+
args.alerts, args.dep, min_events, args.outage))
|
|
432
|
+
except InputError as exc:
|
|
433
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
434
|
+
return 2
|
|
435
|
+
|
|
436
|
+
if args.json:
|
|
437
|
+
text = json.dumps(results if args.batch else results[0], indent=2)
|
|
438
|
+
else:
|
|
439
|
+
text = "\n\n".join(render_text(r) for r in results)
|
|
440
|
+
if args.out:
|
|
441
|
+
with open(args.out, "w", encoding="utf-8") as fh:
|
|
442
|
+
fh.write(text + "\n")
|
|
443
|
+
else:
|
|
444
|
+
print(text)
|
|
445
|
+
return 0
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
if __name__ == "__main__":
|
|
449
|
+
sys.exit(main())
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# SLOs — {service}
|
|
2
|
+
|
|
3
|
+
**{One to three lines: the decisions and anything that blocks them — an unreachable target, an
|
|
4
|
+
SLI measured in the wrong place, a journey too quiet to page on.}**
|
|
5
|
+
|
|
6
|
+
Assumptions: {window, traffic basis, anything tagged [assumption]}
|
|
7
|
+
|
|
8
|
+
## SLOs
|
|
9
|
+
|
|
10
|
+
| Journey | SLI (good / valid) | Measured at | Target / window | Budget |
|
|
11
|
+
|---|---|---|---|---|
|
|
12
|
+
| {journey} | Good: {…} / Valid: {… excluding …} | {LB / client / probe} | {99.x% / 30 d} | {N min · N bad requests at N/day} |
|
|
13
|
+
|
|
14
|
+
{Per journey, only where it changes something: why this target (history, dependency
|
|
15
|
+
ceiling), and what the measurement point still misses.}
|
|
16
|
+
|
|
17
|
+
## Alerts
|
|
18
|
+
|
|
19
|
+
| Journey | Burn | Long / short | Trips at | Budget at fire | Exhausts in | Action |
|
|
20
|
+
|---|---|---|---|---|---|---|
|
|
21
|
+
| {journey} | 14.4× | 1 h / 5 m | {x%} | {2%} | {50 h} | page |
|
|
22
|
+
|
|
23
|
+
{Low-traffic journeys: the expected bad-event count and the method used instead.}
|
|
24
|
+
|
|
25
|
+
## Error-budget policy
|
|
26
|
+
|
|
27
|
+
| Budget state | What changes | Owner |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| {state} | {action} | {role or name from the input} |
|
|
30
|
+
|
|
31
|
+
Exception route: {who approves, in writing, recorded where}. Exempt: {…}. Review: {when}.
|
|
32
|
+
|
|
33
|
+
## To confirm
|
|
34
|
+
|
|
35
|
+
- {each assumption or missing input, once}
|