Monitor TheHive Flow#
6.0 One
TheHive Flow exposes health endpoints and Prometheus metrics, and its Docker Compose stack ships built-in health checks plus an optional observability profile with a preconfigured Grafana dashboard and alert rules.
Docker Compose deployment
This page describes the Docker Compose deployment of TheHive Flow. On a Kubernetes deployment, see Configuration and operations on Kubernetes.
Health endpoints#
The orchestrator service exposes its health on port 9090, published on loopback only: 127.0.0.1:9090. There's no application-level authentication on this listener, so protect it with a host firewall if needed.
| Endpoint | What it checks | Expected response |
|---|---|---|
GET /livez |
HTTP server is up | 200 with an empty body |
GET /readyz |
Internal modules, database connection, and Temporal client | 200, or 503 with a JSON body naming each failing check |
GET /metrics |
Prometheus metrics exposition | 200 with text metric lines |
curl -fsS http://127.0.0.1:9090/livez
curl -fsS http://127.0.0.1:9090/readyz
A failing /readyz indicates that either PostgreSQL or the Temporal frontend is unreachable. The response body reports each check, db, modules, and temporal, with a coarse status such as unreachable or timeout, and deliberately leaves out the underlying error. Check docker compose logs orchestrator for the specific error, and see Troubleshoot TheHive Flow for the common failure signatures.
The blob store isn't part of /readyz: it's checked once at startup with a HeadBucket request that aborts the boot if it fails. Object storage that breaks later shows up as failing runs rather than as a failing probe, so watch the s3-store container health and its disk instead.
Disk alerting for the blob volume#
The blob store has no garbage collection, so the orchestrator-s3-data volume only grows. Nothing in the stack alerts on it, and when the volume fills, blob writes fail mid-run. Add a host-level disk alert on the Docker volume filesystem, the same one you would use for orchestrator-postgres-data:
docker system df -v | grep -E 'orchestrator-(postgres|s3)-data'
Docker health checks#
Docker monitors PostgreSQL, Temporal, and the object storage automatically. These health checks gate service startup: the orchestrator service doesn't start until PostgreSQL and Temporal are healthy and the object storage has been provisioned.
| Service | Check command | Interval | Retries | Start period |
|---|---|---|---|---|
postgresql |
pg_isready -U postgres -d postgres |
5 s | 12 | 30 s |
temporal |
nc -z 127.0.0.1 7233 |
10 s | 30 | 60 s |
s3-store |
curl -sf http://localhost:8333/healthz |
5 s | 10 | 10 s |
init-s3-store |
None. One-shot container, gated on its exit code | — | — | — |
orchestrator |
None. Distroless image without a shell | — | — | — |
The object storage is gated differently from the other services: the orchestrator service waits for init-s3-store to complete successfully, not for s3-store to merely be healthy. A healthy s3-store service whose bucket doesn't exist isn't enough, because the blob store performs a HeadBucket request at startup and fails fast. The init-s3-store container runs to completion and exits, so Exited (0) is its normal steady state, not a failure.
View the current health status:
docker compose ps
Prometheus scraping#
Add the following job to your Prometheus configuration:
scrape_configs:
- job_name: orchestrator
scrape_interval: 30s
static_configs:
- targets: ["<flow_host>:9090"]
Port 9090 is loopback-only by default. If it isn't reachable from your Prometheus server, publish it by adding the following to docker-compose.override.yml:
services:
orchestrator:
ports:
- "<host_ip>:9090:9090"
Apply the change:
docker compose up -d orchestrator
Application metrics#
Besides standard instrumentation for the REST server, the PostgreSQL connection pool, and the Temporal SDK client, the /metrics endpoint exposes the following application metrics:
| Metric | Type | Labels | Purpose |
|---|---|---|---|
beeflow_activity_calls_total |
Counter | activity_type, outcome |
Workflow step executions. outcome is success, error, or canceled: alert on the error rate to catch failing steps |
beeflow_activity_duration_seconds |
Histogram | activity_type, outcome |
Duration of workflow step executions |
beeflow_cache_hits_total, beeflow_cache_misses_total, beeflow_cache_evictions_total |
Counter | cache_name |
In-process cache behavior |
Labels are bounded by design: metrics never carry run, workflow, or organization identifiers. To follow an individual run, use the execution logs instead. The bundled dashboard covers Temporal only, so alerts on these metrics are yours to define.
As a starting point, the following Prometheus rule fires when more than 5% of workflow step executions fail, with the same threshold and pending period as the bundled alert rules:
groups:
- name: thehive-flow
rules:
- alert: FlowActivityErrorRateHigh
expr: >
sum(rate(beeflow_activity_calls_total{outcome="error"}[5m]))
/
sum(rate(beeflow_activity_calls_total[5m]))
> 0.05
for: 5m
labels:
severity: warning
annotations:
summary: More than 5% of TheHive Flow step executions are failing
To alert per step type instead of on the overall rate, aggregate with sum by (activity_type) in both halves of the expression.
Temporal metrics#
The temporal service exposes its own Prometheus metrics on port 8000. They cover the internals of the workflow engine, such as persistence latency and task backlog, and feed the bundled dashboard. The port is internal to the Docker network and isn't published on the host.
Publishing the port is only needed to scrape Temporal with your own Prometheus. With the bundled observability stack, the bundled Collector already reaches temporal:8000 from inside the Docker network.
Publish the port in docker-compose.override.yml, binding it to an address your Prometheus server can reach. Use 127.0.0.1 if Prometheus runs on the same host.
services:
temporal:
ports:
- "<host_ip>:8000:8000"
Apply the change:
docker compose up -d temporal
Then add the scrape target to your Prometheus configuration:
- job_name: temporal
static_configs:
- targets: ["<flow_host>:8000"]
Bundled observability stack#
A reference Grafana dashboard and alerting stack for Temporal ships under observability/, gated behind the observability Compose profile. It's off by default, the same way temporal-admin is gated behind --profile admin.
docker compose --profile observability up -d
If you already run your own Prometheus and Grafana, you don't need this profile: point your own OpenTelemetry Collector at temporal:8000 instead. The bundled pipeline is designed as a drop-in you either use as is or swap pieces out of.
Adding the profile to an already-running deployment: pull the updated bundle, then run bash ./scripts/init.sh again before the command above. The script never touches an existing .env wholesale, but it backfills GRAFANA_ADMIN_PASSWORD specifically if that key is missing or empty, which it is in any .env created before the profile existed. Skipping this step surfaces as Compose refusing to start with GRAFANA_ADMIN_PASSWORD must be set in .env to use the observability profile.
Pipeline#
temporal:8000/metrics --(scrape)--> otel-collector --(re-expose :8889)--> prometheus --(query)--> grafana
The OpenTelemetry Collector, under observability/otel-collector/, is the only component that talks to Temporal Server directly. It never leaves the internal Docker network, and temporal:8000 is never published to the host. The bundled Prometheus, under observability/prometheus/, only ever scrapes the Collector. To feed an existing stack instead of the bundled one, swap the Collector's prometheus exporter for your own back end's exporter, such as otlphttp or prometheusremotewrite.
Access Grafana#
Grafana is published on http://127.0.0.1:3000, loopback-only, following the same convention as the health listener on port 9090. Sign in with admin and the value of GRAFANA_ADMIN_PASSWORD, generated by init.sh into .env like the other secrets. To rotate it, see TheHive Flow Security.
The dashboard lives in the Temporal folder and is provisioned from observability/grafana/dashboards/temporal.json. UI edits aren't persisted, because the dashboard is provisioned with allowUiUpdates: false: edit the file, not the UI, so changes survive a restart.
Grafana has no nginx or TLS front door: it's intentionally loopback-only, a second gate on top of the Grafana login. On a remote host, reach it by tunneling over SSH instead of publishing the port further:
ssh -L 3000:127.0.0.1:3000 <user>@<flow_host>
Then browse to http://127.0.0.1:3000 on your own machine, and close the tunnel when done. Nothing in the stack expects Grafana to be reachable from anywhere but the host itself.
Metric names differ from the raw Temporal names
The OpenTelemetry Collector's Prometheus receiver and exporter round-trip renames metrics per the OpenTelemetry and Prometheus conventions: counters gain a _total suffix, so persistence_requests becomes persistence_requests_total, and the relayed target's job and instance labels become exported_job and exported_instance. Every query in the bundled stack already accounts for this. It only matters if you query the bundled Prometheus directly.
Dashboard panels#
| Panel | Purpose |
|---|---|
| Service Availability | Server-wide gRPC success rate: the single health signal for Temporal that alerts can act on |
| Persistence Service Success % | Persistence errors are the most likely real-world failure mode on a single-node stack with a co-tenanted PostgreSQL |
| Persistence Requests Latency (p95, by operation) | Companion to the success panel: slow persistence shows up here before it shows up as errors |
| Task Schedule-to-Start Latency (p95, by task type) | Primary signal that workers can't keep up and a backlog is building, split by Workflow versus Activity task type |
| Available Worker Task Slots (avg) - Workflow | Capacity headroom explaining why schedule-to-start latency might be trending up, for Workflow-task workers |
| Available Worker Task Slots (avg) - Activity | Same signal for Activity-task workers. The built-in actions block on outbound calls, so these workers starve at least as easily |
| Shard Lock Latency (p99) | Shard-info lock contention in the history service, an early precursor to persistence slowdowns |
| Shard Distribution | Shards owned by this instance, pinned to 16 and immutable for the cluster's life. Flat on a single node |
| Service Calls (by service) | Request rate per internal Temporal service, read alongside the errors panel below |
| Service Errors (by service) (timeout/unavailable/internal/resource-exhausted) | Server-side gRPC error rate per internal service, with known-benign error types excluded |
| Target Health (Temporal Server & OTEL Collector) | Direct up for both scrape targets, plotted separately. Without it, a dead Collector and a dead Temporal Server look identical |
The two latency panels carry no color thresholds, and the matching alert thresholds below, 1 s and 5 s, are provisional defaults: a meaningful latency objective depends on your workload and expectations. Tune both the dashboard and the alert rules to your deployment.
Alert rules#
The following alerts are provisioned from observability/grafana/provisioning/alerting/rules.yml and evaluated by Grafana. Each maps to a dashboard panel above, though some panels are context-only and carry no alert. The pending period, the for field in rules.yml, is how long the condition must hold before the alert fires. No notification channel is configured: alerts are visible in the Grafana Alerting view via the built-in default contact point, but nothing notifies anyone until you wire a contact point of your own.
| Alert | Condition | Pending period | Severity |
|---|---|---|---|
| TemporalServerTargetDown | up{exported_job="temporal"} == 0 |
2 m | Critical |
| TemporalCollectorTargetDown | up{job="otel-collector", exported_job=""} == 0 |
2 m | Critical |
| TemporalTaskScheduleToStartLatencyHigh | p95 schedule-to-start latency, Workflow or Activity, above 5 s | 5 m | Warning |
| TemporalPersistenceLatencyHigh | p95 persistence latency above 1 s | 5 m | Warning |
| TemporalPersistenceErrorRateHigh | Persistence Service Success % below 99% | 5 m | Critical |
| TemporalShardDistributionChanged | Number of shards owned by this instance changes within a 10-minute window | 1 m | Warning |
| TemporalServiceErrorRateHigh | Server-wide gRPC error rate above 5% of the request rate | 5 m | Warning |
TemporalShardDistributionChanged shouldn't fire in normal operation on this single-node stack, restarts included: the sole node always owns all 16 shards, so there's no transition to detect. If it fires, treat it as unexpected and investigate rather than dismissing it as noise.
Alerting in air-gapped networks
Grafana separates evaluating a rule from notifying someone through contact points such as SMTP, webhooks, or messaging APIs. In an air-gapped network, SaaS contact points are unreachable: point the contact point at something that already lives inside the boundary instead, such as an internal SMTP relay or a webhook into the ticketing or SIEM system already running, TheHive itself for example. That internal system then owns whatever happens next, and the alert never needs to leave the network.
Conditions without bundled alerts#
The bundled stack covers Temporal only. The following conditions remain manual to monitor, with your own tooling such as node-exporter or cAdvisor:
| Condition | How to detect | Severity |
|---|---|---|
| TheHive Flow not ready | /readyz returns non-200 for more than 2 minutes |
Critical |
| TheHive Flow not live | /livez returns non-200 for more than 30 seconds |
Critical |
| PostgreSQL unhealthy | docker compose ps shows unhealthy |
Critical |
| Disk above 80% on the PostgreSQL volume | docker system df |
Warning |
| Container terminated by the OOM killer | Docker oom event |
Critical |
Check service status#
To check all services at once:
docker compose ps
To check detailed resource usage:
docker stats --no-stream
To check the PostgreSQL connectivity from the host:
docker compose exec postgresql pg_isready -U postgres
To check the database sizes:
docker compose exec postgresql psql -U postgres -c "\l+"
To check the Temporal connectivity:
docker compose --profile admin run --rm temporal-admin \
temporal operator cluster health --address temporal:7233