Metadata-Version: 2.4
Name: dotcom_slo_overrides_cli
Version: 0.1.1
Summary: CLI to report net downtime from Dotcom-Monitor minus Datadog SLO corrections, and to set those corrections
Author: Yorick Hoorneman
Author-email: Yorick Hoorneman <yhoorneman@schubergphilis.com>
License-Expression: Apache-2.0
License-File: LICENSE
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Dist: niquests>=3.7
Requires-Dist: python-dateutil>=2.9
Requires-Dist: typer>=0.12
Requires-Python: >=3.13
Description-Content-Type: text/markdown

# dotcom slo overrides CLI

[![Version](https://img.shields.io/badge/version-0.0.0-blue)](https://pypi.org/project/dotcom_slo_overrides_cli/)
[![Python](https://img.shields.io/badge/python-3.13%20%7C%203.14-blue?logo=python&logoColor=white)](https://www.python.org)
[![License](https://img.shields.io/badge/license-Apache--2.0-blue)](https://opensource.org/license/apache-2.0)
[![Documentation: Diátaxis](https://img.shields.io/badge/docs-Di%C3%A1taxis-009485?logo=readthedocs&logoColor=white)](https://diataxis.fr/)
[![Build](https://img.shields.io/badge/build-passing-brightgreen)](https://github.com/features/actions)
[![Coverage](https://img.shields.io/badge/coverage-85%25-green)](https://coverage.readthedocs.io/)
[![pyscn quality](https://img.shields.io/badge/pyscn-C-yellow)](https://pyscn.ludo-tech.org)

Report the downtime Dotcom-Monitor observed, net of the Datadog SLO corrections that excuse it — and create
those corrections.

<!-- usage-start -->

## Usage

Dotcom-Monitor's external synthetic checks are the authoritative record of whether a service was up. Datadog
holds the SLOs and their *corrections* (also called overrides) — windows excluded from an SLO's error budget.
This CLI joins the two:

- **`list`** collects the downtime Dotcom-Monitor observed, subtracts every correction window already on the
  matching Datadog SLOs, and prints the downtime that still counts.
- **`set`** creates corrections on those same SLOs from a window you supply.

A Datadog SLO is linked to a Dotcom-Monitor device by a tag of the form **`drt:<device_id>`**, e.g. `drt:309282`.

### Install

```bash
uv tool install dotcom_slo_overrides_cli   # or: uvx dotcom_slo_overrides_cli --help
```

### Commands

| Command | What it does |
|---------|--------------|
| `list` | Report observed downtime minus the corrections that excuse it. Read-only. |
| `set` | Preview (default) or `--apply` corrections on the targeted SLOs. |
| `init-config` | Write a starter config of non-secret defaults. |
| `init-envrc` | Write a starter `.envrc` for direnv-managed credentials. |
| `commands list` | Print a tree of every command. |
| `--version` | Print the version. |

### Credentials

Three credentials are needed. Two are Datadog's; the third is a Dotcom-Monitor **XML Feed** integration UID
(created under *Manage → Integrations → New Integration → XML Feed*), which is what the reporting service takes
as its `PID` parameter.

| Credential | Flag | Environment variable |
|------------|------|----------------------|
| Datadog API key | `--api-key` | `DD_API_KEY` |
| Datadog application key | `--app-key` | `DD_APP_KEY` |
| Dotcom-Monitor XML Feed UID | `--uid` | `DOTCOM_XML_FEED` |

Precedence, highest first: **explicit flag** → **environment variable** → **a direnv-loaded `.envrc`** in the
config directory. direnv is only consulted for a credential that is still missing, so it can never override a
flag or a real environment variable. Credentials are *never* read from the config file.

```bash
dotcom-slo-overrides init-envrc
direnv allow ~/.config/dotcom-slo-overrides
```

The generated `.envrc` pulls from Vault (run `vault login` first), expecting all four values as fields of a
single secret. Tell it which secret with `vault_path` in the config file, or `--vault-path`:

```bash
dotcom-slo-overrides init-envrc --vault-path my-mount/my-secret
```

which writes:

```bash
export DD_API_KEY="$(vault kv get -field=datadog-api-key my-mount/my-secret)"
export DD_APP_KEY="$(vault kv get -field=datadog-application-key my-mount/my-secret)"

# Config API (config_api_v1) UID — not used by this tool, exported for parity.
export DOTCOM_WEB_API="$(vault kv get -field=dotcom_web_api my-mount/my-secret)"

# XML Feed UID — this is the one XRS takes as its PID, and the one this CLI reads.
export DOTCOM_XML_FEED="$(vault kv get -field=dotcom_xml_feed my-mount/my-secret)"
```

With no `vault_path` set, a placeholder is written for you to replace. Nothing about any particular Vault
layout is built into the package. If your secrets live elsewhere, edit the `.envrc` freely — the tool only ever
reads the four variables, never the file itself.

> Dotcom-Monitor issues a **separate UID per integration type**, and they are not interchangeable. XRS accepts
> only the XML Feed UID; sending the Web API UID (or a Datadog key) yields an opaque `HTTP 403` from IIS before
> the credential is even evaluated.

`set --tag` needs only the two Datadog keys; anything selecting by device name also needs the XML Feed UID.

### Selecting devices

Devices are chosen by matching **regexes against the Dotcom-Monitor device name**. A device is selected when
*any* pattern matches. Patterns come from `--name` (repeatable) or from `device_patterns` in the config file;
with neither, `list` reports on every device in the account.

```bash
dotcom-slo-overrides list --name '^prod-' --name '^eu-'
```

### How net downtime is calculated

1. **Observed downtime.** The XRS `StatusChange` report is read for each device that backs a `drt:`-tagged SLO
   — the same data as the "Uptime/Downtime periods" section of the Dotcom-Monitor online reports. It divides
   the window into periods typed `Success`, `Failure`, `Postpone` or `Undefined`; only **`Failure`** counts as
   downtime. The report is already aggregated across monitoring locations.
2. **Corrections are subtracted.** Every correction on the SLOs tagged `drt:<device_id>` is expanded to
   concrete intervals — recurring ones via their `RRULE` — clipped to the report window, and subtracted. A
   correction landing in the middle of an outage splits it into two remaining windows.
3. **Uptime is derived** from what survives, as a share of the report window.

### Example: the report

```bash
dotcom-slo-overrides list --name '^prod-' --start 2026-07-01 --end 2026-07-31
```

```
Patterns   : ^prod-
Window     : 2026-07-01 00:00 → 2026-07-31 00:00 UTC
Rule       : Dotcom-Monitor Failure periods (aggregated across locations), minus SLO corrections
Matched    : 2 SLO(s)

Prod Web Availability  (abc-def-ghi)
  device: prod-web  (309282)
  raw 4h - excluded 2h = net 2h
  uptime 99.722% (99.444% before corrections)
  • 2026-07-03 09:00 → 2026-07-03 10:00  (1h)
  • 2026-07-03 12:00 → 2026-07-03 13:00  (1h)

Prod API Availability  (jkl-mno-pqr)
  device: prod-api  (309283)
  uptime 100.000%
  (no net downtime in window)
```

`uptime` is the share of the **report window** that is not a Dotcom-Monitor `Failure` period, after
corrections. The pre-correction figure is shown in brackets only when a correction actually changed it. Note
that time Dotcom-Monitor reports as `Postpone` (monitoring paused) or `Undefined` counts as *up* under this
definition, since only `Failure` is downtime.

The window defaults to the start of the current month through now. An SLO whose downtime was *entirely*
excused reads `(all observed downtime excluded by corrections)` rather than `(no net downtime in window)`, so
"nothing went wrong" stays distinguishable from "everything was excused".

With many SLOs, `--only-downtime` lists just the ones with downtime left and reports the hidden count in the
header.

### Example: creating corrections

`set` is a dry run unless you pass `--apply`. Targets are chosen either by dotcom device name or by Datadog
tag — the two are mutually exclusive, so `--name` guarantees `set` and `list` act on exactly the same SLOs.

```bash
# Preview
dotcom-slo-overrides set --name '^prod-' --start 2026-08-01T22:00 --end 2026-08-01T23:00

# Write
dotcom-slo-overrides set --name '^prod-' \
  --start 2026-08-01T22:00 --end 2026-08-01T23:00 \
  --description 'Planned database failover' --apply
```

`--apply` additionally requires `--description`, `--start`, and either `--end` or `--rrule`.

### Idempotency

Re-running the same `set` never duplicates a correction. `--strategy` decides when an existing correction
counts as already satisfying the request:

| Strategy | Skips when |
|----------|-----------|
| `skip-if-covered` (default) | The requested window lies entirely inside an existing correction. |
| `skip-if-overlap` | The windows overlap at all — may leave the request partly uncovered. |
| `skip-if-exact` | Only an identical start and end. Creates even when overlapping. |

Recurring corrections are compared on their start plus `rrule` only, since occurrences can't be reasoned about
as a single interval.

### Config file

`init-config` writes `~/.config/dotcom-slo-overrides/config.toml` (honouring `$XDG_CONFIG_HOME`). Only
non-secret keys are read from it:

```toml
site = "datadoghq.eu"
timezone = "UTC"
dotcom_timezone = "UTC"
category = "Scheduled Maintenance"
strategy = "skip-if-covered"
device_patterns = ["^prod-", "^eu-"]
```

`dotcom_timezone` matters: XRS renders its timestamps as wall-clock in the *account's* timezone with no UTC
offset, so set this to the timezone configured on your Dotcom-Monitor account or every window will be skewed by
a fixed number of hours — an error that looks plausible rather than broken.

### Command tree

```
dotcom-slo-overrides
├── commands
│   └── list
├── init-config
├── init-envrc
├── list
└── set
```

<!-- usage-end -->

## Documentation

Full documentation lives in [`docs/`](docs/index.md) — build and open it with `./workflow.cmd document`.

## Developing

> Development flow as [Paleofuturistic Python](https://github.com/schubergphilis/paleofuturistic_python)

Prerequisite: [uv](https://docs.astral.sh/uv/). Every development action runs through `./workflow.cmd <task>` — the first run bootstraps the environment automatically.

The scaffold manual lives in the docs' **Developer** section: start with [First-run setup](docs/developer/tutorials/first-run-setup.md); the full command list is in the [Invoke task catalog](docs/developer/reference/invoke-tasks.md).
