Mapping Python Log Levels to Syslog Severity

Forwarding Python logs to a syslog daemon requires translating Python's five named levels into syslog's eight numeric severities so that filters, alerts, and retention rules behave as intended. This guide is part of the Python Logging Fundamentals and Structured Data guide and sits under the log levels and severity mapping guide, which covers how levels drive routing decisions across a logging pipeline.

Python levels to syslog severities Five Python levels on the left map to a subset of the eight syslog severities on the right; syslog emergency, alert, and informational-notice rows have no direct Python source. Python level Syslog severity CRITICAL ERROR WARNING INFO DEBUG 2 critical 3 error 4 warning 6 info 7 debug
Python's five levels cover only five of syslog's eight severities; emergency, alert, and notice have no default Python source.

Prerequisites

SysLogHandler ships in the standard library, so no install is required. You only need a reachable syslog endpoint. On Linux, the local daemon usually listens on the /dev/log Unix socket; remote daemons typically accept UDP or TCP on port 514.

# Confirm a local syslog socket exists before wiring the handler.
test -S /dev/log && echo "local syslog socket present"

No third-party dependencies are involved. If you assemble this handler declaratively, the log levels and severity mapping guide and the broader guide describe how the same handler slots into a configuration-driven setup.

Implementation

The translation from Python level to syslog severity is performed by SysLogHandler.priority_map, a dictionary keyed by level name. Understanding and, where needed, extending that map is the core task. Getting the mapping right is not cosmetic: operations tooling almost always filters and pages on syslog severity, not on Python level names, so a record that arrives with the wrong severity is either invisible to an alert that should have fired or noisy in a dashboard that should have stayed quiet. The level your code chooses and the severity the daemon sees must agree, and that agreement is entirely defined by priority_map.

Step 1 — Know the default map. SysLogHandler defines a priority_names table for all eight severities and a priority_map from Python level names to syslog severity names. The defaults are: DEBUG to debug, INFO to info, WARNING to warning, ERROR to error, and CRITICAL to critical. Any level name not in priority_map falls back to warning inside mapPriority, which is why custom levels can land in an unexpected severity. The asymmetry matters because the two systems were designed for different audiences: Python's five levels target application authors, while syslog's eight severities span machine-level emergencies down to verbose debugging. Three syslog severities, emergency (0), alert (1), and notice (5), simply have no Python equivalent, so they stay unused unless you register custom levels and map them deliberately.

Step 2 — Create the handler with an explicit facility. Choose a facility that identifies your application; local0 through local7 are reserved for site-local use. Pass address as a tuple for network targets or a path string for the local socket.

import logging
import logging.handlers

# local0 facility keeps app logs separable from system facilities.
handler = logging.handlers.SysLogHandler(
    address="/dev/log",
    facility=logging.handlers.SysLogHandler.LOG_LOCAL0,
)
handler.setFormatter(logging.Formatter("%(name)s: %(levelname)s %(message)s"))

log = logging.getLogger("billing")
log.setLevel(logging.DEBUG)
log.addHandler(handler)

log.warning("retrying charge for invoice 5512")

The handler computes the syslog priority as facility * 8 + severity. With local0 (facility 16) and a WARNING (severity 4), the priority value is 16 * 8 + 4 = 132. The facility is fixed per handler instance, so every record from this logger shares it; only the severity varies per message. This is what lets a downstream daemon route all of an application's output to a dedicated file by facility while still letting alerting rules select on severity. Picking a facility from the local0 through local7 range keeps your application logs cleanly separated from kernel, mail, auth, and cron facilities that the operating system already uses.

Step 3 — Verify the encoded priority. Inspect the handler's mapping directly to confirm the level resolves to the severity you expect before relying on it in production.

print(handler.mapPriority("WARNING"))   # -> 'warning'
print(handler.encodePriority(
    handler.facility,
    handler.mapPriority("CRITICAL"),
))  # -> 130  (16*8 + 2)

Expected Output:

warning
130

Step 4 — Extend the map for custom levels. If you register a custom level such as TRACE below DEBUG, add it to priority_map so it does not silently fall back. Choose the closest syslog severity by intent rather than by numeric proximity: a TRACE level is conceptually more verbose than DEBUG, but since syslog has no level below debug, mapping it to debug is correct. Conversely, if you add a SECURITY level intended to wake an on-call engineer, map it to alert (1) or emergency (0), severities Python would otherwise never produce. The priority_map entry, not the numeric value of the level, is what determines where the record lands.

import logging

TRACE = 5
logging.addLevelName(TRACE, "TRACE")
# Map the custom level to the most appropriate syslog severity.
handler.priority_map["TRACE"] = "debug"

Configuration options

Python level Numeric Syslog severity Syslog code
CRITICAL 50 critical 2
ERROR 40 error 3
WARNING 40 warning 4
INFO 20 info 6
DEBUG 10 debug 7
unmapped / custom n/a warning (fallback) 4

Syslog severities 0 (emergency), 1 (alert), and 5 (notice) have no Python source by default. If your operations team alerts on emergency, reserve it for a deliberate custom level rather than mapping CRITICAL to it, since CRITICAL already carries severity 2.

Transport socktype Trade-off
UDP (default) socket.SOCK_DGRAM Lowest overhead; messages may be dropped under load.
TCP socket.SOCK_STREAM Reliable delivery; back-pressure can block the writer.

Verification

Send one message at each level and confirm the daemon records the matching severity. With rsyslog routing local0, you can tail the file it writes and check the decoded priority. The numeric <priority> prefix in RFC 5424 output is facility * 8 + severity, so a local0 error appears as <131>.

for level in ("debug", "info", "warning", "error", "critical"):
    getattr(log, level)(f"probe {level}")

Expected Output (decoded by the receiver):

<135> billing: DEBUG probe debug
<134> billing: INFO probe info
<132> billing: WARNING probe warning
<131> billing: ERROR probe error
<130> billing: CRITICAL probe critical

For RFC 5424 receivers, note that SysLogHandler emits a traditional BSD-style (RFC 3164) header by default. If your daemon enforces strict RFC 5424 framing, prepend a structured header in the formatter or place an RFC-aware relay in front of Python; the handler itself does not generate the RFC 5424 version digit, ISO-8601 timestamp, hostname, app-name, and structured-data fields for you. A common production pattern is to let Python speak the simple BSD format to a local rsyslog or syslog-ng instance, and let that daemon re-frame messages as RFC 5424 before forwarding them upstream over TLS. This keeps the application code minimal while still satisfying receivers that require the modern format, and it centralizes transport reliability and encryption in the daemon rather than the handler. When you must emit RFC 5424 directly from Python, remember that the priority value rules are unchanged: the <131>-style prefix is still facility * 8 + severity, only the surrounding header structure differs.

Common mistakes

Expecting CRITICAL to become syslog emergency. Python's highest level maps to severity 2 (critical), not 0 (emergency). Alerting rules that watch for emergency will never fire from stock Python logging. Map a dedicated custom level to emergency if you truly need it.

Custom levels silently downgraded to warning. mapPriority returns warning for any name absent from priority_map. Register new level names with addLevelName and add a matching priority_map entry, or severity-based filters on the daemon will misclassify them.

Using UDP for audit-grade logs. The default datagram transport drops messages under pressure with no error surfaced to the application. For records that must not be lost, construct the handler with socktype=socket.SOCK_STREAM to use TCP.

Frequently Asked Questions

Why do all my Python logs show up as syslog severity 'notice'?

SysLogHandler maps any level it does not recognize to NOTICE (severity 5). This happens when a custom or numeric level name is not present in the handler's priority map, so the handler falls back to its default rather than the severity you expected.

Does Python's WARNING map to syslog WARNING?

Yes. Python WARNING maps to syslog severity 4 (warning). The mismatch people hit is at the top: Python CRITICAL maps to syslog severity 2 (critical), not 0 (emergency), because Python has no emergency or alert equivalent.

Should I use UDP or TCP for SysLogHandler?

UDP is the default and is lossy under pressure, which is acceptable for high-volume non-critical logs. Use TCP when you cannot tolerate dropped records, and a TLS-wrapped TCP socket when the syslog path crosses an untrusted network.

What is the difference between facility and severity?

Severity describes how urgent a single message is, from 0 emergency to 7 debug. Facility describes which subsystem produced it, such as local0 to local7 for application use. The two combine into the syslog priority value as facility times eight plus severity.