From 5421f35d5e215a69382f0c195c0c39c8b541eca7 Mon Sep 17 00:00:00 2001 From: Vinay Sajip Date: Fri, 27 Sep 2013 18:18:28 +0100 Subject: logging: added support for Unix domain sockets to SocketHandler and DatagramHandler. --- Doc/howto/logging.rst | 4 +- Doc/library/logging.handlers.rst | 6 ++ Lib/logging/handlers.py | 20 ++++++- Lib/test/test_logging.py | 118 +++++++++++++++++++++++++++++++++------ Misc/NEWS | 3 + 5 files changed, 130 insertions(+), 21 deletions(-) diff --git a/Doc/howto/logging.rst b/Doc/howto/logging.rst index 165486a..c7a802a 100644 --- a/Doc/howto/logging.rst +++ b/Doc/howto/logging.rst @@ -900,10 +900,10 @@ provided: disk files, rotating the log file at certain timed intervals. #. :class:`~handlers.SocketHandler` instances send messages to TCP/IP - sockets. + sockets. Since 3.4, Unix domain sockets are also supported. #. :class:`~handlers.DatagramHandler` instances send messages to UDP - sockets. + sockets. Since 3.4, Unix domain sockets are also supported. #. :class:`~handlers.SMTPHandler` instances send messages to a designated email address. diff --git a/Doc/library/logging.handlers.rst b/Doc/library/logging.handlers.rst index 65e0b67..f52060a 100644 --- a/Doc/library/logging.handlers.rst +++ b/Doc/library/logging.handlers.rst @@ -381,6 +381,9 @@ sends logging output to a network socket. The base class uses a TCP socket. Returns a new instance of the :class:`SocketHandler` class intended to communicate with a remote machine whose address is given by *host* and *port*. + .. versionchanged:: 3.4 + If ``port`` is specified as ``None``, a Unix domain socket is created + using the value in ``host`` - otherwise, a TCP socket is created. .. method:: close() @@ -466,6 +469,9 @@ over UDP sockets. Returns a new instance of the :class:`DatagramHandler` class intended to communicate with a remote machine whose address is given by *host* and *port*. + .. versionchanged:: 3.4 + If ``port`` is specified as ``None``, a Unix domain socket is created + using the value in ``host`` - otherwise, a TCP socket is created. .. method:: emit() diff --git a/Lib/logging/handlers.py b/Lib/logging/handlers.py index 0ae6e45..b0b0a16 100644 --- a/Lib/logging/handlers.py +++ b/Lib/logging/handlers.py @@ -494,6 +494,10 @@ class SocketHandler(logging.Handler): logging.Handler.__init__(self) self.host = host self.port = port + if port is None: + self.address = host + else: + self.address = (host, port) self.sock = None self.closeOnError = False self.retryTime = None @@ -509,7 +513,13 @@ class SocketHandler(logging.Handler): A factory method which allows subclasses to define the precise type of socket they want. """ - return socket.create_connection((self.host, self.port), timeout=timeout) + if self.port is not None: + result = socket.create_connection(self.address, timeout=timeout) + else: + result = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + result.settimeout(timeout) + result.connect(self.address) + return result def createSocket(self): """ @@ -643,7 +653,11 @@ class DatagramHandler(SocketHandler): The factory method of SocketHandler is here overridden to create a UDP socket (SOCK_DGRAM). """ - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + if self.port is None: + family = socket.AF_UNIX + else: + family = socket.AF_INET + s = socket.socket(family, socket.SOCK_DGRAM) return s def send(self, s): @@ -656,7 +670,7 @@ class DatagramHandler(SocketHandler): """ if self.sock is None: self.createSocket() - self.sock.sendto(s, (self.host, self.port)) + self.sock.sendto(s, self.address) class SysLogHandler(logging.Handler): """ diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py index 85e1898..cbdc78c 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -59,7 +59,9 @@ try: import smtpd from urllib.parse import urlparse, parse_qs from socketserver import (ThreadingUDPServer, DatagramRequestHandler, - ThreadingTCPServer, StreamRequestHandler) + ThreadingTCPServer, StreamRequestHandler, + ThreadingUnixStreamServer, + ThreadingUnixDatagramServer) except ImportError: threading = None try: @@ -854,6 +856,9 @@ if threading: super(TestTCPServer, self).server_bind() self.port = self.socket.getsockname()[1] + class TestUnixStreamServer(TestTCPServer): + address_family = socket.AF_UNIX + class TestUDPServer(ControlMixin, ThreadingUDPServer): """ A UDP server which is controllable using :class:`ControlMixin`. @@ -901,6 +906,9 @@ if threading: super(TestUDPServer, self).server_close() self._closed = True + class TestUnixDatagramServer(TestUDPServer): + address_family = socket.AF_UNIX + # - end of server_helper section @unittest.skipUnless(threading, 'Threading required for this test.') @@ -1358,17 +1366,22 @@ class SocketHandlerTest(BaseTest): """Test for SocketHandler objects.""" + server_class = TestTCPServer + address = ('localhost', 0) + def setUp(self): """Set up a TCP server to receive log messages, and a SocketHandler pointing to that server's address and port.""" BaseTest.setUp(self) - addr = ('localhost', 0) - self.server = server = TestTCPServer(addr, self.handle_socket, - 0.01) + self.server = server = self.server_class(self.address, + self.handle_socket, 0.01) server.start() server.ready.wait() - self.sock_hdlr = logging.handlers.SocketHandler('localhost', - server.port) + hcls = logging.handlers.SocketHandler + if isinstance(server.server_address, tuple): + self.sock_hdlr = hcls('localhost', server.port) + else: + self.sock_hdlr = hcls(server.server_address, None) self.log_output = '' self.root_logger.removeHandler(self.root_logger.handlers[0]) self.root_logger.addHandler(self.sock_hdlr) @@ -1426,20 +1439,45 @@ class SocketHandlerTest(BaseTest): @unittest.skipUnless(threading, 'Threading required for this test.') +class UnixSocketHandlerTest(SocketHandlerTest): + + """Test for SocketHandler with unix sockets.""" + + server_class = TestUnixStreamServer + + def setUp(self): + # override the definition in the base class + fd, self.address = tempfile.mkstemp(prefix='test_logging_', + suffix='.sock') + os.close(fd) + os.remove(self.address) # just need a name - file can't be present + SocketHandlerTest.setUp(self) + + def tearDown(self): + SocketHandlerTest.tearDown(self) + os.remove(self.address) + +@unittest.skipUnless(threading, 'Threading required for this test.') class DatagramHandlerTest(BaseTest): """Test for DatagramHandler.""" + server_class = TestUDPServer + address = ('localhost', 0) + def setUp(self): """Set up a UDP server to receive log messages, and a DatagramHandler pointing to that server's address and port.""" BaseTest.setUp(self) - addr = ('localhost', 0) - self.server = server = TestUDPServer(addr, self.handle_datagram, 0.01) + self.server = server = self.server_class(self.address, + self.handle_datagram, 0.01) server.start() server.ready.wait() - self.sock_hdlr = logging.handlers.DatagramHandler('localhost', - server.port) + hcls = logging.handlers.DatagramHandler + if isinstance(server.server_address, tuple): + self.sock_hdlr = hcls('localhost', server.port) + else: + self.sock_hdlr = hcls(server.server_address, None) self.log_output = '' self.root_logger.removeHandler(self.root_logger.handlers[0]) self.root_logger.addHandler(self.sock_hdlr) @@ -1474,21 +1512,45 @@ class DatagramHandlerTest(BaseTest): @unittest.skipUnless(threading, 'Threading required for this test.') +class UnixDatagramHandlerTest(DatagramHandlerTest): + + """Test for DatagramHandler using Unix sockets.""" + + server_class = TestUnixDatagramServer + + def setUp(self): + # override the definition in the base class + fd, self.address = tempfile.mkstemp(prefix='test_logging_', + suffix='.sock') + os.close(fd) + os.remove(self.address) # just need a name - file can't be present + DatagramHandlerTest.setUp(self) + + def tearDown(self): + DatagramHandlerTest.tearDown(self) + os.remove(self.address) + +@unittest.skipUnless(threading, 'Threading required for this test.') class SysLogHandlerTest(BaseTest): """Test for SysLogHandler using UDP.""" + server_class = TestUDPServer + address = ('localhost', 0) + def setUp(self): """Set up a UDP server to receive log messages, and a SysLogHandler pointing to that server's address and port.""" BaseTest.setUp(self) - addr = ('localhost', 0) - self.server = server = TestUDPServer(addr, self.handle_datagram, - 0.01) + self.server = server = self.server_class(self.address, + self.handle_datagram, 0.01) server.start() server.ready.wait() - self.sl_hdlr = logging.handlers.SysLogHandler(('localhost', - server.port)) + hcls = logging.handlers.SysLogHandler + if isinstance(server.server_address, tuple): + self.sl_hdlr = hcls(('localhost', server.port)) + else: + self.sl_hdlr = hcls(server.server_address) self.log_output = '' self.root_logger.removeHandler(self.root_logger.handlers[0]) self.root_logger.addHandler(self.sl_hdlr) @@ -1526,6 +1588,29 @@ class SysLogHandlerTest(BaseTest): @unittest.skipUnless(threading, 'Threading required for this test.') +class UnixSysLogHandlerTest(SysLogHandlerTest): + + """Test for SysLogHandler with Unix sockets.""" + + server_class = TestUnixDatagramServer + + def setUp(self): + # override the definition in the base class + fd, self.address = tempfile.mkstemp(prefix='test_logging_', + suffix='.sock') + os.close(fd) + os.remove(self.address) # just need a name - file can't be present + SysLogHandlerTest.setUp(self) + + def tearDown(self): + SysLogHandlerTest.tearDown(self) + os.remove(self.address) + +# def test_output(self): +# import pdb; pdb.set_trace() +# SysLogHandlerTest.test_output(self) + +@unittest.skipUnless(threading, 'Threading required for this test.') class HTTPHandlerTest(BaseTest): """Test for HTTPHandler.""" @@ -4034,7 +4119,8 @@ def test_main(): SMTPHandlerTest, FileHandlerTest, RotatingFileHandlerTest, LastResortTest, LogRecordTest, ExceptionTest, SysLogHandlerTest, HTTPHandlerTest, NTEventLogHandlerTest, - TimedRotatingFileHandlerTest + TimedRotatingFileHandlerTest, UnixSocketHandlerTest, + UnixDatagramHandlerTest, UnixSysLogHandlerTest ) if __name__ == "__main__": diff --git a/Misc/NEWS b/Misc/NEWS index 0b34293..6239614 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -15,6 +15,9 @@ Core and Builtins Library ------- +- logging: added support for Unix domain sockets to SocketHandler and + DatagramHandler. + - Issue #18996: TestCase.assertEqual() now more cleverly shorten differing strings in error report. -- cgit v0.12