What a FortiGate firewall line looks like
The key=value sample below is fed verbatim into the engine to produce every parser on this page.
date=2026-07-03 time=14:22:15 devname="FGT60F" devid="FGT60FTK20012345" logid="0000000013" type="traffic" subtype="forward" level="notice" srcip=192.0.2.10 srcport=51234 dstip=198.51.100.20 dstport=443 action="accept" service="HTTPS" sentbyte=15320 rcvdbyte=88210
date=2026-07-03 time=14:22:44 devname="FGT60F" devid="FGT60FTK20012345" logid="0000000013" type="traffic" subtype="forward" level="notice" srcip=192.0.2.31 srcport=61002 dstip=203.0.113.80 dstport=22 action="deny" service="SSH" sentbyte=0 rcvdbyte=0 Detected fields
The engine classified this sample as kv and consolidated 16 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.
- date : timestamp · literal
- time : timestamp
- devname : hostname · literal
- devid : quoted_string · literal
- logid : number · literal
- type : quoted_string · literal
- subtype : quoted_string · literal
- level : severity · literal
- srcip : ipv4
- srcport : port
- dstip : ipv4
- dstport : port
- action : quoted_string
- service : quoted_string
- sentbyte : number
- rcvdbyte : number
Regex (named capture groups)
# sample: date=2026-07-03 time=14:22:15 devname="FGT60F" devid="FGT60FTK20012345" logid="0000000013" type="traffic" subtype="forward" level="notice" srcip=192.0.2.10 srcport=51234 dstip=198.51.100.20 dstport=443 action="accept" service="HTTPS" sentbyte=15320 rcvdbyte=88210
# groups: time=14:22:15, srcip=192.0.2.10, srcport=51234, dstip=198.51.100.20, dstport=443, action=accept, service=HTTPS, sentbyte=15320, rcvdbyte=88210
^date=2026-07-03 time=(?<time>\d+:\d+:\d+) devname="FGT60F" devid="FGT60FTK20012345" logid="0000000013" type="traffic" subtype="forward" level="notice" srcip=(?<srcip>\d{1,3}(?:\.\d{1,3}){3}) srcport=(?<srcport>\d{1,5}) dstip=(?<dstip>\d{1,3}(?:\.\d{1,3}){3}) dstport=(?<dstport>\d{1,5}) action="(?<action>[^"]*)" service="(?<service>[^"]*)" sentbyte=(?<sentbyte>-?\d+(?:\.\d+)?) rcvdbyte=(?<rcvdbyte>-?\d+(?:\.\d+)?)$ Grok pattern (Logstash / Elastic)
# custom patterns
FORTIGATE_NOTDQUOTE [^"]*
date=2026-07-03 time=%{TIME:time} devname="FGT60F" devid="FGT60FTK20012345" logid="0000000013" type="traffic" subtype="forward" level="notice" srcip=%{IPV4:srcip} srcport=%{INT:srcport} dstip=%{IPV4:dstip} dstport=%{INT:dstport} action="%{FORTIGATE_NOTDQUOTE:action}" service="%{FORTIGATE_NOTDQUOTE:service}" sentbyte=%{NUMBER:sentbyte} rcvdbyte=%{NUMBER:rcvdbyte} - note kv-structured input — consider the Logstash kv filter instead of (or after) grok
- note constant field "date" embedded as literal anchor "2026-07-03" (varying=false)
- note constant field "devname" embedded as literal anchor "FGT60F" (varying=false)
- note constant field "devid" embedded as literal anchor "FGT60FTK20012345" (varying=false)
- note constant field "logid" embedded as literal anchor "0000000013" (varying=false)
- note constant field "type" embedded as literal anchor "traffic" (varying=false)
- note constant field "subtype" embedded as literal anchor "forward" (varying=false)
- note constant field "level" embedded as literal anchor "notice" (varying=false)
- 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: date=2026-07-03 time=14:22:15 devname="FGT60F" devid="FGT60FTK20012345" logid="0000000013" type="traffic" subtype="forward" level="notice" srcip=192.0.2.10 srcp
test with: /var/ossec/bin/wazuh-logtest
-->
<decoder name="fortigate-kv">
<prematch>^date=\d+-\d+-\d+ time=</prematch>
</decoder>
<decoder name="fortigate-kv">
<parent>fortigate-kv</parent>
<regex offset="after_parent">^(\d+:\d+:\d+) devname="\w+" devid="\w+" logid="\d+" type="\w+" subtype="\w+" level="\w+" srcip=(\d+.\d+.\d+.\d+) srcport=(\d+) dstip=(\d+.\d+.\d+.\d+) dstport=(\d+) action="(\w+)" service="(\w+)" sentbyte=(\d+)</regex>
<order>time, srcip, srcport, dstip, dstport, action, service, sentbyte</order>
</decoder>
<decoder name="fortigate-kv">
<parent>fortigate-kv</parent>
<regex offset="after_parent"> rcvdbyte=(\d+)</regex>
<order>rcvdbyte</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="fortigate,">
<rule id="100000" level="3">
<decoded_as>fortigate-kv</decoded_as>
<description>fortigate: srcip=$(srcip) dstip=$(dstip) action=$(action)</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="action">^deny$</field>
<description>fortigate: a denied action from $(action)</description>
</rule>
-->
</group>
- note constant field "date" skipped (identical in every line)
- 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
# fortigate — liblognorm v2 rulebase (generated by LogForge)
# Usage with rsyslog (mmnormalize runs liblognorm):
# module(load="mmnormalize")
# action(type="mmnormalize" rulebase="/etc/rsyslog.d/fortigate.rb" useRawMsg="on")
# Literal "%" is escaped as "%%"; raw tabs are written as \x09.
rule=fortigate:date=2026-07-03 time=%time:time-24hr% devname="FGT60F" devid="FGT60FTK20012345" logid="0000000013" type="traffic" subtype="forward" level="notice" srcip=%srcip:ipv4% srcport=%srcport:number% dstip=%dstip:ipv4% dstport=%dstport:number% action="%action:char-to{"extradata":"\""}%" service="%service:char-to{"extradata":"\""}%" sentbyte=%sentbyte:number% rcvdbyte=%rcvdbyte:number%
- 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=time-24hr, srcip=ipv4, srcport=number, dstip=ipv4, dstport=number, action=char-to("), service=char-to("), sentbyte=number, rcvdbyte=number
Splunk
# props.conf (search-time extraction)
[<REPLACE_WITH_SOURCETYPE>]
EXTRACT-logforge = date=2026-07-03 time=(?<time>\d+:\d+:\d+) devname="FGT60F" devid="FGT60FTK20012345" logid="0000000013" type="traffic" subtype="forward" level="notice" srcip=(?<srcip>\d{1,3}(?:\.\d{1,3}){3}) srcport=(?<srcport>\d{1,5}) dstip=(?<dstip>\d{1,3}(?:\.\d{1,3}){3}) dstport=(?<dstport>\d{1,5}) action="(?<action>[^"]*)" service="(?<service>[^"]*)" sentbyte=(?<sentbyte>-?\d+(?:\.\d+)?) rcvdbyte=(?<rcvdbyte>-?\d+(?:\.\d+)?)
# Quick search-time test in SPL:
# | rex field=_raw "date=2026-07-03 time=(?<time>\\d+:\\d+:\\d+) devname=\"FGT60F\" devid=\"FGT60FTK20012345\" logid=\"0000000013\" type=\"traffic\" subtype=\"forward\" level=\"notice\" srcip=(?<srcip>\\d{1,3}(?:\\.\\d{1,3}){3}) srcport=(?<srcport>\\d{1,5}) dstip=(?<dstip>\\d{1,3}(?:\\.\\d{1,3}){3}) dstport=(?<dstport>\\d{1,5}) action=\"(?<action>[^\"]*)\" service=\"(?<service>[^\"]*)\" sentbyte=(?<sentbyte>-?\\d+(?:\\.\\d+)?) rcvdbyte=(?<rcvdbyte>-?\\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/fortigate
{
"description": "LogForge-generated ingest pipeline for fortigate",
"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: constant field "date" embedded as literal anchor "2026-07-03" (varying=false)
- note grok: constant field "devname" embedded as literal anchor "FGT60F" (varying=false)
- note grok: constant field "devid" embedded as literal anchor "FGT60FTK20012345" (varying=false)
- note grok: constant field "logid" embedded as literal anchor "0000000013" (varying=false)
- note grok: constant field "type" embedded as literal anchor "traffic" (varying=false)
- note grok: constant field "subtype" embedded as literal anchor "forward" (varying=false)
- note grok: constant field "level" embedded as literal anchor "notice" (varying=false)
- 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/fortigate/_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.)
# FORTIGATE_NOTDQUOTE [^"]*
# --- Graylog processing pipeline rule (primary) ---
# Paste under System > Pipelines > Manage rules, then attach the rule to a pipeline stage.
rule "fortigate-parse"
when
has_field("message")
then
let gp = grok(pattern: "date=2026-07-03 time=%{TIME:time} devname=\"FGT60F\" devid=\"FGT60FTK20012345\" logid=\"0000000013\" type=\"traffic\" subtype=\"forward\" level=\"notice\" srcip=%{IPV4:srcip} srcport=%{INT:srcport} dstip=%{IPV4:dstip} dstport=%{INT:dstport} action=\"%{FORTIGATE_NOTDQUOTE:action}\" service=\"%{FORTIGATE_NOTDQUOTE:service}\" sentbyte=%{NUMBER:sentbyte} rcvdbyte=%{NUMBER:rcvdbyte}", 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": "fortigate",
"extractor_type": "grok",
"converters": [],
"order": 0,
"cursor_strategy": "copy",
"source_field": "message",
"target_field": "",
"extractor_config": {
"grok_pattern": "date=2026-07-03 time=%{TIME:time} devname=\"FGT60F\" devid=\"FGT60FTK20012345\" logid=\"0000000013\" type=\"traffic\" subtype=\"forward\" level=\"notice\" srcip=%{IPV4:srcip} srcport=%{INT:srcport} dstip=%{IPV4:dstip} dstport=%{INT:dstport} action=\"%{FORTIGATE_NOTDQUOTE:action}\" service=\"%{FORTIGATE_NOTDQUOTE:service}\" sentbyte=%{NUMBER:sentbyte} rcvdbyte=%{NUMBER:rcvdbyte}",
"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: constant field "date" embedded as literal anchor "2026-07-03" (varying=false)
- note grok: constant field "devname" embedded as literal anchor "FGT60F" (varying=false)
- note grok: constant field "devid" embedded as literal anchor "FGT60FTK20012345" (varying=false)
- note grok: constant field "logid" embedded as literal anchor "0000000013" (varying=false)
- note grok: constant field "type" embedded as literal anchor "traffic" (varying=false)
- note grok: constant field "subtype" embedded as literal anchor "forward" (varying=false)
- note grok: constant field "level" embedded as literal anchor "notice" (varying=false)
- note grok: custom patterns emitted — save the '# custom patterns' block to a file in your patterns_dir
- note 1 custom grok pattern(s) (FORTIGATE_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 date=2026-07-03 time=%{data:time} devname="FGT60F" devid="FGT60FTK20012345" logid="0000000013" type="traffic" subtype="forward" level="notice" srcip=%{ipv4:srcip} srcport=%{integer:srcport} dstip=%{ipv4:dstip} dstport=%{integer:dstport} action="%{quotedString:action}" service="%{quotedString:service}" sentbyte=%{number:sentbyte} rcvdbyte=%{number:rcvdbyte} - note emitted rule name is "logforge_rule"; rename it to match your "fortigate" 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 constant field "date" embedded as literal anchor "2026-07-03" (varying=false)
- note field "time" (timestamp): could not derive a Joda/Java date format from the sample shape; using %{data} — add a date("…") format by hand if you need a parsed timestamp
- note constant field "devname" embedded as literal anchor "FGT60F" (varying=false)
- note constant field "devid" embedded as literal anchor "FGT60FTK20012345" (varying=false)
- note constant field "logid" embedded as literal anchor "0000000013" (varying=false)
- note constant field "type" embedded as literal anchor "traffic" (varying=false)
- note constant field "subtype" embedded as literal anchor "forward" (varying=false)
- note constant field "level" embedded as literal anchor "notice" (varying=false)
- 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 fortigate
Format regex
Regex ^date=2026-07-03 time=(?<time>\d+:\d+:\d+) devname="FGT60F" devid="FGT60FTK20012345" logid="0000000013" type="traffic" subtype="forward" level="notice" srcip=(?<srcip>\d{1,3}(?:\.\d{1,3}){3}) srcport=(?<srcport>\d{1,5}) dstip=(?<dstip>\d{1,3}(?:\.\d{1,3}){3}) dstport=(?<dstport>\d{1,5}) action="(?<action>[^"]*)" service="(?<service>[^"]*)" sentbyte=(?<sentbyte>-?\d+(?:\.\d+)?) rcvdbyte=(?<rcvdbyte>-?\d+(?:\.\d+)?)$
Time_Key time
Time_Format %H:%M:%S
# Fluentd <parse> block:
# <parse>
# @type regexp
# expression /^date=2026-07-03 time=(?<time>\d+:\d+:\d+) devname="FGT60F" devid="FGT60FTK20012345" logid="0000000013" type="traffic" subtype="forward" level="notice" srcip=(?<srcip>\d{1,3}(?:\.\d{1,3}){3}) srcport=(?<srcport>\d{1,5}) dstip=(?<dstip>\d{1,3}(?:\.\d{1,3}){3}) dstport=(?<dstport>\d{1,5}) action="(?<action>[^"]*)" service="(?<service>[^"]*)" sentbyte=(?<sentbyte>-?\d+(?:\.\d+)?) rcvdbyte=(?<rcvdbyte>-?\d+(?:\.\d+)?)$/
# time_key time
# time_format %H:%M:%S
# </parse>
- note Time_Key set to "time"; Time_Format "%H:%M:%S" 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.fortigate_parse]
type = "remap"
inputs = ["REPLACE_WITH_SOURCE"]
source = '''
. |= parse_regex!(.message, r'date=2026-07-03 time=(?P<time>\d+:\d+:\d+) devname="FGT60F" devid="FGT60FTK20012345" logid="0000000013" type="traffic" subtype="forward" level="notice" srcip=(?P<srcip>\d{1,3}(?:\.\d{1,3}){3}) srcport=(?P<srcport>\d{1,5}) dstip=(?P<dstip>\d{1,3}(?:\.\d{1,3}){3}) dstport=(?P<dstport>\d{1,5}) action="(?P<action>[^"]*)" service="(?P<service>[^"]*)" sentbyte=(?P<sentbyte>-?\d+(?:\.\d+)?) rcvdbyte=(?P<rcvdbyte>-?\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 "fortigate" (generated by LogForge)
# Add these stages under a scrape_config in your promtail config:
# scrape_configs:
# - job_name: fortigate
# pipeline_stages:
# (the stages below are indented to sit under pipeline_stages)
pipeline_stages:
- regex:
expression: '^date=2026-07-03 time=(?P<time>\d+:\d+:\d+) devname="FGT60F" devid="FGT60FTK20012345" logid="0000000013" type="traffic" subtype="forward" level="notice" srcip=(?P<srcip>\d{1,3}(?:\.\d{1,3}){3}) srcport=(?P<srcport>\d{1,5}) dstip=(?P<dstip>\d{1,3}(?:\.\d{1,3}){3}) dstport=(?P<dstport>\d{1,5}) action="(?P<action>[^"]*)" service="(?P<service>[^"]*)" sentbyte=(?P<sentbyte>-?\d+(?:\.\d+)?) rcvdbyte=(?P<rcvdbyte>-?\d+(?:\.\d+)?)$'
- note no low-cardinality field found to promote to a Loki label — omitted the `- labels:` stage; every captured field stays in the extracted map for later stages
- note left in the extracted map (NOT promoted to labels — high cardinality would explode Loki streams): time, srcip, srcport, dstip, dstport, action, service, sentbyte, rcvdbyte
syslog-ng
parser p_fortigate {
regexp-parser(
prefix(".fortigate.")
patterns("date=2026-07-03 time=(?<time>\\d+:\\d+:\\d+) devname=\"FGT60F\" devid=\"FGT60FTK20012345\" logid=\"0000000013\" type=\"traffic\" subtype=\"forward\" level=\"notice\" srcip=(?<srcip>\\d{1,3}(?:\\.\\d{1,3}){3}) srcport=(?<srcport>\\d{1,5}) dstip=(?<dstip>\\d{1,3}(?:\\.\\d{1,3}){3}) dstport=(?<dstport>\\d{1,5}) action=\"(?<action>[^\"]*)\" service=\"(?<service>[^\"]*)\" sentbyte=(?<sentbyte>-?\\d+(?:\\.\\d+)?) rcvdbyte=(?<rcvdbyte>-?\\d+(?:\\.\\d+)?)")
);
}; - note captured fields are stored as name-value pairs under the prefix ".fortigate." (e.g. a group (?<srcip>…) becomes ".fortigate.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 fields does a FortiGate traffic log contain?
- A traffic (type="traffic", subtype="forward") log carries date and time as separate fields, device identity (devname, devid), a numeric logid, the connection five-tuple (srcip, srcport, dstip, dstport), the service name, the action (accept/deny), byte counters (sentbyte, rcvdbyte), and a severity level. UTM and event logs add their own keys — the exact set depends on FortiOS version and enabled features.
- Why are some FortiGate values quoted and others not?
- FortiOS quotes values that may contain spaces or are string-typed (devname="FGT60F", action="accept", msg="…") and leaves numeric or token values bare (srcip=192.0.2.10, dstport=443). A parser must handle both forms and must not split on spaces inside a quoted value, which is the number-one cause of broken FortiGate parsing.
- Why do two FortiGate lines have different keys?
- Because the key set depends on the log type, subtype, FortiOS version, and which UTM inspection profiles are active. A traffic log and a UTM/threat log share the header keys but diverge in the body. Use an order-independent, key-anchored parser (match key=value pairs wherever they appear) rather than a fixed-column pattern.
- How do I tell allowed traffic from blocked traffic in a FortiGate log?
- The action field is the verdict: action="accept" is allowed, action="deny" is blocked (other values like "close", "timeout", and "server-rst" describe session teardown). A deny with sentbyte=0 and rcvdbyte=0 is a connection that was refused outright — a common signal when hunting for scanning or blocked lateral movement.
Try it on your own FortiGate firewall 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 →