@@ -3022,6 +3022,95 @@ refer to the comments in the code snippet for more detailed information.
30223022 if __name__=='__main__':
30233023 main()
30243024
3025+ Logging to syslog with RFC5424 support
3026+ --------------------------------------
3027+
3028+ Although :rfc: `5424 ` dates from 2009, most syslog servers are configured by detault to
3029+ use the older :rfc: `3164 `, which hails from 2001. When ``logging `` was added to Python
3030+ in 2003, it supported the earlier (and only existing) protocol at the time. Since
3031+ RFC5424 came out, as there has not been widespread deployment of it in syslog
3032+ servers, the :class: `~logging.handlers.SysLogHandler ` functionality has not been
3033+ updated.
3034+
3035+ RFC 5424 contains some useful features such as support for structured data, and if you
3036+ need to be able to log to a syslog server with support for it, you can do so with a
3037+ subclassed handler which looks something like this::
3038+
3039+ import datetime
3040+ import logging.handlers
3041+ import re
3042+ import socket
3043+ import time
3044+
3045+ class SysLogHandler5424(logging.handlers.SysLogHandler):
3046+
3047+ tz_offset = re.compile(r'([+-]\d{2})(\d{2})$')
3048+ escaped = re.compile(r'([\]"\\])')
3049+
3050+ def __init__(self, *args, **kwargs):
3051+ self.msgid = kwargs.pop('msgid', None)
3052+ self.appname = kwargs.pop('appname', None)
3053+ super().__init__(*args, **kwargs)
3054+
3055+ def format(self, record):
3056+ version = 1
3057+ asctime = datetime.datetime.fromtimestamp(record.created).isoformat()
3058+ m = self.tz_offset.match(time.strftime('%z'))
3059+ has_offset = False
3060+ if m and time.timezone:
3061+ hrs, mins = m.groups()
3062+ if int(hrs) or int(mins):
3063+ has_offset = True
3064+ if not has_offset:
3065+ asctime += 'Z'
3066+ else:
3067+ asctime += f'{hrs}:{mins}'
3068+ try:
3069+ hostname = socket.gethostname()
3070+ except Exception:
3071+ hostname = '-'
3072+ appname = self.appname or '-'
3073+ procid = record.process
3074+ msgid = '-'
3075+ msg = super().format(record)
3076+ sdata = '-'
3077+ if hasattr(record, 'structured_data'):
3078+ sd = record.structured_data
3079+ # This should be a dict where the keys are SD-ID and the value is a
3080+ # dict mapping PARAM-NAME to PARAM-VALUE (refer to the RFC for what these
3081+ # mean)
3082+ # There's no error checking here - it's purely for illustration, and you
3083+ # can adapt this code for use in production environments
3084+ parts = []
3085+
3086+ def replacer(m):
3087+ g = m.groups()
3088+ return '\\' + g[0]
3089+
3090+ for sdid, dv in sd.items():
3091+ part = f'[{sdid}'
3092+ for k, v in dv.items():
3093+ s = str(v)
3094+ s = self.escaped.sub(replacer, s)
3095+ part += f' {k}="{s}"'
3096+ part += ']'
3097+ parts.append(part)
3098+ sdata = ''.join(parts)
3099+ return f'{version} {asctime} {hostname} {appname} {procid} {msgid} {sdata} {msg}'
3100+
3101+ You'll need to be familiar with RFC 5424 to fully understand the above code, and it
3102+ may be that you have slightly different needs (e.g. for how you pass structural data
3103+ to the log). Nevertheless, the above should be adaptable to your speciric needs. With
3104+ the above handler, you'd pass structured data using something like this::
3105+
3106+ sd = {
3107+ 'foo@12345': {'bar': 'baz', 'baz': 'bozz', 'fizz': r'buzz'},
3108+ 'foo@54321': {'rab': 'baz', 'zab': 'bozz', 'zzif': r'buzz'}
3109+ }
3110+ extra = {'structured_data': sd}
3111+ i = 1
3112+ logger.debug('Message %d', i, extra=extra)
3113+
30253114
30263115.. patterns-to-avoid:
30273116
0 commit comments