Skip to content

Monitoring out of the box

Observability in Soul Stack is built into both binaries: Keeper and the soul agent publish metrics and traces without separate sidecars or exporter processes. This guide shows how to collect the telemetry, what to look at first, and how to deploy a Prometheus exporter onto the souls themselves using Soul Stack.

The telemetry config reference is keeper.yml → metrics/otel and soul.yml → metrics/otel; the operational overview is Operations → Monitoring.

Soul Stack telemetry travels over two different mechanisms — and this asymmetry is the key thing to grasp up front:

ChannelModelWho initiatesWhat it carries
Prometheus /metricspull (scrape)your Prometheus comes to Soul Stackcounters, gauges, histograms
OpenTelemetrypush (OTLP)Soul Stack sends to a collector itselfend-to-end traces (operator → Keeper → agent)
  • Metrics are pull. Soul Stack listens on /metrics; Prometheus scrapes that endpoint periodically. On Keeper this is port :9090.
  • OpenTelemetry is push. Soul Stack has no inbound OTel port. With OTel enabled, Keeper and the agent send OTLP traces to the collector you specify (otel.endpoint), listening for nothing.
flowchart LR
    subgraph pull["Pull — Prometheus scrapes us"]
        direction LR
        P["Prometheus"] -->|scrape| K1["Keeper :9090/metrics"]
        P -->|scrape| S1["soul — metrics.listen"]
    end
    subgraph push["Push — we send OTLP outward"]
        direction LR
        K2["Keeper"] -->|OTLP| O["your OTel collector"]
        S2["soul"] -->|OTLP| O
    end

Logs are a third channel: structured JSON to stdout / the systemd journal, with built-in rotation. A log record carries kid/sid, the run id, and a trace-id — enough to tie a log to a trace and a metric.

Keeper serves /metrics on a separate listener (port :9090 in the examples) — without the Operator API auth chain. Check it by hand:

Terminal window
curl -s http://keeper.example.com:9090/metrics | head -40

The Prometheus scrape config:

prometheus.yml
scrape_configs:
- job_name: keeper
static_configs:
- targets:
- keeper.example.com:9090

Keeper’s metrics are prefixed keeper_.

The agent also publishes /metrics (prefix soul_), but listens on loopback by default (127.0.0.1) — nothing exposed outward. This is deliberate: a managed host shouldn’t open extra inbound ports.

To scrape agents from outside, change the bind in soul.yml to an external interface and enable Basic auth:

soul.yml
metrics:
listen: "0.0.0.0:9091"
auth:
basic:
username: prometheus
password_file: /etc/soul/metrics-password # mode 0400

Otherwise the agent stays on loopback — correct for most installations, where a local node agent collects host metrics. Details in soul.yml → metrics.

Domain identifiers (run id, sid, scenario name) live in traces, not in metric labels — deliberately, to avoid blowing up Prometheus cardinality. So metrics answer “is something wrong?”, while you find the specific culprit through traces and logs.

SignalMetric (guideline)What it tells you
Instance availabilityup{job="keeper"}the instance isn’t responding — check the logs
Connected agentskeeper_grpc_streams_activea drop below expected — some souls dropped off
Background cleanupReaper leadership gauge (sum across the cluster = 1)zero — DB cleanup has stopped and tables are growing; >1 — a coordination anomaly
Schedule plannerplanner leadership gauge (sum = 1 when enabled)zero while enabled — recurring runs aren’t being generated
Failed-run ratiorate of failed / all scenario runsa rise above normal — investigate the scenario or hosts
Vault latencyVault read histogramrising — Vault is slow to respond and operations slow down

OTel is turned on in the config; with otel.enabled: true the binary pushes OTLP over gRPC to otel.endpoint itself:

# keeper.yml (symmetric in soul.yml)
otel:
enabled: true
endpoint: "otel-collector.example.com:4317"

Traces are end-to-end: a single operator operation (a scenario run, say) is traced through Keeper down to a specific agent and back. Traces (by correlation-id and run id) are how you find where something slowed down or failed — metrics give the aggregate, a trace gives the concrete path.

Details on the keys are in keeper.yml → otel and soul.yml → otel.

Deploy an exporter to the souls with Soul Stack

Section titled “Deploy an exporter to the souls with Soul Stack”

Soul Stack’s own metrics are one thing; host metrics (CPU, memory, disk, network) are collected by a Prometheus exporter on each host. Rolling it out is an ordinary job for Soul Stack: the same kind of scenario as any other service.

An exporter is packaged as its own service in git — a Destiny that downloads the release (with a checksum check), installs the binary under a dedicated system user, lays down a systemd unit, and starts it — and applied to the souls exactly like any other service (Your first souls service, Orchestration). It leans on ordinary core modules: core.url.fetched (with checksum:) → core.archive.extractedcore.cmd.shell (install) → core.file.rendered (the unit) → core.service.running; take the host architecture from soulprint.self.os.arch so a single run covers mixed amd64/arm64 souls.

There is no ready-made public exporter service yet — build your own from the first-souls-service template, or port an existing node_exporter playbook onto those modules. The result is :9100/metrics on every host for your Prometheus to scrape, right alongside Soul Stack’s own metrics.