Skip to content

Commit c13dc2c

Browse files
authored
Added loguru logger. (#99)
Closes #97. Signed-off-by: Pavel Kirilin <win10@list.ru>
1 parent 3203905 commit c13dc2c

11 files changed

Lines changed: 170 additions & 4 deletions

File tree

fastapi_template/cli.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,13 @@ def parse_args():
144144
default=None,
145145
dest="sentry_enabled",
146146
)
147+
parser.add_argument(
148+
"--loguru",
149+
help="Add loguru logger",
150+
action="store_true",
151+
default=None,
152+
dest="enable_loguru",
153+
)
147154
parser.add_argument(
148155
"--opentelemetry",
149156
help="Add opentelemetry integration",
@@ -203,6 +210,10 @@ def ask_features(current_context: BuilderContext) -> BuilderContext:
203210
"name": "otlp_enabled",
204211
"value": current_context.otlp_enabled,
205212
},
213+
"Loguru logger": {
214+
"name": "enable_loguru",
215+
"value": current_context.enable_loguru,
216+
},
206217
}
207218
if current_context.db != DatabaseType.none:
208219
features["Migrations support"] = {

fastapi_template/input_model.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ class BuilderContext(BaseModel):
121121
sentry_enabled: Optional[bool]
122122
otlp_enabled: Optional[bool]
123123
enable_rmq: Optional[bool]
124+
enable_loguru: Optional[bool]
124125
force: bool = False
125126
quite: bool = False
126127

fastapi_template/template/cookiecutter.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
"enable_routers": {
3333
"type": "bool"
3434
},
35+
"enable_loguru": {
36+
"type": "bool"
37+
},
3538
"add_dummy": {
3639
"type": "bool"
3740
},

fastapi_template/template/{{cookiecutter.project_name}}/conditional_files.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@
8787
".github"
8888
]
8989
},
90+
"Loguru": {
91+
"enabled": "{{cookiecutter.enable_loguru}}",
92+
"resources": [
93+
"{{cookiecutter.project_name}}/logging.py"
94+
]
95+
},
9096
"Routers": {
9197
"enabled": "{{cookiecutter.enable_routers}}",
9298
"resources": [
@@ -192,4 +198,4 @@
192198
"{{cookiecutter.project_name}}/db_tortoise/migrations/models/1_20210928165300_init_dummy_sqlite.sql"
193199
]
194200
}
195-
}
201+
}

fastapi_template/template/{{cookiecutter.project_name}}/pyproject.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,10 @@ opentelemetry-api = {version = "^1.12.0rc2", allow-prereleases = true}
9696
opentelemetry-sdk = {version = "^1.12.0rc2", allow-prereleases = true}
9797
opentelemetry-exporter-otlp = {version = "^1.12.0rc2", allow-prereleases = true}
9898
opentelemetry-instrumentation = "^0.32b0"
99-
opentelemetry-instrumentation-logging = "^0.32b0"
10099
opentelemetry-instrumentation-fastapi = "^0.32b0"
100+
{%- if cookiecutter.enable_loguru != "True" %}
101+
opentelemetry-instrumentation-logging = "^0.32b0"
102+
{%- endif %}
101103
{%- if cookiecutter.enable_redis == "True" %}
102104
opentelemetry-instrumentation-redis = "^0.32b0"
103105
{%- endif %}
@@ -111,6 +113,9 @@ opentelemetry-instrumentation-sqlalchemy = "^0.32b0"
111113
opentelemetry-instrumentation-aio-pika = "^0.32b0"
112114
{%- endif %}
113115
{%- endif %}
116+
{%- if cookiecutter.enable_loguru == "True" %}
117+
loguru = "^0.6.0"
118+
{%- endif %}
114119

115120
[tool.poetry.dev-dependencies]
116121
pytest = "^7.0"

fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/__main__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ def main() -> None:
4646
host=settings.host,
4747
port=settings.port,
4848
reload=settings.reload,
49+
log_level=settings.log_level.value.lower(),
4950
factory=True,
5051
)
5152

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import logging
2+
import sys
3+
from typing import Any, Union
4+
5+
from loguru import logger
6+
7+
from {{cookiecutter.project_name}}.settings import settings
8+
9+
{%- if cookiecutter.otlp_enabled == "True" %}
10+
from opentelemetry.trace import INVALID_SPAN, INVALID_SPAN_CONTEXT, get_current_span
11+
{%- endif %}
12+
13+
14+
class InterceptHandler(logging.Handler):
15+
"""
16+
Default handler from examples in loguru documentation.
17+
18+
This handler intercepts all log requests and
19+
passes them to loguru.
20+
21+
For more info see:
22+
https://loguru.readthedocs.io/en/stable/overview.html#entirely-compatible-with-standard-logging
23+
"""
24+
25+
def emit(self, record: logging.LogRecord) -> None:
26+
"""
27+
Propagates logs to loguru.
28+
29+
:param record: record to log.
30+
"""
31+
try:
32+
level: Union[str, int] = logger.level(record.levelname).name
33+
except ValueError:
34+
level = record.levelno
35+
36+
# Find caller from where originated the logged message
37+
frame, depth = logging.currentframe(), 2
38+
while frame.f_code.co_filename == logging.__file__:
39+
frame = frame.f_back # type: ignore
40+
depth += 1
41+
42+
logger.opt(depth=depth, exception=record.exc_info).log(
43+
level,
44+
record.getMessage(),
45+
)
46+
47+
{%- if cookiecutter.otlp_enabled == "True" %}
48+
49+
def record_formatter(record: dict[str, Any]) -> str:
50+
"""
51+
Formats the record.
52+
53+
This function formats message
54+
by adding extra trace information to the record.
55+
56+
:param record: record information.
57+
:return: format string.
58+
"""
59+
log_format = (
60+
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> "
61+
"| <level>{level: <8}</level> "
62+
"| <magenta>trace_id={extra[trace_id]}</magenta> "
63+
"| <blue>span_id={extra[span_id]}</blue> "
64+
"| <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> "
65+
"- <level>{message}</level>\n"
66+
)
67+
68+
span = get_current_span()
69+
record["extra"]["span_id"] = 0
70+
record["extra"]["trace_id"] = 0
71+
if span != INVALID_SPAN:
72+
span_context = span.get_span_context()
73+
if span_context != INVALID_SPAN_CONTEXT:
74+
record["extra"]["span_id"] = format(span_context.span_id, "016x")
75+
record["extra"]["trace_id"] = format(span_context.trace_id, "032x")
76+
77+
if record["exception"]:
78+
log_format = f"{log_format}{{'{{'}}exception{{'}}'}}"
79+
80+
return log_format
81+
82+
{%- endif %}
83+
84+
def configure_logging() -> None:
85+
"""Configures logging."""
86+
loggers = (
87+
logging.getLogger(name)
88+
for name in logging.root.manager.loggerDict
89+
if name.startswith("uvicorn.")
90+
)
91+
for uvicorn_logger in loggers:
92+
uvicorn_logger.handlers = []
93+
94+
# change handler for default uvicorn logger
95+
intercept_handler = InterceptHandler()
96+
logging.getLogger("uvicorn").handlers = [intercept_handler]
97+
logging.getLogger("uvicorn.access").handlers = [intercept_handler]
98+
99+
# set logs output, level and format
100+
logger.remove()
101+
logger.add(
102+
sys.stdout,
103+
level=settings.log_level.value,
104+
{%- if cookiecutter.otlp_enabled == "True" %}
105+
format=record_formatter, # type: ignore
106+
{%- endif %}
107+
)

fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/settings.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import enum
12
from pathlib import Path
23
from tempfile import gettempdir
34
from typing import Optional
@@ -7,6 +8,16 @@
78

89
TEMP_DIR = Path(gettempdir())
910

11+
class LogLevel(str, enum.Enum): # noqa: WPS600
12+
"""Possible log levels."""
13+
14+
NOTSET = "NOTSET"
15+
DEBUG = "DEBUG"
16+
INFO = "INFO"
17+
WARNING = "WARNING"
18+
ERROR = "ERROR"
19+
FATAL = "FATAL"
20+
1021

1122
class Settings(BaseSettings):
1223
"""
@@ -26,6 +37,8 @@ class Settings(BaseSettings):
2637
# Current environment
2738
environment: str = "dev"
2839

40+
log_level: LogLevel = LogLevel.INFO
41+
2942
{% if cookiecutter.db_info.name != "none" -%}
3043

3144
# Variables for the database

fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/application.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
{%- endif %}
2424
{%- endif %}
2525

26+
{%- if cookiecutter.enable_loguru == "True" %}
27+
from {{cookiecutter.project_name}}.logging import configure_logging
28+
{%- endif %}
2629

2730
{%- if cookiecutter.self_hosted_swagger == 'True' %}
2831
from fastapi.staticfiles import StaticFiles
@@ -41,6 +44,9 @@ def get_app() -> FastAPI:
4144
4245
:return: application.
4346
"""
47+
{%- if cookiecutter.enable_loguru == "True" %}
48+
configure_logging()
49+
{%- endif %}
4450
app = FastAPI(
4551
title="{{cookiecutter.project_name}}",
4652
description="{{cookiecutter.project_description}}",
@@ -98,7 +104,9 @@ def get_app() -> FastAPI:
98104
environment=settings.environment,
99105
integrations=[
100106
LoggingIntegration(
101-
level=logging.INFO,
107+
level=logging.getLevelName(
108+
settings.log_level.value,
109+
),
102110
event_level=logging.ERROR,
103111
),
104112
{%- if cookiecutter.orm == "sqlalchemy" %}

fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/lifetime.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from typing import Awaitable, Callable
2-
2+
import logging
33
from fastapi import FastAPI
44

55
from {{cookiecutter.project_name}}.settings import settings
@@ -54,6 +54,9 @@
5454
{%- if cookiecutter.enable_rmq == "True" %}
5555
from opentelemetry.instrumentation.aio_pika import AioPikaInstrumentor
5656
{%- endif %}
57+
{%- if cookiecutter.enable_loguru != "True" %}
58+
from opentelemetry.instrumentation.logging import LoggingInstrumentor
59+
{%- endif %}
5760
{%- endif %}
5861

5962
{%- if cookiecutter.orm == "psycopg" %}
@@ -192,6 +195,13 @@ def setup_opentelemetry(app: FastAPI) -> None:
192195
tracer_provider=tracer_provider,
193196
)
194197
{%- endif %}
198+
{%- if cookiecutter.enable_loguru != "True" %}
199+
LoggingInstrumentor().instrument(
200+
tracer_provider=tracer_provider,
201+
set_logging_format=True,
202+
log_level=logging.getLevelName(settings.log_level.value),
203+
)
204+
{%- endif %}
195205

196206
set_tracer_provider(tracer_provider=tracer_provider)
197207

0 commit comments

Comments
 (0)