Sunil Mohan Adapa cc626be728
service: Capture stdout/stderr when running as systemd unit
- Avoid duplicate log messages by not logging to console when running as systemd
unit.

- Retain normal logging when running on the terminal.

Tests:

- When running as systemd unit, output to stdin/stdout is captured in systemd
journal and visible with 'sudo freedombox-logs'.

- When running on terminal manually with 'sudo --user plinth ./run --develop'
both log messages and stdout/stderr prints() are visible.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
2026-03-31 07:48:38 -04:00

170 lines
4.9 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-or-later
"""
Setup logging for the application.
"""
import logging
import logging.config
import sys
import typing
import warnings
from . import cfg
default_level = None
class LogEmitterProtocol(typing.Protocol):
unit: str
class LogEmitter:
"""A mixin for App components that emit logs.
Used as a simple base class for identifying components that have logs. Use
the self.unit property to fetch systemd journal logs of the unit.
"""
unit: str
def get_logs(self: LogEmitterProtocol) -> dict[str, str]:
from plinth.privileged import service as service_privileged
return service_privileged.get_logs(self.unit)
class ColoredFormatter(logging.Formatter):
"""Print parts of log message in color."""
codes = {
'black': 30,
'red': 31,
'green': 32,
'yellow': 33,
'blue': 34,
'magenta': 35,
'cyan': 36,
'white': 37,
'bright_black': 90,
'bright_red': 91,
'bright_green': 92,
'bright_yellow': 93,
'bright_blue': 94,
'bright_magenta': 95,
'bright_cyan': 96,
'bright_white': 97
}
level_colors = {
'DEBUG': 'bright_black',
'INFO': 'bright_white',
'WARNING': 'bright_yellow',
'ERROR': 'red',
'CRITICAL': 'bright_red'
}
def wrap_color(self, string, color=None):
"""Return a string wrapped in terminal escape codes for coloring."""
if not color:
return string
return '\x1b[{}m'.format(self.codes[color]) + string + '\x1b[0m'
def format(self, record):
"""Format a record into a string"""
record_name = '{:<20}'.format(record.name)
record.colored_name = self.wrap_color(record_name, 'bright_blue')
level_color = self.level_colors.get(record.levelname, None)
level_name = '{:>8}'.format(record.levelname)
record.colored_levelname = self.wrap_color(level_name, level_color)
return super().format(record)
def _capture_warnings():
"""Capture all warnings include deprecation warnings."""
# Capture all Python warnings such as deprecation warnings
logging.captureWarnings(True)
# Log all deprecation warnings when in develop mode
if cfg.develop:
warnings.filterwarnings('default', '', DeprecationWarning)
warnings.filterwarnings('default', '', PendingDeprecationWarning)
warnings.filterwarnings('default', '', ImportWarning)
def action_init(console: bool = False):
"""Initialize logging for action scripts."""
_capture_warnings()
configuration = get_configuration()
if console:
configuration['root']['handlers'] = ['console']
else:
configuration['root']['handlers'] = ['journal']
logging.config.dictConfig(configuration)
def init():
"""Setup the logging framework."""
import cherrypy
# Remove default handlers and let the log message propagate to root logger.
for cherrypy_logger in [cherrypy.log.error_log, cherrypy.log.access_log]:
for handler in list(cherrypy_logger.handlers):
cherrypy_logger.removeHandler(handler)
_capture_warnings()
def setup_cherrypy_static_directory(app):
"""Hush output from cherrypy static file request logging.
Static file serving logs are hardly useful.
"""
app.log.access_log.propagate = False
app.log.error_log.propagate = False
def get_configuration():
"""Return the main python logging module configuration."""
configuration = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'color': {
'()': 'plinth.log.ColoredFormatter',
'format': '{colored_levelname} {colored_name} {message}',
'style': '{'
}
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'color'
},
'journal': {
'class': 'systemd.journal.JournalHandler'
}
},
'root': {
'handlers': ['console', 'journal'],
'level': default_level or ('DEBUG' if cfg.develop else 'INFO')
},
'loggers': {
'django.db.backends': {
'level': 'INFO' # Set to 'DEBUG' to log database queries
},
'axes': {
'level': 'INFO' # Too verbose during DEBUG
}
}
}
if not sys.stdin.isatty():
# If running on a console, say due to user manually starting it, then
# log everything to console. However, if we are running as a systemd
# unit then no need to log to console. This allows us to capture
# stdout/stderr in systemd unit without repeating the log messages.
configuration['root']['handlers'].remove('console')
return configuration