Legacy syslog in Grafana Alloy
Importing legacy syslog using Grafana Alloy
Grafana Alloy is an OpenTelemetry Collector wrapper that seems to have slightly better documentation, especially for processing pipelines. It's useful for collecting logs from my homelab and storing them in Loki for easier monitoring.
Alloy 1.13+ supports "raw" format syslog source allowing collection and processing of devices that don't send modern RFC5424 or RFC3164 syslog data. Unifi Network and Protect support an "SIEM Server" integration which uses legacy syslog format, and outputs Common Event Format logs. Unfortunately Alloy doesn't support CEF directly, but a processing pipeline can extract known fields for structured logs.
Pipeline
Alloy allows one syslog format per source so it will listen on one port, process the logs, then write them into Loki.
---
config:
theme: base
themeVariables:
primaryColor: "#552ba9"
primaryTextColor: "#f9f7f0"
secondaryColor: "#6172b0"
fontFamily: Atkinson Hyperlegible Next
---
flowchart LR
A([unifi device])
B([loki.source.syslog "unifi"])
C([loki.process "unifi"])
D([loki.write "unifi"])
A -- port 9516 --> B
B --> C
C --> D
Syslog source
I have loki.source.syslog listening on UDP port 9516
using the "raw" format. It forwards to the unifi processor.
loki.source.syslog "unifi" {
listener {
address = "0.0.0.0:9516"
labels = {
component = "loki.source.syslog",
protocol = "udp",
}
syslog_format = "raw"
}
forward_to = [
loki.process.unifi.receiver,
]
}Debugging
Use loki.echo as a forward_to entry for any of these to output
the current log line from alloy to check your work. For the syslog receiver:
loki.echo "debug" { }
loki.source.syslog "unifi" {
// …
forward_to = [
loki.echo.debug.receiver,
loki.process.unifi.receiver,
]
}Log processor
The loki.process converts the log line into structured
output. I use a set of stages to extract fields from the raw log line, or a
extract further fields.
To start, the log processor will output to the unifi write receiver. I'll fill in the rest as its discussed.
loki.process "unifi" {
// stages
forward_to = [
loki.write.unifi.receiver,
]
}CEF header
First, parse the CEF header using a stage.regex. We'll use
these capture groups in further stages.
stage.regex {
expression = "CEF:(?P<v>.*?)\\|(?P<vendor>.*?)\\|(?P<product>.*?)\\|(?P<version>.*?)\\|(?P<event_id>.*?)\\|(?P<event_name>.*?)\\|(?P<severity>.*?)\\|(?P<extension>.*)$"
}Extension pairs
Next I extract some of the extension pairs. I found these for UniFi Network 11
by enabling debug for the loki.source.syslog
forward_to. Alternatively you can tcpdump -s0 -A port 9516 if you don't
want lines logged twice.
stage.regex {
expression = "UNIFIcategory=(?P<category>\\S+)"
}
stage.regex {
expression = "UNIFIdeviceIp=(?P<device_ip>\\S+)"
}
stage.regex {
expression = "UNIFIdeviceModel=(?P<device_model>\\S+)"
}
stage.regex {
expression = "UNIFIdeviceName=(?P<device>\\S+)"
}Timestamp
Alloy will default to the time the message arrived at the source. Since the
logs include their own timestamp we can extract it and parse it using
stage.timestamp. UNIFIutcTime from UniFi Network is
RFC3339, timestamp from UniFi Protect is milliseconds since the epoch.
stage.regex {
expression = "(UNIFIutcTime|timestamp)=(?P<timestamp>\\S+)"
}
stage.timestamp {
format = "RFC3339"
fallback_formats = [
"UnixMs",
]
source = "timestamp"
action_on_failure = "skip"
}Severity
CEF uses numbers for severity, but the rest of my logs use syslog severities.
I mapped them using stage.replace to something that seemed
reasonable:
// CEF low
stage.replace {
expression = "(0|1|2|3)"
source = "severity"
replace = "informational"
}
// CEF medium
stage.replace {
expression = "(4|5|6)"
source = "severity"
replace = "notice"
}
// CEF high
stage.replace {
expression = "(7|8)"
source = "severity"
replace = "warning"
}
// CEF very high
stage.replace {
expression = "(9|10)"
source = "severity"
replace = "critical"
}
Unquote message
For UniFi Protect the message is wrapped in quotes. I haven't seen escaped quotes so I only remove the wrapping quotes.
// clean wrapping quotes
stage.replace {
expression = "(^\")"
source = "message"
replace = ""
}
stage.replace {
expression = "(\"$)"
source = "message"
replace = ""
}Output
Now that all the processing is complete I set the labels with
stage.labels and the output message with
stage.output. An empty labels value will use the capture
group set above.
stage.labels {
values = {
application = "product",
category = "",
device = "",
hostname = "",
device_ip = "",
device_model = "",
event_id = "",
event_name = "",
product = "",
severity = "",
version = "",
}
}
stage.output {
source = "message"
}
forward_to = [
loki.write.unifi.receiver,
]Writer
In order to make the messages easier to filter I set the alloy_source label
on the loki.write that forwrds the messages to loki.
loki.write "unifi" {
endpoint {
url = "https://otel.example:3100/loki/api/v1/push"
}
external_labels = {
alloy_source = "unifi",
}
}All together
syslog source, processor, writer
loki.source.syslog "unifi" {
listener {
address = "0.0.0.0:9516"
protocol = "udp"
labels = {
component = "loki.source.syslog",
protocol = "udp",
}
syslog_format = "raw"
}
forward_to = [
loki.process.unifi.receiver,
]
}
loki.process "unifi" {
forward_to = [
loki.write.unifi.receiver,
]
stage.regex {
expression = "CEF:(?P<v>.*?)\\|(?P<vendor>.*?)\\|(?P<product>.*?)\\|(?P<version>.*?)\\|(?P<event_id>.*?)\\|(?P<event_name>.*?)\\|(?P<severity>.*?)\\|(?P<extension>.*)$"
}
stage.regex {
expression = "UNIFIcategory=(?P<category>\\S+)"
}
stage.regex {
expression = "UNIFIdeviceIp=(?P<device_ip>\\S+)"
}
stage.regex {
expression = "UNIFIdeviceModel=(?P<device_model>\\S+)"
}
stage.regex {
expression = "UNIFIdeviceName=(?P<device>\\S+)"
}
stage.regex {
expression = "(UNIFIutcTime|timestamp)=(?P<timestamp>\\S+)"
}
stage.timestamp {
format = "RFC3339"
fallback_formats = [
"UnixMs",
]
source = "timestamp"
action_on_failure = "skip"
}
// CEF low
stage.replace {
expression = "(0|1|2|3)"
source = "severity"
replace = "informational"
}
// CEF medium
stage.replace {
expression = "(4|5|6)"
source = "severity"
replace = "notice"
}
// CEF high
stage.replace {
expression = "(7|8)"
source = "severity"
replace = "warning"
}
// CEF very high
stage.replace {
expression = "(9|10)"
source = "severity"
replace = "critical"
}
// use message from extension
stage.regex {
expression = "msg=(?P<message>.*)$"
source = "extension"
}
// clean wrapping quotes
stage.replace {
expression = "(^\")"
source = "message"
replace = ""
}
stage.replace {
expression = "(\"$)"
source = "message"
replace = ""
}
stage.labels {
values = {
application = "product",
category = "",
device = "",
hostname = "",
device_ip = "",
device_model = "",
event_id = "",
event_name = "",
product = "",
severity = "",
version = "",
}
}
stage.output {
source = "message"
}
}
loki.write "unifi" {
endpoint {
url = "https://loki.example:3100/loki/api/v1/push"
}
external_labels = {
alloy_source = "unifi",
}
}Processed event
{
"timestamp": "2026-11-11 17:11:50.996",
"logLevel": "",
"displayLevel": "",
"line": "Side yard has recorded motion.",
"labels": {
"alloy_source": "unifi",
"application": "UniFi Protect",
"category": "detection",
"component": "loki.source.syslog",
"event_id": "2159",
"event_name": "motion",
"product": "UniFi Protect",
"protocol": "udp",
"severity": "informational",
"version": "7.3.74"
}
}
(Duplicate timstamps removed)