Skip to main content

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.

Run a Vector sidecar next to your other containers. It reads each container’s logs from the Docker daemon socket, picks up container.id, container.name, container.image.name, and container.image.id automatically, infers a severity level from the message, packs the result into an OTLP record, and ships to Rootprint over HTTPS.
Mounting /var/run/docker.sock into a container grants it the ability to enumerate and inspect every container on the host. This is standard for log collectors that use the Docker API, but worth being explicit about. The mount below is read-only (:ro).

Setup

1

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.
2

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 you copied in step 1.
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
3

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.
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.
4

Start the Vector service

docker compose up -d rootprint-vector
5

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.
docker run --rm --name rootprint-smoke-test alpine echo "hello from rootprint"
6

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.

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 retryseverityText: 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.

Attributes

service.name is derived from the container’s name (with the leading / Docker prefixes stripped), so a query for service:my-api filters Rootprint to one container’s events. host.name comes from the Vector container’s hostname. Per-event attributes:
OTLP keyValue
container.idfull 64-char ID
container.namename without leading /
container.image.nameimage reference (e.g., nginx:latest)
container.image.idimage digest
container.runtimeliteral docker
log.iostreamstdout or stderr
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, add one line to the to_otlp remap before the . = { ... } assignment:
if exists(.label."com.example.team") {
  attrs = push(attrs, { "key": "team", "value": { "stringValue": string!(.label."com.example.team") } })
}

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.

Troubleshooting

  • permission denied opening /var/run/docker.sock — rootless Docker uses $XDG_RUNTIME_DIR/docker.sock (typically /run/user/<uid>/docker.sock). Adjust the volume mount on the rootprint-vector service accordingly.
  • Vector’s own logs flooding backexclude_containers in vector.yaml does not match the compose service’s container_name. Both must be the same string. If you renamed the service in your compose file, update exclude_containers to match.
  • service.name shows as unknown_service — the Vector compose service was started without container_name set, so Docker assigned a generated name like <project>_rootprint-vector_1. Pin container_name on every service whose service.name you want to read cleanly.
  • 401, 403, 415 from Rootprint — same response codes as a misconfigured Vector setup of any kind. See Send logs with Vector for full diagnoses.