Source code for daq_queuing_service.log
from contextlib import contextmanager
from contextvars import ContextVar
from logging import Filter, Logger, LogRecord, getLogger
from typing import Any
import colorlog
from graypy import GELFTCPHandler
from daq_queuing_service.app.config import (
GraylogConfig,
LoggingHandlerConfig,
LogLevel,
)
LOGGER = getLogger("Queue")
class InstrumentFilter(Filter):
def __init__(self, instrument: str):
self.instrument = instrument
def filter(self, record: LogRecord):
record.instrument = self.instrument
return True
class InstrumentSessionFilter(Filter):
def __init__(self, instrument_session_var: ContextVar[str | None]):
self.instrument_session_var = instrument_session_var
def filter(self, record: LogRecord):
if session := self.instrument_session_var.get():
record.instrument_session = session
return True
[docs]
def setup_logger(
logger: Logger,
stream: LoggingHandlerConfig,
graylog: GraylogConfig | None = None,
propagate: bool = False,
instrument: str | None = None,
):
"""Setup a logger for stream and optionally graylog
Args:
logger (logging.Logger): Logger to set up
stream (LoggingHandlerConfig): Configuration for stream handler
graylog (GraylogConfig | None): Configuration for graylog handler.
Defaults to None
propagate (bool): Whether log records should be forwarded to parent loggers
in the logging hierarchy after being processed by this logger.
Defaults to False.
instrument (str, optional): Optionally add a instrument to the logger, to be
added as context to all logs. Defaults to None
"""
logger.handlers.clear()
logger.filters.clear()
handler = colorlog.StreamHandler()
handler.setFormatter(
colorlog.ColoredFormatter(
"%(log_color)s%(asctime)s [%(name)s] %(levelname)s: %(message)s",
log_colors={
"DEBUG": "cyan",
"INFO": "green",
"WARNING": "yellow",
"ERROR": "red",
"CRITICAL": "bold_red",
},
)
)
logger.addHandler(handler)
handler.setLevel(stream.level)
if graylog and graylog.enabled:
graylog_handler = GELFTCPHandler(host=graylog.url, port=graylog.port)
graylog_handler.setLevel(graylog.level)
logger.addHandler(graylog_handler)
logger.propagate = propagate
if instrument:
logger.addFilter(InstrumentFilter(instrument))
logger.addFilter(
InstrumentSessionFilter(ContextVar("instrument_session", default=None))
)
logger.setLevel(min(handler.level for handler in logger.handlers))
logger.debug(f"Set up logger: {logger} with handlers: {logger.handlers}")
def get_session_filter(logger: Logger) -> InstrumentSessionFilter:
for f in logger.filters:
if isinstance(f, InstrumentSessionFilter):
return f
raise RuntimeError("Logger has no InstrumentSessionFilter attached.")
[docs]
@contextmanager
def add_session_to_logger(instrument_session: str, logger: Logger = LOGGER):
"""Adds a session to logs within this context. Wherever possible, an instrument
session should be added to logs to improve filtering in graylog.
See https://gitlab.diamond.ac.uk/sscc-docs/developer-guide/-/merge_requests/295
TODO: Link to actual docs page once MR is merged
Args:
instrument_session (str): The instrument session to add to log messages
logger (Logger, optional): The logger who's logs the session will be added to.
Defaults to the queue's global LOGGER.
Raises:
ValueError: Raised if two different sessions are added with nested calls of this
context manager. Nested calls are allowed as long as the session is the same,
in which case the inner call is a no-op.
"""
session_filter = get_session_filter(logger)
current = session_filter.instrument_session_var.get()
if current is not None and current != instrument_session:
raise ValueError(
f"Cannot add session {instrument_session} to logger, session {current} "
+ "already added"
)
token = session_filter.instrument_session_var.set(instrument_session)
try:
yield
finally:
session_filter.instrument_session_var.reset(token)
[docs]
def notify(
message: str,
level: LogLevel = "INFO",
logger: Logger = LOGGER,
**kwargs: Any,
):
"""Format a log message such that it gets picked up by graylog notifications and
sent to slack. For example, when the instrument is set to "i15-1"
(see InstrumentFilter above), logs from this function will be picked up by this
event definition: https://graylog.diamond.ac.uk/alerts/definitions/6adb2e7cc6cc415af7f2c9fb
and sent to slack by this notification:
https://graylog.diamond.ac.uk/alerts/notifications/6abbfaefc6cc415af78f167a
Args:
message (str): The message to be logged
level (LogLevel, optional): The severity level of the log. Defaults to "INFO".
logger (Logger, optional): The logger to make the log.
Defaults to the queue's global LOGGER.
"""
kwargs["extra"] = {
**kwargs.get("extra", {}),
"event_type": "notification",
}
getattr(logger, level.lower())(message, **kwargs)