CrabTrap emits an optional OpenTelemetry metric surface, served as Prometheus scrape text on a dedicated HTTP listener. This document is the operator reference: metric catalog, network-exposure guidance, and alert suggestions.
In config/gateway.yaml:
observability:
metrics:
enabled: true # default: false
listen: "127.0.0.1:9090" # bind address for the metrics listener (default)When enabled, the gateway runs a separate HTTP listener that serves GET /metrics in Prometheus text format. Nothing else is mounted on this listener — the admin and proxy ports are unaffected.
CrabTrap surfaces build identification via crabtrap_build_info. Values come from -ldflags -X at link time:
go build -ldflags " \
-X main.version=$(git describe --tags --always) \
-X main.commit=$(git rev-parse HEAD) \
-X main.buildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
./cmd/gatewayWhen not set, these default to dev / unknown.
The metrics listener is unauthenticated by design — Prometheus scrapers should not need to manage admin credentials. Network exposure is controlled by the listen bind address, not by an auth toggle.
listen value |
Behavior | Recommended for |
|---|---|---|
127.0.0.1:9090 (default) |
Listener binds to loopback only. Reachable from the gateway host but not from another machine. | Single-host deployments where Prometheus scrapes from a sidecar or local agent. The default. |
<private-iface-ip>:9090 |
Listener binds to a private interface (e.g. 10.0.1.42:9090). Reachable inside the private network only. |
Cluster deployments where Prometheus runs on a separate node within the trust boundary. |
0.0.0.0:9090 |
Listener binds to all interfaces. Reachable from anywhere routable. | Only behind firewall rules or service-mesh auth that gate access externally. |
The metric surface does not include per-user or per-request labels. Cardinality is bounded by configured providers, models, approval modes, and outcome categories — all of which come from config, not request input.
However, the label values themselves reveal operational posture:
crabtrap_approval_decisions_total{outcome, mode}shows your allow/deny ratiocrabtrap_llm_circuit_breaker_state{provider}reveals which LLM providers you have configuredcrabtrap_approval_latency_secondsreveals LLM judge SLA
Treat the /metrics endpoint as operational intelligence. Do not bind the listener to a public interface without a compensating control (firewall rule, service mesh auth, reverse proxy).
Basic example (loopback bind, scraper running on the same host):
scrape_configs:
- job_name: crabtrap
static_configs:
- targets: ['127.0.0.1:9090']
metrics_path: /metrics
scrape_interval: 15s
scrape_timeout: 5sFor a remote scraper, set listen to a private interface address and target that host from Prometheus:
scrape_configs:
- job_name: crabtrap
static_configs:
- targets: ['crabtrap-host.internal:9090']
metrics_path: /metrics
scheme: http
scrape_interval: 15s| Name | Labels | Description |
|---|---|---|
crabtrap_rate_limit_hits_total |
— | Requests rejected by the per-IP rate limiter (HTTP 429). |
crabtrap_llm_circuit_breaker_trips_total |
provider |
Times an LLM adapter's circuit breaker tripped from closed to open. Fires exactly once per transition. |
crabtrap_approval_decisions_total |
outcome (allow|deny), mode (llm|passthrough) |
Every approval decision emitted by the approval pipeline. |
| Name | Labels | Description |
|---|---|---|
crabtrap_judge_latency_seconds |
provider, model |
Duration of each LLM judge call (the single HTTP round trip, excluding semaphore wait). |
crabtrap_approval_latency_seconds |
mode, outcome |
End-to-end duration of CheckApproval — static rules, LLM judge, fallback, all paths. |
Histograms use OpenTelemetry's default exponential bucket layout, which covers sub-millisecond to ~10 seconds.
| Name | Labels | Description |
|---|---|---|
crabtrap_llm_circuit_breaker_state |
provider |
1 = open (rejecting calls), 0 = closed or half-open probe window. |
crabtrap_build_info |
version, commit, go_version, build_date |
Constant value 1; labels carry the payload. build_date is the build timestamp injected via -ldflags -X main.buildDate. |
These are starting points — tune thresholds to your deployment. All examples use PromQL.
# Any circuit-breaker trip in the last 5 minutes — usually a provider incident.
- alert: CrabTrapCircuitBreakerTripped
expr: rate(crabtrap_llm_circuit_breaker_trips_total[5m]) > 0
for: 1m
# Circuit breaker has been open for 2+ minutes — provider is down or upstream is flapping.
- alert: CrabTrapCircuitBreakerOpen
expr: crabtrap_llm_circuit_breaker_state == 1
for: 2m
# Judge latency p95 exceeds threshold — provider is slow even if not erroring.
- alert: CrabTrapJudgeLatencyHigh
expr: histogram_quantile(0.95, sum(rate(crabtrap_judge_latency_seconds_bucket[5m])) by (provider, le)) > 2.5
for: 5m
# Deny rate spike — policy change or attack.
- alert: CrabTrapDenyRateSpike
expr: |
(
rate(crabtrap_approval_decisions_total{outcome="deny"}[5m])
/ rate(crabtrap_approval_decisions_total[5m])
) > 0.5
for: 10m
# No scrape in 2 minutes — gateway is down or metrics endpoint is misconfigured.
- alert: CrabTrapScrapeDown
expr: up{job="crabtrap"} == 0
for: 2m
provideris drawn from config (llm_judge.provider), bounded to one ofbedrock-anthropic,anthropic,openai.modelcomes from the adapter's configured model ID (typically a small set per provider).outcomehas two values:allow,deny.modehas two values:llm,passthrough.- No label value is ever derived from request paths, headers, or bodies.
Upper bound on combined series count: ~providers × models × outcomes × modes × buckets per histogram, which remains below 1 000 for any realistic deployment. No cardinality blowout risk from user traffic.
- Enabling in prod: keep the default loopback bind, verify a local scrape succeeds, then move
listento a private interface address as your scrape topology requires. - Rotating: changing
observability.metrics.*requires a gateway restart. No hot-reload. - Disabling: set
enabled: false. The metrics listener is not started; scrapes against the configured port fail with connection refused.
- OpenTelemetry traces (separate feature, different exporter choices)
pprofprofiling endpoint (different security posture, deserves its own flag)- Per-user or per-request metrics (cardinality risk)
Contributions welcome for any of these via a follow-up PR.