hat.syslog.server.main

Syslog Server main module

  1"""Syslog Server main module"""
  2
  3from pathlib import Path
  4import argparse
  5import asyncio
  6import contextlib
  7import logging.config
  8import sys
  9import time
 10
 11import appdirs
 12
 13from hat import aio
 14from hat import json
 15
 16from hat.syslog.server.backend import create_backend
 17from hat.syslog.server.syslog import create_syslog_server
 18from hat.syslog.server.ui import create_web_server
 19
 20
 21mlog: logging.Logger = logging.getLogger('hat.syslog.server.main')
 22"""Module logger"""
 23
 24user_data_dir: Path = Path(appdirs.user_data_dir('hat'))
 25"""User data directory path"""
 26
 27default_log_level: str = 'INFO'
 28"""Default console log level"""
 29
 30default_ui_addr: str = 'http://0.0.0.0:23020'
 31"""Default UI listening address"""
 32
 33default_db_path: Path = user_data_dir / 'syslog.db'
 34"""Default DB file path"""
 35
 36default_db_low_size: int = int(1e6)
 37"""Default DB low size count"""
 38
 39default_db_high_size: int = int(1e7)
 40"""Default DB high size count"""
 41
 42default_syslog_addrs: list[str] = ['tcp://0.0.0.0:6514',
 43                                   'udp://0.0.0.0:6514']
 44"""Default syslog listening addresses"""
 45
 46
 47def create_argument_parser() -> argparse.ArgumentParser:
 48    """Create argument parser"""
 49    parser = argparse.ArgumentParser(
 50        description="Syslog Server listening for TCP and/or UDP messages. "
 51                    "If listening addresses are not provided, Syslog Server "
 52                    "listens on 'tcp://0.0.0.0:6514' and "
 53                    "'udp://0.0.0.0:6514'.")
 54    parser.add_argument(
 55        '--log-level', metavar='LEVEL', default=default_log_level,
 56        choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'],
 57        help=f"console log level (default {default_log_level})")
 58    parser.add_argument(
 59        '--log-access', action='store_true',
 60        help="include access logs in console log")
 61    parser.add_argument(
 62        '--log-conf', metavar='PATH', type=Path, default=None,
 63        help="path to json/yaml/toml custom log configuration as specified by "
 64             "https://docs.python.org/3/library/logging.config.html#configuration-dictionary-schema")  # NOQA
 65    parser.add_argument(
 66        '--ui-addr', metavar='ADDR', default=default_ui_addr,
 67        help=f"UI listening address (default {default_ui_addr})")
 68    parser.add_argument(
 69        '--db-path', metavar='PATH', type=Path, default=default_db_path,
 70        help="sqlite database file path "
 71             "(default $XDG_DATA_HOME/hat/syslog.db)")
 72    parser.add_argument(
 73        '--db-low-size', metavar='N', type=int, default=default_db_low_size,
 74        help=f"number of messages kept in database after database "
 75             f"cleanup (default {default_db_low_size})")
 76    parser.add_argument(
 77        '--db-high-size', metavar='N', type=int, default=default_db_high_size,
 78        help=f"number of messages that will trigger database cleanup "
 79             f"(default {default_db_high_size})")
 80    parser.add_argument(
 81        '--db-enable-archive', action='store_true',
 82        help="should messages, deleted during database cleanup, be kept "
 83             "in archive files")
 84    parser.add_argument(
 85        '--db-disable-journal', action='store_true',
 86        help="disable sqlite journaling")
 87    parser.add_argument(
 88        '--syslog-pem-path', metavar='PATH', type=Path, default=None,
 89        help="certificate PEM path used in case of tls syslog")
 90    parser.add_argument(
 91        'syslog_addrs', metavar='ADDR', nargs='*',
 92        default=default_syslog_addrs,
 93        help="syslog listening address formated as <prot>://<host>:<port> "
 94             "(<prot> is 'tcp', 'udp' or 'tls'; <host> is host name or IP "
 95             "address; <port> is UDP/TCP port)")
 96    return parser
 97
 98
 99def get_logging_conf(log_level: str,
100                     log_access: bool,
101                     log_conf_path: Path | None
102                     ) -> json.Data:
103    """Get logging configuration"""
104    if log_conf_path:
105        conf = json.decode_file(log_conf_path)
106
107        validator = json.DefaultSchemaValidator(json.json_schema_repo)
108        validator.validate('hat-json://logging.yaml', conf)
109
110        return conf
111
112    return {
113        'version': 1,
114        'formatters': {
115            'console_formater': {
116                'format': '[%(asctime)s %(levelname)s %(name)s] %(message)s'}},
117        'handlers': {
118            'console_handler': {
119                'class': 'logging.StreamHandler',
120                'formatter': 'console_formater',
121                'level': log_level}},
122        'root': {
123            'level': log_level,
124            'handlers': ['console_handler']},
125        'loggers': {
126            'aiohttp.access': {
127                'propagate': log_access}},
128        'disable_existing_loggers': False}
129
130
131def main():
132    """Syslog Server"""
133    parser = create_argument_parser()
134    args = parser.parse_args()
135
136    logging_conf = get_logging_conf(log_level=args.log_level,
137                                    log_access=args.log_access,
138                                    log_conf_path=args.log_conf)
139    logging.config.dictConfig(logging_conf)
140
141    aio.init_asyncio()
142    with contextlib.suppress(asyncio.CancelledError):
143        aio.run_asyncio(async_main(ui_addr=args.ui_addr,
144                                   db_path=args.db_path,
145                                   db_low_size=args.db_low_size,
146                                   db_high_size=args.db_high_size,
147                                   db_enable_archive=args.db_enable_archive,
148                                   db_disable_journal=args.db_disable_journal,
149                                   syslog_pem_path=args.syslog_pem_path,
150                                   syslog_addrs=args.syslog_addrs))
151
152
153async def async_main(ui_addr: str,
154                     db_path: Path,
155                     db_low_size: int,
156                     db_high_size: int,
157                     db_enable_archive: bool,
158                     db_disable_journal: bool,
159                     syslog_pem_path: Path | None,
160                     syslog_addrs: list[str]):
161    """Syslog Server async main"""
162    async_group = aio.Group()
163
164    async def on_msg(msg):
165        await backend.register(time.time(), msg)
166
167    async def async_close():
168        await async_group.async_close()
169        await asyncio.sleep(0.1)
170
171    try:
172        mlog.debug("creating backend...")
173        backend = await _create_resource(async_group, create_backend,
174                                         db_path, db_low_size, db_high_size,
175                                         db_enable_archive, db_disable_journal)
176
177        mlog.debug("creating web server...")
178        await _create_resource(async_group, create_web_server, ui_addr,
179                               backend)
180
181        mlog.debug("creating syslog servers...")
182        for syslog_addr in syslog_addrs:
183            await _create_resource(async_group, create_syslog_server,
184                                   syslog_addr, on_msg, syslog_pem_path)
185
186        mlog.debug("initialization done")
187        await async_group.wait_closing()
188
189    finally:
190        mlog.debug("closing...")
191        await aio.uncancellable(async_close())
192
193
194async def _create_resource(async_group, fn, *args):
195    resource = await async_group.spawn(fn, *args)
196    async_group.spawn(aio.call_on_cancel, resource.async_close)
197    async_group.spawn(aio.call_on_done, resource.wait_closing(),
198                      async_group.close)
199    return resource
200
201
202if __name__ == '__main__':
203    sys.argv[0] = 'hat-syslog-server'
204    sys.exit(main())
mlog: logging.Logger = <Logger hat.syslog.server.main (WARNING)>

Module logger

user_data_dir: pathlib.Path = PosixPath('/home/runner/.local/share/hat')

User data directory path

default_log_level: str = 'INFO'

Default console log level

default_ui_addr: str = 'http://0.0.0.0:23020'

Default UI listening address

default_db_path: pathlib.Path = PosixPath('/home/runner/.local/share/hat/syslog.db')

Default DB file path

default_db_low_size: int = 1000000

Default DB low size count

default_db_high_size: int = 10000000

Default DB high size count

default_syslog_addrs: list[str] = ['tcp://0.0.0.0:6514', 'udp://0.0.0.0:6514']

Default syslog listening addresses

def create_argument_parser() -> argparse.ArgumentParser:
48def create_argument_parser() -> argparse.ArgumentParser:
49    """Create argument parser"""
50    parser = argparse.ArgumentParser(
51        description="Syslog Server listening for TCP and/or UDP messages. "
52                    "If listening addresses are not provided, Syslog Server "
53                    "listens on 'tcp://0.0.0.0:6514' and "
54                    "'udp://0.0.0.0:6514'.")
55    parser.add_argument(
56        '--log-level', metavar='LEVEL', default=default_log_level,
57        choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'],
58        help=f"console log level (default {default_log_level})")
59    parser.add_argument(
60        '--log-access', action='store_true',
61        help="include access logs in console log")
62    parser.add_argument(
63        '--log-conf', metavar='PATH', type=Path, default=None,
64        help="path to json/yaml/toml custom log configuration as specified by "
65             "https://docs.python.org/3/library/logging.config.html#configuration-dictionary-schema")  # NOQA
66    parser.add_argument(
67        '--ui-addr', metavar='ADDR', default=default_ui_addr,
68        help=f"UI listening address (default {default_ui_addr})")
69    parser.add_argument(
70        '--db-path', metavar='PATH', type=Path, default=default_db_path,
71        help="sqlite database file path "
72             "(default $XDG_DATA_HOME/hat/syslog.db)")
73    parser.add_argument(
74        '--db-low-size', metavar='N', type=int, default=default_db_low_size,
75        help=f"number of messages kept in database after database "
76             f"cleanup (default {default_db_low_size})")
77    parser.add_argument(
78        '--db-high-size', metavar='N', type=int, default=default_db_high_size,
79        help=f"number of messages that will trigger database cleanup "
80             f"(default {default_db_high_size})")
81    parser.add_argument(
82        '--db-enable-archive', action='store_true',
83        help="should messages, deleted during database cleanup, be kept "
84             "in archive files")
85    parser.add_argument(
86        '--db-disable-journal', action='store_true',
87        help="disable sqlite journaling")
88    parser.add_argument(
89        '--syslog-pem-path', metavar='PATH', type=Path, default=None,
90        help="certificate PEM path used in case of tls syslog")
91    parser.add_argument(
92        'syslog_addrs', metavar='ADDR', nargs='*',
93        default=default_syslog_addrs,
94        help="syslog listening address formated as <prot>://<host>:<port> "
95             "(<prot> is 'tcp', 'udp' or 'tls'; <host> is host name or IP "
96             "address; <port> is UDP/TCP port)")
97    return parser

Create argument parser

def get_logging_conf( log_level: str, log_access: bool, log_conf_path: pathlib.Path | None) -> None | bool | int | float | str | List[ForwardRef('Data')] | Dict[str, ForwardRef('Data')]:
100def get_logging_conf(log_level: str,
101                     log_access: bool,
102                     log_conf_path: Path | None
103                     ) -> json.Data:
104    """Get logging configuration"""
105    if log_conf_path:
106        conf = json.decode_file(log_conf_path)
107
108        validator = json.DefaultSchemaValidator(json.json_schema_repo)
109        validator.validate('hat-json://logging.yaml', conf)
110
111        return conf
112
113    return {
114        'version': 1,
115        'formatters': {
116            'console_formater': {
117                'format': '[%(asctime)s %(levelname)s %(name)s] %(message)s'}},
118        'handlers': {
119            'console_handler': {
120                'class': 'logging.StreamHandler',
121                'formatter': 'console_formater',
122                'level': log_level}},
123        'root': {
124            'level': log_level,
125            'handlers': ['console_handler']},
126        'loggers': {
127            'aiohttp.access': {
128                'propagate': log_access}},
129        'disable_existing_loggers': False}

Get logging configuration

def main():
132def main():
133    """Syslog Server"""
134    parser = create_argument_parser()
135    args = parser.parse_args()
136
137    logging_conf = get_logging_conf(log_level=args.log_level,
138                                    log_access=args.log_access,
139                                    log_conf_path=args.log_conf)
140    logging.config.dictConfig(logging_conf)
141
142    aio.init_asyncio()
143    with contextlib.suppress(asyncio.CancelledError):
144        aio.run_asyncio(async_main(ui_addr=args.ui_addr,
145                                   db_path=args.db_path,
146                                   db_low_size=args.db_low_size,
147                                   db_high_size=args.db_high_size,
148                                   db_enable_archive=args.db_enable_archive,
149                                   db_disable_journal=args.db_disable_journal,
150                                   syslog_pem_path=args.syslog_pem_path,
151                                   syslog_addrs=args.syslog_addrs))

Syslog Server

async def async_main( ui_addr: str, db_path: pathlib.Path, db_low_size: int, db_high_size: int, db_enable_archive: bool, db_disable_journal: bool, syslog_pem_path: pathlib.Path | None, syslog_addrs: list[str]):
154async def async_main(ui_addr: str,
155                     db_path: Path,
156                     db_low_size: int,
157                     db_high_size: int,
158                     db_enable_archive: bool,
159                     db_disable_journal: bool,
160                     syslog_pem_path: Path | None,
161                     syslog_addrs: list[str]):
162    """Syslog Server async main"""
163    async_group = aio.Group()
164
165    async def on_msg(msg):
166        await backend.register(time.time(), msg)
167
168    async def async_close():
169        await async_group.async_close()
170        await asyncio.sleep(0.1)
171
172    try:
173        mlog.debug("creating backend...")
174        backend = await _create_resource(async_group, create_backend,
175                                         db_path, db_low_size, db_high_size,
176                                         db_enable_archive, db_disable_journal)
177
178        mlog.debug("creating web server...")
179        await _create_resource(async_group, create_web_server, ui_addr,
180                               backend)
181
182        mlog.debug("creating syslog servers...")
183        for syslog_addr in syslog_addrs:
184            await _create_resource(async_group, create_syslog_server,
185                                   syslog_addr, on_msg, syslog_pem_path)
186
187        mlog.debug("initialization done")
188        await async_group.wait_closing()
189
190    finally:
191        mlog.debug("closing...")
192        await aio.uncancellable(async_close())

Syslog Server async main