What a Docker container line looks like
The key=value sample below is fed verbatim into the engine to produce every parser on this page.
time="2026-07-03T14:22:15.123456789Z" level=info msg="Container started" container=9f3c2d1a4b7e image="nginx:1.27" name=web-1
time="2026-07-03T14:22:19.884211003Z" level=error msg="Health check failed" container=ab112cd3f001 image="api:2.4.1" name=api-2 exitCode=1 Detected fields
The engine classified this sample as kv and consolidated 7 fields across 2 lines. Fields marked literal were identical on every sample line, so they are baked into the pattern as anchors rather than captured.
- time : timestamp
- level : severity
- msg : quoted_string
- container : literal
- image : quoted_string
- name : literal
- exitcode : number
Regex (named capture groups)
# sample: time="2026-07-03T14:22:15.123456789Z" level=info msg="Container started" container=9f3c2d1a4b7e image="nginx:1.27" name=web-1
# groups: time=2026-07-03T14:22:15.123456789Z, level=info, msg=Container started, container=9f3c2d1a4b7e, image=nginx:1.27, name=web-1
^time="(?<time>[^"]*)" level=(?<level>[A-Za-z]+) msg="(?<msg>[^"]*)" container=(?<container>(?:[A-Za-z]+\d+[A-Za-z]+\d+[A-Za-z]+\d+|\d+[A-Za-z]+\d+[A-Za-z]+\d+[A-Za-z]+\d+[A-Za-z]+\d+[A-Za-z]+\d+[A-Za-z]+)) image="(?<image>[^"]*)" name=(?<name>[A-Za-z]+-\d+)(?: exitCode=(?<exitcode>-?\d+(?:\.\d+)?))?$ Grok pattern (Logstash / Elastic)
# custom patterns
DOCKER_NOTDQUOTE [^"]*
time="%{TIMESTAMP_ISO8601:time}" level=%{LOGLEVEL:level} msg="%{DOCKER_NOTDQUOTE:msg}" container=%{NOTSPACE:container} image="%{DOCKER_NOTDQUOTE:image}" name=%{NOTSPACE:name}(?: exitCode=%{NUMBER:exitcode})? - note kv-structured input — consider the Logstash kv filter instead of (or after) grok
- note 1 optional field(s) wrapped in (?:…)? inline regex — grok has no native optional syntax
- note custom patterns emitted — save the '# custom patterns' block to a file in your patterns_dir
Wazuh decoder (OS_Regex XML)
<!--
Generated by LogForge - Wazuh decoder (OS_Regex dialect, not PCRE)
sample: time="2026-07-03T14:22:15.123456789Z" level=info msg="Container started" container=9f3c2d1a4b7e image="nginx:1.27" name=web-1
test with: /var/ossec/bin/wazuh-logtest
-->
<decoder name="docker-kv">
<prematch>^time="</prematch>
</decoder>
<decoder name="docker-kv">
<parent>docker-kv</parent>
<regex offset="after_parent">^(\d+-\d+-\d+T\d+:\d+:\d+.\d+Z)" level=(\w+)</regex>
<order>time, level</order>
</decoder>
<decoder name="docker-kv">
<parent>docker-kv</parent>
<regex offset="after_parent"> container=(\w+)</regex>
<order>container</order>
</decoder>
<decoder name="docker-kv">
<parent>docker-kv</parent>
<regex offset="after_parent"> name=(\w+)</regex>
<order>name</order>
</decoder>
<decoder name="docker-kv">
<parent>docker-kv</parent>
<regex offset="after_parent"> exitCode=(\d+)</regex>
<order>exitcode</order>
</decoder>
<!-- ============================================================
ALERT RULE (starter) — put this in a RULES file, e.g.
/var/ossec/etc/rules/local_rules.xml. Decoders and rules live
in SEPARATE files. The rule matches the decoder above through
<decoded_as>; set <level> and add <field>/<match> conditions so
it alerts only on the events you care about. Rule ids 100000+
are the user range — change them if they collide with yours.
============================================================ -->
<group name="docker,">
<rule id="100000" level="3">
<decoded_as>docker-kv</decoded_as>
<description>docker: time=$(time) level=$(level)</description>
</rule>
<!-- Example — a higher-level alert gated on one field (uncomment and edit):
<rule id="100001" level="10">
<if_sid>100000</if_sid>
<field name="time">^CHANGE_ME$</field>
<description>docker: a value you care about from $(time)</description>
</rule>
-->
</group>
- note field "msg" skipped: no safe OS_Regex pattern for its values (mixed shapes / mid-line free text)
- note field "image" skipped: no safe OS_Regex pattern for its values (mixed shapes / mid-line free text)
- note kv fields are extracted by same-named sibling decoders (offset="after_parent"), so per-line field order/absence is tolerated — the shared name is what makes Wazuh evaluate every sibling
- note added a starter alert <rule> (level 3, matched to the decoder via <decoded_as>) — put it in a RULES file (not the decoders file), set the level, and add <field>/<match> conditions; the commented example child rule shows the pattern
- note decoder order and prematch specificity may need site-specific tuning (other decoders in your ruleset can shadow these) — validate with /var/ossec/bin/wazuh-logtest
Wazuh's OS_Regex is not PCRE — a bare . is a literal dot and \. matches any character.
Test Wazuh OS_Regex patterns →
rsyslog template / liblognorm rulebase
version=2
# docker — liblognorm v2 rulebase (generated by LogForge)
# Usage with rsyslog (mmnormalize runs liblognorm):
# module(load="mmnormalize")
# action(type="mmnormalize" rulebase="/etc/rsyslog.d/docker.rb" useRawMsg="on")
# Literal "%" is escaped as "%%"; raw tabs are written as \x09.
rule=docker:time="%time:date-rfc5424%" level=%level:word% msg="%msg:char-to{"extradata":"\""}%" container=%container:word% image="%image:char-to{"extradata":"\""}%" name=%name:word% exitCode=%exitcode:number%
rule=docker:time="%time:date-rfc5424%" level=%level:word% msg="%msg:char-to{"extradata":"\""}%" container=%container:word% image="%image:char-to{"extradata":"\""}%" name=%name:word%
- note kv structure: rsyslog offers mmfields (fast, fixed single-char separator, untyped) and mmnormalize (this rulebase, typed fields + literal anchors); mmnormalize was chosen for typed extraction
- note chosen parser types: time=date-rfc5424, level=word, msg=char-to("), container=word, image=char-to("), name=word, exitcode=number
- note optional columns (exitcode): liblognorm has no optional parts within a single rule — emitted a second rule variant with only the always-present columns (max 2 variants; lines with other column combinations will not match and need extra rule= lines)
Splunk
# props.conf (search-time extraction)
[<REPLACE_WITH_SOURCETYPE>]
EXTRACT-logforge = time="(?<time>[^"]*)" level=(?<level>[A-Za-z]+) msg="(?<msg>[^"]*)" container=(?<container>(?:[A-Za-z]+\d+[A-Za-z]+\d+[A-Za-z]+\d+|\d+[A-Za-z]+\d+[A-Za-z]+\d+[A-Za-z]+\d+[A-Za-z]+\d+[A-Za-z]+\d+[A-Za-z]+)) image="(?<image>[^"]*)" name=(?<name>[A-Za-z]+-\d+)(?: exitCode=(?<exitcode>-?\d+(?:\.\d+)?))?
# Quick search-time test in SPL:
# | rex field=_raw "time=\"(?<time>[^\"]*)\" level=(?<level>[A-Za-z]+) msg=\"(?<msg>[^\"]*)\" container=(?<container>(?:[A-Za-z]+\\d+[A-Za-z]+\\d+[A-Za-z]+\\d+|\\d+[A-Za-z]+\\d+[A-Za-z]+\\d+[A-Za-z]+\\d+[A-Za-z]+\\d+[A-Za-z]+\\d+[A-Za-z]+)) image=\"(?<image>[^\"]*)\" name=(?<name>[A-Za-z]+-\\d+)(?: exitCode=(?<exitcode>-?\\d+(?:\\.\\d+)?))?" - note EXTRACT-<class> names must be unique within a sourcetype stanza — rename EXTRACT-logforge if you already use that class for this sourcetype
- note a timestamp field was detected: this EXTRACT only makes it a searchable field. To set the event _time at index time, add TIME_PREFIX and TIME_FORMAT to this props.conf stanza (TIME_FORMAT uses Splunk strptime, e.g. %Y-%m-%dT%H:%M:%S) — this generator does not guess the strptime format.
ES ingest
PUT _ingest/pipeline/docker
{
"description": "LogForge-generated ingest pipeline for docker",
"processors": [
{
"kv": {
"field": "message",
"field_split": " ",
"value_split": "="
}
}
]
} - note grok: kv-structured input — consider the Logstash kv filter instead of (or after) grok
- note grok: 1 optional field(s) wrapped in (?:…)? inline regex — grok has no native optional syntax
- note grok: custom patterns emitted — save the '# custom patterns' block to a file in your patterns_dir
- note kv structure: emitted a { kv: { field: "message", field_split: " ", value_split: "=" } } processor — it extracts key=value pairs natively; adjust field_split/value_split if your delimiter differs
- note a grok alternative is available too (the reused grok pattern is shown in the notes above); prefer the kv processor unless you need typed/anchored extraction
- note test in Kibana Dev Tools with: POST _ingest/pipeline/docker/_simulate (supply a docs[] array whose _source.message holds a sample line)
Graylog
# Grok patterns to add under System > Grok Patterns:
# (Graylog needs these custom patterns installed globally BEFORE the rule/extractor below will work.)
# DOCKER_NOTDQUOTE [^"]*
# --- Graylog processing pipeline rule (primary) ---
# Paste under System > Pipelines > Manage rules, then attach the rule to a pipeline stage.
rule "docker-parse"
when
has_field("message")
then
let gp = grok(pattern: "time=\"%{TIMESTAMP_ISO8601:time}\" level=%{LOGLEVEL:level} msg=\"%{DOCKER_NOTDQUOTE:msg}\" container=%{NOTSPACE:container} image=\"%{DOCKER_NOTDQUOTE:image}\" name=%{NOTSPACE:name}(?: exitCode=%{NUMBER:exitcode})?", value: to_string($message.message), only_named_captures: true);
set_fields(gp);
end
# --- Graylog import-ready extractor JSON (secondary) ---
# Save as a .json file and import under System > Inputs > (input) > Manage extractors > Actions > Import extractors.
{
"extractors": [
{
"title": "docker",
"extractor_type": "grok",
"converters": [],
"order": 0,
"cursor_strategy": "copy",
"source_field": "message",
"target_field": "",
"extractor_config": {
"grok_pattern": "time=\"%{TIMESTAMP_ISO8601:time}\" level=%{LOGLEVEL:level} msg=\"%{DOCKER_NOTDQUOTE:msg}\" container=%{NOTSPACE:container} image=\"%{DOCKER_NOTDQUOTE:image}\" name=%{NOTSPACE:name}(?: exitCode=%{NUMBER:exitcode})?",
"named_captures_only": true
},
"condition_type": "none",
"condition_value": ""
}
],
"version": "5.0.0"
} - note grok: kv-structured input — consider the Logstash kv filter instead of (or after) grok
- note grok: 1 optional field(s) wrapped in (?:…)? inline regex — grok has no native optional syntax
- note grok: custom patterns emitted — save the '# custom patterns' block to a file in your patterns_dir
- note 1 custom grok pattern(s) (DOCKER_NOTDQUOTE) must be installed globally first under System > Grok Patterns — see the block at the top of the output
- note primary artifact is the processing-pipeline rule; the extractor JSON is an equivalent import-ready alternative for the classic extractor UI
Datadog
logforge_rule time="%{date("yyyy-MM-dd'T'HH:mm:ss.SSSZ"):time}" level=%{word:level} msg="%{quotedString:msg}" container=%{notSpace:container} image="%{quotedString:image}" name=%{notSpace:name}(?: exitCode=%{number:exitcode})? - note emitted rule name is "logforge_rule"; rename it to match your "docker" convention if desired
- note kv input — consider Datadog's key-value/`keyvalue()` filter in the Grok Parser instead of anchoring each key by hand
- note 1 optional field(s) wrapped in (?:…)? — Datadog Grok has no native optional matcher; a chain of optional columns may need a Helper Rule per shape
- note paste this line into a Grok Parser processor in a Datadog Log Pipeline; matchers are anchored left-to-right and rule whitespace matches log whitespace. Complex or multi-shape logs may need Helper Rules.
Fluent Bit
[PARSER]
Name docker
Format regex
Regex ^time="(?<time>[^"]*)" level=(?<level>[A-Za-z]+) msg="(?<msg>[^"]*)" container=(?<container>(?:[A-Za-z]+\d+[A-Za-z]+\d+[A-Za-z]+\d+|\d+[A-Za-z]+\d+[A-Za-z]+\d+[A-Za-z]+\d+[A-Za-z]+\d+[A-Za-z]+\d+[A-Za-z]+)) image="(?<image>[^"]*)" name=(?<name>[A-Za-z]+-\d+)(?: exitCode=(?<exitcode>-?\d+(?:\.\d+)?))?$
Time_Key time
Time_Format %Y-%m-%dT%H:%M:%S.%LZ
# Fluentd <parse> block:
# <parse>
# @type regexp
# expression /^time="(?<time>[^"]*)" level=(?<level>[A-Za-z]+) msg="(?<msg>[^"]*)" container=(?<container>(?:[A-Za-z]+\d+[A-Za-z]+\d+[A-Za-z]+\d+|\d+[A-Za-z]+\d+[A-Za-z]+\d+[A-Za-z]+\d+[A-Za-z]+\d+[A-Za-z]+\d+[A-Za-z]+)) image="(?<image>[^"]*)" name=(?<name>[A-Za-z]+-\d+)(?: exitCode=(?<exitcode>-?\d+(?:\.\d+)?))?$/
# time_key time
# time_format %Y-%m-%dT%H:%M:%S.%LZ
# </parse>
- note Time_Key set to "time"; Time_Format "%Y-%m-%dT%H:%M:%S.%LZ" is a best-effort strptime derived from the sample shape — verify it against your data (Fluent Bit uses %L for fractional seconds and %z for numeric offsets)
Vector
[transforms.docker_parse]
type = "remap"
inputs = ["REPLACE_WITH_SOURCE"]
source = '''
. |= parse_regex!(.message, r'time="(?P<time>[^"]*)" level=(?P<level>[A-Za-z]+) msg="(?P<msg>[^"]*)" container=(?P<container>(?:[A-Za-z]+\d+[A-Za-z]+\d+[A-Za-z]+\d+|\d+[A-Za-z]+\d+[A-Za-z]+\d+[A-Za-z]+\d+[A-Za-z]+\d+[A-Za-z]+\d+[A-Za-z]+)) image="(?P<image>[^"]*)" name=(?P<name>[A-Za-z]+-\d+)(?: exitCode=(?P<exitcode>-?\d+(?:\.\d+)?))?')
''' - note kv input — parse_key_value!(.message) is the idiomatic Vector parser and is usually preferable to a regex
Loki
# promtail pipeline for "docker" (generated by LogForge)
# Add these stages under a scrape_config in your promtail config:
# scrape_configs:
# - job_name: docker
# pipeline_stages:
# (the stages below are indented to sit under pipeline_stages)
pipeline_stages:
- regex:
expression: '^time="(?P<time>[^"]*)" level=(?P<level>[A-Za-z]+) msg="(?P<msg>[^"]*)" container=(?P<container>(?:[A-Za-z]+\d+[A-Za-z]+\d+[A-Za-z]+\d+|\d+[A-Za-z]+\d+[A-Za-z]+\d+[A-Za-z]+\d+[A-Za-z]+\d+[A-Za-z]+\d+[A-Za-z]+)) image="(?P<image>[^"]*)" name=(?P<name>[A-Za-z]+-\d+)(?: exitCode=(?P<exitcode>-?\d+(?:\.\d+)?))?$'
- labels:
level:
- note promoted low-cardinality field(s) to Loki labels: level
- note left in the extracted map (NOT promoted to labels — high cardinality would explode Loki streams): time, msg, container, image, name, exitcode
syslog-ng
parser p_docker {
regexp-parser(
prefix(".docker.")
patterns("time=\"(?<time>[^\"]*)\" level=(?<level>[A-Za-z]+) msg=\"(?<msg>[^\"]*)\" container=(?<container>(?:[A-Za-z]+\\d+[A-Za-z]+\\d+[A-Za-z]+\\d+|\\d+[A-Za-z]+\\d+[A-Za-z]+\\d+[A-Za-z]+\\d+[A-Za-z]+\\d+[A-Za-z]+\\d+[A-Za-z]+)) image=\"(?<image>[^\"]*)\" name=(?<name>[A-Za-z]+-\\d+)(?: exitCode=(?<exitcode>-?\\d+(?:\\.\\d+)?))?")
);
}; - note captured fields are stored as name-value pairs under the prefix ".docker." (e.g. a group (?<srcip>…) becomes ".docker.srcip")
- note kv structure: syslog-ng has a dedicated kv-parser() that is simpler and more robust than regexp-parser for key=value logs — consider kv-parser(prefix(".logforge.")) instead of the emitted regexp-parser
FAQ
- What is the difference between dockerd logs and container stdout logs?
- They are two formats. dockerd (the daemon) and Go tooling use logrus key=value with a quoted time="…" and level=. A container's own stdout/stderr, captured by the default json-file driver, is wrapped as {"log":"…","stream":"stdout","time":"…"} — your app's line lives inside the "log" key. This page parses the daemon/logrus form; container output needs the JSON unwrap first.
- Why does my Docker daemon msg field get split incorrectly?
- Because logrus quotes the msg value when it contains spaces (msg="API listen on /var/run/docker.sock") and it usually does. Splitting the line on every space breaks it apart. Capture the quoted value as a single unit, and handle escaped quotes inside it, rather than treating each word as a token.
- What timestamp format does the Docker daemon use?
- RFC3339Nano — an ISO 8601 timestamp with nanosecond fractional seconds and a Z or numeric UTC offset, emitted as a quoted value: time="2026-07-03T14:22:15.003042Z". Capture the whole quoted string, then parse it as RFC 3339; do not split on the internal T or the fractional dot.
- How do I unwrap application logs from the json-file driver?
- Each line is a JSON object with log, stream, and time keys. Parse the JSON, take the "log" value (which ends in a newline), and that string is your application's actual output — which you then parse a second time according to whatever format the app emits (its own JSON, a text line, etc.). It is a two-stage parse.
Try it on your own Docker container lines
Paste a few real lines, review the detected fields, and copy whichever format your stack needs. Free, no account, nothing uploaded.
Open this sample in LogForge →