Integrations
Stampede connects a load test to the rest of your tooling:
- Target metrics from Prometheus: after a run, your own PromQL queries are evaluated over the run’s time range and charted in the report next to Stampede’s numbers, so CPU, memory, queue depth or database connections line up with the load that caused them.
- Trace links: every request carries a W3C
traceparent. The report lists the slowest requests of each step with their trace IDs, and with a Jaeger or Tempo link template those IDs become links. - Notifications (server): a generic signed webhook, Slack or Discord hears when a run finishes, misses a target or is killed.
- CI: the GitHub Action.
- Stampede’s own telemetry: OpenTelemetry traces of the server and a Grafana dashboard for its metrics; see configuration.
Target metrics from Prometheus
Section titled “Target metrics from Prometheus”With stampede run
Section titled “With stampede run”Add an observe block to the scenario:
observe: prometheus: url: ${env.PROM_URL} # http://prometheus:9090; may be templated bearerToken: ${secret.PROM_TOKEN} # optional; STAMPEDE_SECRET_PROM_TOKEN queries: cpu: 'rate(process_cpu_seconds_total{job="shop"}[30s])' memory: 'process_resident_memory_bytes{job="shop"}' db_connections: 'sum(pg_stat_activity_count{datname="shop"})'After the run, each query is sent to /api/v1/query_range (Prometheus,
Thanos, Mimir, VictoriaMetrics and other compatible servers) from the run’s
start to its end, with a step equal to the report interval (one second).
Runs long enough to exceed 10,000 points per query use a coarser step, as
Prometheus refuses more than 11,000.
The results appear:
-
in the terminal summary as min, max and last per query;
-
in the HTML report as one chart per query, under Target metrics;
-
in the JSON report as
targetMetrics:"targetMetrics": [{ "name": "cpu", "query": "rate(process_cpu_seconds_total{job=\"shop\"}[30s])","points": [ { "t": 0, "value": 0.21 }, { "t": 1, "value": 0.34 } ] },{ "name": "db_connections", "query": "...", "points": [],"error": "prometheus: bad_data: 1:5: parse error: ..." }]tis seconds since the run started, like the timeline. A query that fails, returns nothing or cannot be reached keeps itserrorand never fails the run. A query returning several series is charted by its first series, with anerrornote asking you to aggregate it (sum(...),max(...)).
Notes:
- Queries run once the run has ended; the last scrape interval of the run may not be in Prometheus yet.
- Values that are
NaNor infinite are left out. - Up to 20 queries; names are letters, digits,
_,.and-.
On the server
Section titled “On the server”A server never contacts a URL written in a scenario: that would let anyone who can edit a scenario make the server fetch internal addresses. Instead an admin configures named integrations for the organisation, and scenarios refer to them by name:
observe: prometheus: integration: prod-prometheus queries: cpu: 'rate(process_cpu_seconds_total{job="shop"}[30s])' traces: integration: jaegerAdd integrations with stampede integrations create (admins only) or the
API; every member can see their names under Settings → Integrations in
the web UI:
curl -X POST $STAMPEDE_SERVER/api/v1/integrations -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -d '{"name":"prod-prometheus","kind":"prometheus","url":"http://prometheus:9090","bearerToken":"..."}'The bearer token is encrypted with the server’s master key and never
returned. A run whose scenario names observe.prometheus.url instead of an
integration, or an integration that does not exist, is refused when it is
started (HTTP 422). The server strips the observe block before sending the
scenario to workers; it queries Prometheus itself once the run ends.
Trace links
Section titled “Trace links”Every request Stampede sends carries a W3C traceparent header; all
requests of one journey iteration share a trace ID. If your services
propagate and sample it, the iteration appears in your tracing system as one
trace.
For each step the report keeps the five slowest requests with their
latency (from the scheduled send time), when they were sent, their status
or error, and their trace ID. Distributed runs keep the five slowest across
all workers. They appear in the JSON report (journeys[].steps[].slowest),
the HTML report and the web UI, and the five slowest overall in the
terminal summary.
To turn trace IDs into links, give a link template containing {traceId}:
observe: traces: url: https://jaeger.example.com/trace/{traceId}Any http(s) URL with {traceId} in it works, so the link can point at
Jaeger, a Tempo-backed Grafana view or any other trace UI.
On the server, use integration: <name> with a traces integration, or
url: directly: nothing is fetched from it, it only builds links. The
template must be an http(s) URL.
A request whose scenario sets its own traceparent header keeps that
header; the report still shows the trace ID Stampede generated for the
iteration, which then does not match.
Notifications
Section titled “Notifications”Notification channels belong to the organisation and are managed by admins
with stampede notify channels create or the API
(/api/v1/notifications/channels); the web UI lists them under
Settings → Notifications.
| Kind | Body |
|---|---|
webhook |
the event as JSON (below), signed |
slack |
a Slack incoming webhook message (text in mrkdwn) |
discord |
a Discord webhook message (content; mentions disabled) |
Events (a channel subscribes to any of them; all by default):
| Event | When |
|---|---|
run.finished |
a run ended: completed, aborted or failed to run; includes the verdict and summary |
run.target_failed |
a run completed and missed a target; lists the failed targets |
run.killed |
a run was stopped with the kill switch; names who pulled it |
A generic webhook receives:
id is the same for every retry of one delivery.
{ "id": "5b0c...", "type": "run.target_failed", "at": "2026-10-06T12:00:00Z", "organisation": "Acme", "message": "checkout missed 1 of 2 targets", "run": { "id": "...", "project": "Shop", "scenario": "checkout", "target": "https://staging.shop.example", "status": "completed", "verdict": "fail", "stopReason": "completed", "url": "https://stampede.example.com/runs/...", "summary": { "requests": 12000, "errorRate": 0.004, "rps": 200, "p95": 0.81, "p99": 1.4 }, "failedTargets": ["checkout.p95 < 500ms"] }}run.url is set when the server knows its public URL (--public-url /
STAMPEDE_PUBLIC_URL).
Verifying webhook signatures
Section titled “Verifying webhook signatures”Each webhook request has these headers:
| Header | Value |
|---|---|
X-Stampede-Signature |
sha256= and the hex HMAC-SHA256 of the raw body, keyed with the channel’s secret |
X-Stampede-Event |
the event type |
X-Stampede-Delivery |
the delivery ID (same as the body’s id) |
X-Stampede-Timestamp |
Unix seconds when the attempt was sent |
The secret is generated when the channel is created (or you supply one of at least 16 characters) and is shown only once. Verify in constant time, for example in Go:
mac := hmac.New(sha256.New, []byte(secret))mac.Write(body)ok := hmac.Equal([]byte("sha256="+hex.EncodeToString(mac.Sum(nil))), []byte(r.Header.Get("X-Stampede-Signature")))Delivery, retries and the log
Section titled “Delivery, retries and the log”Deliveries run in the background after a run ends. A transport error, a
timeout, 408, 425, 429 or a 5xx response is retried after 2, 10 and
30 seconds (a longer Retry-After, up to a minute, is honoured); other
responses are not retried. Redirects are not followed. Every attempt is
recorded, and the last 50 attempts per channel are kept: the delivery
log in Settings → Notifications, or
GET /api/v1/notifications/channels/{id}/deliveries.
Send test (POST /api/v1/notifications/channels/{id}/test) sends a
test event once, without retries, and returns the attempt.
Private destinations
Section titled “Private destinations”To keep the server from being used to reach internal services, a channel’s
destination may not be a loopback, private (RFC 1918, unique local),
link-local (including cloud metadata at 169.254.169.254), carrier-grade
NAT, multicast or reserved address, nor localhost or a .local or
.internal name. The check runs when the channel is created and again on
every connection, against the address actually dialled, so a DNS change
cannot get around it; proxies from the environment are not used.
An admin can tick Allow private destinations for a channel that must reach an internal receiver, such as a chat bridge inside your network.
Channel URLs (which for Slack and Discord are themselves credentials) and
signing secrets are encrypted with the master key; the API only ever
returns the scheme and host. Creating channels therefore needs
STAMPEDE_MASTER_KEY.
All endpoints are under the integrations tag in the
OpenAPI document. Listing integrations needs any
role; everything else needs admin, and every change is audited.
| Method and path | |
|---|---|
GET /integrations |
list (tokens are never returned) |
POST /integrations |
create {name, kind: prometheus|traces, url, bearerToken?} |
DELETE /integrations/{id} |
delete |
GET /notifications/channels |
list, with each channel’s last delivery |
POST /notifications/channels |
create {name, kind: webhook|slack|discord, url, events?, allowPrivate?, secret?}; returns {channel, secret?} |
DELETE /notifications/channels/{id} |
delete |
POST /notifications/channels/{id}/test |
send a test now; returns the attempt |
GET /notifications/channels/{id}/deliveries?limit= |
the delivery log, newest first |