Skip to content

Commit 214435b

Browse files
authored
feat(examples): anchor the incident-investigation dataset to a fixed date (default 2026-06-18) (#80)
Follow-up to #65 (merged). The incident demo stack anchored its telemetry to wall-clock now (never-expiring but non-deterministic). For a reproducible RCA dataset the timestamps should be fixed, so the incident now anchors to a fixed instant by default: DEMO_ANCHOR (default 2026-06-18T02:17Z) anchors entities, logs, and the Prometheus backfill to the same instant — config ~anchor-24h, deploy ~anchor-12h, promotion ~anchor-4h, incident ~anchor — so every run produces the same ground-truth times. DEMO_ANCHOR=now restores the wall-clock / never-expiring behaviour. The live exporter still serves the current breach so an instant get_metrics works on any day; verify.sh resolves the same anchor for its range queries. Scoped to examples/incident-investigation/deploy/ (generators + entrypoint + compose + scripts); no server/query changes.
1 parent c4a27c7 commit 214435b

8 files changed

Lines changed: 86 additions & 26 deletions

File tree

examples/incident-investigation/deploy/README.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,17 @@ sh examples/incident-investigation/deploy/stop.sh --all # also remove the bui
8383
## Notes
8484

8585
- Telemetry is synthetic, shaped to match the modeled incident — a demo, not production data.
86-
- Everything is relative to "now", so the demo never expires. Metric history is generated relative
87-
to now and loaded with `promtool tsdb create-blocks-from openmetrics` before Prometheus starts; the
88-
exporter then continues the same series live. Logs are generated relative to now. The entity
89-
timeline (deployment / config-change / incident / promotion timestamps) is re-anchored to now at
90-
startup by the demo image's entrypoint — config ~now-24h, deploy ~now-12h, promo active ~now-4h,
91-
incident ~now — matching the telemetry.
86+
- The incident is anchored to a **fixed date (2026-06-18) by default**, so the dataset is
87+
reproducible — entities, logs, and the metric history all carry the same timestamps every run
88+
(`config ~anchor-24h`, `deploy ~anchor-12h`, `promotion active ~anchor-4h`, `incident ~anchor`).
89+
The metric history is loaded with `promtool tsdb create-blocks-from openmetrics` before Prometheus
90+
starts; the entity timeline is re-anchored by the demo image's entrypoint; the live exporter still
91+
serves the current breach so an instant `get_metrics` works on any day. Set `DEMO_ANCHOR=now` to
92+
anchor everything to wall-clock instead (a never-expiring live demo):
93+
94+
```bash
95+
DEMO_ANCHOR=now sh examples/incident-investigation/deploy/start.sh
96+
```
9297
- The logs include correlated request traces: a failed checkout shares one `trace_id` across
9398
`checkout-service → payment-gateway → payment-router → channel → provider`, so you can follow a
9499
single request down the stack and see the timeout originate downstream and surface up as retry

examples/incident-investigation/deploy/README.zh-CN.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ sh examples/incident-investigation/deploy/stop.sh --all # 连构建的镜像
6767
## 说明
6868

6969
- 遥测均为合成数据,按建模的故障塑形——是 demo,不是生产数据。
70-
- 一切都相对"现在",所以 demo 永不过期。指标历史相对 now 生成,在 Prometheus 启动前用 `promtool tsdb create-blocks-from openmetrics` 载入,exporter 随后实时续上;日志相对 now 生成;实体时间线(部署 / 配置变更 / 故障 / 促销时间戳)由 demo 镜像入口脚本在启动时统一位移到 now —— 配置 ~now-24h、部署 ~now-12h、促销激活 ~now-4h、故障 ~now —— 与遥测一致。
70+
- 故障**默认锚定在固定日期 2026-06-18**,所以数据集可复现 —— 实体、日志、指标历史每次运行都是同一组时间戳(`配置 ~锚点-24h``部署 ~锚点-12h``促销激活 ~锚点-4h``故障 ~锚点`)。指标历史在 Prometheus 启动前用 `promtool tsdb create-blocks-from openmetrics` 载入;实体时间线由 demo 镜像入口脚本位移;实时 exporter 仍提供当前击穿值,所以任何一天 instant `get_metrics` 都能查到。要让一切相对"现在"(永不过期的 live demo),设 `DEMO_ANCHOR=now`:
71+
72+
```bash
73+
DEMO_ANCHOR=now sh examples/incident-investigation/deploy/start.sh
74+
```
7175
- 日志包含**跨服务关联 trace**:一次失败的 checkout 用同一个 `trace_id` 串起 `checkout-service → payment-gateway → payment-router → channel → provider`,可顺着一条请求下钻、看到超时源自下游并上溯为重试耗尽;另含配置变更、部署、熔断等地标事件。
7276
- pack 还建模了 MySQL 部署事件集和一个 runbook;这里灌的可执行 plan 方法是 `get_metrics`(Prometheus)和 `get_logs`(Elasticsearch)。

examples/incident-investigation/deploy/docker-compose.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ services:
2121
dockerfile: examples/incident-investigation/deploy/umodel.Dockerfile
2222
ports:
2323
- "${UMODEL_PORT:-8080}:8080" # UModel API (umctl --addr / MCP target); override host port with UMODEL_PORT
24+
environment:
25+
# Fixed incident date by default (reproducible); set DEMO_ANCHOR=now for a live demo.
26+
- DEMO_ANCHOR=${DEMO_ANCHOR:-}
2427

2528
exporter:
2629
image: python:3.12-slim
@@ -40,6 +43,7 @@ services:
4043
- backfill:/backfill
4144
environment:
4245
- BACKFILL_OUT=/backfill/openmetrics.txt
46+
- DEMO_ANCHOR=${DEMO_ANCHOR:-}
4347
command: ["python", "gen_backfill.py"]
4448
restart: "no"
4549

@@ -88,6 +92,8 @@ services:
8892
es-seed:
8993
image: python:3.12-slim
9094
working_dir: /app
95+
environment:
96+
- DEMO_ANCHOR=${DEMO_ANCHOR:-}
9197
volumes:
9298
- .:/app:ro
9399
command: ["python", "gen_logs.py"]

examples/incident-investigation/deploy/entrypoint.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
#!/usr/bin/env python3
2-
"""Demo entrypoint: slide the incident-investigation sample timeline so it is always relative
3-
to the current time, then start the UModel server.
2+
"""Demo entrypoint: slide the incident-investigation sample timeline to the demo anchor, then
3+
start the UModel server.
44
55
The sample data carries real, valid ISO timestamps anchored to REF below (so the pack still
66
loads correctly outside this demo — e.g. test-integration.sh). Here we shift every timestamp by
7-
(now - REF) before the server reads them, so the deployment / config-change / incident / promotion
8-
times track wall-clock now (T-N) and the demo never "expires":
7+
(anchor - REF) before the server reads them, so the deployment / config-change / incident /
8+
promotion times land on the anchor:
99
10-
config change ~ now-24h, deploy ~ now-12h, promotion active ~ now-4h, incident ~ now.
10+
config change ~ anchor-24h, deploy ~ anchor-12h, promotion active ~ anchor-4h, incident ~ anchor.
1111
12-
This matches the now-relative Prometheus/Elasticsearch telemetry seeded by the rest of the stack.
13-
The shift is idempotent: it always works from a pristine copy (.orig), so restarts re-anchor to
14-
the current time instead of compounding.
12+
The anchor defaults to a FIXED instant (DEFAULT_ANCHOR) so the dataset is reproducible — the same
13+
ground-truth timestamps every run. Set DEMO_ANCHOR=now to anchor to wall-clock instead (a
14+
never-expiring live demo). This matches the DEMO_ANCHOR handling in gen_logs.py / gen_backfill.py.
15+
16+
The shift is idempotent: it always works from a pristine copy (.orig), so restarts re-anchor
17+
cleanly instead of compounding.
1518
"""
1619
import datetime as dt
1720
import os
@@ -23,13 +26,21 @@
2326
# Anchor baked into sample-data/entities.json (the INC-0042 P99 breach). If you re-date the
2427
# sample, update this to the latest "present" timestamp in the file.
2528
REF = dt.datetime(2026, 6, 17, 18, 10, 0, tzinfo=UTC)
29+
DEFAULT_ANCHOR = "2026-06-18T02:17:00Z"
2630
TARGETS = ["/app/examples/incident-investigation/sample-data/entities.json"]
2731
ISO = re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z")
2832

2933

34+
def resolve_anchor():
35+
a = os.environ.get("DEMO_ANCHOR") or DEFAULT_ANCHOR
36+
if a == "now":
37+
return dt.datetime.now(tz=UTC)
38+
return dt.datetime.strptime(a, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=UTC)
39+
40+
3041
def main():
31-
now = dt.datetime.now(tz=UTC)
32-
delta = now - REF
42+
anchor = resolve_anchor()
43+
delta = anchor - REF
3344
for path in TARGETS:
3445
if not os.path.exists(path):
3546
continue
@@ -44,7 +55,7 @@ def shift(m):
4455

4556
open(path, "w").write(ISO.sub(shift, src))
4657
print(f"[entrypoint] re-anchored {os.path.basename(path)} by {delta} "
47-
f"(REF {REF:%Y-%m-%d} -> now {now:%Y-%m-%d %H:%M}Z)", flush=True)
58+
f"(REF {REF:%Y-%m-%d} -> anchor {anchor:%Y-%m-%d %H:%M}Z)", flush=True)
4859

4960
os.execvp("umodel-server", ["umodel-server"] + sys.argv[1:])
5061

examples/incident-investigation/deploy/gen_backfill.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
continues the series from "now", so instant queries stay fresh while range queries see
66
the full incident arc.
77
8-
The history follows the modeled timeline (README "Timeline"), anchored to wall-clock now:
8+
The history follows the modeled timeline (README "Timeline"), anchored to the demo anchor
9+
(a fixed date by default; DEMO_ANCHOR=now for wall-clock):
910
1011
P0 healthy [now-72h, now-24h) baseline; everything nominal
1112
P1 retries-up [now-24h, now-4h) T-24h config change -> checkout
@@ -20,6 +21,7 @@
2021
Reuses exporter.py's SERVICES / PROFILES / BUCKETS as the P2 peak, so the history and the
2122
live tail share one source of truth. Standard library only.
2223
"""
24+
import datetime as _dt
2325
import math
2426
import os
2527
import time
@@ -28,9 +30,20 @@
2830

2931
WINDOW_S = 72 * 3600 # history depth
3032
STEP_S = 300 # one sample per 5 min (range queries use [15m]/[30m] windows)
31-
END_OFFSET_S = 120 # stop just before now so the live exporter owns the present
33+
END_OFFSET_S = 120 # stop just before the anchor so the live exporter owns the present
3234
OUT = os.environ.get("BACKFILL_OUT", "/backfill/openmetrics.txt")
3335

36+
# The incident is anchored to a fixed instant by default (reproducible dataset). Set
37+
# DEMO_ANCHOR=now to anchor to wall-clock instead (a never-expiring live demo).
38+
DEFAULT_ANCHOR = "2026-06-18T02:17:00Z"
39+
40+
41+
def resolve_now():
42+
a = os.environ.get("DEMO_ANCHOR") or DEFAULT_ANCHOR
43+
if a == "now":
44+
return int(time.time())
45+
return int(_dt.datetime.strptime(a, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=_dt.timezone.utc).timestamp())
46+
3447
# Timeline boundaries, as age (seconds before now).
3548
T_CONFIG = 24 * 3600 # checkout retry config change
3649
T_PROMO = 4 * 3600 # promotion goes active
@@ -117,7 +130,7 @@ def instant(sid, ts, now):
117130

118131

119132
def main():
120-
now = int(time.time())
133+
now = resolve_now()
121134
end = now - END_OFFSET_S
122135
start = end - WINDOW_S
123136
times = list(range(start, end + 1, STEP_S))

examples/incident-investigation/deploy/gen_logs.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
#!/usr/bin/env python3
22
"""Generate ~72h of service logs for the incident-investigation demo and bulk-load them
33
into the demo Elasticsearch, so the pack's get_logs plans return rows that span the whole
4-
incident (not a single burst). Timestamps are relative to wall-clock now, so the demo
5-
always shows "the last three days". Standard library only.
4+
incident (not a single burst). Timestamps are relative to the demo anchor (a fixed date by
5+
default; set DEMO_ANCHOR=now for wall-clock). Standard library only.
66
77
The log volume and severity follow the modeled timeline (README "Timeline"):
88
@@ -18,6 +18,7 @@
1818
severity / log_message / trace_id / span_id / http_status / upstream_service /
1919
latency_ms / error_code / pod).
2020
"""
21+
import datetime as _dt
2122
import json
2223
import os
2324
import random
@@ -29,6 +30,17 @@
2930
HOUR = 3600
3031
RNG = random.Random(0xC0FFEE) # deterministic output run to run
3132

33+
# The incident is anchored to a fixed instant by default (reproducible dataset). Set
34+
# DEMO_ANCHOR=now to anchor to wall-clock instead (a never-expiring live demo).
35+
DEFAULT_ANCHOR = "2026-06-18T02:17:00Z"
36+
37+
38+
def resolve_now():
39+
a = os.environ.get("DEMO_ANCHOR") or DEFAULT_ANCHOR
40+
if a == "now":
41+
return int(time.time())
42+
return int(_dt.datetime.strptime(a, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=_dt.timezone.utc).timestamp())
43+
3244
# service_id -> short name, mirroring exporter.py / the entity ids the plans substitute.
3345
SVC = {
3446
"63718b78868895d2590551b27ec6f51c": "payment-gateway",
@@ -98,7 +110,7 @@ def chain(ts):
98110

99111

100112
def main():
101-
now = int(time.time())
113+
now = resolve_now()
102114
docs = []
103115

104116
# --- P0 healthy: sparse INFO across the platform, one every ~25 min ---

examples/incident-investigation/deploy/start.sh

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,12 @@ echo "==> Bringing up the incident-investigation demo stack with: $COMPOSE"
6161
$COMPOSE -f "$COMPOSE_FILE" up -d --build
6262

6363
echo "==> Waiting for the 72h metric backfill + Elasticsearch seed + Prometheus scrapes (up to ~5 min)..."
64-
hist_ts=$(( $(date +%s) - 216000 )) # 60h ago: confirms the history was backfilled
64+
# Probe the backfill 60h before the incident anchor (fixed date by default, or wall-clock when
65+
# DEMO_ANCHOR=now), so the check lands inside the seeded window regardless of the current date.
66+
anchor_ts=$(python3 -c 'import os,time,datetime as d
67+
a=os.environ.get("DEMO_ANCHOR") or "2026-06-18T02:17:00Z"
68+
print(int(time.time()) if a=="now" else int(d.datetime.strptime(a,"%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=d.timezone.utc).timestamp()))' 2>/dev/null || date +%s)
69+
hist_ts=$(( anchor_ts - 216000 )) # 60h before the anchor: confirms the history was backfilled
6570
# Sentinel query: confirms localhost:$UMODEL_PORT is THIS demo (returns the degraded payment
6671
# path), not some other umodel. A quoted heredoc keeps the SPL single quotes literal, no escaping.
6772
um_body() { cat <<'JSON'

examples/incident-investigation/deploy/verify.sh

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ PROM="${PROM_URL:-http://localhost:${PROM_PORT:-9090}}"
1414
ES="${ES_URL:-http://localhost:${ES_PORT:-9200}}"
1515
PG="63718b78868895d2590551b27ec6f51c" # payment-gateway
1616
CK="149632df43354373835df2717cb8fb19" # checkout-service
17-
NOW=$(date +%s)
17+
# Anchor for the section-5 arc queries: the fixed incident instant by default (must match the
18+
# seeded data), or wall-clock when DEMO_ANCHOR=now. Python keeps the ISO->epoch parse portable.
19+
NOW=$(python3 -c 'import os,time,datetime as d
20+
a=os.environ.get("DEMO_ANCHOR") or "2026-06-18T02:17:00Z"
21+
print(int(time.time()) if a=="now" else int(d.datetime.strptime(a,"%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=d.timezone.utc).timestamp()))' 2>/dev/null || date +%s)
1822

1923
uctl() {
2024
if command -v umctl >/dev/null 2>&1; then umctl "$@"; else

0 commit comments

Comments
 (0)