-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
16 changed files
with
515 additions
and
111 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import logging | ||
import sys | ||
from enum import StrEnum | ||
from typing import Any | ||
|
||
import structlog | ||
from structlog.types import EventDict, Processor # noqa: F401 | ||
|
||
|
||
class CLogLevel(StrEnum): | ||
DEBUG = "debug" | ||
INFO = "info" | ||
WARNING = "warning" | ||
FATAL = "fatal" | ||
CRITICAL = "critical" | ||
|
||
|
||
async def async_ep_log( | ||
logger_name: str, err_msg: str, level: CLogLevel = CLogLevel.WARNING | ||
) -> Any: | ||
lgr = structlog.stdlib.get_logger(logger_name) | ||
coro = getattr(lgr, f"a{level.lower()}") | ||
await coro(err_msg) | ||
|
||
|
||
def ep_log(logger_name: str, err_msg: str, level: CLogLevel = CLogLevel.WARNING) -> Any: | ||
lgr = structlog.stdlib.get_logger(logger_name) | ||
func = getattr(lgr, level.lower()) | ||
func(err_msg) | ||
|
||
|
||
def setup_logging(json_logs: bool = False, log_level: CLogLevel = CLogLevel.INFO): | ||
"""see https://gist.github.com/nymous/f138c7f06062b7c43c060bf03759c29e""" | ||
shared_processors: list[Processor] = [ | ||
structlog.contextvars.merge_contextvars, | ||
structlog.stdlib.add_logger_name, | ||
structlog.stdlib.add_log_level, | ||
structlog.stdlib.PositionalArgumentsFormatter(), | ||
structlog.stdlib.ExtraAdder(), | ||
structlog.processors.TimeStamper(fmt="iso"), | ||
structlog.processors.StackInfoRenderer(), | ||
] | ||
|
||
if json_logs: | ||
shared_processors.append(structlog.processors.format_exc_info) | ||
|
||
structlog.configure( | ||
processors=shared_processors | ||
+ [ | ||
structlog.stdlib.ProcessorFormatter.wrap_for_formatter, | ||
], | ||
logger_factory=structlog.stdlib.LoggerFactory(), | ||
cache_logger_on_first_use=True, # Be careful for testing? | ||
) | ||
|
||
log_renderer: structlog.types.Processor | ||
if json_logs: | ||
log_renderer = structlog.processors.JSONRenderer() | ||
else: | ||
log_renderer = structlog.dev.ConsoleRenderer() | ||
|
||
formatter = structlog.stdlib.ProcessorFormatter( | ||
foreign_pre_chain=shared_processors, | ||
processors=[ | ||
# Remove _record & _from_structlog. | ||
structlog.stdlib.ProcessorFormatter.remove_processors_meta, | ||
log_renderer, | ||
], | ||
) | ||
|
||
handler = logging.StreamHandler() | ||
handler.setFormatter(formatter) | ||
root_logger = logging.getLogger() | ||
root_logger.addHandler(handler) | ||
root_logger.setLevel(log_level.upper()) | ||
|
||
for _log in ["uvicorn", "uvicorn.error"]: | ||
logging.getLogger(_log).handlers.clear() | ||
logging.getLogger(_log).propagate = True | ||
|
||
logging.getLogger("uvicorn.access").handlers.clear() | ||
logging.getLogger("uvicorn.access").propagate = False | ||
|
||
def handle_exception(exc_type, exc_value, exc_traceback): | ||
""" | ||
Log any uncaught exception instead of letting it be printed by Python | ||
(but leave KeyboardInterrupt untouched to allow users to Ctrl+C to stop) | ||
See https://stackoverflow.com/a/16993115/3641865 | ||
""" | ||
if issubclass(exc_type, KeyboardInterrupt): | ||
sys.__excepthook__(exc_type, exc_value, exc_traceback) | ||
return | ||
|
||
root_logger.error( | ||
"Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback) | ||
) | ||
|
||
sys.excepthook = handle_exception |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import time | ||
|
||
import structlog | ||
from asgi_correlation_id.context import correlation_id | ||
from fastapi import Request, Response | ||
from uvicorn.protocols.utils import get_path_with_query_string | ||
|
||
|
||
async def add_logging_middleware(request: Request, call_next) -> Response: | ||
api_access_logger = structlog.stdlib.get_logger("backend.access") | ||
structlog.contextvars.clear_contextvars() | ||
request_id = correlation_id.get() | ||
structlog.contextvars.bind_contextvars(request_id=request_id) | ||
start_time = time.perf_counter_ns() | ||
|
||
response = Response(status_code=500) | ||
try: | ||
response = await call_next(request) | ||
except Exception: | ||
structlog.stdlib.get_logger("api.error").exception("Uncaught exception") | ||
raise | ||
finally: | ||
process_time = time.perf_counter_ns() - start_time | ||
status_code = response.status_code | ||
url = get_path_with_query_string(request.scope) | ||
|
||
# request.client is None for tests, so we need to fill in something here | ||
client_host = getattr(request.client, "host", "testing_client_host") | ||
client_port = getattr(request.client, "port", "testing_client_port") | ||
|
||
http_method = request.method | ||
http_version = request.scope["http_version"] | ||
api_access_logger.info( | ||
f"""{client_host}:{client_port} - "{http_method} {url} HTTP/{http_version}" {status_code}""", | ||
http={ | ||
"url": str(request.url), | ||
"status_code": status_code, | ||
"method": http_method, | ||
"request_id": request_id, | ||
"version": http_version, | ||
}, | ||
network={"client": {"ip": client_host, "port": client_port}}, | ||
duration=process_time, | ||
) | ||
response.headers["X-Process-Time"] = str(process_time / 10**9) | ||
return response |
Oops, something went wrong.