std.syslog
The std.syslog module provides UDP, TCP, and TLS clients and servers, an
RFC 5424 message codec, and a decoder
for canonical RFC 3164 messages.
The UDP transport sends one message per datagram using RFC 5426. The TCP transport uses RFC 6587 octet counting. Pass the destination or listen port explicitly. The standard syslog UDP port is 514. The examples use loopback port 5514.
Sending UDP messages
import net
import std.syslog as syslog
actor main(env):
def finish(client: syslog.Client):
client.close()
env.exit(0)
def connected(client: syslog.Client):
client.log("Hello from Acton", severity=syslog.INFO,
facility=syslog.LOCAL0, hostname="localhost",
app_name="syslog-client",
structured_data={"event@32473": [("instance", "one")]})
after 0.2: finish(client)
def failed(client: syslog.Client, error: str):
print("Syslog client error:", error)
client.close()
env.exit(1)
cap = net.UDPConnectCap(net.UDPCap(net.NetCap(env.cap)))
syslog.Client(cap, "127.0.0.1", 5514, connected, failed)
Client(cap, address, port, on_connect, on_error, max_size=2048, max_pending=64) resolves the destination and opens the UDP socket
asynchronously. on_connect(client) means the local socket is ready to send;
it does not confirm that a server exists at the destination. send(message)
queues messages before that callback, up to max_pending entries. Invalid
messages, messages exceeding max_size, and a full startup queue are reported
through on_error(client, error).
log(text, severity=INFO, facility=USER, hostname=None, app_name=None, procid=None, msgid=None, structured_data={}) supplies the current UTC timestamp
and encodes text as UTF-8 with a BOM. Use send(Message(...)) to choose the
timestamp and body bytes yourself.
UDP has no delivery acknowledgement. Messages can be lost, duplicated, or
reordered. The client does not retry. close() discards queued messages and
does not wait for delivery. The short delay in this example lets the
asynchronous send run before the program exits; it is not a delivery guarantee.
In a long-running application, keep the client open for subsequent messages.
Application logging
Sink(send, facility=USER, hostname=None, app_name=None, procid=None, msgid=None) connects the logging module to a syslog client. Pass
client.send from either Client or StreamClient; this works with UDP,
TCP, and TLS. Add the sink to a logging.Handler as usual:
import logging
import net
import std.syslog as syslog
actor main(env):
def connected(client: syslog.Client):
pass
def failed(client: syslog.Client, error: str):
print("Syslog client error:", error, err=True)
cap = net.UDPConnectCap(net.UDPCap(net.NetCap(env.cap)))
client = syslog.Client(cap, "127.0.0.1", 5514, connected, failed)
handler = logging.Handler("service")
handler.add_sink(syslog.Sink(client.send, facility=syslog.LOCAL0,
hostname="host.example.com"))
log = logging.Logger(handler)
log.info("Service started", {"version": "1.0"})
APP-NAME defaults to the outermost named handler in the record's path
(service above). Logger names are excluded. Set app_name to override this
default. If there is no named handler, or its name cannot fit the syslog
header rules (1–48 printable ASCII characters without spaces), APP-NAME
uses NILVALUE. The full path remains available in the formatted body.
The sink maps logging levels to syslog severities by name, including the
different INFO and NOTICE values. VERBOSE and TRACE become syslog
DEBUG. Handler and logger level filtering still apply.
The RFC 5424 timestamp comes from the original log record, retaining its UTC
offset and truncating fractions to microseconds. A record without a timezone
uses NILVALUE. The body uses UTF-8 with a BOM and logging.DefaultFormatter,
which includes the handler path, logger name, actor class, and actor ID.
Structured log data is appended to the body as a dictionary, matching the
console sinks; it is not converted to RFC 5424 structured-data elements.
Use sink.set_formatter(formatter) to customize the body. A formatter that
returns None suppresses that record, including its data.
The application owns the client and closes it when finished. Startup buffering,
size limits, transport errors, and send rejections use the client's existing
behavior and on_error callback. The sink adds no retries or delivery guarantee.
Receiving UDP messages
import net
import std.syslog as syslog
actor main(env):
def bound(server: syslog.Server, error: ?str):
if error is not None:
print("Syslog server could not listen:", error)
env.exit(1)
else:
print("Listening on", server.local_address(), server.local_port())
def received(server: syslog.Server, message: syslog.Message,
peer: str, port: int):
print("{peer}:{port} facility={message.facility} severity={message.severity}")
print("timestamp:", message.timestamp, "hostname:", message.hostname,
"app:", message.app_name, "message:", repr(message.message))
def failed(server: syslog.Server, error: str):
print("Syslog server error:", error)
cap = net.UDPListenCap(net.UDPCap(net.NetCap(env.cap)))
syslog.Server(cap, "127.0.0.1", 5514, bound, received, failed)
Server(cap, address, port, on_listen, on_message, on_error, max_size=65507, legacy=True) calls on_listen(server, error) when binding
finishes, with None on success. on_message(server, message, peer, port)
provides the parsed message and the source address and port. Malformed or
oversized datagrams are discarded and reported to on_error(server, error);
the listener remains available for subsequent datagrams. Set legacy=False
to accept only RFC 5424 messages.
The server runs until closed or the program exits. close() is safe to call
repeatedly. Port 0 requests an ephemeral port; query local_port() after the
successful on_listen callback to find the selected port. local_address()
returns the bound address. Both return an empty value before the listener is
ready or after it is closed.
Both actors accept max_size from 480 through 65507 bytes. These limits apply
to the entire encoded message. Choose a limit that suits the receiving system
and network; larger UDP datagrams may require IP fragmentation.
Stream transports
StreamClient(cap, address, port, on_connect, on_error, max_size=65536, max_pending=64, connect_timeout=10.0) accepts a net.TCPConnectCap.
Its send() and log() methods accept the same messages and fields as the
UDP client. on_connect(client) runs when the TCP connection is ready;
connect_timeout covers the whole connection attempt, including DNS lookup.
The startup queue holds at most max_pending messages. Invalid messages,
oversized messages, and a full startup queue are reported through on_error.
StreamServer(cap, address, port, on_listen, on_message, on_error, max_size=65536, max_connections=64, legacy=True) accepts a net.TCPListenCap.
Its callbacks have the same arguments as the UDP server, with the TCP peer's
address and port included in on_message. Port 0 selects an ephemeral port;
query local_port() after on_listen succeeds. local_address() returns the
bound address.
Both stream actors require max_size of at least 480 bytes. This limits the
encoded message, excluding its length prefix. The server buffers at most one
partial message per connection and accepts at most max_connections open
connections. It rejects excess connections through on_error.
Malformed complete messages are reported and skipped. Invalid framing or a
truncated frame at EOF closes the affected connection; complete messages
preceding the error are still delivered. Other connections remain usable.
Set legacy=False to reject RFC 3164 messages.
Transport errors close the client and call on_error. Messages are not
retried, since the receiver may already have processed them. Client close()
cancels connection setup and discards startup messages; writes already handed
to TCP are drained before closing. Server close() stops listening and closes
its accepted connections. Repeated calls are safe. TCP supplies no syslog
application acknowledgement.
TLS client
Set tls=True on StreamClient for TLS with the same octet-counting framing.
The standard TLS syslog port is 6514. The server certificate and hostname are
verified by default. ca_pem supplies trusted CA certificates as PEM bytes;
otherwise the system CA bundle is used. server_name selects the expected
hostname and SNI name, defaulting to address.
syslog.StreamClient(cap, "192.0.2.10", 6514, connected, failed,
tls=True, ca_pem=ca_pem, server_name="logs.example.com")
Supply cert_pem and key_pem to present a client certificate. Both must be
provided together. verify_peer(certificates, result) can apply additional
policy to the server's DER certificate chain and verification result. With
verify_tls=True, it can reject a verified certificate; with verify_tls=False,
it makes the trust decision itself. Its return value must be True to proceed.
server_name="" disables SNI and hostname checking. TLS options supplied without
tls=True are rejected.
connect_timeout includes the TLS handshake and verify_peer. A timeout or
verification failure calls on_error and discards queued messages. Closing
during setup cancels the attempt. TLS 1.3 client-certificate rejection can
arrive after on_connect, so a connection callback is not an acknowledgement
that any syslog message was accepted.
TLS server
Supply PEM bytes in both cert_pem and key_pem to enable TLS on StreamServer:
syslog.StreamServer(cap, "0.0.0.0", 6514, bound, received, failed,
cert_pem=cert_pem, key_pem=key_pem,
client_auth=True, ca_pem=ca_pem)
With client_auth=True, the default verify_tls=True requires a trusted
client certificate, verified against ca_pem or the system CA bundle.
verify_peer(certificates, result) can apply additional policy and requires
client_auth=True. Setting verify_tls=False with client authentication
requires verify_peer, which decides whether to accept the presented chain,
including a missing certificate. The default client_auth=False does not
authenticate clients. Missing certificate/key pairs and TLS security options
on a plaintext listener are rejected.
After binding succeeds, failed handshakes are reported through on_error and
the server keeps listening. handshake_timeout=60.0 limits incomplete TLS
handshakes; zero disables that deadline. It ends before the server's
verify_peer callback, which should return promptly. max_connections counts
accepted sessions, not pending handshakes or verification callbacks. Server
close() aborts incomplete handshakes and closes accepted sessions.
Framing
TCP and TLS syslog use the same octet-counting framing: each message starts with its byte length and a space. Newlines and binary content remain part of the body; newline-delimited framing is not supported.
frame(message, max_size=65536) -> bytes encodes one RFC 5424 message with
that prefix. StreamDecoder(max_size=65536) incrementally splits incoming
bytes into complete wire messages. Both require a positive max_size.
feed(data) returns (messages, error), where error is None on success.
Pass each message to decode() or decode_legacy(). Process returned messages
before handling a framing error: a chunk may contain valid messages followed
by invalid framing. A failed decoder remains failed. finish() raises
ValueError for incomplete input at EOF or a previous framing error.
Message codec
Message(message=None, facility=USER, severity=INFO, timestamp=None, hostname=None, app_name=None, procid=None, msgid=None, structured_data={}, version=1) stores a message independently of the network. The constructor
stores fields without validation; encode(message) -> bytes validates them
and produces RFC 5424 version 1. decode(data) -> Message parses that format.
Malformed input or invalid fields raise ValueError. These functions have no
transport size limit.
Missing header values use None, which maps to the protocol's - marker.
timestamp() returns the current UTC time with six fractional digits.
The codec validates calendar dates and RFC 5424 timestamps, including explicit
UTC offsets. Leap seconds and more than six fractional digits are rejected.
The message field is bytes. None omits MSG; b"" represents an explicitly
empty MSG. Arbitrary bytes are preserved. A body beginning with the UTF-8 BOM
b"\xef\xbb\xbf" must contain valid UTF-8; decoding preserves the BOM. This is
why the server example prints repr(message.message) instead of assuming every
body can be decoded as text.
structured_data is a dict[str, list[(name: str, value: str)]]. Each key is
an SD-ID and its value is an ordered list of parameter pairs. Parameters accept
both positional values such as ("instance", "one") and named values such as
(name="instance", value="one"). Read them using param.name and param.value.
The codec escapes quotes, backslashes, and closing brackets in values. It preserves dictionary insertion order and parameter order, including repeated parameter names. The dictionary makes SD-IDs unique; decoding rejects duplicate SD-IDs in wire data. Use an enterprise-qualified ID for private structured data, for example:
event = syslog.Message(
b"service started",
facility=syslog.LOCAL0,
severity=syslog.NOTICE,
timestamp=syslog.timestamp(),
hostname="host.example.com",
app_name="service",
structured_data={"event@32473": [("instance", "one")]}
)
wire = syslog.encode(event)
parsed = syslog.decode(wire)
param = parsed.structured_data["event@32473"][0]
print("{param.name}={param.value}")
Facility numbers range from 0 through 23. Constants include KERN,
USER, MAIL, DAEMON, AUTH, SYSLOG, and LOCAL0 through LOCAL7.
The wire priority is facility * 8 + severity. Severity constants use syslog's
conventional numbering:
| Constant | Value |
|---|---|
EMERGENCY | 0 |
ALERT | 1 |
CRITICAL | 2 |
ERROR | 3 |
WARNING | 4 |
NOTICE | 5 |
INFO | 6 |
DEBUG | 7 |
Use these constants from syslog. The separate logging module has different
level values, so logging.INFO is not a syslog severity.
Legacy messages
decode_legacy(data) -> Message accepts canonical RFC 3164 packets with the
shape <PRI>Mmm dd hh:mm:ss HOST MSG. A single-digit day uses a leading space.
The resulting message has version=0; timestamp retains the original text
without inventing a year or timezone. message retains the entire MSG,
including its TAG and any process ID. The remaining RFC 5424 fields are absent.
The decoder rejects missing headers and does not guess vendor-specific forms.
encode() and Client.send() reject version 0 messages. To forward a legacy
event as RFC 5424, construct a new version 1 Message and explicitly choose
how to supply its timestamp and other fields.