container operator, derives container.id from the log file path with a transform processor, and ships records over OTLP HTTP to Logwiz. No changes to the apps themselves.
Setup
Create the collector config
Save this as
otel-collector-config.yaml next to your docker-compose.yml. Replace <your-logwiz> and <your-ingest-token> with your values.The
contrib distribution is required — the container operator that parses Docker’s per-line JSON log format ships only in otel/opentelemetry-collector-contrib, not the core image.The
container operator itself doesn’t read Docker’s config.v2.json, so it never emits container.name — only log.iostream and the parsed timestamp. transform/enrich works around this by pulling the 64-char container ID out of the log file path and using it as service.name when the app hasn’t set one itself. Containers already exporting service.name via an OTel SDK keep theirs (the where service.name == nil guard).Add the collector to your compose file
Drop this service alongside your existing ones. No changes to any other service needed — the collector reads every container’s logs from the host.
If your Logwiz runs on the same host (not a remote endpoint), point
logs_endpoint at http://host.docker.internal:<port>/api/otlp/v1/logs and add extra_hosts: ["host.docker.internal:host-gateway"] to this service. Required on Linux; Docker Desktop resolves it automatically.Send a test log line
Run a throwaway container that prints one line and exits. The collector picks up the line from the host log file and ships it with
service.name set to the container’s ID.Verify in Logwiz
Open Search and query for
hello from logwiz. The record typically arrives within ~5 seconds (one batch interval). For apps that set service.name themselves via the OTel SDK, you’ll see the SDK-provided name; for bare containers (like the smoke test above), you’ll see the container ID.Why the self-exclusion filter?
Without it, every line the collector emits would be tailed from its own log file on the host and re-shipped — creating an amplification loop. Since thecontainer operator doesn’t provide container.name, the filter matches on container_id instead: ${env:HOSTNAME} is expanded at collector startup to the short (12-char) container ID that Docker assigns as the container’s hostname by default, and that prefixes the full 64-char ID of the collector’s own log file. If you set an explicit hostname: in compose, the match breaks — either leave it unset or update the filter to whatever you chose.
Troubleshooting
- No logs arriving — confirm the collector is running (
docker logs otel-collector). Check that your app containers write to stdout/stderr, not to files that bypass Docker’s log driver. service.nameshows asunknown_service—transform/enrichis missing from the pipeline, orinclude_file_path: trueis missing on thefilelogreceiver (that’s what populateslog.file.path). Confirm both. For a meaningful service name, instrument your app with an OTel SDK that setsservice.nameitself —transform/enrichonly provides the container ID as a fallback.failed to detect a valid log patherrors in the collector log — thecontaineroperator’s k8s-metadata step runs a regex that only matches pod log paths (/var/log/pods/...) and fails loudly on Docker paths. Noisy but non-fatal: entries are still forwarded. Theon_error: send_quietin the config above suppresses it.connection refusedtolocalhost:<port>— inside the collector container,localhostis the container itself, not the Docker host. See thehost.docker.internalNote in the compose step above.- Collector logs flooding back — the self-exclusion isn’t matching. Confirm you haven’t set a custom
hostname:on the collector service in compose;${env:HOSTNAME}needs to equal the short container ID for the match to work. permission deniedon/var/lib/docker/containers— theotel/opentelemetry-collector-contribimage runs asotel(UID 10001) by default, but the Docker log directory is root-readable only. Theuser: "0:0"line in the compose snippet above addresses this. On rootless Docker or SELinux hosts, also addgroup_add: [docker]or append:zto the mount.- 413 from Logwiz — individual batches exceed 10 MB (rare). Lower
batch.send_batch_sizein the processor config.
Related
- OTLP reference — endpoint semantics and response codes
- Indexes —
otel-logs-v0_9schema,resource_attributesfield
