Skip to content

Commit

Permalink
Add logging extraction to background logger
Browse files Browse the repository at this point in the history
  • Loading branch information
ahopkins committed Jun 26, 2024
1 parent 7ab93d9 commit 16e632f
Show file tree
Hide file tree
Showing 33 changed files with 285 additions and 273 deletions.
4 changes: 4 additions & 0 deletions sanic_ext/bootstrap.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

import os

from types import SimpleNamespace
from typing import Any, Callable, Dict, List, Mapping, Optional, Type, Union
from warnings import warn
Expand Down Expand Up @@ -114,6 +116,8 @@ def __init__(
started.add(ext)

def _display(self):
if "SANIC_WORKER_IDENTIFIER" in os.environ:
return
init_logs = ["Sanic Extensions:"]
for extension in self.extensions:
label = extension.render_label()
Expand Down
8 changes: 7 additions & 1 deletion sanic_ext/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,13 @@ def __init__(
injection_load_custom_constants: bool = False,
logging: bool = False,
logging_queue_max_size: int = 4096,
loggers: List[str] = ["sanic.access", "sanic.error", "sanic.root"],
loggers: List[str] = [
"sanic.access",
"sanic.error",
"sanic.root",
"sanic.server",
"sanic.websockets",
],
oas: bool = True,
oas_autodoc: bool = True,
oas_custom_file: Optional[os.PathLike] = None,
Expand Down
6 changes: 2 additions & 4 deletions sanic_ext/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ class ValidationError(SanicException):
status_code = 400


class InitError(SanicException):
...
class InitError(SanicException): ...


class ExtensionNotFound(SanicException):
...
class ExtensionNotFound(SanicException): ...
3 changes: 1 addition & 2 deletions sanic_ext/extensions/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ def _startup(self, bootstrap):
self._started = True

@abstractmethod
def startup(self, bootstrap) -> None:
...
def startup(self, bootstrap) -> None: ...

def label(self):
return ""
Expand Down
3 changes: 1 addition & 2 deletions sanic_ext/extensions/health/monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@
from sanic import Sanic


class Stale(ValueError):
...
class Stale(ValueError): ...


@dataclass
Expand Down
116 changes: 116 additions & 0 deletions sanic_ext/extensions/logging/extractor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import logging

from typing import Any, Dict, Optional, TypedDict


class LoggerConfig(TypedDict):
level: str
propagate: bool
handlers: list[str]


class HandlerConfig(TypedDict):
class_: str
level: str
stream: Optional[str]
formatter: Optional[str]


class FormatterConfig(TypedDict):
class_: str
format: Optional[str]
datefmt: Optional[str]


class LoggingConfig(TypedDict):
version: int
disable_existing_loggers: bool
formatters: Dict[str, FormatterConfig]
handlers: Dict[str, HandlerConfig]
loggers: Dict[str, LoggerConfig]


class LoggingConfigExtractor:
def __init__(self):
self.version = 1
self.disable_existing_loggers = False
self.formatters: Dict[str, FormatterConfig] = {}
self.handlers: Dict[str, HandlerConfig] = {}
self.loggers: Dict[str, LoggerConfig] = {}

def add_logger(self, logger: logging.Logger):
self._extract_logger_config(logger)
self._extract_handlers(logger)

def compile(self) -> LoggingConfig:
output = {
"version": self.version,
"disable_existing_loggers": self.disable_existing_loggers,
"formatters": self.formatters,
"handlers": self.handlers,
"loggers": self.loggers,
}
return self._clean(output)

def _extract_logger_config(self, logger: logging.Logger):
config: LoggerConfig = {
"level": logging.getLevelName(logger.level),
"propagate": logger.propagate,
"handlers": [handler.get_name() for handler in logger.handlers],
}
self.loggers[logger.name] = config

def _extract_handlers(self, logger: logging.Logger):
for handler in logger.handlers:
self._extract_handler_config(handler)

def _extract_handler_config(self, handler: logging.Handler):
handler_name = handler.get_name()
if handler_name in self.handlers:
return
config: HandlerConfig = {
"class_": self._full_name(handler),
"level": logging.getLevelName(handler.level),
"formatter": (
self._formatter_name(handler.formatter)
if handler.formatter
else None
),
"stream": None,
}
# if (stream := getattr(handler, "stream", None)) and (
# stream_name := getattr(stream, "name", None)
# ):
# config["stream"] = stream_name
self.handlers[handler_name] = config
if handler.formatter:
self._extract_formatter_config(handler.formatter)

def _extract_formatter_config(self, formatter: logging.Formatter):
formatter_name = self._formatter_name(formatter)
if formatter_name in self.formatters:
return
config: FormatterConfig = {
"class_": self._full_name(formatter),
"format": formatter._fmt,
"datefmt": formatter.datefmt,
}
self.formatters[formatter_name] = config

def _clean(self, d: Dict[str, Any]) -> Dict[str, Any]:
return {
k.replace("class_", "class"): self._clean(v)
if isinstance(v, dict)
else v
for k, v in d.items()
}

@staticmethod
def _formatter_name(
formatter: logging.Formatter, prefix: str = "formatter"
):
return f"{prefix}_{formatter.__class__.__name__}".lower()

@staticmethod
def _full_name(obj):
return f"{obj.__module__}.{obj.__class__.__name__}"
30 changes: 27 additions & 3 deletions sanic_ext/extensions/logging/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
from typing import List

from sanic import Sanic
from sanic.log import server_logger
from sanic.log import logger as root_logger
from sanic.log import logger as server_logger
from sanic.logging.setup import setup_logging

from sanic_ext.extensions.logging.extractor import LoggingConfigExtractor


async def prepare_logger(app: Sanic, *_):
Expand All @@ -19,12 +23,18 @@ async def prepare_logger(app: Sanic, *_):

async def setup_logger(app: Sanic, *_):
logger = Logger()
extractor = LoggingConfigExtractor()
for logger_name in app.config.LOGGERS:
l = logging.getLogger(logger_name)
extractor.add_logger(l)
app.manager.manage(
"Logger",
logger,
{
"queue": app.shared_ctx.logger_queue,
"config": extractor.compile(),
},
transient=True,
)


Expand Down Expand Up @@ -59,18 +69,32 @@ async def remove_server_logging(app: Sanic):


class Logger:
LOGGERS = []
LOGGERS: List[str] = []

def __init__(self):
self.run = True
self.loggers = {
logger: logging.getLogger(logger) for logger in self.LOGGERS
}

def __call__(self, queue) -> None:
def __call__(self, queue, config) -> None:
signal_func(SIGINT, self.stop)
signal_func(SIGTERM, self.stop)

logging.config.dictConfig(config)

setup_loggers = set(config["loggers"].keys())
enabled_loggers = set(self.loggers.keys())
missing = enabled_loggers - setup_loggers
root_logger.info(
f"Setup background logging for: {', '.join(setup_loggers)}"
)
if missing:
root_logger.warning(
f"Logger config not found for: {', '.join(missing)}"
)
setup_logging(True, no_color=False, log_extra=True)

while self.run:
try:
record: LogRecord = queue.get(timeout=0.05)
Expand Down
6 changes: 3 additions & 3 deletions sanic_ext/extensions/openapi/blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,9 @@ def build_spec(app, loop):
):
operation.autodoc(docstring)

operation._default[
"operationId"
] = f"{method.lower()}~{route_name}"
operation._default["operationId"] = (
f"{method.lower()}~{route_name}"
)
operation._default["summary"] = clean_route_name(route_name)

if host:
Expand Down
8 changes: 5 additions & 3 deletions sanic_ext/extensions/openapi/builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,8 +438,10 @@ def _build_paths(self, app: Sanic) -> Dict:

def _build_security(self):
return [
{sec.fields["name"]: sec.fields["value"]}
if sec.fields["name"] is not None
else {}
(
{sec.fields["name"]: sec.fields["value"]}
if sec.fields["name"] is not None
else {}
)
for sec in self.security
]
1 change: 1 addition & 0 deletions sanic_ext/extensions/openapi/definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
I.e., the objects described https://swagger.io/docs/specification
"""

from __future__ import annotations

from inspect import isclass
Expand Down
16 changes: 6 additions & 10 deletions sanic_ext/extensions/openapi/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
documentation to OperationStore() and components created in the blueprints.
"""

from functools import wraps
from inspect import isawaitable, isclass
from typing import (
Expand Down Expand Up @@ -94,13 +95,11 @@ def _content_or_component(content):


@overload
def exclude(flag: bool = True, *, bp: Blueprint) -> None:
...
def exclude(flag: bool = True, *, bp: Blueprint) -> None: ...


@overload
def exclude(flag: bool = True) -> Callable:
...
def exclude(flag: bool = True) -> Callable: ...


def exclude(flag: bool = True, *, bp: Optional[Blueprint] = None):
Expand Down Expand Up @@ -247,8 +246,7 @@ def parameter(
*,
parameter: definitions.Parameter,
**kwargs,
) -> Callable[[T], T]:
...
) -> Callable[[T], T]: ...


@overload
Expand All @@ -258,8 +256,7 @@ def parameter(
location: None,
parameter: definitions.Parameter,
**kwargs,
) -> Callable[[T], T]:
...
) -> Callable[[T], T]: ...


@overload
Expand All @@ -269,8 +266,7 @@ def parameter(
location: Optional[str] = None,
parameter: None = None,
**kwargs,
) -> Callable[[T], T]:
...
) -> Callable[[T], T]: ...


def parameter(
Expand Down
8 changes: 5 additions & 3 deletions sanic_ext/extensions/openapi/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,9 +344,11 @@ def make(cls, value: Any, **kwargs):
fields = [
MsgspecAdapter(
name=f.name,
default=MISSING
if f.default in (UNSET, NODEFAULT)
else f.default,
default=(
MISSING
if f.default in (UNSET, NODEFAULT)
else f.default
),
metadata=getattr(f.type, "extra", {}),
)
for f in msgspec_type_info(value).fields
Expand Down
3 changes: 1 addition & 2 deletions sanic_ext/extensions/templating/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@
from jinja2 import Environment


class TemplateResponse(HTTPResponse):
...
class TemplateResponse(HTTPResponse): ...


class LazyResponse(TemplateResponse):
Expand Down
4 changes: 2 additions & 2 deletions sanic_ext/extras/validation/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ def validate_body(
except VALIDATION_ERROR as e:
raise ValidationError(
f"Invalid request body: {model.__name__}. Error: {e}",
extra={"exception": e},
)
extra={"exception": str(e)},
) from None


def _msgspec_validate_instance(model, body, allow_coerce):
Expand Down
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""
Sanic
"""

from setuptools import setup

setup()
4 changes: 1 addition & 3 deletions tests/extensions/http/test_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,7 @@ def test_auto_trace(bare_app: Sanic):
async def foo_handler(_):
return text("...")

request, response = bare_app.test_client.request(
"/foo", http_method="trace"
)
request, response = bare_app.test_client.request("/foo", http_method="trace")
assert response.status == 200
assert response.body.startswith(request.head)

Expand Down
Loading

0 comments on commit 16e632f

Please sign in to comment.