Skip to main content
Caddy writes structured JSON logs by default. This page ships them to Rootprint’s OTLP endpoint with either the OpenTelemetry Collector or Vector. Both land records in the otel-logs-v0_9 index with service.name: caddy and OpenTelemetry HTTP attributes (http.request.method, url.path, http.response.status_code, client.address, user_agent.original, …) pre-populated.

Prerequisites

  • A running Rootprint instance and its base URL — you’ll substitute it for <your-rootprint>.
  • A host running Caddy that writes its logs to files. Stock Caddy logs to journald; this page enables file logging below.
  • A log agent that can read those files — the OpenTelemetry Collector or Vector.
  • An ingest API key scoped to your target index. See API keys.
This config assumes the agent runs where it can read /var/log/caddy.
1

Enable Caddy file logging

Stock Caddy logs to journald. Add a log block per site so Caddy writes JSON to file, then sudo systemctl reload caddy. Keep Caddy’s default JSON encoder; do not add a format directive.
2

Configure the Collector

Replace <your-rootprint> with your Rootprint base URL and <your-ingest-token> with the key from above.
3

Grant read access to /var/log/caddy

Add the user the Collector runs as to the caddy group that owns the logs (the Debian/RPM Collector package creates otelcol-contrib).
4

Start the Collector

Start (or restart) the Collector so it loads the config. Startup logs should show no config-parse or exporter errors.
5

Send a test request

6

Verify in Rootprint

Open Search, pick otel-logs-v0_9, and query service_name:caddy. Records typically appear within 5–10 seconds.

What you get in Rootprint

Two remap transforms (Vector) or OTTL transform statements (OTEL Collector) map Caddy’s JSON fields to OpenTelemetry semantic conventions. Both agents produce the same attributes in otel-logs-v0_9. service.name is hard-coded to caddy (resource attribute). The if exists(.request) / where body["request"] != nil guard makes one config handle both access and error logs cleanly: error-log entries (no .request block) get only log.file.path/caddy.logger, while access-log entries get the full HTTP attribute set. timeUnixNano is derived from Caddy’s .ts (float seconds since epoch); if the field is missing or unparseable, the agent’s read time is used as a fallback.

Useful searches

Run these in the Rootprint search box against otel-logs-v0_9. Every 5xx response Caddy returned, across all sites:
All 4xx responses for one URL prefix, useful for spotting bad clients hammering one endpoint:
Activity from a single source IP, pivot from a suspect address back to everything it touched:
Failed POSTs only:
Access-log entries only (skip Caddy’s runtime/error logger):

Troubleshooting

  • Records arrive but attributes.http.* are missing: the entry came from error.log (no .request block), by design. Filter access-log entries with attributes.caddy.logger:http.log.access.
  • body looks like raw JSON instead of just the message: the access log is using a non-default format directive in the Caddyfile. The parser expects Caddy’s default JSON encoder. Either remove the format directive or extend the config to handle the alternate shape.
  • severity_text is always INFO even for 5xx responses: by design. Caddy emits every HTTP access at level: info regardless of status code; both agents trust the source. Filter on attributes.http.response.status_code:>=500 to surface server errors.
  • Ingest transport errors (401, 403, 413, 415): see OTLP reference → Response codes.