|
| 1 | +"""Structured logger utility for creating JSON logs in Delphi pipelines.""" |
| 2 | +import logging |
| 3 | +import sys |
| 4 | +import threading |
| 5 | +import structlog |
| 6 | + |
| 7 | + |
| 8 | +def handle_exceptions(logger): |
| 9 | + """Handle exceptions using the provided logger.""" |
| 10 | + def exception_handler(etype, value, traceback): |
| 11 | + logger.exception("Top-level exception occurred", |
| 12 | + exc_info=(etype, value, traceback)) |
| 13 | + |
| 14 | + def multithread_exception_handler(args): |
| 15 | + exception_handler(args.exc_type, args.exc_value, args.exc_traceback) |
| 16 | + |
| 17 | + sys.excepthook = exception_handler |
| 18 | + threading.excepthook = multithread_exception_handler |
| 19 | + |
| 20 | + |
| 21 | +def get_structured_logger(name=__name__, |
| 22 | + filename=None, |
| 23 | + log_exceptions=True): |
| 24 | + """Create a new structlog logger. |
| 25 | +
|
| 26 | + Use the logger returned from this in indicator code using the standard |
| 27 | + wrapper calls, e.g.: |
| 28 | +
|
| 29 | + logger = get_structured_logger(__name__) |
| 30 | + logger.warning("Error", type="Signal too low"). |
| 31 | +
|
| 32 | + The output will be rendered as JSON which can easily be consumed by logs |
| 33 | + processors. |
| 34 | +
|
| 35 | + See the structlog documentation for details. |
| 36 | +
|
| 37 | + Parameters |
| 38 | + --------- |
| 39 | + name: Name to use for logger (included in log lines), __name__ from caller |
| 40 | + is a good choice. |
| 41 | + filename: An (optional) file to write log output. |
| 42 | + """ |
| 43 | + # Configure the underlying logging configuration |
| 44 | + handlers = [logging.StreamHandler()] |
| 45 | + if filename: |
| 46 | + handlers.append(logging.FileHandler(filename)) |
| 47 | + |
| 48 | + logging.basicConfig( |
| 49 | + format="%(message)s", |
| 50 | + level=logging.INFO, |
| 51 | + handlers=handlers |
| 52 | + ) |
| 53 | + |
| 54 | + # Configure structlog. This uses many of the standard suggestions from |
| 55 | + # the structlog documentation. |
| 56 | + structlog.configure( |
| 57 | + processors=[ |
| 58 | + # Filter out log levels we are not tracking. |
| 59 | + structlog.stdlib.filter_by_level, |
| 60 | + # Include logger name in output. |
| 61 | + structlog.stdlib.add_logger_name, |
| 62 | + # Include log level in output. |
| 63 | + structlog.stdlib.add_log_level, |
| 64 | + # Allow formatting into arguments e.g., logger.info("Hello, %s", |
| 65 | + # name) |
| 66 | + structlog.stdlib.PositionalArgumentsFormatter(), |
| 67 | + # Add timestamps. |
| 68 | + structlog.processors.TimeStamper(fmt="iso"), |
| 69 | + # Match support for exception logging in the standard logger. |
| 70 | + structlog.processors.StackInfoRenderer(), |
| 71 | + structlog.processors.format_exc_info, |
| 72 | + # Decode unicode characters |
| 73 | + structlog.processors.UnicodeDecoder(), |
| 74 | + # Render as JSON |
| 75 | + structlog.processors.JSONRenderer() |
| 76 | + ], |
| 77 | + # Use a dict class for keeping track of data. |
| 78 | + context_class=dict, |
| 79 | + # Use a standard logger for the actual log call. |
| 80 | + logger_factory=structlog.stdlib.LoggerFactory(), |
| 81 | + # Use a standard wrapper class for utilities like log.warning() |
| 82 | + wrapper_class=structlog.stdlib.BoundLogger, |
| 83 | + # Cache the logger |
| 84 | + cache_logger_on_first_use=True, |
| 85 | + ) |
| 86 | + |
| 87 | + logger = structlog.get_logger(name) |
| 88 | + |
| 89 | + if log_exceptions: |
| 90 | + handle_exceptions(logger) |
| 91 | + |
| 92 | + return logger |
0 commit comments