Advertisement
Open Source Projects by Phil Schwartz

Building a Python service that listens for UDP syslog traffic

A UDP syslog listener is one of those quiet workhorses that sits at the bottom of a network stack, capturing everything from router heartbeats to application error traces. Because UDP is connectionless, a small Python program can receive thousands of small messages per second with very little overhead, which is why the format stays popular where reliability is handled at the application layer. The goal is straightforward: bind to a port, accept a datagram, decode the bytes, and produce a structured record that downstream systems can index or alert on.

In Australian IT shops the use case still comes up often. Plenty of regional businesses in Brisbane and Melbourne run on-prem Linux fleets because of data residency obligations, and syslog remains the cheapest way to centralise events from switches, firewalls, and application servers. A developer in Parramatta or Perth can have a collector running on a spare box in an afternoon, and the rest of this article walks through the choices that matter when you build one from scratch.

Why UDP and what syslog looks like on the wire

Syslog was originally specified in RFC 3164, often called BSD syslog, and later refined by RFC 5424 which adds structured data and proper UTF-8 handling. Both define a message as a small text payload prefixed by a priority value in angle brackets, followed by a timestamp, hostname, application name, and message body. The wire form looks like <165>Oct 11 22:14:15 myhost sshd: Accepted publickey for admin, where 165 encodes facility 20 (authpriv) and severity 5 (notice).

UDP is the default transport because the protocol was designed when logs were best-effort and a lost packet was acceptable. Port 514 is the well-known port, but anything above 1024 works for unprivileged listeners. The trade-off is real: out-of-order delivery, duplicates, and silent drops all happen. For a tool that just keeps an eye on things and stores a rough trail of events, that is usually fine.

Binding the socket in Python

The core of the listener is a standard Berkeley socket created with socket.socket(socket.AF_INET, socket.SOCK_DGRAM). Binding to ('0.0.0.0', 514) or a high port for testing is a one-liner, but it pays to set SO_REUSEADDR so restarts after a crash do not fail with Address already in use. Setting a sensible receive buffer with setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, ...) helps when bursts arrive from a busy firewall.

The receive loop is a simple while True: calling sock.recvfrom(65535). For a service that also needs to handle health probes or DNS lookups, wrapping the loop with selectors or asyncio keeps things responsive without spawning threads. In production the process is typically managed by systemd, standard on most Linux distributions in Australian data centres, which gives you a journal to inspect when things go pear-shaped.

Parsing the priority and header fields

Once a datagram lands, the parser strips the priority first. The bytes after the opening < up to the first > are an integer; dividing by eight gives the facility, the modulo gives the severity. A dictionary mapping numeric facilities to names (local0, auth, daemon) makes downstream filters far more readable, and the same applies to severity levels where 2 becomes critical.

After the priority the rest of the line follows one of two shapes. The legacy BSD form starts with a timestamp like Oct 11 22:14:15, then a hostname, then a tag, then the body. The modern form starts with a full ISO 8601 timestamp and includes a message ID plus a structured data block. Detecting which one you have is easy: count the spaces. A robust parser tries the modern grammar first, falls back to BSD, and surfaces the raw text if neither matches so nothing is silently discarded.

Decoding timestamps with Australian offsets in mind

Timestamps are where a syslog collector for an Australian audience often gets tricky. Devices in Sydney and Brisbane send events stamped in AEDT during summer and AEST in winter, while Perth logs arrive in AWST all year and Adelaide uses ACDT or ACST. UTC offsets of plus nine, ten, ten and a half, and eleven all show up in the wild, and a naive parser that assumes the server's local time will quietly mislabel events by hours.

The cleanest approach is to parse everything into UTC. For RFC 5424 the timestamp already carries an offset, so converting is straightforward. For BSD messages the year is missing entirely, so the parser should infer it from the current year and apply the device's known offset, defaulting to the host's local zone when the device is unknown. Storing every record as a UTC datetime keeps dashboards honest regardless of where the log originated.

Privacy, redaction, and local retention rules

Syslog payloads often contain usernames, IP addresses, and sometimes email addresses, which means a collector running in Australia sits under the Australian Privacy Principles. Principle 11 requires that personal information be protected from unauthorised access, interference, or loss. A modest redaction step, hashing usernames with a salted SHA-256 and masking the last octet of source IPs, goes a long way toward satisfying that requirement without losing useful signal.

Retention also matters. Banks and healthcare providers in Australia commonly operate under five to seven year retention windows, while operational logs for a small retailer might be cycled after thirty days. Encoding a per-stream TTL and pruning the destination store nightly keeps the service tidy. The Australian Cyber Security Centre's Essential Eight guidance encourages logging that is both backed up and write-once in spirit, so a simple append-only file or a SQLite database in WAL mode is often enough for a single-team deployment.

Running the service as a background daemon

Once parsing and storage are working, the final step is packaging the listener so it survives reboots and crashes. A systemd unit file with Restart=on-failure, an explicit User=, and a hardened ProtectSystem=strict stanza is the conventional answer. A small Unix domain socket exposing /healthz and /metrics lets monitoring tools confirm the process is alive without poking the UDP port itself.

For local debugging, a developer can run the same code on a laptop bound to port 1514 and point a test device at it, then ship the unit file to production. Logging the listener's own diagnostics to stderr, captured by the systemd journal, keeps troubleshooting paths consistent with the rest of the infrastructure. The result is a small, transparent service that handles UDP syslog cleanly and slots into a mixed Windows-and-Linux estate without drama.