Observability¶
Canopy ships a lightweight observability pipeline:
canopy-api / canopy-worker write JSON log lines
-> Promtail tails the log files
-> Loki stores + indexes them
-> Grafana dashboards query Loki
canopy-api exposes Prometheus metrics at /metrics
-> Prometheus scrapes them
-> Grafana queries Prometheus
Everything runs from one compose overlay — see the setup guide.
Structured logging¶
Both process types call canopy.core.logging.configure_logging() at startup.
Configuration comes from core settings:
| Env var | Default | Meaning |
|---|---|---|
CANOPY_LOG_FORMAT |
text |
text (human-readable) or json (one JSON object per line) |
CANOPY_LOG_PATH |
unset | When set, also log to this file (directory auto-created, rotated at 50 MiB with 3 backups kept); Promtail tails it |
CANOPY_LOG_LEVEL |
INFO |
Root log level |
In json mode each line carries ts, level, logger, message, plus any
extra={...} attributes from the call site as first-class fields. Exceptions
are rendered into an error field. Those five names are reserved: an extra
field that collides is written as extra_<name> rather than overwriting. The
docker compose stack enables JSON logging to /tmp/canopy-logs/ by default.
Framework loggers are folded in: uvicorn's access and error lines and arq's
worker lines go through the same handlers, so HTTP access logs reach the file
too, and CANOPY_LOG_LEVEL governs them.
Frontend analytics + error ingestion¶
Frontends POST usage and error events to the analytics module:
POST /analytics/v1/events
Content-Type: application/json
{
"event": "screen_opened",
"sessionId": "abc123", // optional
"route": "/screens/demo", // optional
"path": "/screens/demo?x=1", // optional
"clientTs": "2026-07-20T12:00:00Z", // optional
"properties": { "screen": "demo" }
}
- Auth is optional: anonymous events are accepted; a valid
X-API-Keyonly enriches the event with scope metadata (has_api_key,api_key_scopes). The key token itself is never logged. An invalid key does not cause a 4xx — ingestion must never break a client workflow. - Returns
204 No Content. Malformed bodies get a standard422; bodies overCANOPY_ANALYTICS__MAX_EVENT_BYTES(default 64 KiB) get413, so keep stack traces trimmed. sessionIdandpropertiesare stored verbatim in the log. TreatsessionIdas a pseudonymous identifier and keep personal data (names, email addresses, free-text user input) out ofproperties.- Error reporting convention: send
event: "frontend_error"withproperties.error/properties.stack— the Canopy Error Reporting dashboard keys off that name. - Kill switch:
CANOPY_ANALYTICS__ENABLED=falseaccepts but drops events.
Each accepted event becomes one structured log line from the
canopy.analytics logger with type="analytics", which is how dashboards
select analytics traffic.
Label discipline¶
Promtail indexes only low-cardinality fields as Loki labels: app, job,
level, type, source. Unbounded fields (event, route, session_id,
properties.*) are parsed at query time with LogQL's | json — keeping
Loki's index small. Follow the same rule when adding new log fields.
Metrics¶
The parent app serves an unauthenticated GET /metrics in Prometheus
exposition format: request counts and latency histograms for every module's
routes, labelled by module and route template rather than raw path, plus
module gauges read from the same code that feeds /health/summary (Hare's
client, monitor and subscription counts to start). The overlay's Prometheus
scrapes it and Grafana gets a provisioned datasource. Naming, label and
collection rules for adding module metrics are on the metrics page.
Dashboards¶
Grafana is auto-provisioned with Loki and Prometheus datasources and two
Loki-backed dashboards (Canopy Frontend Analytics, Canopy Error Reporting) from
docker/observability/grafana/. Edit the JSON there (or export from the
Grafana UI and copy back) to evolve them under review.