From 91e548569d2557cdaeb7ebc170032d088aa86330 Mon Sep 17 00:00:00 2001 From: Evgeniy Alekseev Date: Mon, 29 May 2023 03:41:11 +0300 Subject: [PATCH] runtime logger handler selector --- Dockerfile | 6 +-- docker/entrypoint.sh | 9 +--- docs/ahriman.1 | 8 ++- docs/ahriman.core.log.rst | 8 +++ docs/ahriman.models.rst | 8 +++ docs/completions/bash/_ahriman | 3 +- docs/completions/zsh/_ahriman | 1 + docs/faq.rst | 20 ++++---- package/archlinux/PKGBUILD | 4 +- package/archlinux/ahriman.tmpfiles | 3 +- .../settings/ahriman.ini.d/logging.ini | 12 +---- setup.py | 3 ++ src/ahriman/application/ahriman.py | 5 ++ src/ahriman/application/handlers/handler.py | 3 +- src/ahriman/core/log/journal_handler.py | 47 ++++++++++++++++++ src/ahriman/core/log/log.py | 49 +++++++++++++++++-- src/ahriman/models/log_handler.py | 35 +++++++++++++ .../application/handlers/test_handler.py | 6 ++- tests/ahriman/application/test_ahriman.py | 9 ++++ .../ahriman/core/log/test_journal_handler.py | 31 ++++++++++++ tests/ahriman/core/log/test_log.py | 48 ++++++++++++++++-- tests/ahriman/models/test_log_handler.py | 0 tox.ini | 2 +- 23 files changed, 272 insertions(+), 48 deletions(-) create mode 100644 src/ahriman/core/log/journal_handler.py create mode 100644 src/ahriman/models/log_handler.py create mode 100644 tests/ahriman/core/log/test_journal_handler.py create mode 100644 tests/ahriman/models/test_log_handler.py diff --git a/Dockerfile b/Dockerfile index a9ddd925..d9964d94 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ ENV AHRIMAN_DEBUG="" ENV AHRIMAN_FORCE_ROOT="" ENV AHRIMAN_HOST="0.0.0.0" ENV AHRIMAN_MULTILIB="yes" -ENV AHRIMAN_OUTPUT="journald" +ENV AHRIMAN_OUTPUT="" ENV AHRIMAN_PACKAGER="ahriman bot " ENV AHRIMAN_PACMAN_MIRROR="" ENV AHRIMAN_PORT="" @@ -28,9 +28,9 @@ RUN useradd -m -d "/home/build" -s "/usr/bin/nologin" build && \ COPY "docker/install-aur-package.sh" "/usr/local/bin/install-aur-package" ## install package dependencies ## darcs is not installed by reasons, because it requires a lot haskell packages which dramatically increase image size -RUN pacman -Sy --noconfirm --asdeps devtools git pyalpm python-cerberus python-inflection python-passlib python-requests python-srcinfo python-systemd && \ +RUN pacman -Sy --noconfirm --asdeps devtools git pyalpm python-cerberus python-inflection python-passlib python-requests python-srcinfo && \ pacman -Sy --noconfirm --asdeps python-build python-installer python-wheel && \ - pacman -Sy --noconfirm breezy mercurial python-aiohttp python-aiohttp-cors python-boto3 python-cryptography python-jinja python-requests-unixsocket rsync subversion && \ + pacman -Sy --noconfirm --asdeps breezy mercurial python-aiohttp python-aiohttp-cors python-boto3 python-cryptography python-jinja python-requests-unixsocket python-systemd rsync subversion && \ runuser -u build -- install-aur-package python-aioauth-client python-aiohttp-apispec-git python-aiohttp-jinja2 \ python-aiohttp-debugtoolbar python-aiohttp-session python-aiohttp-security diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 2b5dbb0d..fb6fd55a 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -15,15 +15,10 @@ database = $AHRIMAN_REPOSITORY_ROOT/ahriman.db host = $AHRIMAN_HOST EOF -sed -i "s|handlers = journald_handler|handlers = ${AHRIMAN_OUTPUT}_handler|g" "/etc/ahriman.ini.d/logging.ini" AHRIMAN_DEFAULT_ARGS=("--architecture" "$AHRIMAN_ARCHITECTURE") -if [[ "$AHRIMAN_OUTPUT" == "syslog" ]]; then - if [ ! -e "/dev/log" ]; then - # by default ahriman uses syslog which is not available inside container - # to make noise less we force quiet mode in case if /dev/log was not mounted - AHRIMAN_DEFAULT_ARGS+=("--quiet") - fi +if [ -n "$AHRIMAN_OUTPUT" ]; then + AHRIMAN_DEFAULT_ARGS+=("--log-handler" "$AHRIMAN_OUTPUT") fi # create repository root inside the [[mounted]] directory and set correct ownership diff --git a/docs/ahriman.1 b/docs/ahriman.1 index 8e35ce94..cb2923c0 100644 --- a/docs/ahriman.1 +++ b/docs/ahriman.1 @@ -1,9 +1,9 @@ -.TH AHRIMAN "1" "2023\-05\-25" "ahriman" "Generated Python Manual" +.TH AHRIMAN "1" "2023\-05\-28" "ahriman" "Generated Python Manual" .SH NAME ahriman .SH SYNOPSIS .B ahriman -[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--report | --no-report] [-q] [--unsafe] [-V] {aur-search,search,help,help-commands-unsafe,help-updates,help-version,version,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,patch-set-add,repo-backup,repo-check,check,repo-create-keyring,repo-create-mirrorlist,repo-daemon,daemon,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-restore,repo-sign,sign,repo-status-update,repo-sync,sync,repo-tree,repo-triggers,repo-update,update,service-clean,clean,repo-clean,service-config,config,repo-config,service-config-validate,config-validate,repo-config-validate,service-key-import,key-import,service-setup,init,repo-init,repo-setup,setup,service-shell,shell,user-add,user-list,user-remove,web} ... +[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--log-handler {console,syslog,journald}] [--report | --no-report] [-q] [--unsafe] [-V] {aur-search,search,help,help-commands-unsafe,help-updates,help-version,version,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,patch-set-add,repo-backup,repo-check,check,repo-create-keyring,repo-create-mirrorlist,repo-daemon,daemon,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-restore,repo-sign,sign,repo-status-update,repo-sync,sync,repo-tree,repo-triggers,repo-update,update,service-clean,clean,repo-clean,service-config,config,repo-config,service-config-validate,config-validate,repo-config-validate,service-key-import,key-import,service-setup,init,repo-init,repo-setup,setup,service-shell,shell,user-add,user-list,user-remove,web} ... .SH DESCRIPTION ArcH linux ReposItory MANager @@ -24,6 +24,10 @@ force run, remove file lock \fB\-l\fR \fI\,LOCK\/\fR, \fB\-\-lock\fR \fI\,LOCK\/\fR lock file +.TP +\fB\-\-log\-handler\fR \fI\,{console,syslog,journald}\/\fR +explicit log handler specification. If none set, the handler will be guessed from environment + .TP \fB\-\-report\fR, \fB\-\-no\-report\fR force enable or disable reporting to web service diff --git a/docs/ahriman.core.log.rst b/docs/ahriman.core.log.rst index 0959000f..19624dec 100644 --- a/docs/ahriman.core.log.rst +++ b/docs/ahriman.core.log.rst @@ -20,6 +20,14 @@ ahriman.core.log.http\_log\_handler module :no-undoc-members: :show-inheritance: +ahriman.core.log.journal\_handler module +---------------------------------------- + +.. automodule:: ahriman.core.log.journal_handler + :members: + :no-undoc-members: + :show-inheritance: + ahriman.core.log.lazy\_logging module ------------------------------------- diff --git a/docs/ahriman.models.rst b/docs/ahriman.models.rst index a29c9a8a..669f95e0 100644 --- a/docs/ahriman.models.rst +++ b/docs/ahriman.models.rst @@ -60,6 +60,14 @@ ahriman.models.internal\_status module :no-undoc-members: :show-inheritance: +ahriman.models.log\_handler module +---------------------------------- + +.. automodule:: ahriman.models.log_handler + :members: + :no-undoc-members: + :show-inheritance: + ahriman.models.log\_record\_id module ------------------------------------- diff --git a/docs/completions/bash/_ahriman b/docs/completions/bash/_ahriman index 4c375629..0ba6cfb4 100644 --- a/docs/completions/bash/_ahriman +++ b/docs/completions/bash/_ahriman @@ -2,7 +2,7 @@ _shtab_ahriman_subparsers=('aur-search' 'search' 'help' 'help-commands-unsafe' 'help-updates' 'help-version' 'version' 'package-add' 'add' 'package-update' 'package-remove' 'remove' 'package-status' 'status' 'package-status-remove' 'package-status-update' 'status-update' 'patch-add' 'patch-list' 'patch-remove' 'patch-set-add' 'repo-backup' 'repo-check' 'check' 'repo-create-keyring' 'repo-create-mirrorlist' 'repo-daemon' 'daemon' 'repo-rebuild' 'rebuild' 'repo-remove-unknown' 'remove-unknown' 'repo-report' 'report' 'repo-restore' 'repo-sign' 'sign' 'repo-status-update' 'repo-sync' 'sync' 'repo-tree' 'repo-triggers' 'repo-update' 'update' 'service-clean' 'clean' 'repo-clean' 'service-config' 'config' 'repo-config' 'service-config-validate' 'config-validate' 'repo-config-validate' 'service-key-import' 'key-import' 'service-setup' 'init' 'repo-init' 'repo-setup' 'setup' 'service-shell' 'shell' 'user-add' 'user-list' 'user-remove' 'web') -_shtab_ahriman_option_strings=('-h' '--help' '-a' '--architecture' '-c' '--configuration' '--force' '-l' '--lock' '--report' '--no-report' '-q' '--quiet' '--unsafe' '-V' '--version') +_shtab_ahriman_option_strings=('-h' '--help' '-a' '--architecture' '-c' '--configuration' '--force' '-l' '--lock' '--log-handler' '--report' '--no-report' '-q' '--quiet' '--unsafe' '-V' '--version') _shtab_ahriman_aur_search_option_strings=('-h' '--help' '-e' '--exit-code' '--info' '--no-info' '--sort-by') _shtab_ahriman_search_option_strings=('-h' '--help' '-e' '--exit-code' '--info' '--no-info' '--sort-by') _shtab_ahriman_help_option_strings=('-h' '--help') @@ -73,6 +73,7 @@ _shtab_ahriman_web_option_strings=('-h' '--help') _shtab_ahriman_pos_0_choices=('aur-search' 'search' 'help' 'help-commands-unsafe' 'help-updates' 'help-version' 'version' 'package-add' 'add' 'package-update' 'package-remove' 'remove' 'package-status' 'status' 'package-status-remove' 'package-status-update' 'status-update' 'patch-add' 'patch-list' 'patch-remove' 'patch-set-add' 'repo-backup' 'repo-check' 'check' 'repo-create-keyring' 'repo-create-mirrorlist' 'repo-daemon' 'daemon' 'repo-rebuild' 'rebuild' 'repo-remove-unknown' 'remove-unknown' 'repo-report' 'report' 'repo-restore' 'repo-sign' 'sign' 'repo-status-update' 'repo-sync' 'sync' 'repo-tree' 'repo-triggers' 'repo-update' 'update' 'service-clean' 'clean' 'repo-clean' 'service-config' 'config' 'repo-config' 'service-config-validate' 'config-validate' 'repo-config-validate' 'service-key-import' 'key-import' 'service-setup' 'init' 'repo-init' 'repo-setup' 'setup' 'service-shell' 'shell' 'user-add' 'user-list' 'user-remove' 'web') +_shtab_ahriman___log_handler_choices=('console' 'syslog' 'journald') _shtab_ahriman_aur_search___sort_by_choices=('description' 'first_submitted' 'id' 'last_modified' 'maintainer' 'name' 'num_votes' 'out_of_date' 'package_base' 'package_base_id' 'popularity' 'repository' 'submitter' 'url' 'url_path' 'version') _shtab_ahriman_search___sort_by_choices=('description' 'first_submitted' 'id' 'last_modified' 'maintainer' 'name' 'num_votes' 'out_of_date' 'package_base' 'package_base_id' 'popularity' 'repository' 'submitter' 'url' 'url_path' 'version') _shtab_ahriman_package_add__s_choices=('auto' 'archive' 'aur' 'directory' 'local' 'remote' 'repository') diff --git a/docs/completions/zsh/_ahriman b/docs/completions/zsh/_ahriman index 488b262f..12302492 100644 --- a/docs/completions/zsh/_ahriman +++ b/docs/completions/zsh/_ahriman @@ -81,6 +81,7 @@ _shtab_ahriman_options=( {-c,--configuration}"[configuration path]:configuration:" "--force[force run, remove file lock]" {-l,--lock}"[lock file]:lock:" + "--log-handler[explicit log handler specification. If none set, the handler will be guessed from environment]:log_handler:(console syslog journald)" {--report,--no-report}"[force enable or disable reporting to web service]:report:" {-q,--quiet}"[force disable any logging]" "--unsafe[allow to run ahriman as non-ahriman user. Some actions might be unavailable]" diff --git a/docs/faq.rst b/docs/faq.rst index 87d9b43d..19b6f914 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -391,7 +391,7 @@ The following environment variables are supported: * ``AHRIMAN_FORCE_ROOT`` - force run ahriman as root instead of guessing by subcommand. * ``AHRIMAN_HOST`` - host for the web interface, default is ``0.0.0.0``. * ``AHRIMAN_MULTILIB`` - if set (default) multilib repository will be used, disabled otherwise. -* ``AHRIMAN_OUTPUT`` - controls logging handler, e.g. ``syslog``, ``console``. The name must be found in logging configuration. Note that if ``syslog`` (the default) handler is used you will need to mount ``/dev/log`` inside container because it is not available there. +* ``AHRIMAN_OUTPUT`` - controls logging handler, e.g. ``syslog``, ``console``. The name must be found in logging configuration. Note that if ``syslog`` handler is used you will need to mount ``/dev/log`` inside container because it is not available there. * ``AHRIMAN_PACKAGER`` - packager name from which packages will be built, default is ``ahriman bot ``. * ``AHRIMAN_PACMAN_MIRROR`` - override pacman mirror server if set. * ``AHRIMAN_PORT`` - HTTP server port if any, default is empty. @@ -663,7 +663,7 @@ How to report by email .. code-block:: shell - yay -S python-jinja + yay -S --asdeps python-jinja #. Configure the service: @@ -690,7 +690,7 @@ How to generate index page for S3 .. code-block:: shell - yay -S python-jinja + yay -S --asdeps python-jinja #. Configure the service: @@ -714,7 +714,7 @@ How to post build report to telegram .. code-block:: shell - yay -S python-jinja + yay -S --asdeps python-jinja #. Register bot in telegram. You can do it by talking with `@BotFather `_. For more details please refer to `official documentation `_. @@ -838,7 +838,7 @@ How to setup web service .. code-block:: shell - yay -S python-aiohttp python-aiohttp-jinja2 python-aiohttp-apispec>=3.0.0 python-aiohttp-cors + yay -S --asdeps python-aiohttp python-aiohttp-jinja2 python-aiohttp-apispec>=3.0.0 python-aiohttp-cors #. Configure service: @@ -859,7 +859,7 @@ How to enable basic authorization .. code-block:: shell - yay -S python-aiohttp-security python-aiohttp-session python-cryptography + yay -S --asdeps python-aiohttp-security python-aiohttp-session python-cryptography #. Configure the service to enable authorization: @@ -915,7 +915,7 @@ How to enable OAuth authorization .. code-block:: shell - yay -S python-aiohttp-security python-aiohttp-session python-cryptography python-aioauth-client + yay -S --asdeps python-aiohttp-security python-aiohttp-session python-cryptography python-aioauth-client #. Configure the service: @@ -1053,17 +1053,17 @@ It is automation tools for ``repoctl`` mentioned above. Except for using shell i How to check service logs ^^^^^^^^^^^^^^^^^^^^^^^^^ -By default, the service writes logs to ``/dev/log`` which can be accessed by using ``journalctl`` command (logs are written to the journal of the user under which command is run). In order to retrieve logs for the process you can use the following command: +By default, the service writes logs to ``journald`` which can be accessed by using ``journalctl`` command (logs are written to the journal of the user under which command is run). In order to retrieve logs for the process you can use the following command: .. code-block:: shell sudo journalctl SYSLOG_IDENTIFIER=ahriman -You can also edit configuration and forward logs to ``stderr``, just change ``handlers`` value, e.g.: +You can also ask to forward logs to ``stderr``, just set ``--log-handler`` flag, e.g.: .. code-block:: shell - sed -i 's/handlers = journald_handler/handlers = console_handler/g' /etc/ahriman.ini.d/logging.ini + ahriman --log-handler console ... You can even configure logging as you wish, but kindly refer to python ``logging`` module `configuration `_. The application uses java concept to log messages, e.g. class ``Application`` imported from ``ahriman.application.application`` package will have logger called ``ahriman.application.application.Application``. In order to e.g. change logger name for whole application package it is possible to change values for ``ahriman.application`` package; thus editing ``ahriman`` logger configuration will change logging for whole application (unless there are overrides for another logger). diff --git a/package/archlinux/PKGBUILD b/package/archlinux/PKGBUILD index c5e47b27..da4b539b 100644 --- a/package/archlinux/PKGBUILD +++ b/package/archlinux/PKGBUILD @@ -7,8 +7,7 @@ pkgdesc="ArcH linux ReposItory MANager" arch=('any') url="https://github.com/arcan1s/ahriman" license=('GPL3') -depends=('devtools>=1:1.0.0' 'git' 'pyalpm' 'python-cerberus' 'python-inflection' 'python-passlib' 'python-requests' - 'python-srcinfo' 'python-systemd') +depends=('devtools>=1:1.0.0' 'git' 'pyalpm' 'python-cerberus' 'python-inflection' 'python-passlib' 'python-requests' 'python-srcinfo') makedepends=('python-build' 'python-installer' 'python-wheel') optdepends=('breezy: -bzr packages support' 'darcs: -darcs packages support' @@ -25,6 +24,7 @@ optdepends=('breezy: -bzr packages support' 'python-cryptography: web server with authorization' 'python-requests-unixsocket: client report to web server by unix socket' 'python-jinja: html report generation' + 'python-systemd: journal support' 'rsync: sync by using rsync' 'subversion: -svn packages support') source=("https://github.com/arcan1s/ahriman/releases/download/$pkgver/$pkgname-$pkgver-src.tar.xz" diff --git a/package/archlinux/ahriman.tmpfiles b/package/archlinux/ahriman.tmpfiles index 8ecb3e2a..6a1b6bad 100644 --- a/package/archlinux/ahriman.tmpfiles +++ b/package/archlinux/ahriman.tmpfiles @@ -1,2 +1 @@ -d /var/lib/ahriman 0755 ahriman ahriman -d /var/log/ahriman 0755 ahriman ahriman \ No newline at end of file +d /var/lib/ahriman 0755 ahriman ahriman \ No newline at end of file diff --git a/package/share/ahriman/settings/ahriman.ini.d/logging.ini b/package/share/ahriman/settings/ahriman.ini.d/logging.ini index d1d74fdd..aac4fb44 100644 --- a/package/share/ahriman/settings/ahriman.ini.d/logging.ini +++ b/package/share/ahriman/settings/ahriman.ini.d/logging.ini @@ -8,13 +8,13 @@ keys = console_handler,journald_handler,syslog_handler keys = generic_format,syslog_format [handler_console_handler] -class = StreamHandler +class = logging.StreamHandler level = DEBUG formatter = generic_format args = (sys.stderr,) [handler_journald_handler] -class = systemd.journal.JournalHandler +class = ahriman.core.log.journal_handler.JournalHandler level = DEBUG formatter = syslog_format kwargs = {"SYSLOG_IDENTIFIER": "ahriman"} @@ -27,20 +27,16 @@ args = ("/dev/log",) [formatter_generic_format] format = [%(levelname)s %(asctime)s] [%(name)s]: %(message)s -datefmt = [formatter_syslog_format] format = [%(levelname)s] [%(name)s]: %(message)s -datefmt = [logger_root] level = DEBUG -handlers = journald_handler qualname = root [logger_http] level = DEBUG -handlers = journald_handler qualname = http propagate = 0 @@ -51,24 +47,20 @@ qualname = stderr [logger_boto3] level = INFO -handlers = journald_handler qualname = boto3 propagate = 0 [logger_botocore] level = INFO -handlers = journald_handler qualname = botocore propagate = 0 [logger_nose] level = INFO -handlers = journald_handler qualname = nose propagate = 0 [logger_s3transfer] level = INFO -handlers = journald_handler qualname = s3transfer propagate = 0 diff --git a/setup.py b/setup.py index 56226e62..436a1020 100644 --- a/setup.py +++ b/setup.py @@ -121,6 +121,9 @@ setup( "sphinx-rtd-theme>=1.1.1", # https://stackoverflow.com/a/74355734 "sphinxcontrib-napoleon", ], + "journald": [ + "systemd-python", + ], # FIXME technically this dependency is required, but in some cases we do not have access to # the libalpm which is required in order to install the package. Thus in case if we do not # really need to run the application we can move it to "optional" dependencies diff --git a/src/ahriman/application/ahriman.py b/src/ahriman/application/ahriman.py index 76cdefc3..21ba2f7c 100644 --- a/src/ahriman/application/ahriman.py +++ b/src/ahriman/application/ahriman.py @@ -30,6 +30,7 @@ from ahriman.application import handlers from ahriman.core.util import enum_values from ahriman.models.action import Action from ahriman.models.build_status import BuildStatusEnum +from ahriman.models.log_handler import LogHandler from ahriman.models.package_source import PackageSource from ahriman.models.sign_settings import SignSettings from ahriman.models.user_access import UserAccess @@ -58,6 +59,7 @@ def _formatter(prog: str) -> argparse.HelpFormatter: return argparse.ArgumentDefaultsHelpFormatter(prog, width=120) +# pylint: disable=too-many-statements def _parser() -> argparse.ArgumentParser: """ command line parser generator @@ -75,6 +77,9 @@ def _parser() -> argparse.ArgumentParser: parser.add_argument("--force", help="force run, remove file lock", action="store_true") parser.add_argument("-l", "--lock", help="lock file", type=Path, default=Path(tempfile.gettempdir()) / "ahriman.lock") + parser.add_argument("--log-handler", help="explicit log handler specification. If none set, the handler will be " + "guessed from environment", + type=LogHandler, choices=enum_values(LogHandler)) parser.add_argument("--report", help="force enable or disable reporting to web service", action=argparse.BooleanOptionalAction, default=True) parser.add_argument("-q", "--quiet", help="force disable any logging", action="store_true") diff --git a/src/ahriman/application/handlers/handler.py b/src/ahriman/application/handlers/handler.py index 186ca259..3f0a3da2 100644 --- a/src/ahriman/application/handlers/handler.py +++ b/src/ahriman/application/handlers/handler.py @@ -94,7 +94,8 @@ class Handler: """ try: configuration = Configuration.from_path(args.configuration, architecture) - Log.load(configuration, quiet=args.quiet, report=args.report) + log_handler = Log.handler(args.log_handler) + Log.load(configuration, log_handler, quiet=args.quiet, report=args.report) with Lock(args, architecture, configuration): cls.run(args, architecture, configuration, report=args.report, unsafe=args.unsafe) return True diff --git a/src/ahriman/core/log/journal_handler.py b/src/ahriman/core/log/journal_handler.py new file mode 100644 index 00000000..64acf042 --- /dev/null +++ b/src/ahriman/core/log/journal_handler.py @@ -0,0 +1,47 @@ +# +# Copyright (c) 2021-2023 ahriman team. +# +# This file is part of ahriman +# (see https://github.com/arcan1s/ahriman). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +from logging import NullHandler +from typing import Any + + +__all__ = ["JournalHandler"] + + +class _JournalHandler(NullHandler): + """ + wrapper for unexpected args and kwargs + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """ + default constructor + + Args: + *args(Any): positional arguments + **kwargs(Any): keyword arguments + """ + NullHandler.__init__(self) + del args, kwargs + + +try: + from systemd.journal import JournalHandler # type: ignore[import] +except ImportError: + JournalHandler = _JournalHandler diff --git a/src/ahriman/core/log/log.py b/src/ahriman/core/log/log.py index 4e25dda2..7125d973 100644 --- a/src/ahriman/core/log/log.py +++ b/src/ahriman/core/log/log.py @@ -20,9 +20,11 @@ import logging from logging.config import fileConfig +from pathlib import Path from ahriman.core.configuration import Configuration from ahriman.core.log.http_log_handler import HttpLogHandler +from ahriman.models.log_handler import LogHandler class Log: @@ -32,24 +34,65 @@ class Log: Attributes: DEFAULT_LOG_FORMAT(str): (class attribute) default log format (in case of fallback) DEFAULT_LOG_LEVEL(int): (class attribute) default log level (in case of fallback) + DEFAULT_SYSLOG_DEVICE(Path): (class attribute) default path to syslog device """ DEFAULT_LOG_FORMAT = "[%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d %(funcName)s]: %(message)s" DEFAULT_LOG_LEVEL = logging.DEBUG + DEFAULT_SYSLOG_DEVICE = Path("/dev") / "log" @staticmethod - def load(configuration: Configuration, *, quiet: bool, report: bool) -> None: + def handler(selected: LogHandler | None) -> LogHandler: + """ + try to guess default log handler. In case if ``selected`` is set, it will return specified value with appended + _handler suffix. Otherwise, it will try to import journald handler and returns ``journald_handler`` if library + is available. Otherwise, it will check if there is ``/dev/log`` device and returns ``syslog_handler`` in this + case. And, finally, it will fall back to ``console_handler`` if none were found + + Args: + selected(LogHandler | None): user specified handler if any + + Returns: + LogHandler: selected log handler + """ + if selected is not None: + return selected + + try: + from systemd.journal import JournalHandler # type: ignore[import] + del JournalHandler + return LogHandler.Journald # journald import was found + except ImportError: + if Log.DEFAULT_SYSLOG_DEVICE.exists(): + return LogHandler.Syslog + return LogHandler.Console + + @staticmethod + def load(configuration: Configuration, handler: LogHandler, *, quiet: bool, report: bool) -> None: """ setup logging settings from configuration Args: configuration(Configuration): configuration instance + handler(LogHandler): selected default log handler, which will be used if no handlers were set quiet(bool): force disable any log messages report(bool): force enable or disable reporting """ + default_handler = f"{handler.value}_handler" + try: - path = configuration.logging_path - fileConfig(path) + log_configuration = Configuration() + log_configuration.read(configuration.logging_path) + + # set handlers if they are not set + for section in filter(lambda s: s.startswith("logger_"), log_configuration.sections()): + if "handlers" in log_configuration[section]: + continue + log_configuration.set_option(section, "handlers", default_handler) + + # load logging configuration + fileConfig(log_configuration, disable_existing_loggers=True) + logging.debug("using %s logger", default_handler) except Exception: logging.basicConfig(filename=None, format=Log.DEFAULT_LOG_FORMAT, level=Log.DEFAULT_LOG_LEVEL) diff --git a/src/ahriman/models/log_handler.py b/src/ahriman/models/log_handler.py new file mode 100644 index 00000000..abc4c5a1 --- /dev/null +++ b/src/ahriman/models/log_handler.py @@ -0,0 +1,35 @@ +# +# Copyright (c) 2021-2023 ahriman team. +# +# This file is part of ahriman +# (see https://github.com/arcan1s/ahriman). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +from enum import Enum + + +class LogHandler(str, Enum): + """ + log handler as described by default configuration + + Attributes: + Console(LogHandler): (class attribute) write logs to console + Syslog(LogHandler): (class attribute) write logs to syslog device /dev/null + Journald(LogHandler): (class attribute) write logs to journald directly + """ + + Console = "console" + Syslog = "syslog" + Journald = "journald" diff --git a/tests/ahriman/application/handlers/test_handler.py b/tests/ahriman/application/handlers/test_handler.py index 703b8aea..8e81afb3 100644 --- a/tests/ahriman/application/handlers/test_handler.py +++ b/tests/ahriman/application/handlers/test_handler.py @@ -7,6 +7,7 @@ from pytest_mock import MockerFixture from ahriman.application.handlers import Handler from ahriman.core.configuration import Configuration from ahriman.core.exceptions import ExitCode, MissingArchitectureError, MultipleArchitecturesError +from ahriman.models.log_handler import LogHandler def test_architectures_extract(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: @@ -56,17 +57,20 @@ def test_call(args: argparse.Namespace, configuration: Configuration, mocker: Mo must call inside lock """ args.configuration = Path("") + args.log_handler = LogHandler.Console args.quiet = False args.report = False mocker.patch("ahriman.application.handlers.Handler.run") configuration_mock = mocker.patch("ahriman.core.configuration.Configuration.from_path", return_value=configuration) + log_handler_mock = mocker.patch("ahriman.core.log.Log.handler", return_value=args.log_handler) log_load_mock = mocker.patch("ahriman.core.log.Log.load") enter_mock = mocker.patch("ahriman.application.lock.Lock.__enter__") exit_mock = mocker.patch("ahriman.application.lock.Lock.__exit__") assert Handler.call(args, "x86_64") configuration_mock.assert_called_once_with(args.configuration, "x86_64") - log_load_mock.assert_called_once_with(configuration, quiet=args.quiet, report=args.report) + log_handler_mock.assert_called_once_with(args.log_handler) + log_load_mock.assert_called_once_with(configuration, args.log_handler, quiet=args.quiet, report=args.report) enter_mock.assert_called_once_with() exit_mock.assert_called_once_with(None, None, None) diff --git a/tests/ahriman/application/test_ahriman.py b/tests/ahriman/application/test_ahriman.py index ab97d656..46826bd2 100644 --- a/tests/ahriman/application/test_ahriman.py +++ b/tests/ahriman/application/test_ahriman.py @@ -6,6 +6,7 @@ from pytest_mock import MockerFixture from ahriman.application.handlers import Handler from ahriman.models.action import Action from ahriman.models.build_status import BuildStatusEnum +from ahriman.models.log_handler import LogHandler from ahriman.models.sign_settings import SignSettings from ahriman.models.user_access import UserAccess @@ -37,6 +38,14 @@ def test_parser_option_lock(parser: argparse.ArgumentParser) -> None: assert isinstance(args.lock, Path) +def test_parser_option_log_handler(parser: argparse.ArgumentParser) -> None: + """ + must convert log-handler option to LogHandler instance + """ + args = parser.parse_args(["--log-handler", "console", "service-config"]) + assert isinstance(args.log_handler, LogHandler) + + def test_multiple_architectures(parser: argparse.ArgumentParser) -> None: """ must accept multiple architectures diff --git a/tests/ahriman/core/log/test_journal_handler.py b/tests/ahriman/core/log/test_journal_handler.py new file mode 100644 index 00000000..19dee0c6 --- /dev/null +++ b/tests/ahriman/core/log/test_journal_handler.py @@ -0,0 +1,31 @@ +import sys + +from pytest_mock import MockerFixture + + +# because of how imports work it must be first test +def test_dummy_journal_handler(mocker: MockerFixture) -> None: + """ + must import dummy journal handler if upstream systemd was not found + """ + mocker.patch.dict(sys.modules, {"systemd.journal": None}) + from logging import NullHandler + from ahriman.core.log.journal_handler import JournalHandler + assert issubclass(JournalHandler, NullHandler) + + +def test_init() -> None: + """ + must init dummy handler + """ + from ahriman.core.log.journal_handler import _JournalHandler + assert _JournalHandler(42, answer=42) + + +def test_journal_handler() -> None: + """ + must import journal handler + """ + from systemd.journal import JournalHandler as UpstreamJournalHandler + from ahriman.core.log.journal_handler import JournalHandler + assert JournalHandler is UpstreamJournalHandler diff --git a/tests/ahriman/core/log/test_log.py b/tests/ahriman/core/log/test_log.py index ef0a5177..b6652481 100644 --- a/tests/ahriman/core/log/test_log.py +++ b/tests/ahriman/core/log/test_log.py @@ -1,21 +1,59 @@ import logging +import pytest +import sys +from logging.config import fileConfig from pytest_mock import MockerFixture +from systemd.journal import JournalHandler from ahriman.core.configuration import Configuration from ahriman.core.log import Log +from ahriman.models.log_handler import LogHandler + + +def test_handler() -> None: + """ + must extract journald handler if available + """ + assert Log.handler(None) == LogHandler.Journald + + +def test_handler_selected() -> None: + """ + must return selected log handler + """ + assert Log.handler(LogHandler.Console) == LogHandler.Console + + +def test_handler_syslog(mocker: MockerFixture) -> None: + """ + must return syslog handler if no journal is available + """ + mocker.patch("pathlib.Path.exists", return_value=True) + mocker.patch.dict(sys.modules, {"systemd.journal": None}) + assert Log.handler(None) == LogHandler.Syslog + + +def test_handler_console(mocker: MockerFixture) -> None: + """ + must return console handler if no journal is available and no log device was found + """ + mocker.patch("pathlib.Path.exists", return_value=False) + mocker.patch.dict(sys.modules, {"systemd.journal": None}) + assert Log.handler(None) == LogHandler.Console def test_load(configuration: Configuration, mocker: MockerFixture) -> None: """ must load logging """ - logging_mock = mocker.patch("ahriman.core.log.log.fileConfig") + logging_mock = mocker.patch("ahriman.core.log.log.fileConfig", side_effect=fileConfig) http_log_mock = mocker.patch("ahriman.core.log.http_log_handler.HttpLogHandler.load") - Log.load(configuration, quiet=False, report=False) - logging_mock.assert_called_once_with(configuration.logging_path) + Log.load(configuration, LogHandler.Journald, quiet=False, report=False) + logging_mock.assert_called_once_with(pytest.helpers.anyvar(int), disable_existing_loggers=True) http_log_mock.assert_called_once_with(configuration, report=False) + assert all(isinstance(handler, JournalHandler) for handler in logging.getLogger().handlers) def test_load_fallback(configuration: Configuration, mocker: MockerFixture) -> None: @@ -23,7 +61,7 @@ def test_load_fallback(configuration: Configuration, mocker: MockerFixture) -> N must fall back to stderr without errors """ mocker.patch("ahriman.core.log.log.fileConfig", side_effect=PermissionError()) - Log.load(configuration, quiet=False, report=False) + Log.load(configuration, LogHandler.Journald, quiet=False, report=False) def test_load_quiet(configuration: Configuration, mocker: MockerFixture) -> None: @@ -31,5 +69,5 @@ def test_load_quiet(configuration: Configuration, mocker: MockerFixture) -> None must disable logging in case if quiet flag set """ disable_mock = mocker.patch("logging.disable") - Log.load(configuration, quiet=True, report=False) + Log.load(configuration, LogHandler.Journald, quiet=True, report=False) disable_mock.assert_called_once_with(logging.WARNING) diff --git a/tests/ahriman/models/test_log_handler.py b/tests/ahriman/models/test_log_handler.py new file mode 100644 index 00000000..e69de29b diff --git a/tox.ini b/tox.ini index ff76590f..695512e0 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = check, tests -dependencies = -e .[pacman,s3,web] +dependencies = -e .[journald,pacman,s3,web] project_name = ahriman [mypy]