chore: rename project coven → hack-house ⛧
Rebrand the Rust client crate (coven/ → hh/, package+binary "hack-house"), README, CLI strings, and branch (coven → hack-house). Gitea repo renamed cmd-chat → hack-house to match. Crypto/server logic unchanged; selftest + golden-vector test still green, binary is now `hack-house`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
@@ -0,0 +1,116 @@
|
||||
import logging
|
||||
|
||||
from typing import Any, 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__}"
|
||||
@@ -0,0 +1,126 @@
|
||||
import logging
|
||||
|
||||
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 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, *_):
|
||||
Logger.prepare(app)
|
||||
|
||||
|
||||
async def setup_logger(app: Sanic, *_):
|
||||
logger = Logger()
|
||||
extractor = LoggingConfigExtractor()
|
||||
for logger_name in app.config.LOGGERS:
|
||||
extractor.add_logger(logging.getLogger(logger_name))
|
||||
app.manager.manage(
|
||||
"Logger",
|
||||
logger,
|
||||
{
|
||||
"queue": app.shared_ctx.logger_queue,
|
||||
"config": extractor.compile(),
|
||||
},
|
||||
transient=True,
|
||||
)
|
||||
|
||||
|
||||
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_name in app.config.LOGGERS:
|
||||
logger_instance = logging.getLogger(logger_name)
|
||||
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:
|
||||
LOGGERS: list[str] = []
|
||||
|
||||
def __init__(self):
|
||||
self.run = True
|
||||
self.loggers = {
|
||||
logger: logging.getLogger(logger) for logger in self.LOGGERS
|
||||
}
|
||||
|
||||
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)
|
||||
except Empty:
|
||||
continue
|
||||
logger = self.loggers.get(record.name)
|
||||
logger.handle(record)
|
||||
|
||||
def stop(self, *_):
|
||||
if self.run:
|
||||
self.run = False
|
||||
|
||||
@classmethod
|
||||
def update_cls_loggers(cls, logger_names: list[str]):
|
||||
cls.LOGGERS = logger_names
|
||||
|
||||
@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
|
||||
cls.update_cls_loggers(app.config.LOGGERS)
|
||||
|
||||
@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)
|
||||
Reference in New Issue
Block a user