This guide covers installing, configuring, and operating Elevarq Signals in development and production environments.
You can go from zero to your first snapshot in under five minutes.
From source:
git clone https://github.com/elevarq/signals.git
cd signals
make buildThis produces bin/signals (daemon) and bin/signalsctl (CLI).
Docker:
docker pull ghcr.io/elevarq/signals:<version>Throughout this guide, <version> stands for a concrete released tag —
for example 1.0.0. Pin the exact tag you deploy (see the
releases page); the image
does not publish a latest tag, so do not rely on one.
Create a minimal configuration file:
# signals.yaml
env: dev
targets:
- name: my-database
host: localhost
port: 5432
dbname: postgres
user: signals
password_file: /path/to/pg_password
sslmode: prefer
enabled: trueThe config file is called signals.yaml. Elevarq Signals searches for it at /etc/signals/signals.yaml and ./signals.yaml by default, or you can pass --config <path>.
./bin/signals --config signals.yamlThe daemon begins collecting on the configured poll_interval (default 5m).
signalsctl collect nowsignalsctl talks to the running Elevarq Signals daemon over its HTTP API. Set SIGNALS_API_TOKEN to the token shown at daemon startup (or configure a fixed token via the same env var).
Export the collected data as a snapshot:
signalsctl export --output snapshot.zipThe output is a self-contained ZIP archive in signals-snapshot.v1 format.
signalsctl status
signalsctl versionRun Elevarq Signals as a long-lived container:
docker run -d \
--name signals \
-v /etc/signals/signals.yaml:/etc/signals/signals.yaml:ro \
-v signals-data:/data \
-p 127.0.0.1:8081:8081 \
ghcr.io/elevarq/signals:<version>The container runs as a non-root user (UID 10001) on Alpine 3.21. The API listens on port 8081. Bind it to loopback unless you need external access.
For simple deployments with a single PostgreSQL target, you can configure everything through environment variables instead of a config file:
docker run -d \
--name signals \
-e SIGNALS_TARGET_HOST=db.example.com \
-e SIGNALS_TARGET_PORT=5432 \
-e SIGNALS_TARGET_DBNAME=postgres \
-e SIGNALS_TARGET_USER=signals \
-e SIGNALS_TARGET_PASSWORD_FILE=/run/secrets/pg_password \
-e SIGNALS_TARGET_SSLMODE=verify-full \
-e SIGNALS_ENV=prod \
-v /run/secrets/pg_password:/run/secrets/pg_password:ro \
-v signals-data:/data \
-p 127.0.0.1:8081:8081 \
ghcr.io/elevarq/signals:<version>The following target-level env vars are supported:
| Variable | Description | Default |
|---|---|---|
SIGNALS_TARGET_HOST |
PostgreSQL host (required to activate env-based target) | -- |
SIGNALS_TARGET_PORT |
PostgreSQL port | 5432 |
SIGNALS_TARGET_DBNAME |
Database name | postgres |
SIGNALS_TARGET_USER |
Username | -- |
SIGNALS_TARGET_NAME |
Target name | default |
SIGNALS_TARGET_PASSWORD_FILE |
Path to password file | -- |
SIGNALS_TARGET_PASSWORD_ENV |
Env var containing the password | -- |
SIGNALS_TARGET_PGPASS_FILE |
Path to pgpass file | -- |
SIGNALS_TARGET_SSLMODE |
TLS mode | -- |
SIGNALS_TARGET_SSLROOTCERT_FILE |
Path to CA certificate (required for verify-ca/verify-full) |
-- |
The single-target environment path covers the password auth method
only (password file, env var, or pgpass). The cloud-identity,
secret_store, and mtls methods below need a config file — set
auth_method and its fields in signals.yaml.
Elevarq Signals connects to PostgreSQL over TLS when the target's sslmode field is set to require or stricter. For production, use verify-ca or verify-full and provide sslrootcert_file pointing to the CA certificate:
targets:
- name: prod-primary
host: db.example.com
port: 5432
dbname: postgres
user: signals
password_file: /run/secrets/pg_password
sslmode: verify-full
sslrootcert_file: /etc/ssl/certs/pg-ca.crt
enabled: trueIn production (env: prod), weak TLS modes (disable, allow, prefer, require) are rejected. In non-production environments, set SIGNALS_ALLOW_INSECURE_PG_TLS=true to allow weak TLS for local development.
For the HTTP API, place Elevarq Signals behind a TLS-terminating reverse proxy (nginx, Caddy, or a cloud load balancer).
Each target picks one auth_method. The method decides how Elevarq
Signals obtains the credential it connects with; it never changes what
the connection may do — the read-only, least-privilege model is identical
for every method. Omitting auth_method keeps the default password
behaviour, so existing deployments need no change.
No database password has to live in Signals' config. The
cloud-identity methods (aws_rds_iam, azure_entra, gcp_cloudsql_iam)
mint a short-lived token from the collector's ambient cloud identity at
connect time, and secret_store fetches the password live from a cloud
vault. The credential is re-resolved on every reconnect (automatic
rotation) and is never written to disk, logs, audit events, metrics, or
exports.
| Platform | auth_method |
Credential |
|---|---|---|
| Amazon RDS / Aurora | aws_rds_iam |
Short-lived RDS IAM token — passwordless |
| Amazon RDS / Aurora | secret_store |
Password from AWS Secrets Manager or SSM Parameter Store |
| Azure Flexible Server | azure_entra |
Entra ID token via Managed Identity — passwordless |
| Azure Flexible Server | secret_store |
Password from Azure Key Vault |
| Google Cloud SQL | gcp_cloudsql_iam |
Google IAM token via Workload Identity / ADC — passwordless |
| Google Cloud SQL | secret_store |
Password from GCP Secret Manager |
| Self-managed | password (default) |
Password from a file, env var, or pgpass file |
| Self-managed | mtls |
Client X.509 certificate |
| Self-managed | secret_store |
Password from any of the four cloud vaults |
Every method authenticates as the same read-only role — a LOGIN
role granted pg_monitor (see Monitoring Role Setup).
The cloud-identity and secret_store methods also require
sslmode: verify-full with an sslrootcert_file, in every
environment (stricter than the general prod-only TLS rule below).
Full per-cloud recipes — the exact database grants, least-privilege IAM
policies, prerequisites, and limitations for aws_rds_iam,
azure_entra, gcp_cloudsql_iam, secret_store (all four vaults), and
mtls — live in the canonical reference:
docs/database-connections.md. The rest of
this section covers the default password method.
The credential is a password supplied locally via exactly one of
password_file, password_env, or pgpass_file per target:
| Source | Config field | Description |
|---|---|---|
| Password file | password_file: /path/to/file |
Reads the password from a file. Compatible with Docker secrets and Kubernetes secret volumes. |
| Environment variable | password_env: PG_PASSWORD |
Reads the password from the named environment variable. The value of that variable is the password. |
| pgpass file | pgpass_file: /path/to/.pgpass |
Reads credentials from a pgpass-format file. |
Example using password_file:
targets:
- name: prod-primary
host: db.example.com
port: 5432
dbname: postgres
user: signals
password_file: /run/secrets/pg_password
sslmode: verify-full
sslrootcert_file: /etc/ssl/certs/pg-ca.crt
enabled: trueExample using password_env:
targets:
- name: prod-primary
host: db.example.com
port: 5432
dbname: postgres
user: signals
password_env: PG_PASSWORD_PROD
sslmode: verify-full
sslrootcert_file: /etc/ssl/certs/pg-ca.crt
enabled: trueCredentials are read fresh on each connection attempt. They are never cached in memory beyond a single connection, never written to SQLite, and never included in snapshot exports.
Create a dedicated read-only role for Elevarq Signals. The following works across RDS, Cloud SQL, Aurora, and self-managed PostgreSQL:
CREATE ROLE signals WITH LOGIN PASSWORD 'your-secure-password';
-- Grant read access to statistics views
GRANT pg_monitor TO signals;
-- Optional: enable query-level statistics
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;For the passwordless cloud-identity methods (aws_rds_iam,
azure_entra, gcp_cloudsql_iam) and mtls, create the role without
a password (CREATE ROLE signals LOGIN;) and add the method-specific
binding — see docs/database-connections.md.
On Amazon RDS / Aurora, pg_monitor is available on all supported versions (14+).
On Google Cloud SQL, grant the cloudsqlsuperuser role or assign pg_monitor directly.
Elevarq Signals enforces strict role safety by default. If your monitoring role has superuser, replication, or bypassrls attributes, collection is blocked with an actionable error message. Use the recommended monitoring role setup:
CREATE ROLE signals WITH LOGIN PASSWORD '...';
GRANT pg_monitor TO signals;
-- Do NOT grant superuser, replication, or bypassrlsFor managed databases (RDS, Cloud SQL, Aurora), the equivalent role grants are documented in each provider's documentation for pg_monitor.
An explicit override (SIGNALS_ALLOW_UNSAFE_ROLE=true) exists for lab/dev environments only and is not recommended for production.
Elevarq Signals supports concurrent collection across multiple targets:
# signals.yaml
env: prod
signals:
poll_interval: 5m
retention_days: 30
max_concurrent_targets: 4
target_timeout: 60s
query_timeout: 10s
targets:
- name: prod-primary
host: primary.db.internal
port: 5432
dbname: app
user: signals
password_file: /run/secrets/pg_password_primary
sslmode: verify-full
sslrootcert_file: /etc/ssl/certs/pg-ca.crt
enabled: true
- name: prod-replica
host: replica.db.internal
port: 5432
dbname: app
user: signals
password_file: /run/secrets/pg_password_replica
sslmode: verify-full
sslrootcert_file: /etc/ssl/certs/pg-ca.crt
enabled: true
- name: staging
host: staging.db.internal
port: 5432
dbname: app
user: signals
password_env: PG_PASSWORD_STAGING
sslmode: require
enabled: true
database:
path: /data/signals.db
wal: true
api:
listen_addr: "127.0.0.1:8081"Each target is collected independently. A failure on one target does not block collection from others. The max_concurrent_targets setting (default 4) controls how many targets are collected in parallel.
Here is the complete signals.yaml schema with all available fields and their defaults:
# signals.yaml
env: dev # dev, lab, prod
signals:
poll_interval: 5m
retention_days: 30
log_level: info # debug, info, warn, error
log_json: false
max_concurrent_targets: 4
target_timeout: 60s
query_timeout: 10s
targets:
- name: my-database
host: localhost
port: 5432
dbname: postgres
user: signals
auth_method: password # password (default), aws_rds_iam, azure_entra,
# gcp_cloudsql_iam, secret_store, mtls
password_file: /path/to/password # password method: or password_env or pgpass_file
sslmode: prefer
sslrootcert_file: /path/to/ca.crt # required for verify-ca/verify-full
enabled: true
database:
path: /data/signals.db
wal: true
api:
listen_addr: "127.0.0.1:8081"
read_timeout: 30s
write_timeout: 180sThe per-target fields for the non-password methods (region,
azure_client_id, gcp_impersonate_service_account, secret_ref,
secret_json_key, max_cache_ttl, sslcert, sslkey,
sslkey_passphrase_file) are documented with each method in
docs/database-connections.md.
By default Elevarq Signals is pull-based: it collects and stores locally, and
you fetch a snapshot ZIP on demand via GET /export or signalsctl export
(see Getting Started). Nothing is written to a directory
unless you ask for it.
For a hands-off feed — for example to hand snapshots to a downstream watcher or the Elevarq Analyzer inbox — Signals can push a fresh ZIP per database to a directory after each collection cycle.
The destination is the switch: set export_dest (env SIGNALS_EXPORT_DEST)
to a directory and Signals writes the latest snapshot for each database there
after every collection cycle — and once immediately when it starts. No
export_dest ⇒ pull-only (the default).
signals:
export_dest: /var/lib/signals/exports # enables the push
# export_on_collect: false # explicit opt-out: keep a dest but suppress the push
# Bound the directory so it doesn't grow without limit (~288 files/db/day at a
# 5m cadence). 0 = unbounded (the default). Both may be set together.
export_retention_days: 7 # delete ZIPs older than N days
export_max_files: 500 # keep at most N ZIPs per databaseEquivalent environment variables: SIGNALS_EXPORT_DEST,
SIGNALS_EXPORT_ON_COLLECT (opt-out), SIGNALS_EXPORT_RETENTION_DAYS,
SIGNALS_EXPORT_MAX_FILES.
Notes:
- Latest, not the backlog. Each cycle writes the latest snapshot per
database — one ZIP per database, named
<instance>-t<target>-<timestamp>.zip. Enabling the push does not replay previously collected snapshots; it writes the current latest (immediately on enable) and each new latest going forward. This is intentional — a consumer wants the current state, not a replay of stale snapshots. - Restart to enable. The exporter is wired at startup.
POST /reload(SIGHUP) re-reads targets, not the export wiring — so turning the push on/off takes effect on restart. - Bounded + safe. Pruning keeps only this instance's files per target, so several instances writing to one shared directory never delete each other's exports; a failed prune or export is logged and never disrupts collection.
Prefer pull on a timer? Run Elevarq Signals as a daemon and export snapshots on a schedule instead:
# Export a snapshot every hour
0 * * * * /usr/local/bin/signalsctl export --output /var/snapshots/signals-$(date +\%Y\%m\%d-\%H\%M).zipThe signals-snapshot.v1 format is a ZIP archive containing NDJSON files. Parse it with standard tools:
# List contents
unzip -l snapshot.zip
# Extract and process with jq
unzip -p snapshot.zip "*.ndjson" | jq '.query_name'Snapshots are self-contained and immutable. Store them in any object store (S3, GCS, MinIO) or local filesystem for historical analysis.
# Upload to S3
aws s3 cp snapshot.zip s3://my-bucket/signals/$(date +%Y/%m/%d)/Snapshot archives include a format version identifier (signals-snapshot.v1). When the format changes, the version number increments. Older snapshots remain readable by newer versions of Elevarq Signals.
Replace the binary or container image and restart. Elevarq Signals uses SQLite with WAL mode for local storage; the schema is migrated automatically on startup. No manual migration steps are required.
- Snapshot format versions follow semantic versioning. Minor versions add fields; major versions may change structure.
- Configuration file format is stable within a major version.
- SQL collectors may be added or updated between releases; existing collector output remains structurally compatible.
Symptom: dial tcp: connect: connection refused
Causes:
- PostgreSQL is not running or not listening on the specified host/port.
- A firewall or security group is blocking the connection.
- The target
hostis set tolocalhostbut PostgreSQL is bound to a different interface.
Fix: Verify connectivity with psql using the same host, port, user, and dbname. Check listen_addresses in postgresql.conf and any network-level firewall rules.
Symptom: permission denied for relation pg_stat_activity
Causes:
- The monitoring role does not have
pg_monitormembership. - On RDS/Aurora, the role was not granted the required managed policy role.
Fix: Grant the pg_monitor role: GRANT pg_monitor TO signals;
Symptom: collection blocked: role has superuser attribute
Causes:
- The configured user has superuser, replication, or bypassrls privileges.
Fix: Create a dedicated monitoring role without those privileges:
CREATE ROLE signals WITH LOGIN PASSWORD '...';
GRANT pg_monitor TO signals;For lab/dev environments only, set SIGNALS_ALLOW_UNSAFE_ROLE=true to override.
Symptom: sslmode=prefer is not allowed in prod
Causes:
- Production mode (
env: prod) requiresverify-caorverify-fullwith a CA certificate.
Fix: Set sslmode: verify-full and provide sslrootcert_file in your target config. For non-production environments, set SIGNALS_ALLOW_INSECURE_PG_TLS=true to allow weaker TLS modes.
Symptom: Collector skipped with extension pg_stat_statements not available
Causes:
pg_stat_statementsis not installed or not listed inshared_preload_libraries.
Fix: This is informational, not an error. Elevarq Signals automatically skips collectors that depend on unavailable extensions. To enable the extension, add pg_stat_statements to shared_preload_libraries in postgresql.conf and run CREATE EXTENSION pg_stat_statements; in each target database.
Symptom: database is locked errors during collection
Causes:
- Another process holds a write lock on the SQLite database file.
- The filesystem does not support WAL mode (e.g., some network filesystems).
Fix: Ensure only one Elevarq Signals instance writes to a given SQLite database file. Use a local filesystem that supports fcntl locking.
Symptom: Memory spikes when exporting large snapshots.
Fix: Export more frequently to reduce the volume of data per snapshot. Elevarq Signals streams results to disk during collection, but export assembles the ZIP archive in memory.