Skip to main content
RunBook Academy

LinuxXLVI · OpenTelemetryOTel collectors

OTel logs on a Linux host - the journald and filelog receivers

Advanced⏱ ~14 minotel-collectorjournalctlsystemctl

What you'll learn

  • Ingest the systemd journal into a Collector pipeline with the journald receiver
  • Tail application log files with the filelog receiver, including multiline records
  • Parse timestamps, severity and structured fields into OTLP log attributes
  • Checkpoint file offsets so a Collector restart neither duplicates nor loses lines

Prerequisites

Verified against Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL 9.x · Rocky Linux 9.x · AlmaLinux 9.x · Linux kernel 6.1 LTS / 6.6 LTS · systemd 255+ · OpenSSH 8.7p1 (RHEL 9) / 9.6p1 (Ubuntu 24.04) · nftables 1.0.x · chrony 4.x · Pacemaker 2.1.x · Corosync 3.1.x · 2026-08-11

Not yet marked complete on this device.

Metrics and traces reach the Collector because something was instrumented to send them. Logs mostly do not. On a Linux host the logs that matter are already being written by software that has never heard of OTLP: systemd services write to the journal, and applications write lines to files.

Two receivers close that gap, and both of them have failure modes that show up as missing or duplicated data rather than as an error.

Three ways logs enter the pipeline

SourceReceiverUse it for
An instrumented applicationotlpAnything already emitting OTLP logs, usually alongside its traces
The systemd journaljournaldEverything a systemd unit writes, plus kernel messages
A file on diskfilelogApplications that write their own log files and bypass syslog and the journal

The syslog receiver covers a fourth case - terminating an existing rsyslog forwarding estate - and is in OTel collectors and agents.

All four are in the contrib distribution. A core-only build fails at start-up with an unknown-type error, which is a packaging problem rather than a configuration one.

The journald receiver

receivers:
  journald:
    directory: /var/log/journal
    units:
      - nginx.service
      - myapp.service
    priority: info
    start_at: end

It reads by running journalctl as a subprocess, which has three consequences worth knowing before you deploy it.

  1. The journalctl binary must exist on PATH in whatever runs the Collector - a minimal container image usually does not have it
  2. The Collector process must be able to read the journal: root, or a user in the systemd-journal group
  3. It can only read what journald kept. On a host with a volatile journal there is nothing from before the current boot to read
Read-only / Safepreconditions for the journald receiver
$ journalctl --header | grep -i storage; journalctl --list-boots | tail -3; id -nG otelcol
Storage: persistent
-2 4f1c... Fri 2026-07-25 09:11:02 UTC—Sat 2026-08-09 02:59:41 UTC
-1 8c2d... Sat 2026-08-09 03:01:10 UTC—Tue 2026-08-11 03:59:02 UTC
0 a91b... Tue 2026-08-11 04:03:12 UTC—Tue 2026-08-11 09:20:44 UTC
otelcol systemd-journal

Illustrative output

The filelog receiver

extensions:
  file_storage/checkpoints:
    directory: /var/lib/otelcol/file_storage

receivers:
  filelog:
    include:
      - /var/log/myapp/*.log
    exclude:
      - /var/log/myapp/*.gz
    start_at: end
    storage: file_storage/checkpoints
    multiline:
      line_start_pattern: '^\d{4}-\d{2}-\d{2}T'
    operators:
      - type: regex_parser
        regex: '^(?P<ts>\S+) (?P<sev>[A-Z]+) (?P<msg>.*)$'
        timestamp:
          parse_from: attributes.ts
          layout: '%Y-%m-%dT%H:%M:%S.%fZ'
        severity:
          parse_from: attributes.sev

Four settings carry most of the weight:

  • start_at. end starts from the current end of file; beginning reads the whole file. On first deployment against a 4 GB log, beginning sends four gigabytes into your log store in one burst and will be rate-limited, dropped, or billed.
  • storage. Names a file_storage extension in which the receiver checkpoints its read offsets. Without it, offsets live only in memory.
  • multiline. Without a line_start_pattern, every line of a Java or Python stack trace becomes a separate log record, and the one line naming the exception is separated from the one naming the file.
  • operators. A pipeline that turns a line of text into fields. regex_parser and json_parser are the common entry points; both accept nested timestamp and severity blocks.

Give every record a host

A log record with no resource attributes is nearly useless in a central store: you can read the message but not say which host produced it. The resourcedetection processor fills that in without hard-coding anything per host.

processors:
  resourcedetection:
    detectors: [env, system]
    system:
      hostname_sources: ["os"]
  transform/trim:
    log_statements:
      - context: log
        statements:
          - delete_key(attributes, "ts")
          - delete_key(attributes, "sev")

The transform statements remove the raw fields once they have been parsed into the record proper. Leaving them in ships every timestamp twice - once in the OTLP record and once as a string attribute - which is pure cost.

Wiring it together

service:
  extensions: [file_storage/checkpoints]
  pipelines:
    logs:
      receivers: [otlp, journald, filelog]
      processors: [memory_limiter, resourcedetection, transform/trim, batch]
      exporters: [otlphttp/loki]

exporters:
  otlphttp/loki:
    logs_endpoint: http://loki.example.com:3100/otlp/v1/logs

memory_limiter must be the first processor in the list. It works by refusing data when the Collector is over its memory ceiling, and it can only protect the processors that come after it.

Verify before and after deployment

Service impact possiblevalidate, restart, confirm
$ otelcol validate --config /etc/otel-collector/agent.yaml; systemctl restart otelcol-contrib; systemctl is-active otelcol-contrib; journalctl -u otelcol-contrib -n 20 --no-pager
active
Aug 11 09:31:02 web02 otelcol-contrib[4411]: info service@v0.108.0/service.go:169 Starting otelcol-contrib...
Aug 11 09:31:02 web02 otelcol-contrib[4411]: info fileconsumer/file.go:257 Started watching file path=/var/log/myapp/app.log
Aug 11 09:31:03 web02 otelcol-contrib[4411]: info service@v0.108.0/service.go:225 Everything is ready.

Illustrative output

To see records rather than start-up messages, add the debug exporter temporarily. It prints what is flowing through the pipeline to the Collector’s own log.

exporters:
  debug:
    verbosity: detailed

Keep the local copy

Shipping logs off the host is not a reason to stop writing them on it. Set MaxLevelStore and the journal size limits so the local journal still holds a useful window, because the Collector is a single point of failure for telemetry and the incidents you care about most are the ones where the host lost its network.

Knowledge check

Knowledge check · 5 questions

  1. Q1. A filelog receiver is configured with start_at: end and no storage extension. The Collector is restarted by a package upgrade that takes 40 seconds. What happens to the log lines written during those 40 seconds?

  2. Q2. The journald receiver is configured correctly, but after a host crash the Collector ships nothing from the boot that crashed. What is the most likely cause?

  3. Q3. Which of these are genuine prerequisites for the journald receiver to work? Select all that apply.

  4. Q4. A logrotate configuration using copytruncate can cause a filelog receiver to miss records even though nothing reports an error.

  5. Q5. Why must memory_limiter be the first processor in a pipeline?

Passing score: 75%. Answers are checked in this browser.