Caddy writes structured JSON logs by default. Vector parses each line withDocumentation Index
Fetch the complete documentation index at: https://docs.rootprint.io/llms.txt
Use this file to discover all available pages before exploring further.
parse_json, maps Caddy’s level field to OTel severity, and ships the result to Rootprint’s OTLP endpoint with a Bearer token. Records land 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.
Two setup paths are documented below. Bare-metal runs Vector as a systemd service that tails /var/log/caddy/*.log — pick this if Caddy is installed via your distro’s package manager. Docker runs Vector as a sidecar container that reads Caddy’s stdout via the Docker socket — pick this if Caddy runs as a container.
Setup (bare-metal)
Pick the target index and create an ingest API key
The default index for OTLP traffic is
otel-logs-v0_9 — see Indexes for its
schema. In Settings → API keys, click Create API key, give it a name, pick
otel-logs-v0_9, and choose the Ingest role. The value is shown once — copy it before
clicking Done. API keys are scoped to one index — you cannot reuse one across indexes.Enable Caddy file logging
Stock Caddy installs log to
journald. Add the following to /etc/caddy/Caddyfile so Caddy
writes JSON to file instead, then reload Caddy. The log block has to be added to every
site whose access log you want shipped — Caddy’s default JSON encoder is what the parser
expects, so do not add a format directive.Install Vector
Install the Vector package for your platform from the official installation
page. Per-distro instructions (Debian/Ubuntu apt,
RHEL/Fedora dnf, container images) are maintained upstream.
Write the Vector config
Save the following at
/etc/vector/vector.yaml. Replace <your-rootprint> with your Rootprint
base URL and <your-ingest-token> with the API key you copied in step 1.read_from: end skips existing content on first start, so installing Vector against an
existing access.log does not replay every historical request. Flip it to beginning if
you want a one-time backfill.Grant Vector read access to /var/log/caddy
The default Caddy package creates
/var/log/caddy/ owned by caddy:caddy with mode 750,
so Vector’s vector user cannot read it. Add vector to the caddy group:Restart Vector
active (running) and the most recent log lines should not
contain config-parse or sink-startup errors.Send a test request
Hit any site Caddy is serving so it writes a fresh access-log line:Replace
http://localhost/ with whichever site URL you configured the log block on.Setup (Docker)
Pick the target index and create an ingest API key
Same as the bare-metal setup — see step 1 above. The API key works identically across both
paths.
Enable access logs if needed
If If you change the
docker logs caddy already shows the request/access logs you want to ship, skip this
step. If you only see startup logs and runtime errors, add the following log block to each
site whose access logs you want shipped. Leave the default JSON encoder in place.Caddyfile, restart the Caddy container in step 4 below.Add the Vector sidecar to your compose file
Drop this service alongside your existing ones. Vector reads every container’s logs through
the Docker socket — no changes needed to your application services.The
include_containers value in vector.yaml (next step) is set to caddy. Either set
container_name: caddy on your Caddy compose service, or update include_containers to match
the actual container name shown by docker ps --format '{{.Names}}'.Save the Vector config
Save the following as
vector.yaml next to your compose.yml. Replace <your-rootprint> with
your Rootprint base URL and <your-ingest-token> with the API key you copied in step 1.Start Vector and restart Caddy if needed
Bring the Vector sidecar up first so it’s already streaming before Caddy restarts on the new
logging config.Replace
<your-caddy-service-name> with the Compose service key for Caddy. That’s the name
under services:, which can differ from container_name. If you skipped step 1 because
access logs are already enabled, you only need to start Vector here.Send a test request
curl from inside the compose network.Verify in Rootprint
Open Search, pick
otel-logs-v0_9 from the index selector, and query service.name:caddy.
Records typically appear within 5–10 seconds. attributes.container.name reads whatever
Docker reports for your Caddy container. If you set container_name: caddy, it reads
caddy. attributes.container.image.name reads whichever Caddy image you’re running.What the parsing does
Tworemap transforms run in sequence. The first, parse_caddy, turns each raw JSON line into structured fields and assigns severity. The second, to_otlp, packs those fields into the OTLP wire format that Rootprint’s ingest endpoint expects.
parse_caddy:
- JSON parse —
parse_json(.message)returns Caddy’s structured object;merge!lifts every key (ts,level,logger,msg,request,status,size, …) onto the event. If parsing fails (transitional config, non-JSON output), the line falls through with the raw.messageintact rather than being dropped. - Severity — Caddy’s
levelfield maps directly to OTel severity:debug→ 5/DEBUG,info→ 9/INFO,warn→ 13/WARN,error→ 17/ERROR,panic/fatal→ 21/FATAL. No status-code override; a 5xx access-log entry stays atseverity_text: INFObecause Caddy logs every access at info level. Filter onattributes.http.response.status_codefor triage.
to_otlp assembles the OTLP envelope. service.name is hard-coded to caddy (resource attribute). The if exists(.request) branch is what makes one VRL block handle both access and error logs cleanly: error-log entries (no .request block) get only log.file.path and caddy.logger, while access-log entries get the full HTTP semantic-convention attribute set.
The Docker setup additionally attaches Vector’s docker_logs metadata as container.id, container.name, container.image.name, container.runtime: "docker", and log.iostream (stdout/stderr) — log.file.path is omitted because the source is the Docker daemon, not a file on disk.
| OTLP key | Source field |
|---|---|
http.request.method | .request.method |
url.path | .request.uri |
server.address | .request.host |
client.address | .request.remote_ip |
client.port | .request.remote_port (parsed to int) |
network.protocol.name | first half of .request.proto split on / |
network.protocol.version | second half of .request.proto |
user_agent.original | first element of .request.headers."User-Agent" |
http.response.status_code | .status |
http.response.body.size | .size |
caddy.logger | .logger (e.g., http.log.access, http.log.error) |
log.file.path | Vector’s .file source attribute |
container.id, container.name, container.image.name, container.runtime, log.iostream | Vector’s docker_logs source (Docker setup only) |
timeUnixNano is derived from Caddy’s .ts (float seconds since epoch); if the field is missing or unparseable, Vector’s read time is used as a fallback. The final . = { resourceLogs: [...] } reassignment replaces the event entirely, so the OTLP sink sees only the envelope.
Useful searches
Run these in the Rootprint search box againstotel-logs-v0_9.
Every 5xx response Caddy returned, across all sites:
<your-caddy-container-name> with the actual name from
docker ps --format '{{.Names}}':
Troubleshooting
permission deniedreading/var/log/caddy/access.log— Vector’svectoruser is not in thecaddygroup. Runsudo usermod -aG caddy vectorand restart Vector. If that’s not available,sudo chmod a+r /var/log/caddy/*.logworks but loosens permissions for every user on the host.- Records arrive but
attributes.http.*are missing — the entry came fromerror.log(no.requestblock), by design. Filter access-log entries withattributes.caddy.logger:http.log.access. bodylooks like raw JSON instead of just the message — the access log is using a non-defaultformatdirective in the Caddyfile. The parser expects Caddy’s default JSON encoder. Either remove theformatdirective or extend theparse_caddyremap to handle the alternate shape.severity_textis alwaysINFOeven for 5xx responses — by design. Caddy emits every HTTP access atlevel: inforegardless of status code; this parser trusts the source. Filter onattributes.http.response.status_code:>=500to surface server errors.401,403,415from Rootprint — same response codes as a misconfigured Vector setup of any kind. See Send logs with Vector for full diagnoses.- Docker setup ships zero logs —
include_containersdoes not match the Caddy container’s actual name. Rundocker ps --format '{{.Names}}'to see the real names. Either setcontainer_name: caddyon your Caddy compose service or updateinclude_containersinvector.yaml. - Vector container exits with
permission deniedon/var/run/docker.sock— rootless Docker or SELinux. Either run Docker rootful, add:zto the socket mount on SELinux hosts, or grant the Vector image’s user access to the docker group inside the image. host.namealways reads the Vector container ID — by defaultget_hostname!()returns the container’s hostname, which Docker sets to the container ID. Sethostname: <your-host>on therootprint-vectorcompose service to override.
Related
- Send logs with Vector — generic Vector setup for tailing files on a host.
- OTLP reference — endpoint URL, response codes, body limits.
- Indexes — the
otel-logs-v0_9schema, so you know what you can search.
