Skip to content

Commit

Permalink
Add logging process extension (#134)
Browse files Browse the repository at this point in the history
  • Loading branch information
ahopkins authored Sep 28, 2022
1 parent 293d884 commit f1b2c71
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 4 deletions.
10 changes: 6 additions & 4 deletions sanic_ext/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from sanic_ext.extensions.http.extension import HTTPExtension
from sanic_ext.extensions.injection.extension import InjectionExtension
from sanic_ext.extensions.injection.registry import InjectionRegistry
from sanic_ext.extensions.logging.extension import LoggingExtension
from sanic_ext.extensions.openapi.builders import SpecificationBuilder
from sanic_ext.extensions.openapi.extension import OpenAPIExtension
from sanic_ext.utils.string import camel_to_snake
Expand Down Expand Up @@ -91,14 +92,15 @@ def __init__(
OpenAPIExtension,
HTTPExtension,
HealthExtension,
LoggingExtension,
]
)

if TEMPLATING_ENABLED:
extensions.append(TemplatingExtension)

started = set()
for ext in extensions[::-1]:
for ext in extensions:
if ext in started:
continue
extension = Extension.create(ext, app, self.config)
Expand All @@ -109,9 +111,9 @@ def __init__(
def _display(self):
init_logs = ["Sanic Extensions:"]
for extension in self.extensions:
init_logs.append(
f" > {extension.name} {extension.render_label()}"
)
label = extension.render_label()
if extension.included():
init_logs.append(f" > {extension.name} {label}")

list(map(logger.info, init_logs))

Expand Down
4 changes: 4 additions & 0 deletions sanic_ext/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ def __init__(
http_auto_options: bool = True,
http_auto_trace: bool = False,
injection_signal: Union[str, Event] = Event.HTTP_ROUTING_AFTER,
logging: bool = False,
logging_queue_max_size: int = 4096,
oas: bool = True,
oas_autodoc: bool = True,
oas_ignore_head: bool = True,
Expand Down Expand Up @@ -83,6 +85,8 @@ def __init__(
self.HTTP_AUTO_OPTIONS = http_auto_options
self.HTTP_AUTO_TRACE = http_auto_trace
self.INJECTION_SIGNAL = injection_signal
self.LOGGING = logging
self.LOGGING_QUEUE_MAX_SIZE = logging_queue_max_size
self.OAS = oas
self.OAS_AUTODOC = oas_autodoc
self.OAS_IGNORE_HEAD = oas_ignore_head
Expand Down
5 changes: 5 additions & 0 deletions sanic_ext/extensions/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,16 @@ def label(self):
return ""

def render_label(self):
if not self.included:
return "~~disabled~~"
label = self.label()
if not label:
return ""
return f"[{label}]"

def included(self):
return True

@classmethod
def create(
cls,
Expand Down
3 changes: 3 additions & 0 deletions sanic_ext/extensions/health/extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,6 @@ def startup(self, bootstrap) -> None:

if self.config.HEALTH_ENDPOINT:
setup_health_endpoint(self.app)

def included(self):
return self.config.HEALTH
Empty file.
24 changes: 24 additions & 0 deletions sanic_ext/extensions/logging/extension.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from sanic.exceptions import SanicException

from ..base import Extension
from .logger import Logger


class LoggingExtension(Extension):
name = "logging"
MIN_VERSION = (22, 9)

def startup(self, bootstrap) -> None:
if self.included():
if self.MIN_VERSION > bootstrap.sanic_version:
min_version = ".".join(map(str, self.MIN_VERSION))
sanic_version = ".".join(map(str, bootstrap.sanic_version))
raise SanicException(
f"The logging extension only works with Sanic "
f"v{min_version} and above. It looks like you are "
f"running {sanic_version}."
)
Logger.setup(self.app)

def included(self):
return self.config.LOGGING
96 changes: 96 additions & 0 deletions sanic_ext/extensions/logging/logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from collections import defaultdict
from logging import LogRecord
from logging.handlers import QueueHandler
from multiprocessing import Manager
from queue import Empty, Full
from signal import SIGINT, SIGTERM
from signal import signal as signal_func

from sanic import Sanic
from sanic.log import access_logger, error_logger
from sanic.log import logger as root_logger
from sanic.log import server_logger


async def prepare_logger(app: Sanic, *_):
Logger.prepare(app)


async def setup_logger(app: Sanic, *_):
logger = Logger()
app.manager.manage(
"Logger",
logger,
{
"queue": app.shared_ctx.logger_queue,
},
)


class SanicQueueHandler(QueueHandler):
def emit(self, record: LogRecord) -> None:
try:
return super().enqueue(record)
except Full:
server_logger.warning(
"Background logger is full. Emitting log in process."
)
server_logger.handle(record)


async def setup_server_logging(app: Sanic):
qhandler = SanicQueueHandler(app.shared_ctx.logger_queue)
app.ctx._logger_handlers = defaultdict(list)
app.ctx._qhandler = qhandler

for logger_instance in (root_logger, access_logger, error_logger):
for handler in logger_instance.handlers:
logger_instance.removeHandler(handler)
logger_instance.addHandler(qhandler)


async def remove_server_logging(app: Sanic):
for logger, handlers in app.ctx._logger_handlers.items():
logger.removeHandler(app.ctx._qhandler)
for handler in handlers:
logger.addHandler(handler)


class Logger:
def __init__(self):
self.run = True
self.loggers = {
logger.name: logger
for logger in (root_logger, access_logger, error_logger)
}

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

while self.run:
try:
record: LogRecord = queue.get_nowait()
except Empty:
continue
logger = self.loggers.get(record.name)
logger.handle(record)

def stop(self, *_):
if self.run:
self.run = False

@classmethod
def prepare(cls, app: Sanic):
sync_manager = Manager()
logger_queue = sync_manager.Queue(
maxsize=app.config.LOGGING_QUEUE_MAX_SIZE
)
app.shared_ctx.logger_queue = logger_queue

@classmethod
def setup(cls, app: Sanic):
app.main_process_start(prepare_logger)
app.main_process_ready(setup_logger)
app.before_server_start(setup_server_logging)
app.before_server_stop(remove_server_logging)
3 changes: 3 additions & 0 deletions sanic_ext/extensions/openapi/extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ def label(self):

return ""

def included(self):
return self.config.OAS

def _make_url(self):
name = f"{self.bp.name}.index"
_server = (
Expand Down

0 comments on commit f1b2c71

Please sign in to comment.