> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rootprint.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Send logs from Docker

> Ship every container on a Linux host to Rootprint with Vector (Docker socket, default) or the OpenTelemetry Collector (filelog), emitting OTLP.

This page ships container logs from a Linux host to Rootprint over OTLP. **Vector** reads the Docker daemon socket: it auto-discovers containers and attaches `container.name`, `container.image.name`, and friends with no extra wiring.

<Note>
  This page collects **every** container's stdout — Vector via the Docker daemon socket, the
  Collector via on-disk container logs. To run an agent **as a container** that tails specific
  files you bind-mount in, see the agent pages: [Vector](/send-logs/log-agents/vector),
  [OpenTelemetry Collector](/send-logs/log-agents/otel-collector),
  [Fluent Bit](/send-logs/log-agents/fluent-bit).
</Note>

## Prerequisites

* A running Rootprint instance and its base URL — you'll substitute it for `<your-rootprint>`.
* A Linux host with Docker (and Docker Compose for the examples below).
* A log agent: [Vector](https://vector.dev/docs/setup/installation/) or the [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/installation/).
* An **ingest API key** scoped to your target index. See [API keys](/api/overview).

<Tabs>
  <Tab title="Vector">
    <Warning>
      Mounting `/var/run/docker.sock` into a container lets it enumerate and inspect
      every container on the host. This is standard for log collectors that use the Docker API. The
      mount below is read-only (`:ro`).
    </Warning>

    <Steps>
      <Step title="Create the Vector config">
        Save this as `vector.yaml` next to your `docker-compose.yml`. Replace `<your-rootprint>` with
        your Rootprint base URL and `<your-ingest-token>` with the API key.

        <Expandable title="vector.yaml">
          ```yaml theme={"theme":"github-light"}
          sources:
            docker:
              type: docker_logs
              exclude_containers:
                - rootprint-vector

          transforms:
            enrich:
              type: remap
              inputs: [docker]
              source: |
                .message = to_string(.message) ?? ""
                lower = downcase(.message)
                if match(lower, r'\berror\b|\bfatal\b|\bpanic\b|\bexception\b') {
                  .severity_number = 17
                  .severity_text   = "ERROR"
                } else if match(lower, r'\bwarn(ing)?\b|\bdeprecated\b|\bretry\b') {
                  .severity_number = 13
                  .severity_text   = "WARN"
                } else {
                  .severity_number = 9
                  .severity_text   = "INFO"
                }

            to_otlp:
              type: remap
              inputs: [enrich]
              source: |
                msg = string!(.message)
                ts_nano = to_unix_timestamp(now(), unit: "nanoseconds")
                if exists(.timestamp) && is_timestamp(.timestamp) {
                  ts_nano = to_unix_timestamp!(.timestamp, unit: "nanoseconds")
                }
                sev_num  = to_int(.severity_number) ?? 9
                sev_text = string(.severity_text)   ?? "INFO"

                cname = string(.container_name) ?? ""
                cname = replace(cname, r'^/', "")
                svc_name = "unknown_service"
                if cname != "" { svc_name = cname }

                attrs = [
                  { "key": "container.runtime", "value": { "stringValue": "docker" } }
                ]
                if exists(.container_id) { attrs = push(attrs, { "key": "container.id",         "value": { "stringValue": string!(.container_id) } }) }
                if cname != ""           { attrs = push(attrs, { "key": "container.name",       "value": { "stringValue": cname } }) }
                if exists(.image)        { attrs = push(attrs, { "key": "container.image.name", "value": { "stringValue": string!(.image) } }) }
                if exists(.image_id)     { attrs = push(attrs, { "key": "container.image.id",   "value": { "stringValue": string!(.image_id) } }) }
                if exists(.stream)       { attrs = push(attrs, { "key": "log.iostream",         "value": { "stringValue": string!(.stream) } }) }

                . = {
                  "resourceLogs": [{
                    "resource": {
                      "attributes": [
                        { "key": "service.name", "value": { "stringValue": svc_name } },
                        { "key": "host.name",    "value": { "stringValue": get_hostname!() } }
                      ]
                    },
                    "scopeLogs": [{
                      "scope": { "name": "vector", "version": "" },
                      "logRecords": [{
                        "timeUnixNano":         ts_nano,
                        "observedTimeUnixNano": to_unix_timestamp(now(), unit: "nanoseconds"),
                        "severityNumber":       sev_num,
                        "severityText":         sev_text,
                        "body":                 { "stringValue": msg },
                        "attributes":           attrs,
                        "traceId":              "",
                        "spanId":               "",
                        "flags":                0,
                        "droppedAttributesCount": 0
                      }]
                    }]
                  }]
                }

          sinks:
            rootprint:
              type: opentelemetry
              inputs: [to_otlp]
              protocol:
                type: http
                uri: https://<your-rootprint>/v1/logs
                method: post
                encoding:
                  codec: otlp
                compression: gzip
                request:
                  headers:
                    Authorization: "Bearer <your-ingest-token>"
                batch:
                  timeout_secs: 1
                  max_bytes: 8388608
          ```
        </Expandable>
      </Step>

      <Step title="Add the Vector service to your compose file">
        Drop this service alongside your existing ones. The Vector container reads every other
        container's logs through the Docker socket. No changes needed to your application services.

        ```yaml theme={"theme":"github-light"}
        services:
          rootprint-vector:
            image: timberio/vector:0.54.0-alpine
            container_name: rootprint-vector
            restart: unless-stopped
            volumes:
              - ./vector.yaml:/etc/vector/vector.yaml:ro
              - /var/run/docker.sock:/var/run/docker.sock:ro
        ```

        The `container_name` and the `exclude_containers` value in `vector.yaml` must match. That's
        how Vector skips its own logs. Change one, change the other.
      </Step>

      <Step title="Start the Vector service">
        ```bash theme={"theme":"github-light"}
        docker compose up -d rootprint-vector
        ```
      </Step>

      <Step title="Send a test log line">
        Run a throwaway container that prints one line and exits. Vector picks it up from the daemon
        and ships it with `service.name` set to the container's name.

        ```bash theme={"theme":"github-light"}
        docker run --rm --name rootprint-smoke-test alpine echo "hello from rootprint"
        ```
      </Step>

      <Step title="Verify in Rootprint">
        Open Search, pick `otel-logs-v0_9` from the index selector, and query for
        `hello from rootprint`. The record typically arrives within 5 seconds (one batch interval).
        `service.name` reads `rootprint-smoke-test`; `attributes.container.image.name` reads
        `alpine`.
      </Step>
    </Steps>

    ### What the remap does

    Two `remap` transforms run in sequence. `enrich` infers a severity level from the message body. `to_otlp` packs the message and the Docker-supplied metadata into the OTLP wire format that Rootprint's ingest endpoint expects.

    #### Severity inference

    The message body is lowercased and matched against two pattern families:

    * `error`, `fatal`, `panic`, or `exception` (word-boundary) → `severityText: ERROR` (severity number 17).
    * `warn` / `warning`, `deprecated`, or `retry` → `severityText: WARN` (13).
    * Everything else → `severityText: INFO` (9).

    `DEBUG` is not inferred. `\bdebug\b` against arbitrary container output false-positives constantly. Apps that need debug-level visibility should emit it via an OpenTelemetry SDK that sets `severityNumber` itself; the OTLP record will carry that through unchanged because the SDK writes to the same endpoint.

    #### Apps that emit JSON

    If a container writes structured JSON to stdout, the body arrives as a string but its content is JSON. Add a `parse_json` step in `enrich` and assign parsed fields to attributes. The OTLP attribute list takes them as-is.
  </Tab>

  <Tab title="OpenTelemetry Collector">
    <Warning>
      The Collector reads container logs from `/var/lib/docker/containers` on disk. This requires the **`json-file`** logging driver (Docker's default) and does **not** auto-resolve friendly container names: records carry `container.id` and severity, but `service.name`/`container.name` need extra wiring the socket-based Vector path handles for free. For rich per-container metadata, use the Vector tab.
    </Warning>

    <Steps>
      <Step title="Confirm the json-file logging driver">
        ```bash theme={"theme":"github-light"}
        docker inspect -f '{{.HostConfig.LogConfig.Type}}' <a-running-container>
        ```

        Must print `json-file`. The `local` and `journald` drivers are not readable by `filelog`.
      </Step>

      <Step title="Add the Collector sidecar to your compose file">
        ```yaml theme={"theme":"github-light"}
        services:
          rootprint-otelcol:
            image: otel/opentelemetry-collector-contrib:latest
            container_name: rootprint-otelcol
            restart: unless-stopped
            command: ["--config=/etc/otelcol/config.yaml"]
            volumes:
              - ./otel-config.yaml:/etc/otelcol/config.yaml:ro
              - /var/lib/docker/containers:/var/lib/docker/containers:ro
        ```
      </Step>

      <Step title="Save otel-config.yaml">
        Replace `<your-rootprint>` and `<your-ingest-token>`.

        <Expandable title="otel-config.yaml">
          ```yaml theme={"theme":"github-light"}
          receivers:
            filelog:
              include:
                - /var/lib/docker/containers/*/*-json.log
              start_at: end
              operators:
                - type: container
                  add_metadata_from_filepath: true

          processors:
            transform:
              log_statements:
                - context: log
                  statements:
                    - set(severity_number, SEVERITY_NUMBER_ERROR) where IsString(body) and IsMatch(body, "(?i)\\b(error|fatal|panic|exception)\\b")
                    - set(severity_text, "ERROR") where severity_number == SEVERITY_NUMBER_ERROR
                    - set(severity_number, SEVERITY_NUMBER_WARN) where severity_number == 0 and IsString(body) and IsMatch(body, "(?i)\\b(warn|warning|deprecated|retry)\\b")
                    - set(severity_text, "WARN") where severity_number == SEVERITY_NUMBER_WARN
                    - set(severity_number, SEVERITY_NUMBER_INFO) where severity_number == 0
                    - set(severity_text, "INFO") where severity_text == ""
                    - set(attributes["container.runtime"], "docker")
            batch: {}

          exporters:
            otlphttp:
              logs_endpoint: https://<your-rootprint>/v1/logs
              compression: gzip
              headers:
                Authorization: "Bearer <your-ingest-token>"

          service:
            pipelines:
              logs:
                receivers: [filelog]
                processors: [transform, batch]
                exporters: [otlphttp]
          ```
        </Expandable>
      </Step>

      <Step title="Start the sidecar">
        ```bash theme={"theme":"github-light"}
        docker compose up -d rootprint-otelcol
        ```
      </Step>

      <Step title="Send a test log line">
        ```bash theme={"theme":"github-light"}
        docker run --rm --name rootprint-smoke-test alpine echo "hello from rootprint"
        ```
      </Step>

      <Step title="Verify in Rootprint">
        Query `hello from rootprint` in `otel-logs-v0_9`. The record carries `attributes.container.runtime: docker` and an inferred severity. `container.id` is present; friendly names are not (see the warning above).
      </Step>
    </Steps>
  </Tab>
</Tabs>

## What you get in Rootprint

`service.name` is derived from the container's name (with the leading `/` Docker prefixes stripped) when using Vector; it is not set automatically by the OTEL Collector path.

Per-event attributes by agent:

| OTLP key               | Vector                                 | OpenTelemetry Collector         |
| ---------------------- | -------------------------------------- | ------------------------------- |
| `container.id`         | full 64-char ID                        | full 64-char ID (from filepath) |
| `container.name`       | name without leading `/`               | not populated                   |
| `container.image.name` | image reference (e.g., `nginx:latest`) | not populated                   |
| `container.image.id`   | image digest                           | not populated                   |
| `container.runtime`    | literal `docker`                       | literal `docker`                |
| `log.iostream`         | `stdout` or `stderr`                   | not populated                   |
| severity               | inferred from message body             | inferred from message body      |

The Vector path populates all rows. The OTEL Collector path populates `container.id`, `container.runtime`, and severity only; it cannot resolve `container.name` or `container.image.name` from the json-file path without the Docker socket.

Container labels are not promoted by default. They're user-defined and unbounded, and adding all of them risks attribute-cardinality issues. To promote a specific label with Vector, add one line to the `to_otlp` remap before the `. = { ... }` assignment:

```vrl theme={"theme":"github-light"}
if exists(.label."com.example.team") {
  attrs = push(attrs, { "key": "team", "value": { "stringValue": string!(.label."com.example.team") } })
}
```

## Troubleshooting

* **`401` from Rootprint**: the `Authorization` header is missing or the token is wrong. Check the token value in your config.
* **`403` from Rootprint**: the API key exists but is not scoped to `otel-logs-v0_9`. Create a new key scoped to that index.
* **`413` from Rootprint**: the batch is too large. Lower `max_bytes` (Vector) or add `send_batch_max_size` to the `batch` processor (OTEL Collector).
* **`415` from Rootprint**: the `Content-Type` is not `application/x-protobuf`. Both agents set this automatically; if you see a 415, check for a proxy stripping headers.

## Related

* [Send logs with Vector](/send-logs/log-agents/vector): generic Vector setup for tailing files on a host.
* [OTLP reference](/send-logs/otlp): endpoint URL, response codes, body limits.
* [Indexes](/indexes): the `otel-logs-v0_9` schema.
