Skip to content

Commit 16e632f

Browse files
committed
Add logging extraction to background logger
1 parent 7ab93d9 commit 16e632f

33 files changed

+285
-273
lines changed

sanic_ext/bootstrap.py

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
import os
4+
35
from types import SimpleNamespace
46
from typing import Any, Callable, Dict, List, Mapping, Optional, Type, Union
57
from warnings import warn
@@ -114,6 +116,8 @@ def __init__(
114116
started.add(ext)
115117

116118
def _display(self):
119+
if "SANIC_WORKER_IDENTIFIER" in os.environ:
120+
return
117121
init_logs = ["Sanic Extensions:"]
118122
for extension in self.extensions:
119123
label = extension.render_label()

sanic_ext/config.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,13 @@ def __init__(
4444
injection_load_custom_constants: bool = False,
4545
logging: bool = False,
4646
logging_queue_max_size: int = 4096,
47-
loggers: List[str] = ["sanic.access", "sanic.error", "sanic.root"],
47+
loggers: List[str] = [
48+
"sanic.access",
49+
"sanic.error",
50+
"sanic.root",
51+
"sanic.server",
52+
"sanic.websockets",
53+
],
4854
oas: bool = True,
4955
oas_autodoc: bool = True,
5056
oas_custom_file: Optional[os.PathLike] = None,

sanic_ext/exceptions.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@ class ValidationError(SanicException):
55
status_code = 400
66

77

8-
class InitError(SanicException):
9-
...
8+
class InitError(SanicException): ...
109

1110

12-
class ExtensionNotFound(SanicException):
13-
...
11+
class ExtensionNotFound(SanicException): ...

sanic_ext/extensions/base.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,7 @@ def _startup(self, bootstrap):
4646
self._started = True
4747

4848
@abstractmethod
49-
def startup(self, bootstrap) -> None:
50-
...
49+
def startup(self, bootstrap) -> None: ...
5150

5251
def label(self):
5352
return ""

sanic_ext/extensions/health/monitor.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@
1717
from sanic import Sanic
1818

1919

20-
class Stale(ValueError):
21-
...
20+
class Stale(ValueError): ...
2221

2322

2423
@dataclass
+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import logging
2+
3+
from typing import Any, Dict, Optional, TypedDict
4+
5+
6+
class LoggerConfig(TypedDict):
7+
level: str
8+
propagate: bool
9+
handlers: list[str]
10+
11+
12+
class HandlerConfig(TypedDict):
13+
class_: str
14+
level: str
15+
stream: Optional[str]
16+
formatter: Optional[str]
17+
18+
19+
class FormatterConfig(TypedDict):
20+
class_: str
21+
format: Optional[str]
22+
datefmt: Optional[str]
23+
24+
25+
class LoggingConfig(TypedDict):
26+
version: int
27+
disable_existing_loggers: bool
28+
formatters: Dict[str, FormatterConfig]
29+
handlers: Dict[str, HandlerConfig]
30+
loggers: Dict[str, LoggerConfig]
31+
32+
33+
class LoggingConfigExtractor:
34+
def __init__(self):
35+
self.version = 1
36+
self.disable_existing_loggers = False
37+
self.formatters: Dict[str, FormatterConfig] = {}
38+
self.handlers: Dict[str, HandlerConfig] = {}
39+
self.loggers: Dict[str, LoggerConfig] = {}
40+
41+
def add_logger(self, logger: logging.Logger):
42+
self._extract_logger_config(logger)
43+
self._extract_handlers(logger)
44+
45+
def compile(self) -> LoggingConfig:
46+
output = {
47+
"version": self.version,
48+
"disable_existing_loggers": self.disable_existing_loggers,
49+
"formatters": self.formatters,
50+
"handlers": self.handlers,
51+
"loggers": self.loggers,
52+
}
53+
return self._clean(output)
54+
55+
def _extract_logger_config(self, logger: logging.Logger):
56+
config: LoggerConfig = {
57+
"level": logging.getLevelName(logger.level),
58+
"propagate": logger.propagate,
59+
"handlers": [handler.get_name() for handler in logger.handlers],
60+
}
61+
self.loggers[logger.name] = config
62+
63+
def _extract_handlers(self, logger: logging.Logger):
64+
for handler in logger.handlers:
65+
self._extract_handler_config(handler)
66+
67+
def _extract_handler_config(self, handler: logging.Handler):
68+
handler_name = handler.get_name()
69+
if handler_name in self.handlers:
70+
return
71+
config: HandlerConfig = {
72+
"class_": self._full_name(handler),
73+
"level": logging.getLevelName(handler.level),
74+
"formatter": (
75+
self._formatter_name(handler.formatter)
76+
if handler.formatter
77+
else None
78+
),
79+
"stream": None,
80+
}
81+
# if (stream := getattr(handler, "stream", None)) and (
82+
# stream_name := getattr(stream, "name", None)
83+
# ):
84+
# config["stream"] = stream_name
85+
self.handlers[handler_name] = config
86+
if handler.formatter:
87+
self._extract_formatter_config(handler.formatter)
88+
89+
def _extract_formatter_config(self, formatter: logging.Formatter):
90+
formatter_name = self._formatter_name(formatter)
91+
if formatter_name in self.formatters:
92+
return
93+
config: FormatterConfig = {
94+
"class_": self._full_name(formatter),
95+
"format": formatter._fmt,
96+
"datefmt": formatter.datefmt,
97+
}
98+
self.formatters[formatter_name] = config
99+
100+
def _clean(self, d: Dict[str, Any]) -> Dict[str, Any]:
101+
return {
102+
k.replace("class_", "class"): self._clean(v)
103+
if isinstance(v, dict)
104+
else v
105+
for k, v in d.items()
106+
}
107+
108+
@staticmethod
109+
def _formatter_name(
110+
formatter: logging.Formatter, prefix: str = "formatter"
111+
):
112+
return f"{prefix}_{formatter.__class__.__name__}".lower()
113+
114+
@staticmethod
115+
def _full_name(obj):
116+
return f"{obj.__module__}.{obj.__class__.__name__}"

sanic_ext/extensions/logging/logger.py

+27-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@
1010
from typing import List
1111

1212
from sanic import Sanic
13-
from sanic.log import server_logger
13+
from sanic.log import logger as root_logger
14+
from sanic.log import logger as server_logger
15+
from sanic.logging.setup import setup_logging
16+
17+
from sanic_ext.extensions.logging.extractor import LoggingConfigExtractor
1418

1519

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

2024
async def setup_logger(app: Sanic, *_):
2125
logger = Logger()
26+
extractor = LoggingConfigExtractor()
27+
for logger_name in app.config.LOGGERS:
28+
l = logging.getLogger(logger_name)
29+
extractor.add_logger(l)
2230
app.manager.manage(
2331
"Logger",
2432
logger,
2533
{
2634
"queue": app.shared_ctx.logger_queue,
35+
"config": extractor.compile(),
2736
},
37+
transient=True,
2838
)
2939

3040

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

6070

6171
class Logger:
62-
LOGGERS = []
72+
LOGGERS: List[str] = []
6373

6474
def __init__(self):
6575
self.run = True
6676
self.loggers = {
6777
logger: logging.getLogger(logger) for logger in self.LOGGERS
6878
}
6979

70-
def __call__(self, queue) -> None:
80+
def __call__(self, queue, config) -> None:
7181
signal_func(SIGINT, self.stop)
7282
signal_func(SIGTERM, self.stop)
7383

84+
logging.config.dictConfig(config)
85+
86+
setup_loggers = set(config["loggers"].keys())
87+
enabled_loggers = set(self.loggers.keys())
88+
missing = enabled_loggers - setup_loggers
89+
root_logger.info(
90+
f"Setup background logging for: {', '.join(setup_loggers)}"
91+
)
92+
if missing:
93+
root_logger.warning(
94+
f"Logger config not found for: {', '.join(missing)}"
95+
)
96+
setup_logging(True, no_color=False, log_extra=True)
97+
7498
while self.run:
7599
try:
76100
record: LogRecord = queue.get(timeout=0.05)

sanic_ext/extensions/openapi/blueprint.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,9 @@ def build_spec(app, loop):
173173
):
174174
operation.autodoc(docstring)
175175

176-
operation._default[
177-
"operationId"
178-
] = f"{method.lower()}~{route_name}"
176+
operation._default["operationId"] = (
177+
f"{method.lower()}~{route_name}"
178+
)
179179
operation._default["summary"] = clean_route_name(route_name)
180180

181181
if host:

sanic_ext/extensions/openapi/builders.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -438,8 +438,10 @@ def _build_paths(self, app: Sanic) -> Dict:
438438

439439
def _build_security(self):
440440
return [
441-
{sec.fields["name"]: sec.fields["value"]}
442-
if sec.fields["name"] is not None
443-
else {}
441+
(
442+
{sec.fields["name"]: sec.fields["value"]}
443+
if sec.fields["name"] is not None
444+
else {}
445+
)
444446
for sec in self.security
445447
]

sanic_ext/extensions/openapi/definitions.py

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
I.e., the objects described https://swagger.io/docs/specification
55
66
"""
7+
78
from __future__ import annotations
89

910
from inspect import isclass

sanic_ext/extensions/openapi/openapi.py

+6-10
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
documentation to OperationStore() and components created in the blueprints.
44
55
"""
6+
67
from functools import wraps
78
from inspect import isawaitable, isclass
89
from typing import (
@@ -94,13 +95,11 @@ def _content_or_component(content):
9495

9596

9697
@overload
97-
def exclude(flag: bool = True, *, bp: Blueprint) -> None:
98-
...
98+
def exclude(flag: bool = True, *, bp: Blueprint) -> None: ...
9999

100100

101101
@overload
102-
def exclude(flag: bool = True) -> Callable:
103-
...
102+
def exclude(flag: bool = True) -> Callable: ...
104103

105104

106105
def exclude(flag: bool = True, *, bp: Optional[Blueprint] = None):
@@ -247,8 +246,7 @@ def parameter(
247246
*,
248247
parameter: definitions.Parameter,
249248
**kwargs,
250-
) -> Callable[[T], T]:
251-
...
249+
) -> Callable[[T], T]: ...
252250

253251

254252
@overload
@@ -258,8 +256,7 @@ def parameter(
258256
location: None,
259257
parameter: definitions.Parameter,
260258
**kwargs,
261-
) -> Callable[[T], T]:
262-
...
259+
) -> Callable[[T], T]: ...
263260

264261

265262
@overload
@@ -269,8 +266,7 @@ def parameter(
269266
location: Optional[str] = None,
270267
parameter: None = None,
271268
**kwargs,
272-
) -> Callable[[T], T]:
273-
...
269+
) -> Callable[[T], T]: ...
274270

275271

276272
def parameter(

sanic_ext/extensions/openapi/types.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -344,9 +344,11 @@ def make(cls, value: Any, **kwargs):
344344
fields = [
345345
MsgspecAdapter(
346346
name=f.name,
347-
default=MISSING
348-
if f.default in (UNSET, NODEFAULT)
349-
else f.default,
347+
default=(
348+
MISSING
349+
if f.default in (UNSET, NODEFAULT)
350+
else f.default
351+
),
350352
metadata=getattr(f.type, "extra", {}),
351353
)
352354
for f in msgspec_type_info(value).fields

sanic_ext/extensions/templating/render.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@
1616
from jinja2 import Environment
1717

1818

19-
class TemplateResponse(HTTPResponse):
20-
...
19+
class TemplateResponse(HTTPResponse): ...
2120

2221

2322
class LazyResponse(TemplateResponse):

sanic_ext/extras/validation/validators.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ def validate_body(
2727
except VALIDATION_ERROR as e:
2828
raise ValidationError(
2929
f"Invalid request body: {model.__name__}. Error: {e}",
30-
extra={"exception": e},
31-
)
30+
extra={"exception": str(e)},
31+
) from None
3232

3333

3434
def _msgspec_validate_instance(model, body, allow_coerce):

setup.py

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""
22
Sanic
33
"""
4+
45
from setuptools import setup
6+
57
setup()

tests/extensions/http/test_methods.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,7 @@ def test_auto_trace(bare_app: Sanic):
5757
async def foo_handler(_):
5858
return text("...")
5959

60-
request, response = bare_app.test_client.request(
61-
"/foo", http_method="trace"
62-
)
60+
request, response = bare_app.test_client.request("/foo", http_method="trace")
6361
assert response.status == 200
6462
assert response.body.startswith(request.head)
6563

0 commit comments

Comments
 (0)