Sign inSign up

baraverkstad/journald-plus

By baraverkstad

Updated 3 months ago

A Docker journald log driver plugin that adds multiline message merging and log priority parsing.

Plugin
Monitoring & observability
0

1.1K

baraverkstad/journald-plus repository overview

Docker Journald Plus

A Docker journald log driver plugin that adds multiline message merging and log priority parsing.

Implemented as a Docker managed plugin (v2), installed via docker plugin install. Available on Docker Hub.

Read more: https://github.com/baraverkstad/docker-journald-plus

Features

  • Multiline merging — consecutive log lines are merged into single journal entries based on configurable patterns
  • Priority detection — log priority is inferred from message content using sd-daemon <N> prefixes and configurable regex patterns
  • JSON log parsing — optional structured log parsing to extract level, message, and custom fields from JSON-formatted logs
  • All built-in journald fields — writes the same container metadata fields as the built-in driver (CONTAINER_ID, CONTAINER_NAME, IMAGE_NAME, etc.)
  • Minimal footprint — plugin image is 7.4MB on disk (1.7MB compressed)
journald-plus vs built-in journald driver
FeatureBuilt-in journaldjournald-plus
Metadata fieldsYesYes
Tag default{{.ID}}{{.Name}}
Multiline mergingNoYes
Priority detectionNoYes
JSON log parsingNoYes
docker logsYes (via journald)Yes (via local)1

1: Docker automatically enables a limited dual logging for plugins without API support for reading logs. It has been omitted in journald-plus to achieve a minimal footprint. Use journalctl to access processed log output.

Output

Each log entry is written to journald with the following fields:

FieldDescription
MESSAGELog message (after merge and prefix stripping)
PRIORITYNumeric syslog priority (0-7)
SYSLOG_IDENTIFIERThe tag value
SYSLOG_TIMESTAMPRFC 3339 timestamp from Docker
CONTAINER_IDShort (12-char) container ID
CONTAINER_ID_FULLFull container ID
CONTAINER_NAMEContainer name
CONTAINER_TAGFormatted tag
IMAGE_NAMEContainer image name

Plus any fields from:

Usage

docker run --name myapp \
  --log-driver baraverkstad/journald-plus:[VERSION] \
  --log-opt json-parse=true \
  myimage

Or in docker-compose.yml:

services:
  app:
    image: myapp:latest
    logging:
      driver: baraverkstad/journald-plus:[VERSION]
      options:
        json-parse: "true"

Or set as default in /etc/docker/daemon.json:

{
  "log-driver": "baraverkstad/journald-plus:[VERSION]",
  "log-opts": {
    "json-parse": "true"
  }
}

👉 See Configuration for all options.

Reading logs

docker logs reads from a local cache and shows raw output (no merging or parsing). For full processed entries, use journalctl:

journalctl -t myapp -f                # follow (like tail -f)
journalctl -t myapp -p warning        # warnings and above
journalctl -t myapp --since -1h       # last hour
journalctl CONTAINER_ID=abc123def456  # filter by container ID
journalctl REQUEST_ID=abc123          # custom field from JSON or field-*
Examples
JSON log parsing

Basic usage with default keys:

docker run --log-driver baraverkstad/journald-plus:[VERSION] \
  --log-opt json-parse=true \
  myapp

Your application logs JSON:

{
  "level": "error",
  "message": "database connection failed",
  "request_id": "abc123",
  "retry_count": 3
}

Results in:

  • MESSAGE=database connection failed
  • PRIORITY=3 (ERR)
  • JSON_REQUEST_ID=abc123
  • JSON_RETRY_COUNT=3

Custom key names for non-standard formats (e.g. OpenTelemetry):

--log-opt json-level-keys='severity,lvl' \
--log-opt json-message-keys='body,text'

Skip high-cardinality fields and keep remaining fields inline in the message:

--log-opt json-skip-keys='ts,time,@timestamp' \
--log-opt json-extra=inline
Field extraction

Extract journal fields from log messages using regex capture groups:

docker run --log-driver baraverkstad/journald-plus:[VERSION] \
  --log-opt field-REQUEST_ID='request_id=([a-z0-9]+)' \
  --log-opt field-USER_ID='user=(\d+)' \
  --log-opt field-TRACE_ID='trace[:\s]+([a-f0-9]{32})' \
  myapp

Per-service in docker-compose.yml:

services:
  api:
    image: myapi:latest
    logging:
      driver: baraverkstad/journald-plus:[VERSION]
      options:
        field-REQUEST_ID: "request_id=([a-z0-9]+)"
        field-USER_ID: 'user=(\d+)'
  worker:
    image: myworker:latest
    logging:
      driver: baraverkstad/journald-plus:[VERSION]
      options:
        field-JOB_ID: "job=([0-9]+)"

In /etc/docker/daemon.json (backslashes must be escaped):

{
  "log-driver": "baraverkstad/journald-plus:[VERSION]",
  "log-opts": {
    "field-REQUEST_ID": "request_id=([a-z0-9]+)",
    "field-USER_ID": "user=(\\d+)"
  }
}

Installation

ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
docker plugin install --alias baraverkstad/journald-plus:[VERSION] \
  baraverkstad/journald-plus:[VERSION]-$ARCH

The alias (baraverkstad/journald-plus:[VERSION]) is what you reference in daemon.json, compose.yml, or --log-driver. Never reference the arch-specific tag directly in config -- only the alias is portable across machines.

👉 No latest tag is published. Use latest-amd64 or latest-arm64 if you want the latest build without pinning a version. But be aware that doing so makes upgrading difficult.

Upgrading

A plugin in use by running containers cannot be disabled or upgraded in-place. The versioned alias pattern lets two plugin versions coexist for zero-downtime migration:

  1. Install the new version under a new alias:
    ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
    docker plugin install --alias baraverkstad/journald-plus:[NEW-VERSION] \
      baraverkstad/journald-plus:[NEW-VERSION]-$ARCH
    
  2. Update daemon.json or compose.yml to reference the new alias.
  3. If using daemon.json, restart dockerd to pick up the new default driver.
  4. Recreate services to switch them to the new alias one at a time.
  5. Once no containers reference the old alias, remove it:
    docker plugin disable baraverkstad/journald-plus:[OLD-VERSION]
    docker plugin rm baraverkstad/journald-plus:[OLD-VERSION]
    

Configuration

OptionDefaultSection
tag{{.Name}}Inherited options
labels(none)Inherited options
labels-regex(none)Inherited options
env(none)Inherited options
env-regex(none)Inherited options
field-FIELDNAME(none)Field extraction
multiline-regex^\sMultiline
multiline-timeout10msMultiline
multiline-max-lines100Multiline
multiline-max-bytes1048576Multiline
multiline-separator\nMultiline
priority-prefixtruePriority
priority-default-stdoutinfoPriority
priority-default-stderrerrPriority
priority-match-emerg(none)Priority
priority-match-alert(none)Priority
priority-match-crit(regex)Priority
priority-match-err(regex)Priority
priority-match-warning(regex)Priority
priority-match-notice(none)Priority
priority-match-info(none)Priority
priority-match-debug(regex)Priority
strip-timestampfalseTimestamp stripping
strip-timestamp-regex(regex)Timestamp stripping
strip-priorityfalsePriority stripping
strip-priority-regex(regex)Priority stripping
normalize-whitespacefalseWhitespace
json-parsefalseJSON log parsing
json-level-keyslevel,severity,log_levelJSON log parsing
json-message-keysmessage,msg,logJSON log parsing
json-skip-keys(none)JSON log parsing
json-extrafieldsJSON log parsing
Inherited options
OptionDefaultDescription
tag{{.Name}}Template for SYSLOG_IDENTIFIER; supports Go templates.
labels(none)Container label keys to include as journal fields.
labels-regex(none)Container label keys to include (regex).
env(none)Container env var keys to include as journal fields.
env-regex(none)Container env var keys to include (regex).

Tag template variables:

VariableDescriptionExample
{{.Name}}Container namemycontainer
{{.ID}}Short container ID (12 chars)abcdef123456
{{.FullID}}Full container IDabcdef123456...
{{.ImageName}}Image namenginx:latest
{{.ImageID}}Short image ID (12 chars)deadbeef1234
{{.ImageFullID}}Full image IDsha256:deadbeef...
{{.Command}}Entrypoint + argsnginx -g daemon off
{{.DaemonName}}Docker daemon namedocker

Example: --log-opt tag="{{.ImageName}}/{{.Name}}"

👉 The built-in journald driver defaults tag to {{.ID}} (short container ID). This plugin defaults to {{.Name}} (container name), which is more useful with journalctl -t.

Field name sanitization:

Label and environment variable names are converted to journald-compatible field names: lowercase letters become uppercase, non-alphanumeric characters become _, and names starting with a digit are prefixed with _. For example, a label com.example.my-app becomes the journal field COM_EXAMPLE_MY_APP.

Field extraction
OptionDescription
field-FIELDNAMEExtract a journal field via regex capture group.1

1: The option name is the field name (e.g. field-REQUEST_ID); the value is a regex with one capture group (...). Multiple field-* options can be used.

👉 See Examples for usage.

Multiline
OptionDefaultDescription
multiline-regex^\sRegex matching continuation lines.1
multiline-timeout10msMax wait before flush (Go duration).2
multiline-max-lines100Max lines to merge into one entry.
multiline-max-bytes1048576Max bytes for a merged entry (1 MB).
multiline-separator\nString inserted between merged lines.

1: Lines matching this pattern are appended to the previous message. Set to empty string to disable multiline merging.
2: Accepts Go duration format: 10ms, 100ms, 1s.

The default ^\s pattern matches any line beginning with whitespace, which handles Java stack traces (at com.example.Foo continuation lines are indented) and Python tracebacks (frame lines start with spaces) out of the box.

Priority
OptionDefaultDescription
priority-prefixtruesd-daemon <N> prefix (0-7).1
priority-default-stdoutinfoDefault priority for stdout.
priority-default-stderrerrDefault priority for stderr.
priority-match-emerg(none)Match first line -> EMERG (0).
priority-match-alert(none)Match first line -> ALERT (1).
priority-match-crit_(regex)2Match first line -> CRIT (2).
priority-match-err_(regex)2Match first line -> ERR (3).
priority-match-warning_(regex)2Match first line -> WARNING (4).
priority-match-notice(none)Match first line -> NOTICE (5).
priority-match-info(none)Match first line -> INFO (6).
priority-match-debug_(regex)2Match first line -> DEBUG (7).

1: The <N> prefix is stripped from MESSAGE before writing to journal. See sd-daemon(3).
2: Default regex patterns (all allow up to 30 chars of prefix before keyword):

  • priority-match-crit: ^.{0,30}(CRITICAL|\[Critical\])
  • priority-match-err: ^.{0,30}(ERROR|FATAL|\[ERROR\]|\[Fatal\])
  • priority-match-warning: ^.{0,30}(WARN|WARNING|\[Warning\])
  • priority-match-debug: ^.{0,30}(DEBUG|\[Debug\])

Priority is resolved in this order (first match wins):

  1. <N> sd-daemon prefix (if priority-prefix=true)
  2. priority-match-* regex patterns (checked from emerg to debug)
  3. Default: priority-default-stdout or priority-default-stderr
Priority names

The priority-default-stdout and priority-default-stderr options accept these values: emerg, alert, crit, err, warning, notice, info, debug.

Timestamp stripping
OptionDefaultDescription
strip-timestampfalseStrip leading timestamps.1
strip-timestamp-regex(regex)Override built-in regex.2

1: Journald records its own timestamps; application-level ones are redundant.
2: Only used when strip-timestamp=true.

When enabled, timestamps are stripped before priority detection. The default priority patterns allow up to 30 characters prefix, which handles cases where timestamp stripping leaves behind other prefixes. For example, MariaDB logs like 2026-02-15 15:15:16 0 [Warning] InnoDB:... become 0 [Warning] InnoDB:... after timestamp stripping, and the [Warning] pattern will still match.

Built-in patterns recognize these formats:

FormatExample
ISO 86012024-01-15T10:30:45Z, 2024-01-15 10:30:45 UTC
Go log2024/01/15 10:30:45
SyslogJan 15 10:30:45
Apache/nginx CLF15/Oct/2024:10:30:45 +0200
Log4j DATE14 Nov 2017 20:30:20,434
Apache errorWed Oct 15 19:41:46.123456 2019

Trailing separators (whitespace, -, |, :) after the timestamp are also stripped. Timezone abbreviations are limited to Z/UTC/GMT to avoid accidentally matching log level words like ERROR or WARN.

Priority stripping
OptionDefaultDescription
strip-priorityfalseStrip leading log level.1
strip-priority-regex(regex)Override built-in regex.2

1: Having log level in both the PRIORITY field and MESSAGE is redundant.
2: Only used when strip-priority=true.

When enabled, the leading log level keyword is stripped after priority detection. The default regex matches common level keywords at the start of the message, optionally bracketed:

(?i)^\[?(trace|debug|info|notice|note|warning|warn|critical|error|fatal|alert|emerg)\]?

Trailing separators (whitespace, -, |, :) after the match are also stripped. Because the regex is anchored to ^, only the first line of a multiline-merged message is affected; continuation lines are never touched.

InputAfter stripping
INFO request completedrequest completed
[Error] connection refusedconnection refused
WARN: disk space lowdisk space low
Whitespace
OptionDefaultDescription
normalize-whitespacefalseNormalize tabs and repeated spaces.1

1: Any sequence of one or more tabs (or two or more consecutive spaces) is replaced with a single space. Applied last in the pipeline, after timestamp stripping and priority detection/stripping.

InputAfter normalization
INFO\t\tStarting serverINFO Starting server
ERROR connection refusedERROR connection refused
[WARN]\t disk space low[WARN] disk space low
JSON log parsing
OptionDefaultDescription
json-parsefalseParse log lines as JSON.
json-level-keyslevel,severity,log_levelJSON keys for log level (first match).
json-message-keysmessage,msg,logJSON keys for message body (first match).
json-skip-keys(none)JSON keys to ignore entirely.
json-extrafieldsHow to handle remaining fields.1

1: fields (default): stored as JSON_* journal fields; inline: appended to MESSAGE.

When json-parse=true, the driver attempts to parse each log line as a JSON object:

  1. Level extraction — Checks json-level-keys (in order) and maps the value to a syslog priority
  2. Message extraction — Checks json-message-keys (in order) and uses the value as MESSAGE
  3. Skip keys — Keys in json-skip-keys are discarded
  4. Remaining fields — Stored as JSON_* journal fields (json-extra=fields) or appended to the message as JSON (json-extra=inline); ignored if empty
  5. Graceful fallback — If parsing fails or no message key is found, the original line is used

Supported level mappings:

JSON LevelSyslog Priority
debug, traceDEBUG (7)
info, informationINFO (6)
noticeNOTICE (5)
warn, warningWARNING (4)
error, errERR (3)
fatal, critical, critCRIT (2)
panic, alertALERT (1)
emerg, emergencyEMERG (0)

Level strings are case-insensitive.

👉 See Examples for usage.

Behavior:

  • Field names are sanitized for journald compatibility (uppercase, special chars replaced with _)
  • Nested JSON objects/arrays are serialized as JSON strings
  • Null values are omitted
  • If JSON parsing fails, the original line is logged as-is (no data loss)
  • Zero overhead when disabled (single boolean check)

Contributing

Ideas, bug reports, and pull requests are welcome. See DEVELOPMENT.md for build instructions, testing, and development workflow.

License

Docker Journald Plus is licensed under the MIT License.

Copyright (c) 2026 Per Cederberg

Tag summary

Content type

Plugin

Digest

sha256:246c4d7d3

Size

3.2 MB

Last updated

6 months ago

docker plugin install baraverkstad/journald-plus