Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 2 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,11 @@ You need `l2sl` if

## How do I get started?

In the most minimal setup, you only need change two things:

1. Add the `l2sl.StdlibRecordParser()` to the list of `processors` in
`structlog.configure`.
2. Call `l2sl.configure_stdlib_logging()` after you are done configuring `structlog`.
In the most minimal setup, you only need to do add one thing to your logging setup,
preferably after the `structlog.configure()` call:

```python
import l2sl
import structlog

structlog.configure(
processors=[
l2sl.StdlibRecordParser(),
...,
],
...,
)

l2sl.configure_stdlib_log_forwarding()
```
Expand Down
6 changes: 2 additions & 4 deletions src/l2sl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,20 @@

__version__ = "UNKNOWN"

from ._builtin_parsers import builtin_parsers
from ._forward import configure_stdlib_log_forwarding
from ._log_level import (
LogLevel,
LogLevelNumber,
StdlibLogLevelName,
StructlogLogLevelName,
)
from ._parsers import (
from ._parse import (
Parser,
RegexpEventHandler,
RegexpEventParser,
builtin_parsers,
default_fallback_parser,
)
from ._process import StdlibRecordParser

__all__ = [
"LogLevel",
Expand All @@ -36,5 +35,4 @@
"default_fallback_parser",
"RegexpEventHandler",
"RegexpEventParser",
"StdlibRecordParser",
]
41 changes: 41 additions & 0 deletions src/l2sl/_builtin_parsers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
__all__ = ["builtin_parsers", "register_builtin_parser"]

import importlib
from pathlib import Path
from typing import Callable, TypeVar, overload

from .._parse import Parser

_BUILTIN: dict[str, Parser] = {}

TParser = TypeVar("TParser", bound=Parser)


@overload
def register_builtin_parser(parser: TParser, /, *, logger: str) -> TParser: ...


@overload
def register_builtin_parser(
parser: None = None, /, *, logger: str
) -> Callable[[TParser], TParser]: ...


def register_builtin_parser(
parser: TParser | None = None, /, *, logger: str
) -> TParser | Callable[[TParser], TParser]:
def register(parser: TParser) -> TParser:
_BUILTIN[logger] = parser
return parser

if parser is None:
return register
else:
return register(parser)


def builtin_parsers() -> dict[str, Parser]:
if not _BUILTIN:
for p in sorted(Path(__file__).parent.glob("[!_]*.py")):
importlib.import_module(f"{__package__}.{p.stem}")
return _BUILTIN.copy()
26 changes: 11 additions & 15 deletions src/l2sl/_parsers/bokeh.py → src/l2sl/_builtin_parsers/bokeh.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import logging
from typing import Any

from ._core import register_builtin_parser
from ._regexp import RegexpEventParser
from structlog.typing import EventDict

from .._parse import RegexpEventParser
from . import register_builtin_parser

bokeh_server_server = register_builtin_parser(
RegexpEventParser(), logger="bokeh.server.server"
Expand All @@ -12,10 +13,8 @@
@bokeh_server_server.register_event_handler(
r"(?P<event>Starting Bokeh server) version (?P<bokeh_version>\d+\.\d+\.\d+) \(running on Tornado (?P<tornado_version>\d+\.\d+\.\d+)\)"
)
def starting_server(
groups: dict[str, str], record: logging.LogRecord
) -> tuple[str, dict[str, Any]]:
return groups.pop("event"), groups
def starting_server(groups: dict[str, str], record: logging.LogRecord) -> EventDict:
return groups


bokeh_server_tornado = register_builtin_parser(
Expand All @@ -24,23 +23,20 @@ def starting_server(


@bokeh_server_tornado.register_event_handler(r"\[pid \d+\] \d+ clients connected")
def clients(
groups: dict[str, str], record: logging.LogRecord
) -> tuple[str, dict[str, Any]]:
def clients(groups: dict[str, str], record: logging.LogRecord) -> EventDict:
assert record.args is not None
pid, number = record.args
return "clients", {"pid": pid, "number": number}
return {"event": "clients", "pid": pid, "number": number}


@bokeh_server_tornado.register_event_handler(
r"\[pid \d+\]\s+.*? has \d+ sessions with \d+ unused"
)
def sessions(
groups: dict[str, str], record: logging.LogRecord
) -> tuple[str, dict[str, Any]]:
def sessions(groups: dict[str, str], record: logging.LogRecord) -> EventDict:
assert record.args is not None
pid, endpoint, number, unused = record.args
return "sessions", {
return {
"event": "sessions",
"pid": pid,
"endpoint": endpoint,
"number": number,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import logging
from typing import Any

from ._core import register_builtin_parser
from structlog.typing import EventDict

from . import register_builtin_parser


@register_builtin_parser(logger="httpx")
def httpx(event: str, record: logging.LogRecord) -> tuple[str, dict[str, Any]]:
def httpx(record: logging.LogRecord) -> EventDict:
assert record.args is not None
method, url, protocol, status_code, _ = record.args
return "request", {
return {
"event": "request",
"method": method,
"url": str(url),
"protocol": protocol,
Expand Down
19 changes: 8 additions & 11 deletions src/l2sl/_parsers/panel.py → src/l2sl/_builtin_parsers/panel.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import logging
from typing import Any

from ._core import register_builtin_parser
from ._regexp import RegexpEventParser
from structlog.typing import EventDict

from .._parse import RegexpEventParser
from . import register_builtin_parser

panel_io = register_builtin_parser(RegexpEventParser(), logger="panel.io")


@panel_io.register_event_handler(r"(?P<event>Session) (?P<id>\d+) (?P<state>.+)")
def session(
groups: dict[str, str], record: logging.LogRecord
) -> tuple[str, dict[str, Any]]:
return groups.pop("event"), groups
def session(groups: dict[str, str], record: logging.LogRecord) -> EventDict:
return groups


panel_viewable = register_builtin_parser(RegexpEventParser(), logger="panel.viewable")
Expand All @@ -20,9 +19,7 @@ def session(
@panel_viewable.register_event_handler(
r"Session \d+ (?P<event>(received|finished processing) events)"
)
def handler(
groups: dict[str, str], record: logging.LogRecord
) -> tuple[str, dict[str, Any]]:
def handler(groups: dict[str, str], record: logging.LogRecord) -> EventDict:
assert record.args is not None
session_id, events = record.args
return groups["event"], {"sesion_id": session_id, "events": events}
return {"event": groups["event"], "sesion_id": session_id, "events": events}
18 changes: 18 additions & 0 deletions src/l2sl/_builtin_parsers/tornado.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import logging

from structlog.typing import EventDict

from .._parse import RegexpEventParser
from . import register_builtin_parser

tornado_access = register_builtin_parser(RegexpEventParser(), logger="tornado.access")


@tornado_access.register_event_handler(
r"(?P<status_code>\d{3}) (?P<method>[A-Z]+) (?P<endpoint>.*) \((?P<origin>.*)\) (?P<elapsed_time>\d+\.\d+)ms"
)
def event_handler(groups: dict[str, str], record: logging.LogRecord) -> EventDict:
return {
"event": "request",
"elapsed_time": float(groups.pop("elapsed_time")) * 1e-3,
} | groups
Original file line number Diff line number Diff line change
@@ -1,35 +1,33 @@
import logging
from typing import Any

from ._core import register_builtin_parser
from ._regexp import RegexpEventParser
from structlog.typing import EventDict

from .._parse import RegexpEventParser
from . import register_builtin_parser

uvicorn_error = register_builtin_parser(RegexpEventParser(), logger="uvicorn.error")


@uvicorn_error.register_event_handler(r"(?P<event>(Started|Finished) server process)")
def server_process(
groups: dict[str, str], record: logging.LogRecord
) -> tuple[str, dict[str, Any]]:
def server_process(groups: dict[str, str], record: logging.LogRecord) -> EventDict:
assert record.args is not None
(pid,) = record.args
return groups["event"], {"pid": pid}
return groups | {"pid": pid}


@uvicorn_error.register_event_handler(r"(?P<event>Uvicorn running) on")
def uvicorn_running(
groups: dict[str, str], record: logging.LogRecord
) -> tuple[str, dict[str, Any]]:
def uvicorn_running(groups: dict[str, str], record: logging.LogRecord) -> EventDict:
assert record.args is not None
_, host, port = record.args
return groups["event"], {"host": host, "port": port}
return groups | {"host": host, "port": port}


@register_builtin_parser(logger="uvicorn.access")
def uvicorn_access(event: str, record: logging.LogRecord) -> tuple[str, dict[str, Any]]:
def uvicorn_access(record: logging.LogRecord) -> EventDict:
assert record.args is not None
origin, method, endpoint, protocol_version, status_code = record.args
return "request", {
return {
"event": "request",
"origin": origin,
"method": method,
"endpoint": endpoint,
Expand Down
73 changes: 58 additions & 15 deletions src/l2sl/_forward.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,77 @@

import logging
import logging.config
from collections.abc import Mapping

import structlog
from structlog.typing import FilteringBoundLogger

from ._builtin_parsers import builtin_parsers
from ._parse import Parser, ResolvingParser, default_fallback_parser

class _RecordForwarder(logging.Handler):
def __init__(self) -> None:
super().__init__()
self._logger = structlog.get_logger()

def emit(self, record: logging.LogRecord) -> None:
self._logger.log(
record.levelno,
record.msg,
*record.args,
record=record,
)
def configure_stdlib_log_forwarding(
*,
parsers: Mapping[str, Parser] | None = None,
fallback_parser: Parser | None = None,
logger: FilteringBoundLogger | None = None,
validate_structlog_config: bool | None = None,
) -> None:
if parsers is None:
parsers = builtin_parsers()
if fallback_parser is None:
fallback_parser = default_fallback_parser
if logger is None:
logger = structlog.get_logger()

if validate_structlog_config is None:
validate_structlog_config = structlog.is_configured()
if validate_structlog_config:
if not structlog.is_configured():
raise RuntimeError(
"unable to validate structlog for usage with l2sl, because it is not configured"
)

config = structlog.get_config()
if isinstance(
logger_factory := config.get("logger_factory"),
structlog.stdlib.LoggerFactory,
):
raise RuntimeError(
f"l2sl is not compatible with structlog's standard library logging, "
f"but {logger_factory=} is configured"
)

def configure_stdlib_log_forwarding() -> None:
logging.config.dictConfig(
{
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"structlog": {
"class": "l2sl._forward._RecordForwarder",
"l2sl": {
"()": _RecordForwarder,
"parser": ResolvingParser(
parsers=parsers, fallback=fallback_parser, logger=logger
),
"logger": logger,
}
},
"loggers": {"root": {"level": "NOTSET", "handlers": ["structlog"]}},
"loggers": {
"root": {"level": "NOTSET", "handlers": ["l2sl"]},
},
}
)


class _RecordForwarder(logging.Handler):
def __init__(self, parser: Parser, logger: FilteringBoundLogger) -> None:
super().__init__()
self._parser = parser
self._logger = logger

def emit(self, record: logging.LogRecord) -> None:
try:
event_dict = self._parser(record)
except structlog.exceptions.DropEvent:
return

self._logger.log(record.levelno, event_dict.pop("event", ""), **event_dict)
Loading
Loading