runtime logger handler selector

This commit is contained in:
Evgenii Alekseev 2023-05-29 03:41:11 +03:00
parent 6c3b2ef266
commit 91e548569d
23 changed files with 272 additions and 48 deletions

View File

@ -6,7 +6,7 @@ ENV AHRIMAN_DEBUG=""
ENV AHRIMAN_FORCE_ROOT="" ENV AHRIMAN_FORCE_ROOT=""
ENV AHRIMAN_HOST="0.0.0.0" ENV AHRIMAN_HOST="0.0.0.0"
ENV AHRIMAN_MULTILIB="yes" ENV AHRIMAN_MULTILIB="yes"
ENV AHRIMAN_OUTPUT="journald" ENV AHRIMAN_OUTPUT=""
ENV AHRIMAN_PACKAGER="ahriman bot <ahriman@example.com>" ENV AHRIMAN_PACKAGER="ahriman bot <ahriman@example.com>"
ENV AHRIMAN_PACMAN_MIRROR="" ENV AHRIMAN_PACMAN_MIRROR=""
ENV AHRIMAN_PORT="" 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" COPY "docker/install-aur-package.sh" "/usr/local/bin/install-aur-package"
## install package dependencies ## install package dependencies
## darcs is not installed by reasons, because it requires a lot haskell packages which dramatically increase image size ## 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 --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 \ 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 python-aiohttp-debugtoolbar python-aiohttp-session python-aiohttp-security

View File

@ -15,15 +15,10 @@ database = $AHRIMAN_REPOSITORY_ROOT/ahriman.db
host = $AHRIMAN_HOST host = $AHRIMAN_HOST
EOF EOF
sed -i "s|handlers = journald_handler|handlers = ${AHRIMAN_OUTPUT}_handler|g" "/etc/ahriman.ini.d/logging.ini"
AHRIMAN_DEFAULT_ARGS=("--architecture" "$AHRIMAN_ARCHITECTURE") AHRIMAN_DEFAULT_ARGS=("--architecture" "$AHRIMAN_ARCHITECTURE")
if [[ "$AHRIMAN_OUTPUT" == "syslog" ]]; then if [ -n "$AHRIMAN_OUTPUT" ]; then
if [ ! -e "/dev/log" ]; then AHRIMAN_DEFAULT_ARGS+=("--log-handler" "$AHRIMAN_OUTPUT")
# 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
fi fi
# create repository root inside the [[mounted]] directory and set correct ownership # create repository root inside the [[mounted]] directory and set correct ownership

View File

@ -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 .SH NAME
ahriman ahriman
.SH SYNOPSIS .SH SYNOPSIS
.B ahriman .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 .SH DESCRIPTION
ArcH linux ReposItory MANager 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 \fB\-l\fR \fI\,LOCK\/\fR, \fB\-\-lock\fR \fI\,LOCK\/\fR
lock file 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 .TP
\fB\-\-report\fR, \fB\-\-no\-report\fR \fB\-\-report\fR, \fB\-\-no\-report\fR
force enable or disable reporting to web service force enable or disable reporting to web service

View File

@ -20,6 +20,14 @@ ahriman.core.log.http\_log\_handler module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :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 ahriman.core.log.lazy\_logging module
------------------------------------- -------------------------------------

View File

@ -60,6 +60,14 @@ ahriman.models.internal\_status module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.models.log\_handler module
----------------------------------
.. automodule:: ahriman.models.log_handler
:members:
:no-undoc-members:
:show-inheritance:
ahriman.models.log\_record\_id module ahriman.models.log\_record\_id module
------------------------------------- -------------------------------------

View File

@ -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_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_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_search_option_strings=('-h' '--help' '-e' '--exit-code' '--info' '--no-info' '--sort-by')
_shtab_ahriman_help_option_strings=('-h' '--help') _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_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_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_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') _shtab_ahriman_package_add__s_choices=('auto' 'archive' 'aur' 'directory' 'local' 'remote' 'repository')

View File

@ -81,6 +81,7 @@ _shtab_ahriman_options=(
{-c,--configuration}"[configuration path]:configuration:" {-c,--configuration}"[configuration path]:configuration:"
"--force[force run, remove file lock]" "--force[force run, remove file lock]"
{-l,--lock}"[lock 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:" {--report,--no-report}"[force enable or disable reporting to web service]:report:"
{-q,--quiet}"[force disable any logging]" {-q,--quiet}"[force disable any logging]"
"--unsafe[allow to run ahriman as non-ahriman user. Some actions might be unavailable]" "--unsafe[allow to run ahriman as non-ahriman user. Some actions might be unavailable]"

View File

@ -391,7 +391,7 @@ The following environment variables are supported:
* ``AHRIMAN_FORCE_ROOT`` - force run ahriman as root instead of guessing by subcommand. * ``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_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_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@example.com>``. * ``AHRIMAN_PACKAGER`` - packager name from which packages will be built, default is ``ahriman bot <ahriman@example.com>``.
* ``AHRIMAN_PACMAN_MIRROR`` - override pacman mirror server if set. * ``AHRIMAN_PACMAN_MIRROR`` - override pacman mirror server if set.
* ``AHRIMAN_PORT`` - HTTP server port if any, default is empty. * ``AHRIMAN_PORT`` - HTTP server port if any, default is empty.
@ -663,7 +663,7 @@ How to report by email
.. code-block:: shell .. code-block:: shell
yay -S python-jinja yay -S --asdeps python-jinja
#. #.
Configure the service: Configure the service:
@ -690,7 +690,7 @@ How to generate index page for S3
.. code-block:: shell .. code-block:: shell
yay -S python-jinja yay -S --asdeps python-jinja
#. #.
Configure the service: Configure the service:
@ -714,7 +714,7 @@ How to post build report to telegram
.. code-block:: shell .. code-block:: shell
yay -S python-jinja yay -S --asdeps python-jinja
#. #.
Register bot in telegram. You can do it by talking with `@BotFather <https://t.me/botfather>`_. For more details please refer to `official documentation <https://core.telegram.org/bots>`_. Register bot in telegram. You can do it by talking with `@BotFather <https://t.me/botfather>`_. For more details please refer to `official documentation <https://core.telegram.org/bots>`_.
@ -838,7 +838,7 @@ How to setup web service
.. code-block:: shell .. 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: Configure service:
@ -859,7 +859,7 @@ How to enable basic authorization
.. code-block:: shell .. 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: Configure the service to enable authorization:
@ -915,7 +915,7 @@ How to enable OAuth authorization
.. code-block:: shell .. 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: 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 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 .. code-block:: shell
sudo journalctl SYSLOG_IDENTIFIER=ahriman 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 .. 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 <https://docs.python.org/3/library/logging.config.html>`_. 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). You can even configure logging as you wish, but kindly refer to python ``logging`` module `configuration <https://docs.python.org/3/library/logging.config.html>`_. 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).

View File

@ -7,8 +7,7 @@ pkgdesc="ArcH linux ReposItory MANager"
arch=('any') arch=('any')
url="https://github.com/arcan1s/ahriman" url="https://github.com/arcan1s/ahriman"
license=('GPL3') license=('GPL3')
depends=('devtools>=1:1.0.0' 'git' 'pyalpm' 'python-cerberus' 'python-inflection' 'python-passlib' 'python-requests' depends=('devtools>=1:1.0.0' 'git' 'pyalpm' 'python-cerberus' 'python-inflection' 'python-passlib' 'python-requests' 'python-srcinfo')
'python-srcinfo' 'python-systemd')
makedepends=('python-build' 'python-installer' 'python-wheel') makedepends=('python-build' 'python-installer' 'python-wheel')
optdepends=('breezy: -bzr packages support' optdepends=('breezy: -bzr packages support'
'darcs: -darcs packages support' 'darcs: -darcs packages support'
@ -25,6 +24,7 @@ optdepends=('breezy: -bzr packages support'
'python-cryptography: web server with authorization' 'python-cryptography: web server with authorization'
'python-requests-unixsocket: client report to web server by unix socket' 'python-requests-unixsocket: client report to web server by unix socket'
'python-jinja: html report generation' 'python-jinja: html report generation'
'python-systemd: journal support'
'rsync: sync by using rsync' 'rsync: sync by using rsync'
'subversion: -svn packages support') 'subversion: -svn packages support')
source=("https://github.com/arcan1s/ahriman/releases/download/$pkgver/$pkgname-$pkgver-src.tar.xz" source=("https://github.com/arcan1s/ahriman/releases/download/$pkgver/$pkgname-$pkgver-src.tar.xz"

View File

@ -1,2 +1 @@
d /var/lib/ahriman 0755 ahriman ahriman d /var/lib/ahriman 0755 ahriman ahriman
d /var/log/ahriman 0755 ahriman ahriman

View File

@ -8,13 +8,13 @@ keys = console_handler,journald_handler,syslog_handler
keys = generic_format,syslog_format keys = generic_format,syslog_format
[handler_console_handler] [handler_console_handler]
class = StreamHandler class = logging.StreamHandler
level = DEBUG level = DEBUG
formatter = generic_format formatter = generic_format
args = (sys.stderr,) args = (sys.stderr,)
[handler_journald_handler] [handler_journald_handler]
class = systemd.journal.JournalHandler class = ahriman.core.log.journal_handler.JournalHandler
level = DEBUG level = DEBUG
formatter = syslog_format formatter = syslog_format
kwargs = {"SYSLOG_IDENTIFIER": "ahriman"} kwargs = {"SYSLOG_IDENTIFIER": "ahriman"}
@ -27,20 +27,16 @@ args = ("/dev/log",)
[formatter_generic_format] [formatter_generic_format]
format = [%(levelname)s %(asctime)s] [%(name)s]: %(message)s format = [%(levelname)s %(asctime)s] [%(name)s]: %(message)s
datefmt =
[formatter_syslog_format] [formatter_syslog_format]
format = [%(levelname)s] [%(name)s]: %(message)s format = [%(levelname)s] [%(name)s]: %(message)s
datefmt =
[logger_root] [logger_root]
level = DEBUG level = DEBUG
handlers = journald_handler
qualname = root qualname = root
[logger_http] [logger_http]
level = DEBUG level = DEBUG
handlers = journald_handler
qualname = http qualname = http
propagate = 0 propagate = 0
@ -51,24 +47,20 @@ qualname = stderr
[logger_boto3] [logger_boto3]
level = INFO level = INFO
handlers = journald_handler
qualname = boto3 qualname = boto3
propagate = 0 propagate = 0
[logger_botocore] [logger_botocore]
level = INFO level = INFO
handlers = journald_handler
qualname = botocore qualname = botocore
propagate = 0 propagate = 0
[logger_nose] [logger_nose]
level = INFO level = INFO
handlers = journald_handler
qualname = nose qualname = nose
propagate = 0 propagate = 0
[logger_s3transfer] [logger_s3transfer]
level = INFO level = INFO
handlers = journald_handler
qualname = s3transfer qualname = s3transfer
propagate = 0 propagate = 0

View File

@ -121,6 +121,9 @@ setup(
"sphinx-rtd-theme>=1.1.1", # https://stackoverflow.com/a/74355734 "sphinx-rtd-theme>=1.1.1", # https://stackoverflow.com/a/74355734
"sphinxcontrib-napoleon", "sphinxcontrib-napoleon",
], ],
"journald": [
"systemd-python",
],
# FIXME technically this dependency is required, but in some cases we do not have access to # 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 # 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 # really need to run the application we can move it to "optional" dependencies

View File

@ -30,6 +30,7 @@ from ahriman.application import handlers
from ahriman.core.util import enum_values from ahriman.core.util import enum_values
from ahriman.models.action import Action from ahriman.models.action import Action
from ahriman.models.build_status import BuildStatusEnum from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.log_handler import LogHandler
from ahriman.models.package_source import PackageSource from ahriman.models.package_source import PackageSource
from ahriman.models.sign_settings import SignSettings from ahriman.models.sign_settings import SignSettings
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
@ -58,6 +59,7 @@ def _formatter(prog: str) -> argparse.HelpFormatter:
return argparse.ArgumentDefaultsHelpFormatter(prog, width=120) return argparse.ArgumentDefaultsHelpFormatter(prog, width=120)
# pylint: disable=too-many-statements
def _parser() -> argparse.ArgumentParser: def _parser() -> argparse.ArgumentParser:
""" """
command line parser generator 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("--force", help="force run, remove file lock", action="store_true")
parser.add_argument("-l", "--lock", help="lock file", type=Path, parser.add_argument("-l", "--lock", help="lock file", type=Path,
default=Path(tempfile.gettempdir()) / "ahriman.lock") 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", parser.add_argument("--report", help="force enable or disable reporting to web service",
action=argparse.BooleanOptionalAction, default=True) action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("-q", "--quiet", help="force disable any logging", action="store_true") parser.add_argument("-q", "--quiet", help="force disable any logging", action="store_true")

View File

@ -94,7 +94,8 @@ class Handler:
""" """
try: try:
configuration = Configuration.from_path(args.configuration, architecture) 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): with Lock(args, architecture, configuration):
cls.run(args, architecture, configuration, report=args.report, unsafe=args.unsafe) cls.run(args, architecture, configuration, report=args.report, unsafe=args.unsafe)
return True return True

View File

@ -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 <http://www.gnu.org/licenses/>.
#
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

View File

@ -20,9 +20,11 @@
import logging import logging
from logging.config import fileConfig from logging.config import fileConfig
from pathlib import Path
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.log.http_log_handler import HttpLogHandler from ahriman.core.log.http_log_handler import HttpLogHandler
from ahriman.models.log_handler import LogHandler
class Log: class Log:
@ -32,24 +34,65 @@ class Log:
Attributes: Attributes:
DEFAULT_LOG_FORMAT(str): (class attribute) default log format (in case of fallback) 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_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_FORMAT = "[%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d %(funcName)s]: %(message)s"
DEFAULT_LOG_LEVEL = logging.DEBUG DEFAULT_LOG_LEVEL = logging.DEBUG
DEFAULT_SYSLOG_DEVICE = Path("/dev") / "log"
@staticmethod @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 setup logging settings from configuration
Args: Args:
configuration(Configuration): configuration instance 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 quiet(bool): force disable any log messages
report(bool): force enable or disable reporting report(bool): force enable or disable reporting
""" """
default_handler = f"{handler.value}_handler"
try: try:
path = configuration.logging_path log_configuration = Configuration()
fileConfig(path) 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: except Exception:
logging.basicConfig(filename=None, format=Log.DEFAULT_LOG_FORMAT, logging.basicConfig(filename=None, format=Log.DEFAULT_LOG_FORMAT,
level=Log.DEFAULT_LOG_LEVEL) level=Log.DEFAULT_LOG_LEVEL)

View File

@ -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 <http://www.gnu.org/licenses/>.
#
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"

View File

@ -7,6 +7,7 @@ from pytest_mock import MockerFixture
from ahriman.application.handlers import Handler from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import ExitCode, MissingArchitectureError, MultipleArchitecturesError 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: 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 must call inside lock
""" """
args.configuration = Path("") args.configuration = Path("")
args.log_handler = LogHandler.Console
args.quiet = False args.quiet = False
args.report = False args.report = False
mocker.patch("ahriman.application.handlers.Handler.run") mocker.patch("ahriman.application.handlers.Handler.run")
configuration_mock = mocker.patch("ahriman.core.configuration.Configuration.from_path", return_value=configuration) 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") log_load_mock = mocker.patch("ahriman.core.log.Log.load")
enter_mock = mocker.patch("ahriman.application.lock.Lock.__enter__") enter_mock = mocker.patch("ahriman.application.lock.Lock.__enter__")
exit_mock = mocker.patch("ahriman.application.lock.Lock.__exit__") exit_mock = mocker.patch("ahriman.application.lock.Lock.__exit__")
assert Handler.call(args, "x86_64") assert Handler.call(args, "x86_64")
configuration_mock.assert_called_once_with(args.configuration, "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() enter_mock.assert_called_once_with()
exit_mock.assert_called_once_with(None, None, None) exit_mock.assert_called_once_with(None, None, None)

View File

@ -6,6 +6,7 @@ from pytest_mock import MockerFixture
from ahriman.application.handlers import Handler from ahriman.application.handlers import Handler
from ahriman.models.action import Action from ahriman.models.action import Action
from ahriman.models.build_status import BuildStatusEnum from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.log_handler import LogHandler
from ahriman.models.sign_settings import SignSettings from ahriman.models.sign_settings import SignSettings
from ahriman.models.user_access import UserAccess 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) 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: def test_multiple_architectures(parser: argparse.ArgumentParser) -> None:
""" """
must accept multiple architectures must accept multiple architectures

View File

@ -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

View File

@ -1,21 +1,59 @@
import logging import logging
import pytest
import sys
from logging.config import fileConfig
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from systemd.journal import JournalHandler
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.log import Log 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: def test_load(configuration: Configuration, mocker: MockerFixture) -> None:
""" """
must load logging 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") http_log_mock = mocker.patch("ahriman.core.log.http_log_handler.HttpLogHandler.load")
Log.load(configuration, quiet=False, report=False) Log.load(configuration, LogHandler.Journald, quiet=False, report=False)
logging_mock.assert_called_once_with(configuration.logging_path) logging_mock.assert_called_once_with(pytest.helpers.anyvar(int), disable_existing_loggers=True)
http_log_mock.assert_called_once_with(configuration, report=False) 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: 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 must fall back to stderr without errors
""" """
mocker.patch("ahriman.core.log.log.fileConfig", side_effect=PermissionError()) 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: 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 must disable logging in case if quiet flag set
""" """
disable_mock = mocker.patch("logging.disable") 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) disable_mock.assert_called_once_with(logging.WARNING)

View File

View File

@ -1,6 +1,6 @@
[tox] [tox]
envlist = check, tests envlist = check, tests
dependencies = -e .[pacman,s3,web] dependencies = -e .[journald,pacman,s3,web]
project_name = ahriman project_name = ahriman
[mypy] [mypy]