From 44626e904c14c58ccc5af8c7753f222e9ae9f6f7 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 23 Jul 2026 18:52:02 +0300 Subject: [PATCH 1/2] gh-96339: Support address=None in SysLogHandler to use the syslog module SysLogHandler now logs to the local system logger via the syslog module when the address is None. This works even where there is no syslog socket, such as on recent versions of macOS. Co-Authored-By: Claude Opus 4.8 (1M context) --- Doc/library/logging.handlers.rst | 12 ++++++++-- Doc/whatsnew/3.16.rst | 5 ++++ Lib/logging/handlers.py | 23 +++++++++++++++---- Lib/test/test_logging.py | 22 ++++++++++++++++++ ...6-07-23-15-50-43.gh-issue-96339.rs3Pr1.rst | 3 +++ 5 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-23-15-50-43.gh-issue-96339.rs3Pr1.rst diff --git a/Doc/library/logging.handlers.rst b/Doc/library/logging.handlers.rst index 5152c7561fa1f2..a1f0f05c2366fa 100644 --- a/Doc/library/logging.handlers.rst +++ b/Doc/library/logging.handlers.rst @@ -632,7 +632,11 @@ supports sending logging messages to a remote or local Unix syslog. ``('localhost', 514)`` is used. The address is used to open a socket. An alternative to providing a ``(host, port)`` tuple is providing an address as a string, for example '/dev/log'. In this case, a Unix domain socket is used to - send the message to the syslog. If *facility* is not specified, + send the message to the syslog. + If *address* is ``None``, the :mod:`syslog` module is used to log to the + local system logger; this works even where there is no syslog socket, such + as on recent versions of macOS. + If *facility* is not specified, :const:`LOG_USER` is used. The type of socket opened depends on the *socktype* argument, which defaults to :const:`socket.SOCK_DGRAM` and thus opens a UDP socket. To open a TCP socket (for use with the newer syslog @@ -654,7 +658,8 @@ supports sending logging messages to a remote or local Unix syslog. .. note:: On macOS 12.x (Monterey), Apple has changed the behaviour of their syslog daemon - it no longer listens on a domain socket. Therefore, you cannot - expect :class:`SysLogHandler` to work on this system. + expect :class:`SysLogHandler` to work on this system with the default + *address*. Pass ``address=None`` to use the :mod:`syslog` module instead. See :gh:`91070` for more information. @@ -664,6 +669,9 @@ supports sending logging messages to a remote or local Unix syslog. .. versionchanged:: 3.14 *timeout* was added. + .. versionchanged:: next + *address* can now be ``None`` to use the :mod:`syslog` module. + .. method:: close() Closes the socket to the remote host. diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 662defa709a246..f79127cca4d92d 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -349,6 +349,11 @@ logging before the rotation interval expires. (Contributed by Iván Márton and Serhiy Storchaka in :gh:`84649`.) +* :class:`~logging.handlers.SysLogHandler` now accepts ``address=None`` to log + to the local system logger via the :mod:`syslog` module, which works even + where there is no syslog socket, such as on recent versions of macOS. + (Contributed by Serhiy Storchaka in :gh:`96339`.) + lzma ---- diff --git a/Lib/logging/handlers.py b/Lib/logging/handlers.py index a5394d2dbea649..71220f54506d43 100644 --- a/Lib/logging/handlers.py +++ b/Lib/logging/handlers.py @@ -888,6 +888,9 @@ def __init__(self, address=('localhost', SYSLOG_UDP_PORT), If address is specified as a string, a UNIX socket is used. To log to a local syslogd, "SysLogHandler(address="/dev/log")" can be used. + If address is None, the syslog module is used to log to the local + system logger; this works even where there is no syslog socket, such + as on recent versions of macOS. If facility is not specified, LOG_USER is used. If socktype is specified as socket.SOCK_DGRAM or socket.SOCK_STREAM, that specific socket type will be used. For Unix sockets, you can also specify a @@ -901,7 +904,12 @@ def __init__(self, address=('localhost', SYSLOG_UDP_PORT), self.socktype = socktype self.timeout = timeout self.socket = None - self.createSocket() + if address is None: + # Use the syslog module to log to the local system logger. + import syslog + self.syslog = syslog + else: + self.createSocket() def _connect_unixsocket(self, address): use_socktype = self.socktype @@ -1023,13 +1031,18 @@ def emit(self, record): msg = self.format(record) if self.ident: msg = self.ident + msg - if self.append_nul: - msg += '\000' # We need to convert record level to lowercase, maybe this will # change in the future. - prio = '<%d>' % self.encodePriority(self.facility, - self.mapPriority(record.levelname)) + prio = self.encodePriority(self.facility, + self.mapPriority(record.levelname)) + if self.address is None: + self.syslog.syslog(prio, msg) + return + + if self.append_nul: + msg += '\000' + prio = '<%d>' % prio prio = prio.encode('utf-8') # Message is a string. Convert to bytes as required by RFC 5424 msg = msg.encode('utf-8') diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py index 06b3aa66fc47a3..8e4d20065463fa 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -72,6 +72,11 @@ except ImportError: win32evtlog = win32evtlogutil = pywintypes = None +try: + import syslog +except ImportError: + syslog = None + try: import zlib except ImportError: @@ -2155,6 +2160,23 @@ def tearDown(self): self.server_class.address_family = socket.AF_INET super(IPv6SysLogHandlerTest, self).tearDown() +@unittest.skipUnless(syslog, 'syslog module required') +class LocalSysLogHandlerTest(BaseTest): + + """Test for SysLogHandler using the syslog module (address=None).""" + + def test_emit(self): + h = logging.handlers.SysLogHandler(address=None) + self.addCleanup(h.close) + h.setFormatter(logging.Formatter('%(message)s')) + record = self.next_message() + logrec = logging.makeLogRecord({'msg': record, 'levelname': 'WARNING', + 'levelno': logging.WARNING}) + with patch.object(syslog, 'syslog') as mock_syslog: + h.emit(logrec) + mock_syslog.assert_called_once_with( + syslog.LOG_USER | syslog.LOG_WARNING, record) + @support.requires_working_socket() @threading_helper.requires_working_threading() class HTTPHandlerTest(BaseTest): diff --git a/Misc/NEWS.d/next/Library/2026-07-23-15-50-43.gh-issue-96339.rs3Pr1.rst b/Misc/NEWS.d/next/Library/2026-07-23-15-50-43.gh-issue-96339.rs3Pr1.rst new file mode 100644 index 00000000000000..b91ef62ec8b76e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-23-15-50-43.gh-issue-96339.rs3Pr1.rst @@ -0,0 +1,3 @@ +:class:`logging.handlers.SysLogHandler` now accepts ``address=None`` to log to +the local system logger via the :mod:`syslog` module. This works even where +there is no syslog socket, such as on recent versions of macOS. From b97136222c19cca2c46c3715dc002443ddfd00d6 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 24 Jul 2026 20:29:50 +0300 Subject: [PATCH 2/2] gh-96339: Reject socktype and timeout when address is None They are not applicable to the syslog module and were silently ignored. Also set unixsocket to False in this case. Co-Authored-By: Claude Opus 4.8 (1M context) --- Doc/library/logging.handlers.rst | 7 +++++-- Lib/logging/handlers.py | 5 +++++ Lib/test/test_logging.py | 8 ++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/Doc/library/logging.handlers.rst b/Doc/library/logging.handlers.rst index 570e890ed98578..b3af53920901a7 100644 --- a/Doc/library/logging.handlers.rst +++ b/Doc/library/logging.handlers.rst @@ -635,8 +635,11 @@ supports sending logging messages to a remote or local Unix syslog. In this case, a Unix domain socket is used to send the message to the syslog. If *address* is ``None``, the :mod:`syslog` module is used to log to the - local system logger; this works even where there is no syslog socket, such - as on recent versions of macOS. + local system logger; + this works even where there is no syslog socket, + such as on recent versions of macOS. + In this case *socktype* and *timeout* are not applicable + and passing them raises :exc:`ValueError`. If *facility* is not specified, :const:`LOG_USER` is used. The type of socket opened depends on the *socktype* argument, which defaults to :const:`socket.SOCK_DGRAM` and thus diff --git a/Lib/logging/handlers.py b/Lib/logging/handlers.py index fdfed3cc31a6bc..e9c3ef42b5f70b 100644 --- a/Lib/logging/handlers.py +++ b/Lib/logging/handlers.py @@ -907,7 +907,12 @@ def __init__(self, address=('localhost', SYSLOG_UDP_PORT), self.socket = None if address is None: # Use the syslog module to log to the local system logger. + if socktype is not None: + raise ValueError("socktype is not supported if address is None") + if timeout is not None: + raise ValueError("timeout is not supported if address is None") import syslog + self.unixsocket = False self.syslog = syslog else: self.createSocket() diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py index a3edb63d044ad2..4425eb05c4dfdd 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -2207,6 +2207,7 @@ class LocalSysLogHandlerTest(BaseTest): def test_emit(self): h = logging.handlers.SysLogHandler(address=None) self.addCleanup(h.close) + self.assertFalse(h.unixsocket) h.setFormatter(logging.Formatter('%(message)s')) record = self.next_message() logrec = logging.makeLogRecord({'msg': record, 'levelname': 'WARNING', @@ -2216,6 +2217,13 @@ def test_emit(self): mock_syslog.assert_called_once_with( syslog.LOG_USER | syslog.LOG_WARNING, record) + def test_reject_socktype_and_timeout(self): + with self.assertRaises(ValueError): + logging.handlers.SysLogHandler(address=None, + socktype=socket.SOCK_DGRAM) + with self.assertRaises(ValueError): + logging.handlers.SysLogHandler(address=None, timeout=1) + @support.requires_working_socket() @threading_helper.requires_working_threading() class HTTPHandlerTest(BaseTest):