Prometheus Metrics
Rift exposes two metrics surfaces:
- A Prometheus endpoint (
GET /metricson the metrics port, default9090) with the full instrumentation described below. - A small plain-text counter set on the admin API’s own
GET /metrics(admin port, default2525) — imposter counts and per-imposter request totals.
Every metric name and label on this page is taken from the source; if a metric isn’t listed here, it isn’t emitted.
Enabling & scraping
The metrics server starts alongside the admin server. Point Prometheus at the metrics port:
curl http://localhost:9090/metrics
Change the port with --metrics-port or RIFT_METRICS_PORT:
rift --metrics-port 8090
# or
RIFT_METRICS_PORT=8090 rift
There is no environment variable to disable the metrics server; if you don’t scrape it, it sits idle.
Prometheus endpoint metrics (port 9090)
Histogram metrics expand into the usual _bucket{le="…"}, _sum, and _count series.
On every fault and script family below, rule_id is the imposter’s port, as a string: the imposter path has no named rules the way the removed reverse proxy did, so the port is what identifies which imposter produced the event. source is rift for a _rift.fault decision and script for one a _rift.script returned.
| Metric | Type | Labels | Meaning |
|---|---|---|---|
rift_requests_total | counter | method, status | Requests served, by method and response status. |
rift_faults_injected_total | counter | type, rule_id, source | Faults injected (type = latency/error/tcp). |
rift_latency_injected_ms | histogram | rule_id | Injected latency, in milliseconds. |
rift_error_status_total | counter | status, rule_id | Error-fault responses, by status. |
rift_script_execution_duration_ms | histogram | rule_id, result | Script execution time, in milliseconds. result is pass (no fault), fault (the script decided a fault) or error. |
rift_script_errors_total | counter | rule_id, error_type | Script failures. error_type is timeout (the script missed its deadline) or runtime (any other failure). |
rift_flow_state_ops_total | counter | operation, result | Flow-store operations, by operation (get, set, exists, delete, increment, increment_by, set_ttl, clear_flow, flow_ids, entry_count) and result (success/error). |
rift_upstream_request_duration_ms | histogram | method, status | Upstream request time for proxy responses, in milliseconds — the forward itself, not Rift’s own handling. |
rift_accepted_connections_total | counter | worker | Connections accepted per accept-loop worker slot. Under --runtime per-core the slot is the worker index, making SO_REUSEPORT skew observable; in the default topology everything lands on slot 0. |
rift_accept_errors_total | counter | listener, class | Accept errors by listener (imposter, admin, metrics, front-door) and class (transient, systemic). The accept loops classify and retry rather than terminate, so a listener can stay bound while unable to serve — this is the signal that it is degraded. Fatal (broken-fd) errors are excluded on the admin, metrics and proxy listeners — they end the loop and surface through its owner. The imposter loops have no fatal class by design (a dying imposter loop is recoverable through the still-live admin API), so there a broken fd counts as systemic. No port label, deliberately (unbounded cardinality); the log lines carry the port. Present at 0 from startup for every running listener, so rate()/increase() behave on the first error. |
rift_preface_failures_total | counter | listener, kind | Connections dropped before an HTTP/1-or-HTTP/2 decision could be made, by listener (imposter, admin, metrics, front-door, intercept) and cause. kind is timeout (the client completed the transport handshake and then sent nothing in time), eof (it hung up first), or io (the read itself failed). The distinction is the point: timeout and eof are ordinary client behaviour, whereas a rising io rate is a systemic signal — a bad certificate, a resolver rollout — and the call sites deliberately log all three at debug! only, since the trigger is client-controlled and a per-connection warning would be an unbounded log-volume lever (#718). Present at 0 from startup for every running listener, so rate()/increase() behave on the first failure. |
rift_accept_error_outage | gauge | listener | 1 while any accept loop for that listener is in a systemic outage, 0 otherwise — the imposter plane runs many loops behind one label, so this counts depth rather than tracking whichever loop wrote last. Released when a loop ends while wedged, so it cannot stick at 1. Present at 0 from startup, so absent() alerts work. The one to alert on. |
Example scrape output:
rift_requests_total{method="GET",status="200"} 1234
rift_faults_injected_total{type="latency",rule_id="4545",source="rift"} 300
rift_faults_injected_total{type="error",rule_id="4545",source="script"} 17
rift_latency_injected_ms_bucket{rule_id="4545",le="100"} 120
rift_latency_injected_ms_sum{rule_id="4545"} 45670
rift_latency_injected_ms_count{rule_id="4545"} 300
rift_error_status_total{status="503",rule_id="4545"} 42
rift_script_execution_duration_ms_count{rule_id="4545",result="fault"} 17
rift_script_errors_total{rule_id="4545",error_type="timeout"} 2
rift_flow_state_ops_total{operation="increment",result="success"} 5000
rift_upstream_request_duration_ms_count{method="GET",status="200"} 88
Admin GET /metrics (port 2525)
The admin API serves a minimal hand-written counter set (Content-Type: text/plain; version=0.0.4):
| Metric | Type | Labels | Meaning |
|---|---|---|---|
rift_imposters_total | gauge | — | Number of imposters currently registered. |
rift_imposter_requests_total | counter | port | Requests per imposter (one line per port). |
rift_imposters_total 5
rift_imposter_requests_total{port="4545"} 500
rift_imposter_requests_total{port="4546"} 128
Prometheus configuration
# prometheus.yml
scrape_configs:
- job_name: 'rift'
static_configs:
- targets: ['localhost:9090']
scrape_interval: 15s
Kubernetes service discovery
scrape_configs:
- job_name: 'rift'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_label_app]
regex: rift
action: keep
Useful queries
Request rate by status:
sum(rate(rift_requests_total[5m])) by (status)
5xx error ratio:
sum(rate(rift_requests_total{status=~"5.."}[5m]))
/
sum(rate(rift_requests_total[5m]))
Fault injection rate by type:
sum(rate(rift_faults_injected_total[5m])) by (type)
P99 upstream latency (proxy responses):
histogram_quantile(0.99, sum(rate(rift_upstream_request_duration_ms_bucket[5m])) by (le))
Script error rate:
sum(rate(rift_script_errors_total[5m])) by (error_type)
Alerting rules
groups:
- name: rift
rules:
- alert: RiftHighErrorRate
expr: |
sum(rate(rift_requests_total{status=~"5.."}[5m]))
/
sum(rate(rift_requests_total[5m])) > 0.05
for: 5m
labels: { severity: warning }
annotations:
summary: "High 5xx rate in Rift"
- alert: RiftScriptErrors
expr: rate(rift_script_errors_total[5m]) > 0
for: 1m
labels: { severity: critical }
annotations:
summary: "Script errors in Rift"
description: "error_type={{ $labels.error_type }}"
Best practices
- Reasonable scrape intervals — 15–30s is typical.
- Use recording rules for expensive dashboard queries.
- Alert on error ratio and fault rate, not raw counts.
- Mind label cardinality —
rule_idis bounded by your config; avoid adding unbounded labels.