Search code examples
python-3.xsyslog

python3/syslog: multiple syslog streams?


My python3 program has a number of submodules, and I want them each to send syslog messages with a different syslog ident value. For example, one of them might send to myprogram/submod0, and another one might send to myprogram/submod1. I use syslog-ng to route these messages to different log files.

What I'd like to do is something like this, which I know is not currently possible in the way that I'm writing it here:

syslog0 = syslog.openlog('myprogram/submod0', 
                         syslog.LOG_PID, syslog.LOG_MAIL)
syslog1 = syslog.openlog('myprogram/submod1',
                         syslog.LOG_PID, syslog.LOG_MAIL)

... and then, within submod0, I want to send syslog messages like this ...

syslog0.syslog('some sort of message')

... and this way within submod1 ...

syslog1.syslog('another message')

But, of course, syslog.openlog doesn't return any kind of object that I can use as a handle in this way.

Is there any way that I can accomplish what I want using the syslog facilities of python3?

I suppose that I could issue a new openlog for each syslog message that I want to send. For example ...

def mysyslog(ident, message):
    syslog.openlog('myprogram/{}'.format(ident),
                   syslog.LOG_PID, syslog.LOG_MAIL)
    syslog.syslog(message)

... and then use mysyslog('submod0', message) within my submod0 and mysyslog('submod1', message) within my submod1. Is this the only way I can accomplish what I want to do?

Thank you in advance.


Solution

  • I couldn't find any existing python modules which do what I want, so I decided to write my own syslog wrapper. It is written to open a syslog connection based on either host:port or a socket file such as /dev/log, and to then accept all of the other parameters like facility, severity, program, etc. on each call to send a syslog message.

    With all the parameters at the level of the individual logging method call, this class could be wrapped at a higher level to provide handles to, for example, unique connections via program, which is what I have specified in my original question here.

    I have only tested the following code with python3.6 and in the /dev/log case. It works for me, but use it at your own risk.

    #!/usr/bin/python3.6
    
    import os
    import sys
    import socket
    import datetime
    
    # Only imported for the syslog.LOG_INFO and syslog.LOG_USER constants.
    import syslog
    
    # Appends a newline in all cases.
    def _log_stderr(message):
        if message:
            sys.stderr.write(message)
        sys.stderr.write('\n')
        sys.stderr.flush()
    
    # XSyslog: a syslog wrapper class.
    #
    # This module allows the facility (such as LOG_USER), the
    # severity (such as LOG_INFO), and other syslog parameters
    # to be set on a message-by-message basis via one, single
    # syslog connection.
    #
    # Usage:
    #
    #   slog = XSyslog([server=server], [port=port], [proto=proto],
    #                  [clientname=clientname], [maxlen=maxlen])
    #
    # This allows three  cases:
    # (1) Connect to syslog via UDP to a host and port:
    #     Specify host, port, and proto='UDP'.
    # (2) Connect to syslog via TCP to a host and port:
    #     Specify host, port, and proto='TCP'.
    # (3) Connect to syslog via a socket file such as /dev/log.
    #     Specify proto=filename (e.g., proto='/dev/log').
    #     In this case, host and port are ignored.
    #
    # clientname is an optional field for the syslog message.
    # maxlen is the maximum message length.
    #
    # Once the XSyslog object is created, the message can be sent as follows:
    #
    #   slog = XSyslog([... parameters ...])
    #   slog.log(message, [facility=facility], [severity=severity],
    #                     [timestamp=timestamp], [hostame=hostname],
    #                     [program=program], [pid=pid])
    #     facility  defaults to LOG_USER
    #     severity  defaults to LOG_INFO
    #     timestamp defaults to now
    #     hostname  if None, use clientname if it exists; if '', no hostname.
    #     program   defaults to "logger"
    #     pid       defaults to os.getpid()
    
    class XSyslog(object):
    
        def __init__(self, server=None, port=None, proto='udp', clientname=None, maxlen=1024):
            self.server       = server
            self.port         = port
            self.proto        = socket.SOCK_DGRAM
            self.clientname   = None
            self.maxlen       = maxlen
            self._protoname   = ''
            self._socket      = None
            self._sockfile    = None
            self._connectargs = ()
            self._me          = os.path.splitext(self.__class__.__name__)[1][1:]
    
            if proto:
                if proto.lower() == 'udp':
                    self._protoname  = proto.lower()
                    self.proto       = socket.SOCK_DGRAM
                    self._socketargs = (self.server, self.port, socket.AF_UNSPEC, self.proto)
                elif proto.lower() == 'tcp':
                    self._protoname  = proto.lower()
                    self.proto       = socket.SOCK_STREAM
                    self._socketargs = (self.server, self.port, socket.AF_UNSPEC, self.proto)
                elif len(proto) > 0:
                    self._sockfile   = proto
                    self._protoname  = self._sockfile
                    self.proto       = socket.SOCK_DGRAM
                    self._socketargs = (socket.AF_UNIX, self.proto)
    
            badargs = False
            if self._sockfile:
                pass
            elif self.server and self.port:
                pass
            else:
                badargs = True
            if not self.proto:
                badargs = True
            if badargs:
                raise ValueError("'proto' must be 'udp' or 'tcp' with 'server' and 'port', or else a socket filename like '/dev/log'")
    
            if not self.clientname:
                try:
                    self.clientname = socket.getfqdn()
                    if not self.clientname:
                        self.clientname = socket.gethostname()
                except:
                    self.clientname = None
    
        def _connect(self):
            if self._socket is None:
                if self._sockfile:
                    self._socket = socket.socket(*self._socketargs)
                    if not self._socket:
                        _log_stderr(':::::::: {}: unable to open socket file {}'.format(self._me, self._sockfile))
                        return False
                    try:
                        self._socket.connect(self._sockfile)
                        return True
                    except socket.timeout as e:
                        _log_stderr(':::::::: {}: sockfile timeout e={}'.format(self._me, e))
                        # Force-close the socket and its contained fd, to avoid fd leaks.
                        self.close()
                    except socket.error as e:
                        _log_stderr(':::::::: {}: sockfile error f={}, e={}'.format(self._me, self._sockfile, e))
                        # Force-close the socket and its contained fd, to avoid fd leaks.
                        self.close()
                    except Exception as e:
                        # Any other exception which might occur ...
                        _log_stderr(':::::::: {}: sockfile exception f={}, e={}'.format(self._me, self._sockfile, e))
                        # Force-close the socket and its contained fd, to avoid fd leaks.
                        self.close()
                    return False
                else:
                    addrinfo = socket.getaddrinfo(*self._socketargs)
                    if addrinfo is None:
                        return False
                    # Check each socket family member until we find one we can connect to.
                    for (addr_fam, sock_kind, proto, ca_name, sock_addr) in addrinfo:
                        self._socket = socket.socket(addr_fam, self.proto)
                        if not self._socket:
                            _log_stderr(':::::::: {}: unable to get a {} socket'.format(self._me, self._protoname))
                            return False
                        try:
                            self._socket.connect(sock_addr)
                            return True
                        except socket.timeout as e:
                            _log_stderr(':::::::: {}: {} timeout e={}'.format(self.me, self._protoname, e))
                            # Force-close the socket and its contained fd, to avoid fd leaks.
                            self.close()
                            continue
                        except socket.error as e:
                            _log_stderr(':::::::: {}: {} error e={}'.format(self._me, self._protoname, e))
                            # Force-close the socket and its contained fd, to avoid fd leaks.
                            self.close()
                            continue
                        except Exception as e:
                            # Any other exception which might occur ...
                            _log_stderr(':::::::: {}: {} exception e={}'.format(self._me, self._protoname, e))
                            # Force-close the socket and its contained fd, to avoid fd leaks.
                            self.close()
                            continue
                    # Force-close the socket and its contained fd, to avoid fd leaks.
                    self.close()
                    return False
            else:
                return True
    
        def close(self):
            try:
                self._socket.close()
            except:
                pass
            self._socket = None
    
        def log(self, message, facility=None, severity=None, timestamp=None, hostname=None, program=None, pid=None):
    
            if message is None:
                return
    
            if not facility:
                facility = syslog.LOG_USER
    
            if not severity:
                severity = syslog.LOG_INFO
    
            pri = facility + severity
    
            data = '<{}>'.format(pri)
    
            if timestamp:
                t = timestamp
            else:
                t = datetime.datetime.now()
            data = '{}{}'.format(data, t.strftime('%Y-%m-%dT%H:%M:%S.%f'))
    
            if hostname is None:
                if self.clientname:
                    data = '{} {}'.format(data, self.clientname)
            elif not hostname:
                # For hostname == '', leave out the hostname, altogether.
                pass
            else:
                data = '{} {}'.format(data, hostname)
    
            if program:
                data = '{} {}'.format(data, program)
            else:
                data = '{} logger'.format(data)
    
            if not pid:
                pid = os.getpid()
    
            data = '{}[{}]: {}'.format(data, pid, message).encode('ascii', 'ignore')
    
            if not self._socket:
                self._connect()
    
            if not self._socket:
                raise Exception('{}: unable to connect to {} syslog via {}'.format(self._me, self._protoname, self._socketargs))
            try:
                if self.maxlen:
                    self._socket.sendall(data[:self.maxlen])
                else:
                    self._socket.sendall(data)
            except IOError as e:
                _log_stderr(':::::::: {}: {} syslog io error {} via {}'.format(self._me, self._protoname, e, self._socketargs))
                self.close()
                raise
            except Exception as e:
                # Any other exception which might occur ...
                _log_stderr(':::::::: {}: {} syslog exception {} via {}'.format(self._me, self._protoname, e, self._socketargs))
                self.close()
                raise