hat.syslog.encoder

Syslog message encoder/decoder

  1"""Syslog message encoder/decoder"""
  2
  3import datetime
  4import re
  5
  6from hat import json
  7
  8from hat.syslog import common
  9
 10
 11def msg_to_str(msg: common.Msg) -> str:
 12    """Create string representation of message according to RFC 5424"""
 13    buff = [
 14        f'<{msg.facility.value * 8 + msg.severity.value}>{msg.version}',
 15        _timestamp_to_str(msg.timestamp),
 16        msg.hostname if msg.hostname else '-',
 17        msg.app_name if msg.app_name else '-',
 18        msg.procid if msg.procid else '-',
 19        msg.msgid if msg.msgid else '-',
 20        _data_to_str(msg.data)]
 21    if msg.msg:
 22        buff.append('BOM' + msg.msg)
 23    return ' '.join(buff)
 24
 25
 26def msg_from_str(msg_str: str) -> common.Msg:
 27    """Parse message string formatted according to RFC 5424"""
 28    match = _msg_pattern.fullmatch(msg_str).groupdict()
 29    prival = int(match['prival'])
 30    return common.Msg(
 31        facility=common.Facility(prival // 8),
 32        severity=common.Severity(prival % 8),
 33        version=int(match['version']),
 34        timestamp=_parse_timestamp(match['timestamp']),
 35        hostname=None if match['hostname'] == '-' else match['hostname'],
 36        app_name=None if match['app_name'] == '-' else match['app_name'],
 37        procid=None if match['procid'] == '-' else match['procid'],
 38        msgid=None if match['msgid'] == '-' else match['msgid'],
 39        data=_parse_data(match['data']),
 40        msg=(match['msg'][3:] if match['msg'] and match['msg'][:3] == 'BOM'
 41             else match['msg']))
 42
 43
 44def msg_to_json(msg: common.Msg) -> json.Data:
 45    """Convert message to json serializable data"""
 46    return {'facility': msg.facility.name,
 47            'severity': msg.severity.name,
 48            'version': msg.version,
 49            'timestamp': msg.timestamp,
 50            'hostname': msg.hostname,
 51            'app_name': msg.app_name,
 52            'procid': msg.procid,
 53            'msgid': msg.msgid,
 54            'data': msg.data,
 55            'msg': msg.msg}
 56
 57
 58def msg_from_json(data: json.Data) -> common.Msg:
 59    """Convert json serializable data to message"""
 60    return common.Msg(facility=common.Facility[data['facility']],
 61                      severity=common.Severity[data['severity']],
 62                      version=data['version'],
 63                      timestamp=data['timestamp'],
 64                      hostname=data['hostname'],
 65                      app_name=data['app_name'],
 66                      procid=data['procid'],
 67                      msgid=data['msgid'],
 68                      data=data['data'],
 69                      msg=data['msg'])
 70
 71
 72_msg_pattern = re.compile(r'''
 73    < (?P<prival> \d+) >
 74    (?P<version> \d+)
 75    \ (?P<timestamp> - |
 76                     [^ ]+)
 77    \ (?P<hostname> - |
 78                    [^ ]+)
 79    \ (?P<app_name> - |
 80                    [^ ]+)
 81    \ (?P<procid> - |
 82                  [^ ]+)
 83    \ (?P<msgid> - |
 84                 [^ ]+)
 85    \ (?P<data> - |
 86                (\[
 87                    ((\\(\\\\)*\]) |
 88                     [^\]])*
 89                \])+)
 90    (\ (?P<msg> .*))?
 91''', re.X | re.DOTALL)
 92
 93_timestamp_pattern = re.compile(r'''
 94    (?P<year> \d{4})
 95    -
 96    (?P<month> \d{2})
 97    -
 98    (?P<day> \d{2})
 99    T
100    (?P<hour> \d{2})
101    :
102    (?P<minute> \d{2})
103    :
104    (?P<second> \d{2})
105    (\. (?P<fraction> \d+))?
106    ((?P<tz_utc> Z) |
107     ((?P<tz_sign> \+ |
108                   -)
109      (?P<tz_hour> \d{2})
110      :
111      (?P<tz_minute> \d{2})))
112''', re.X | re.DOTALL)
113
114_data_pattern = re.compile(r'''
115    \[
116        (?P<id> [^ \]]+)
117        (?P<param> ((\\(\\\\)*\]) |
118                   [^\]])*)
119    \]
120    (?P<rest> .*)
121''', re.X | re.DOTALL)
122
123_param_pattern = re.compile(r'''
124    \ (?P<name> [^=\]]+)
125    ="
126    (?P<value> ((\\\\) |
127                (\\") |
128                (\\\]) |
129                [^"\]\\])*)
130    "
131    (?P<rest> .*)
132''', re.X | re.DOTALL)
133
134_escape_pattern = re.compile(r'''((\\\\)|(\\")|(\\]))''')
135
136
137def _timestamp_to_str(timestamp):
138    if not timestamp:
139        return '-'
140    return datetime.datetime.fromtimestamp(
141        timestamp, datetime.timezone.utc).replace(
142        tzinfo=None).isoformat() + 'Z'
143
144
145def _data_to_str(data_json):
146    data = json.decode(data_json) if data_json else None
147    if not data:
148        return '-'
149    return ''.join(f'[{sd_id}{_param_to_str(param)}]'
150                   for sd_id, param in data.items())
151
152
153def _param_to_str(param):
154    if not param:
155        return ''
156    return ' ' + ' '.join(f'{k}="{_escape_value(v)}"'
157                          for k, v in param.items())
158
159
160def _parse_timestamp(timestamp_str):
161    if timestamp_str == '-':
162        return
163    match = _timestamp_pattern.fullmatch(timestamp_str).groupdict()
164    return datetime.datetime(
165        year=int(match['year']),
166        month=int(match['month']),
167        day=int(match['day']),
168        hour=int(match['hour']),
169        minute=int(match['minute']),
170        second=int(match['second']),
171        microsecond=(int(int(match['fraction']) *
172                         pow(10, 6 - len(match['fraction'])))
173                     if match['fraction'] else None),
174        tzinfo=(datetime.timezone.utc if match['tz_utc'] else
175                datetime.timezone(datetime.timedelta(
176                    hours=((1 if match['tz_sign'] == '+' else -1) *
177                           int(match['tz_hour'])),
178                    minutes=int(match['tz_hour']))))).timestamp()
179
180
181def _parse_data(data_str):
182    if data_str == '-':
183        return
184    data = {}
185    while data_str:
186        match = _data_pattern.fullmatch(data_str).groupdict()
187        data[match['id']] = _parse_param(match['param'])
188        data_str = match['rest']
189    data_json = json.encode(data)
190    return data_json
191
192
193def _parse_param(param_str):
194    param = {}
195    while param_str:
196        match = _param_pattern.fullmatch(param_str).groupdict()
197        param[match['name']] = _unescape_value(match['value'])
198        param_str = match['rest']
199    return param
200
201
202def _escape_value(value):
203    return value.replace('\\', '\\\\').replace('"', '\\"').replace(']', '\\]')
204
205
206def _unescape_value(value):
207    return re.sub(_escape_pattern, _unescape_value_char, value)
208
209
210def _unescape_value_char(match):
211    return {r'\\': '\\',
212            r'\"': r'"',
213            r'\]': r']'}[match.group(0)]
def msg_to_str(msg: hat.syslog.common.Msg) -> str:
12def msg_to_str(msg: common.Msg) -> str:
13    """Create string representation of message according to RFC 5424"""
14    buff = [
15        f'<{msg.facility.value * 8 + msg.severity.value}>{msg.version}',
16        _timestamp_to_str(msg.timestamp),
17        msg.hostname if msg.hostname else '-',
18        msg.app_name if msg.app_name else '-',
19        msg.procid if msg.procid else '-',
20        msg.msgid if msg.msgid else '-',
21        _data_to_str(msg.data)]
22    if msg.msg:
23        buff.append('BOM' + msg.msg)
24    return ' '.join(buff)

Create string representation of message according to RFC 5424

def msg_from_str(msg_str: str) -> hat.syslog.common.Msg:
27def msg_from_str(msg_str: str) -> common.Msg:
28    """Parse message string formatted according to RFC 5424"""
29    match = _msg_pattern.fullmatch(msg_str).groupdict()
30    prival = int(match['prival'])
31    return common.Msg(
32        facility=common.Facility(prival // 8),
33        severity=common.Severity(prival % 8),
34        version=int(match['version']),
35        timestamp=_parse_timestamp(match['timestamp']),
36        hostname=None if match['hostname'] == '-' else match['hostname'],
37        app_name=None if match['app_name'] == '-' else match['app_name'],
38        procid=None if match['procid'] == '-' else match['procid'],
39        msgid=None if match['msgid'] == '-' else match['msgid'],
40        data=_parse_data(match['data']),
41        msg=(match['msg'][3:] if match['msg'] and match['msg'][:3] == 'BOM'
42             else match['msg']))

Parse message string formatted according to RFC 5424

def msg_to_json( msg: hat.syslog.common.Msg) -> None | bool | int | float | str | list[ForwardRef('Data')] | dict[str, ForwardRef('Data')]:
45def msg_to_json(msg: common.Msg) -> json.Data:
46    """Convert message to json serializable data"""
47    return {'facility': msg.facility.name,
48            'severity': msg.severity.name,
49            'version': msg.version,
50            'timestamp': msg.timestamp,
51            'hostname': msg.hostname,
52            'app_name': msg.app_name,
53            'procid': msg.procid,
54            'msgid': msg.msgid,
55            'data': msg.data,
56            'msg': msg.msg}

Convert message to json serializable data

def msg_from_json( data: None | bool | int | float | str | list[ForwardRef('Data')] | dict[str, ForwardRef('Data')]) -> hat.syslog.common.Msg:
59def msg_from_json(data: json.Data) -> common.Msg:
60    """Convert json serializable data to message"""
61    return common.Msg(facility=common.Facility[data['facility']],
62                      severity=common.Severity[data['severity']],
63                      version=data['version'],
64                      timestamp=data['timestamp'],
65                      hostname=data['hostname'],
66                      app_name=data['app_name'],
67                      procid=data['procid'],
68                      msgid=data['msgid'],
69                      data=data['data'],
70                      msg=data['msg'])

Convert json serializable data to message