Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

ConstantValue
EMERGENCY0
ALERT1
CRITICAL2
ERROR3
WARNING4
NOTICE5
INFO6
DEBUG7

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.