This is the setup all five detectors in this repo were actually built and
validated against: the OpenTelemetry demo app, self-hosted Elasticsearch +
Kibana + EDOT via Elastic's start-local installer, and each detector
pointed at localhost:9200. See RUNNING_ON_ELASTIC_CLOUD.md
for the equivalent against a real Elastic Cloud/Serverless deployment
instead — the demo app and every detector script support both via the
same ES_URL/ES_API_KEY environment variables, switched at the bottom
of this doc's §3.
For the full narrative behind every decision here (why these exact compose
files, why the port remap, why two real Docker bugs needed fixing), see
DETECTORS.md — this document is the condensed,
step-by-step version to actually run from.
- Docker Desktop, with real headroom for it. Budget at least 8GB of
RAM for Docker specifically. If you're on Windows and the host itself
starts acting starved (not just Docker), check
C:\Users\<user>\.wslconfig'smemory=setting — an over-allocated WSL2 VM was the actual root cause the one time this happened while building this repo (seeml-flag-detection/README.mdfor the full story). Changing.wslconfigrequireswsl --shutdown+ restarting Docker Desktop, which restarts every container on the host, not just this demo. - Python 3.11+. A virtualenv is recommended; none was used originally (packages were installed globally per-detector — see §4 below).
- ~10GB free disk for the demo images + Elasticsearch data.
curl(for thestart-localinstaller in §2).- Optional: Node.js (
npm) if you want theelasticCLI for the Elastic ML detector (§4d) — see that section for the install command.
git clone <this-repo-url>
cd <this-repo>
git submodule update --init # pulls opentelemetry-demo/opentelemetry-demo/ is a git submodule pointing at
elastic/opentelemetry-demo
(Elastic's fork of the OpenTelemetry Astronomy Shop demo, using Elastic's
EDOT agents/collector) — not vendored directly into this repo.
cd opentelemetry-demo
curl -fsSL https://elastic.co/start-local | sh -s -- --edotThis creates opentelemetry-demo/elastic-start-local/ — Elasticsearch,
Kibana, and an EDOT collector, plus a .env file with a generated
ES_LOCAL_PASSWORD that every detector's default (no-env-vars) mode reads
directly from that file to authenticate.
If Docker Desktop ever restarts, these three containers do not auto-restart (no restart policy is set on them) — bring them back with:
cd opentelemetry-demo/elastic-start-local
docker compose --env-file .env -f docker-compose.yml up -dcd opentelemetry-demo
docker compose --env-file .env \
-f compose.yaml -f compose.full.yaml \
-f docker-compose.elastic.yml -f docker-compose.elastic-self-hosted.yml \
up -dcompose.full.yamlis not optional even though it reads like an add-on — it defineskafka/accounting/fraud-detection, which several flagd faults need.- If another instance of this same demo is already running on your Docker
host (container names collide —
compose.yamlhardcodescontainer_name:per service), use the renaming/port overlay indemo-overlay/instead:Edit the port numbers incd opentelemetry-demo cp ../demo-overlay/.env.override ../demo-overlay/docker-compose.rename.yml . docker compose --env-file .env --env-file .env.override \ -f compose.yaml -f compose.full.yaml \ -f docker-compose.elastic.yml -f docker-compose.elastic-self-hosted.yml \ -f docker-compose.rename.yml \ up -d
demo-overlay/.env.overrideif 18080/18090 are also taken on your host.
Two real bugs to expect on a from-scratch bring-up (both hit and
documented in depth in ml-flag-detection/README.md):
- Stale prebuilt images. If any service's image already exists
locally under the same
IMAGE_NAME:DEMO_VERSIONtag (e.g. from a previous checkout), Docker won't rebuild it automatically even though the source changed.frontend-proxy,load-generator,checkout, andshippingall needed an explicit rebuild here:Symptoms if you skip this: Envoy config validation errors ondocker compose -f compose.yaml -f compose.full.yaml -f docker-compose.elastic.yml -f docker-compose.elastic-self-hosted.yml build <service>
frontend-proxy,wget: not foundorexec: no such file or directoryon healthcheck binaries. - CRLF line endings on Windows.
src/load-generator/entrypoint.shcan get checked out with CRLF endings, which breaks its#!/bin/shshebang inside the Linux container. Fix, then rebuild that one service:sed -i 's/\r$//' opentelemetry-demo/src/load-generator/entrypoint.sh docker compose -f compose.yaml -f compose.full.yaml -f docker-compose.elastic.yml -f docker-compose.elastic-self-hosted.yml build load-generator
docker ps --format "{{.Names}}\t{{.Status}}"Everything should show Up/healthy, except checkout, shipping, and
product-catalog, which this fork's own healthchecks report as
unhealthy even when functioning correctly (a known quirk, not something
you introduced). The bundled k6 load-generator runs automatically once
load-generator is up — no separate step needed to generate traffic.
Every detector in this repo is validated the same way: edit
opentelemetry-demo/src/flagd/demo.flagd.json, change one flag's
defaultVariant to its "on" value, wait, observe, then set it back to
"off". flagd watches this file and hot-reloads it — no container
restart needed.
| Flag | "on" variant | Target service |
|---|---|---|
adFailure |
on |
ad |
adHighCpu |
on |
ad |
adManualGc |
on |
ad |
cartFailure |
100% |
cart |
paymentFailure |
100% |
payment |
recommendationCacheFailure |
on |
recommendation |
imageSlowLoad |
10sec |
frontend |
intlShippingSlowdown |
10sec |
shipping |
productCatalogFailure |
on |
product-catalog |
emailMemoryLeak |
10x* |
email |
kafkaQueueProblems |
on |
(no direct trace/metric target — Kafka isn't traced) |
* emailMemoryLeak's strongest variant is 10000x; every detector's
validator here uses 10x instead — see ml-flag-detection/README.md for
why (it coincided with host memory exhaustion during testing on this
project's original machine).
productCatalogLockContention, one of the 12 flags originally targeted
for this whole project, does not exist in this fork's demo.flagd.json
and is skipped everywhere.
All five detectors default to http://localhost:9200 with credentials
read from opentelemetry-demo/elastic-start-local/.env — nothing to
configure for local use beyond having §2 running. (Setting ES_URL/
ES_API_KEY switches every one of these to a remote cluster instead — see
RUNNING_ON_ELASTIC_CLOUD.md.)
cd ml-flag-detection
pip install requests scikit-learn pandas numpy matplotlib joblib
# Collect a labeled dataset (~40-70 min; resumable with --append if interrupted)
python collect_dataset.py --out dataset.csv
# Train + evaluate
python train_classifier.py --data dataset.csv
# Score one live window against the trained model (no retraining)
python predict_and_bundle.py --model model.joblib --out result.jsonNot a live/continuous detector — it's trained once, then predicts which flag (if any) a fresh 25-40s telemetry window looks like. See its README §6 for the actual numbers (55% test accuracy) and §7 for known gaps.
cd causal-changepoint-detection
# (Re)derive the service call graph -- worth re-running after enough uptime
# to exercise rare paths (checkout -> payment/email need real checkout traffic)
python derive_service_graph.py --minutes 240
# One-shot live scan
python changepoint_detector.py --lookback 15 --bucket 20 --out result.json
# Full 11-flag validation battery (~20-30 min -- each scan is 24 sequential ES|QL queries)
python validate_against_demo.py --settle 60 --lookback 15 --bucket 20No pip installs beyond the standard library. --verbose on
changepoint_detector.py prints real ES|QL errors instead of silently
treating them as "no signal" — use it while developing; two real bugs
found while building this both looked identical to a quiet system until
--verbose was added.
cd probe-detector
# One-shot live scan
python detector.py --out result.json
# Full 11-flag validation battery (~15-20 min)
python validate_against_demo.py --settle 15No pip installs beyond the standard library. This is the fastest of the four to react (5 lightweight aggregation queries per scan, ~10s polling in the validator) and had the best detection rate in testing — see its README §3.
Needs the elastic CLI:
npm install -g @elastic/cli
# Point it at your local cluster (ES_LOCAL_PASSWORD is in the .env from step 2b)
elastic config context add local-otel-demo \
--es-url "http://localhost:9200" --es-username elastic --es-password "$ES_LOCAL_PASSWORD" \
--kb-url "http://localhost:5601" --kb-username elastic --kb-password "$ES_LOCAL_PASSWORD"
elastic config current-context set local-otel-demo
elastic status # confirms connectivity -- do this before anything elseThen create the two jobs, in this exact order (job → datafeed → open job → start datafeed — the skill this was built with is explicit that this order is mandatory):
cd elastic-ml-anomaly-detection
elastic es ml put-job --input-file jobs/job-latency.json
elastic es ml put-datafeed --input-file jobs/datafeed-latency.json
elastic es ml open-job --job-id otel-demo-latency
elastic es ml start-datafeed --datafeed-id datafeed-otel-demo-latency
elastic es ml put-job --input-file jobs/job-error-rate.json
elastic es ml put-datafeed --input-file jobs/datafeed-error-rate.json
elastic es ml open-job --job-id otel-demo-error-rate
elastic es ml start-datafeed --datafeed-id datafeed-otel-demo-error-rate
# Verify both are actually running, not just created
elastic es ml get-job-stats --job-id otel-demo-latency
elastic es ml get-datafeed-stats --datafeed-id datafeed-otel-demo-latencyImportant: starting a datafeed with no explicit start time backfills the job through the entire matching index history immediately, not just from "now" forward — expect it to process everything in seconds, not gradually in real time.
# Cross-reference against the known fault windows in ml-flag-detection/dataset.csv
python validate_against_history.py
# Live one-shot scan, same shared result schema as the other three detectors
python scan_current.py --minutes 30 --out result.jsonRead that folder's README §3 before trusting a 0/11 result at face
value — it's a confirmed, real finding (the job's 5-minute bucket span
dilutes this repo's 25-40s fault windows), not evidence the method is
broken.
Teardown when done (stop datafeed before closing job, always in that order):
elastic es ml stop-datafeed --datafeed-id datafeed-otel-demo-latency
elastic es ml close-job --job-id otel-demo-latency
elastic es ml stop-datafeed --datafeed-id datafeed-otel-demo-error-rate
elastic es ml close-job --job-id otel-demo-error-ratecd probe-two-tier-detector
# One-shot scan: Tier 1 (z-score) shouts, Tier 2 (CHANGE_POINT) confirms every candidate
python detector.py --out result.json
# Full 11-flag validation battery (~20-30 min)
python validate_against_demo.py --settle 15
# Call Tier 2 on its own, the way a future Correlator would (e.g. one hop
# further up the dependency graph from a service Tier 1 never shouted about)
python change_point.py ad p95_latency --lookback 5 --bucket 2No pip installs beyond the standard library. This is probe-detector's
z-score scan (Tier 1) gated by a CHANGE_POINT significance test per
candidate (Tier 2) — no ranking, no root cause, exactly per the spec.
Avoid retesting the same flag twice within its own 10-minute lookback
window — the second activation's "baseline" ends up contaminated by the
first, collapsing the z-score (see its README §4 for exactly how this was
found).
cd probe-two-tier-detector-v2
# 9 unit tests, no ES needed — dilution/contended-baseline/persistence/error-floor/partial-bucket
python test_zscore.py
# One-shot scan (streak starts at 0 -- persistence needs repeated calls, see --loop)
python detector.py --out result.json
# Long-running scan, so persistence (2 consecutive shouts) can actually fire
python detector.py --loop 10
# Full battery per FIX-tier1-zscore.md §6: 5 cycles/flag + 3 null windows
python validate_against_demo.pyNo pip installs beyond the standard library. Tier 1 rebuilt (median/MAD
z-score, no window dilution, persistence, error-count floor); Tier 2
(change_point.py) copied unchanged from v1. Read its README §5 before
citing its numbers — the battery there was intentionally stopped after
8 of 11 flags to free the demo stack for probe-two-tier-detector-v3/'s
own run; it's real partial data, not a completed battery.
cd probe-two-tier-detector-v3
python test_zscore.py # same 9 Tier-1 tests, unchanged from v2
python test_change_point.py # 3 new Tier-2 tests (insufficient-data, earliest-break, cascade batching)
python detector.py --out result.json
python validate_against_demo.pyBuilds on v2 unchanged for Tier 1; fixes change_point.py and its caller
in detector.py — see its README for the full list of changes once its
battery completes.
DETECTORS.md §3 has the full four-way comparison table (per-flag,
per-detector) — this doc is deliberately just the "how to run it" half.