Environment config

HorizonConfig — env-aware operational settings with .env stacking. State directories, metrics port, log level, tracer exporter, retention policy.

Operational knobs — where the audit DB lives, which port Prometheus listens on, where OTel ships traces — belong in config, not in code. HorizonConfig centralizes them with a predictable precedence chain and per-environment overlay files.

Credentials do not live here. Use Secrets for broker keys, signing material, and any value that must rotate independently of config.

Import

python
from horizon.config import HorizonConfig, ENV_DEV, ENV_STAGING, ENV_PROD

Defaults (no config file needed)

python
cfg = HorizonConfig()
print(cfg.state_dir)               # ./state
print(cfg.audit_db_path)           # ./state/audit.db
print(cfg.metrics_port)            # 9100
print(cfg.retention_years)         # 5

All fields default sensibly. Tier 1–3 never constructs a config.

Precedence

Highest wins:

  1. Explicit kwargs passed to HorizonConfig(...) or HorizonConfig.load(...).
  2. Process environment (HORIZON_STATE_DIR, HORIZON_METRICS_PORT, …).
  3. <config_dir>/<env>.env (e.g. /etc/horizon/prod.env).
  4. <config_dir>/base.env (shared defaults).
  5. Field defaults on the class.
python
cfg = HorizonConfig.load(
    env="prod",                              # or env var HORIZON_ENV
    config_dir="/etc/horizon",               # or env var HORIZON_CONFIG_DIR
    metrics_port=9200,                       # explicit override (wins over all)
)

.env file stacking

/etc/horizon/base.env
    HORIZON_STATE_DIR=/var/lib/horizon
    HORIZON_METRICS_PORT=9100
    HORIZON_LOG_LEVEL=INFO

/etc/horizon/dev.env
    HORIZON_LOG_LEVEL=DEBUG

/etc/horizon/prod.env
    HORIZON_RETENTION_YEARS=7
    HORIZON_TRACER_EXPORTER=otlp
    HORIZON_OTLP_ENDPOINT=https://otlp.internal:4317

HorizonConfig.load(env="prod") merges base.env then prod.env, then overlays process env, then explicit kwargs.

Quoted values are unwrapped: HORIZON_STATE_DIR="/var/lib/horizon" and HORIZON_STATE_DIR=/var/lib/horizon behave identically. Comments start with #.

Fields

FieldEnv varDefaultNotes
envHORIZON_ENV"dev"Selects the overlay file.
state_dirHORIZON_STATE_DIR./stateBase for every derived path.
audit_db_pathHORIZON_AUDIT_DB_PATH<state_dir>/audit.dbSQLite WORM audit sink.
dlq_db_pathHORIZON_DLQ_DB_PATH<state_dir>/dlq.dbFailed-submit DLQ.
archive_db_pathHORIZON_ARCHIVE_DB_PATH<state_dir>/audit_archive.dbRetention cold store.
destruction_log_pathHORIZON_DESTRUCTION_LOG_PATH<state_dir>/destruction.jsonlRetention destruction receipts.
preclearance_db_pathHORIZON_PRECLEARANCE_DB_PATH<state_dir>/preclearance.dbPre-clearance queue.
log_levelHORIZON_LOG_LEVEL"INFO"DEBUG / INFO / WARNING / ERROR.
metrics_hostHORIZON_METRICS_HOST"0.0.0.0"Prometheus bind address.
metrics_portHORIZON_METRICS_PORT9100Prometheus port.
tracer_exporterHORIZON_TRACER_EXPORTER"console""otlp" / "console" / "none".
otlp_endpointHORIZON_OTLP_ENDPOINTNoneOTLP-over-gRPC endpoint.
retention_yearsHORIZON_RETENTION_YEARS5SEC 204-2 baseline.
live_poll_interval_sHORIZON_LIVE_POLL_INTERVAL_S0.05Live-loop tick cadence.
idle_timeout_sHORIZON_IDLE_TIMEOUT_SNoneSeconds of feed idle before LiveWatchdog fires.

Wiring into hz.run()

python
cfg = HorizonConfig.load(env="prod")

hz.run(
    mode="live",
    feed=...,
    venues={...},
    # Point each concrete module at the configured path:
    audit_log=AuditLog(sink=SQLiteSink(cfg.audit_db_path)),
    ...
)

The run loop does not consume HorizonConfig directly — it is a thin layer you opt into in deployment scripts. Each module (audit, DLQ, metrics, retention) takes explicit paths, so nothing in the hot path reads the environment on its own.

Serialisation

python
cfg.to_dict()
# {'env': 'prod', 'state_dir': '/var/lib/horizon', 'audit_db_path': '...', ...}

Useful for structured log context on startup or for a /healthz endpoint that echoes the effective config.

Out of scope

  • Secrets. Broker API keys, signing material, TLS certs. Use Secrets.
  • Per-account risk limits. Those live on RiskConfig / IPS.
  • Feature flags. The SDK does not ship a flag system; toggle via construction.

Related