You've already forked mariadb-columnstore-engine
mirror of
https://github.com/mariadb-corporation/mariadb-columnstore-engine.git
synced 2025-11-02 06:13:16 +03:00
Don't log trace_params in tracing logger, because it already has all this data Don't print span attrs, it can contain lots of headers Save small part of the response into the span, if the response was a JSON string Added JSON logging of trace details into a separate file (to not spam the main log with machine readable stuff) Record part of the response into the span Set duration attribute in server spans Log 404 errors Colorize the traces (each span slightly changes the color of the parent span) Improve trace visualization with duration formatting and notes for request/response pairs
180 lines
6.5 KiB
Python
180 lines
6.5 KiB
Python
import json
|
|
import logging
|
|
import logging.config
|
|
from functools import partial, partialmethod
|
|
|
|
import cherrypy
|
|
from cherrypy import _cperror
|
|
|
|
from cmapi_server.constants import CMAPI_LOG_CONF_PATH
|
|
from tracing.tracer import get_tracer
|
|
|
|
|
|
class AddIpFilter(logging.Filter):
|
|
"""Filter to add IP address to logging record."""
|
|
def filter(self, record):
|
|
record.ip = cherrypy.request.remote.name or cherrypy.request.remote.ip
|
|
return True
|
|
|
|
|
|
class TraceParamsFilter(logging.Filter):
|
|
"""Filter that adds trace_params to log records, except for the 'tracer' logger."""
|
|
def filter(self, record: logging.LogRecord) -> bool:
|
|
# Don't print trace params for tracer logger family; it already prints trace data
|
|
if record.name == 'tracer' or record.name.startswith('tracer.'):
|
|
record.trace_params = ""
|
|
return True
|
|
|
|
trace_id, span_id, parent_span_id = get_tracer().current_trace_ids()
|
|
if trace_id and span_id:
|
|
trace_params = f"rid={trace_id} sid={span_id}"
|
|
if parent_span_id:
|
|
trace_params += f" psid={parent_span_id}"
|
|
record.trace_params = trace_params
|
|
else:
|
|
record.trace_params = ""
|
|
return True
|
|
|
|
def custom_cherrypy_error(
|
|
self, msg='', context='', severity=logging.INFO, traceback=False
|
|
):
|
|
"""Write the given ``msg`` to the error log. [now without hardcoded time]
|
|
|
|
This is not just for errors! [looks awful, but cherrypy realisation as is]
|
|
Applications may call this at any time to log application-specific
|
|
information.
|
|
|
|
If ``traceback`` is True, the traceback of the current exception
|
|
(if any) will be appended to ``msg``.
|
|
|
|
..Note:
|
|
All informatio
|
|
"""
|
|
exc_info = None
|
|
if traceback:
|
|
exc_info = _cperror._exc_info()
|
|
|
|
self.error_log.log(severity, ' '.join((context, msg)), exc_info=exc_info)
|
|
|
|
|
|
def dict_config(config_filepath: str):
|
|
with open(config_filepath, 'r', encoding='utf-8') as json_config:
|
|
config_dict = json.load(json_config)
|
|
logging.config.dictConfig(config_dict)
|
|
|
|
|
|
def add_logging_level(level_name, level_num, method_name=None):
|
|
"""
|
|
Comprehensively adds a new logging level to the `logging` module and the
|
|
currently configured logging class.
|
|
|
|
`level_name` becomes an attribute of the `logging` module with the value
|
|
`level_num`.
|
|
`methodName` becomes a convenience method for both `logging` itself
|
|
and the class returned by `logging.getLoggerClass()` (usually just
|
|
`logging.Logger`).
|
|
If `methodName` is not specified, `levelName.lower()` is used.
|
|
|
|
To avoid accidental clobberings of existing attributes, this method will
|
|
raise an `AttributeError` if the level name is already an attribute of the
|
|
`logging` module or if the method name is already present
|
|
|
|
Example
|
|
-------
|
|
>>> add_logging_level('TRACE', logging.DEBUG - 5)
|
|
>>> logging.getLogger(__name__).setLevel('TRACE')
|
|
>>> logging.getLogger(__name__).trace('that worked')
|
|
>>> logging.trace('so did this')
|
|
>>> logging.TRACE
|
|
5
|
|
|
|
"""
|
|
if not method_name:
|
|
method_name = level_name.lower()
|
|
|
|
if hasattr(logging, level_name):
|
|
raise AttributeError(f'{level_name} already defined in logging module')
|
|
if hasattr(logging, method_name):
|
|
raise AttributeError(
|
|
f'{method_name} already defined in logging module'
|
|
)
|
|
if hasattr(logging.getLoggerClass(), method_name):
|
|
raise AttributeError(f'{method_name} already defined in logger class')
|
|
|
|
# This method was inspired by the answers to Stack Overflow post
|
|
# http://stackoverflow.com/q/2183233/2988730, especially
|
|
# https://stackoverflow.com/a/35804945
|
|
# https://stackoverflow.com/a/55276759
|
|
logging.addLevelName(level_num, level_name)
|
|
setattr(logging, level_name, level_num)
|
|
setattr(
|
|
logging.getLoggerClass(), method_name,
|
|
partialmethod(logging.getLoggerClass().log, level_num)
|
|
)
|
|
setattr(logging, method_name, partial(logging.log, level_num))
|
|
|
|
|
|
def enable_console_logging(logger: logging.Logger) -> None:
|
|
"""Enable logging to console for passed logger by adding a StreamHandler to it"""
|
|
console_handler = logging.StreamHandler()
|
|
logger.addHandler(console_handler)
|
|
|
|
|
|
def config_cmapi_server_logging():
|
|
# add custom level TRACE only for develop purposes
|
|
# could be activated using API endpoints or cli tool without relaunching
|
|
add_logging_level('TRACE', 5)
|
|
cherrypy._cplogging.LogManager.error = custom_cherrypy_error
|
|
# reconfigure cherrypy.access log message format
|
|
# Default access_log_format '{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}"'
|
|
# h - remote.name or remote.ip, l - "-",
|
|
# u - getattr(request, 'login', None) or '-', t - self.time(),
|
|
# r - request.request_line, s - status,
|
|
# b - dict.get(outheaders, 'Content-Length', '') or '-',
|
|
# f - dict.get(inheaders, 'Referer', ''),
|
|
# a - dict.get(inheaders, 'User-Agent', ''),
|
|
# o - dict.get(inheaders, 'Host', '-'),
|
|
# i - request.unique_id, z - LazyRfc3339UtcTime()
|
|
cherrypy._cplogging.LogManager.access_log_format = (
|
|
'{h} ACCESS "{r}" code {s}, bytes {b}, user-agent "{a}"'
|
|
)
|
|
# trace_params are populated via TraceParamsFilter configured in logging config
|
|
dict_config(CMAPI_LOG_CONF_PATH)
|
|
disable_unwanted_loggers()
|
|
|
|
|
|
def change_loggers_level(level: str):
|
|
"""Set level for each custom logger except cherrypy library.
|
|
|
|
:param level: logging level to set
|
|
:type level: str
|
|
"""
|
|
loggers = [
|
|
logging.getLogger(name) for name in logging.root.manager.loggerDict
|
|
if 'cherrypy' not in name
|
|
]
|
|
loggers.append(logging.getLogger()) # add RootLogger
|
|
for logger in loggers:
|
|
logger.setLevel(level)
|
|
|
|
def disable_unwanted_loggers():
|
|
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
|
|
|
|
|
class JsonFormatter(logging.Formatter):
|
|
# Standard LogRecord fields
|
|
skip_fields = set(logging.LogRecord('',0,'',0,'',(),None).__dict__.keys())
|
|
|
|
def format(self, record):
|
|
data = {
|
|
"ts": self.formatTime(record, self.datefmt),
|
|
"level": record.levelname,
|
|
"logger": record.name,
|
|
"msg": record.getMessage(),
|
|
}
|
|
# Extract extras from the record (all attributes except standard LogRecord fields)
|
|
for k, v in record.__dict__.items():
|
|
if k not in self.skip_fields:
|
|
data[k] = v
|
|
# Allow non-serializable extras (e.g., bytes, datetime) to be stringified
|
|
return json.dumps(data, ensure_ascii=False, sort_keys=True, default=str) |