
Turning syslog lines into events with regular expressions
A syslog stream is a pile of text. A monitoring system wants structure: which host, which object, what state, which user, how bad. The bridge is a set of pattern rules, usually regular expressions with capture groups, that recognize a known message and copy its pieces into event fields. Done well, a port going down becomes an "interface down" event on the right port of the right switch, and clears when the port comes back. Done badly, everything lands as "unclassified syslog" and nobody reads it.
This guide uses real message formats and assumes you already collect logs in one place; if not, start with syslog for small networks.
What a syslog line contains
Two header formats are in common use.
The older BSD format, written down after the fact in RFC 3164, is loose: a priority in angle brackets, a timestamp with no year or time zone, a host name, a tag and the message. Network gear bends it further. A Cisco IOS switch with sequence numbers and millisecond timestamps turned on sends something like this (the exact header depends on platform and logging settings):
<187>1473: Oct 14 03:15:02.117: %LINK-3-UPDOWN: Interface GigabitEthernet1/0/24, changed state to down
The newer format in RFC 5424 is strict: priority, version, a full timestamp with time zone, host name, application name, process ID, message ID, structured data, then the message. A Linux server forwarding through rsyslog sends:
<86>1 2026-10-14T03:15:02.003Z web1.example.com sshd 2210 - - Failed password for invalid user oracle from 203.0.113.45 port 52116 ssh2
The two dashes are "no value" placeholders for the message ID and the structured data.
Priority, facility and severity
The number in angle brackets is the priority, calculated as PRI = facility * 8 + severity. Divide it by 8: the quotient is the facility and the remainder is the severity.
| PRI | Facility | Severity | What it usually is |
|---|---|---|---|
| 187 | 23 (local7) | 3 (err) | A Cisco device on its default facility, reporting an error |
| 189 | 23 (local7) | 5 (notice) | The same device, a normal but significant event |
| 86 | 10 (authpriv) | 6 (info) | A Linux authentication message |
Severities run from 0 (emergency) to 7 (debug). Facilities 0 to 15 have fixed meanings (kern, user, mail, auth and so on), and 16 to 23 are local0 to local7, free for local use. Giving each device class its own local facility lets the syslog daemon sort them into separate files before any pattern runs.
The Cisco label
Cisco IOS messages carry a label of the form %FACILITY-SEVERITY-MNEMONIC. In %LINK-3-UPDOWN, LINK is the IOS facility (a software component, unrelated to the syslog facility), 3 is the severity on the same 0 to 7 scale, and UPDOWN names the message. It does not change with timestamps, host names or interface names, which makes it the best anchor a pattern can have.
Decide on the event fields first
First decide what an event looks like in your system. The fields most monitoring tools use (see events, alarms and severities) are:
- host: the device the line came from
- object: the interface, session, line or service the message is about
- state: up, down, failed, ok, or empty for one-off messages
- user: who did it, if anyone
- extra: anything else worth keeping, such as a source address
- type: the event type, which decides how the event is shown and whether it opens or closes an alarm
Take the host from the syslog header or the sender's address, not from your pattern. Patterns then deal only with the message text, and one rule works for every switch.
Writing the patterns
Most tools accept Perl-compatible regular expressions (PCRE) or something close to them. Parentheses create capture groups, numbered from 1 in the order of their opening brackets. Named groups such as (?P<state>...) (PCRE also accepts (?<state>...)) are easier to maintain than numbers, so the examples use them.
Example 1: interface up and down
A port going down produces two messages, one for the physical link and one for the line protocol:
%LINK-3-UPDOWN: Interface GigabitEthernet1/0/24, changed state to down
%LINEPROTO-5-UPDOWN: Line protocol on Interface GigabitEthernet1/0/24, changed state to down
One pattern covers both:
%(?P<proto>LINK|LINEPROTO)-\d-UPDOWN: (?:Line protocol on )?Interface (?P<object>[^,]+), changed state to (?P<state>up|down)\b
On the first line, proto is LINK, object is GigabitEthernet1/0/24 and state is down. On the second, proto is LINEPROTO with the same object and state. Three choices in it deserve a word:
[^,]+takes everything up to the comma, so names with slashes, subinterfaces likeGi0/1.100and names without any slash all match.\S+would swallow the comma too.(?:...)groups without capturing, so the optional "Line protocol on " text does not shift the group numbers.(up|down)lists the states you expect instead of grabbing any word. An administrative shutdown arrives as a different message,%LINK-5-CHANGED: Interface GigabitEthernet1/0/24, changed state to administratively down, and it deserves its own rule and event type: nobody should be paged for a port an engineer shut on purpose.
Many teams map LINK and LINEPROTO to the same event type, keep proto in the extra field, and let de-duplication fold the pair into a single alarm.
Example 2: configuration changes
%SYS-5-CONFIG_I: Configured from console by netops on vty0 (192.0.2.50)
%SYS-5-CONFIG_I: Configured from console by console
The first is a change made over a remote session, the second a change from the physical console port, where there is no line number or address. A pattern that handles both:
%SYS-5-CONFIG_I: Configured from (?P<method>\S+) by (?P<user>\S+)(?: on (?P<line>\S+))?(?: \((?P<src>[0-9A-Fa-f.:]+)\))?
The first line yields method console, user netops, line vty0 and src 192.0.2.50. The second yields console for both method and user, with the other two empty. The literal parentheses around the address are escaped as \( and \). Map user to the user field, line to the object and src to extra, and you have an audit trail of who changed which switch from where.
Example 3: SSH login failures on Linux
Failed password for invalid user oracle from 203.0.113.45 port 52116 ssh2
Failed password for root from 198.51.100.23 port 40022 ssh2
Failed (?P<method>password|publickey) for (?:invalid user )?(?P<user>\S+) from (?P<src>[0-9A-Fa-f.:]+) port (?P<port>\d+)
The first line gives user oracle and src 203.0.113.45, the second user root and src 198.51.100.23. "Invalid user" means the account does not exist, which is worth keeping: capture it with (?P<invalid>invalid user )? and store it in extra.
One warning applies to every log rule and to this one most of all: part of the line was chosen by a stranger. The user name here is whatever the attacker typed. Treat every capture as untrusted data. Never paste it into a shell command or a hand-built SQL string in an action script, and escape it before it appears in a web page.
Mapping captures to event fields
How you bind groups to fields depends on the tool. Some offer a table with one column per field and a group number in each cell. Zabbix log items take a regular expression plus an output template that can use \1 style back-references. The syslog-ng pattern database and the rsyslog normalization module use their own pattern languages instead of regular expressions. Whatever the syntax, the binding for the three examples comes down to this:
| Rule | Event type | object | state | user | extra |
|---|---|---|---|---|---|
| Interface up and down | interface-state | object |
state |
proto |
|
| Configuration change | configuration | line |
user |
src |
|
| SSH login failure | auth-failure | user |
src |
Two details make the result usable. First, normalize states. If one rule captures down, another DOWN and a third failed, the alarm logic needs one word for "down", so lower-case everything and map synonyms in one place. Second, set severity per rule instead of trusting the number in the message. LINEPROTO-5 says notice, but a core uplink going down is critical to you, while an access port in a classroom dropping at 15:30 is usually a laptop leaving.
The same port events can also arrive as linkDown and linkUp traps. If you collect both, give them the same event type so they de-duplicate cleanly (the SNMP traps guide covers the trap side).
Testing patterns before they go live
Keep a sample file of real lines next to your rules (replace real user names and public addresses first) and test against it every time a rule changes. GNU grep with -P uses PCRE. The first command counts the lines a rule catches; the second lists lines that no rule matches, which are the ones to read next:
grep -cP '%(LINK|LINEPROTO)-\d-UPDOWN: ' samples.log
grep -vP 'UPDOWN|CONFIG_I|Failed (password|publickey)' samples.log | head -20
To see what each group holds, a few lines of Python do the job:
import re, sys
rule = re.compile(r'%(?P<proto>LINK|LINEPROTO)-\d-UPDOWN: (?:Line protocol on )?'
r'Interface (?P<object>[^,]+), changed state to (?P<state>up|down)\b')
for line in open(sys.argv[1]):
m = rule.search(line)
if m:
print(m.groupdict())
Python's re module is not PCRE, but for the constructs used here the two behave the same. When a pattern misses a line in production, add that line to the sample file before fixing the rule, so the fix stays tested.
The mistakes that turn up most:
- An unescaped dot. In
Gi0/1.100the dot is literal text, but in a pattern it matches any character. Write\.when you mean a dot. - A greedy
.*in the middle of a pattern, which swallows more than intended and leaves later groups holding the wrong text. - Patterns written against one platform only. Other operating systems from the same vendor, and sometimes a firmware upgrade, change the wording. Retest after upgrades.
Rule order and performance
Most engines try rules in a fixed order and stop at the first match; some run every rule against every line. Find out which yours does. With first match wins:
- Put specific rules before general ones. A rule for
%LINK-5-CHANGEDmust sit above any broad%LINK-rule. - Put the most frequent messages near the top, or better, drop pure noise such as firewall permit lines before it reaches the rules.
- End with a catch-all that turns anything unmatched into a low-severity "unclassified" event, so nothing disappears silently and you can see which rules to write next.
For speed, anchor each pattern on a fixed literal such as %SYS-5-CONFIG_I:, or check the program name (sshd) before running the expression at all. Avoid nested quantifiers like (\S+\s*)+, which can backtrack for a very long time on a line that almost matches. Compile patterns once, at startup. Noise filtering belongs in rsyslog or syslog-ng; the guide to feeding the monitor from outside programs covers that hand-off.
A checklist for each new rule
- Collect five to ten real examples of the message, including the odd ones.
- Anchor on the fixed part: the Cisco label or the program's unchanging wording.
- Capture only what maps to a field, with named groups.
- Test against the sample file, then against a full day of logs to catch false matches.
- Set event type and severity by what the message means on your network.