Skip to content

Commit d507a4a

Browse files
committed
feat(config): Add debug environment option and update service terminology
1 parent c0a3e10 commit d507a4a

19 files changed

Lines changed: 1098 additions & 143 deletions

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ repos:
2828
[
2929
"--fix",
3030
"--select=ALL",
31-
"--ignore=RUF100,BLE001,COM812,ISC001,D100,D104,D105,D107,D205,D211,D212,E203,E266,ANN204,ANN401,S104,S602,ERA001,PGH003,PLR0913,TRY400,N803,N805,N815,UP046",
31+
"--ignore=RUF100,BLE001,COM812,ISC001,D100,D104,D105,D107,D205,D211,D212,E203,E266,ANN204,ANN401,S104,S602,ERA001,PGH003,PLR0913,TRY400,N803,N805,N815,UP046,FBT001,FBT002",
3232
"--line-length=100",
3333
]
3434
# Run the formatter.

app/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def main() -> None:
3636
"""Entrypoint of the application."""
3737
set_multiproc_dir()
3838
uvicorn.run(
39-
"app.web.application:get_app",
39+
"app.core.application:get_app",
4040
workers=settings.UVICORN_WORKERS_COUNT,
4141
host=settings.UVICORN_HOST,
4242
port=settings.UVICORN_PORT,

app/core/application.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import logging
2+
from pathlib import Path
3+
4+
import sentry_sdk
5+
from fastapi import FastAPI
6+
from fastapi.middleware.cors import CORSMiddleware
7+
from fastapi.middleware.trustedhost import TrustedHostMiddleware
8+
from fastapi.responses import ORJSONResponse
9+
from fastapi.staticfiles import StaticFiles
10+
from sentry_sdk.integrations.fastapi import FastApiIntegration
11+
from sentry_sdk.integrations.logging import LoggingIntegration
12+
from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration
13+
14+
from app.controller.api.router import api_router
15+
from app.controller.errors.exception_manager import manage_api_exceptions
16+
from app.core.config import settings
17+
from app.core.lifespan import lifespan_setup
18+
from app.core.logger import configure_logging
19+
20+
APP_ROOT = Path(__file__).parent.parent
21+
22+
23+
def get_app() -> FastAPI:
24+
"""
25+
Get FastAPI application.
26+
27+
This is the main constructor of an application.
28+
29+
:return: application.
30+
"""
31+
configure_logging()
32+
if settings.SENTRY_DSN and settings.ENVIRONMENT != "local":
33+
# Enables sentry integration.
34+
sentry_sdk.init(
35+
dsn=settings.SENTRY_DSN,
36+
send_client_reports=settings.SENTRY_ALLOW_BEACON_REPORTS,
37+
traces_sample_rate=settings.SENTRY_TRACES_SAMPLE_RATE,
38+
profiles_sample_rate=settings.SENTRY_PROFILES_SAMPLE_RATE,
39+
environment=settings.ENVIRONMENT,
40+
integrations=[
41+
FastApiIntegration(transaction_style="endpoint"),
42+
LoggingIntegration(
43+
level=logging.getLevelName(
44+
settings.LOG_LEVEL.value,
45+
),
46+
event_level=logging.ERROR,
47+
),
48+
SqlalchemyIntegration(),
49+
],
50+
_experiments={"metrics": False},
51+
)
52+
app = FastAPI(
53+
title=settings.PROJECT_NAME,
54+
version=settings.API_VERSION,
55+
description=settings.PROJECT_DESCRIPTION,
56+
contact={
57+
"name": settings.CONTACT_NAME,
58+
"email": settings.CONTACT_EMAIL,
59+
},
60+
lifespan=lifespan_setup,
61+
docs_url=f"{settings.API_BASE_PATH}/docs",
62+
redoc_url=f"{settings.API_BASE_PATH}/redoc",
63+
openapi_url=f"{settings.API_BASE_PATH}/openapi.json",
64+
default_response_class=ORJSONResponse,
65+
)
66+
67+
app.add_middleware(
68+
CORSMiddleware,
69+
allow_origins=[str(origin) for origin in settings.CORS_ORIGINS],
70+
allow_credentials=True,
71+
allow_methods=["*"],
72+
allow_headers=["X-Requested-With", "X-Request-ID", "Content-Type"],
73+
expose_headers=["X-Request-ID"],
74+
)
75+
76+
app.add_middleware(TrustedHostMiddleware, allowed_hosts=settings.ALLOWED_HOSTS)
77+
78+
# Main router for the API.
79+
app.include_router(router=api_router, prefix=settings.API_BASE_PATH)
80+
81+
# Adds exceptions handler
82+
manage_api_exceptions(app=app)
83+
84+
# Adds static directory.
85+
# This directory is used to access swagger files.
86+
app.mount(
87+
f"{settings.API_BASE_PATH}",
88+
StaticFiles(directory=APP_ROOT / "static"),
89+
name="static",
90+
)
91+
92+
return app

app/core/config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ class Settings(BaseSettings):
6363
LOG_LEVEL: LogLevel = LogLevel.INFO
6464
LOG_FILE_PATH: str = "logs/app.log"
6565
USERS_SECRET: str = ""
66-
ENVIRONMENT: Literal["local", "pytest", "staging", "production"] = "local"
66+
ENVIRONMENT: Literal["debug", "local", "pytest", "staging", "production"] = "local"
6767
## CORS_ORIGINS and ALLOWED_HOSTS are a JSON-formatted list of origins
6868
## For example: ["http://localhost:4200", "https://myfrontendapp.com"]
6969
ALLOWED_HOSTS: list[str] = ["localhost", "127.0.0.1", "api.localhost"]
@@ -74,7 +74,7 @@ class Settings(BaseSettings):
7474
APP_VERSION: str = "latest"
7575
APP_HOST: str = "0.0.0.0"
7676

77-
# Docker services` hosts
77+
# Docker adapters` hosts
7878
API_CONTAINER_HOST: str = "app-api"
7979
API_TASKIQ_CONTAINER_HOST: str = "api-taskiq"
8080
DB_CONTAINER_HOST: str = "app-db"

app/core/lifespan.py

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import os
2+
from collections.abc import AsyncGenerator
3+
from contextlib import asynccontextmanager
4+
5+
from fastapi import FastAPI
6+
from loguru import logger
7+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
8+
from opentelemetry.instrumentation.aio_pika import AioPikaInstrumentor
9+
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
10+
from opentelemetry.instrumentation.redis import RedisInstrumentor
11+
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
12+
from opentelemetry.sdk.resources import (
13+
DEPLOYMENT_ENVIRONMENT,
14+
SERVICE_NAME,
15+
TELEMETRY_SDK_LANGUAGE,
16+
Resource,
17+
)
18+
from opentelemetry.sdk.trace import TracerProvider
19+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
20+
from opentelemetry.trace import set_tracer_provider
21+
from prometheus_fastapi_instrumentator.instrumentation import (
22+
PrometheusFastApiInstrumentator,
23+
)
24+
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
25+
26+
from app.adapters.kafka.lifespan import init_kafka, shutdown_kafka
27+
from app.adapters.rabbit.lifespan import init_rabbit, shutdown_rabbit
28+
from app.adapters.redis.lifespan import init_redis, shutdown_redis
29+
from app.core.config import settings
30+
from app.core.tkq import broker
31+
32+
33+
def _setup_db(app: FastAPI) -> None: # pragma: no cover
34+
"""
35+
Creates connection to the database.
36+
37+
This function creates SQLAlchemy engine instance,
38+
session_factory for creating sessions
39+
and stores them in the application's state property.
40+
41+
:param app: fastAPI application.
42+
"""
43+
engine = create_async_engine(str(settings.db_url), echo=settings.DB_ECHO)
44+
session_factory = async_sessionmaker(
45+
engine,
46+
# See https://fastapi-users.github.io/fastapi-users/latest/configuration/databases/sqlalchemy/#asynchronous-driver
47+
expire_on_commit=False,
48+
)
49+
app.state.db_engine = engine
50+
app.state.db_session_factory = session_factory
51+
52+
53+
def setup_opentelemetry(app: FastAPI) -> None: # pragma: no cover
54+
"""
55+
Enables opentelemetry instrumentation.
56+
57+
:param app: current application.
58+
"""
59+
if not settings.OPENTELEMETRY_ENDPOINT:
60+
return
61+
62+
tracer_provider = TracerProvider(
63+
resource=Resource(
64+
attributes={
65+
SERVICE_NAME: settings.API_CONTAINER_HOST,
66+
TELEMETRY_SDK_LANGUAGE: "python",
67+
DEPLOYMENT_ENVIRONMENT: settings.ENVIRONMENT,
68+
},
69+
),
70+
)
71+
72+
tracer_provider.add_span_processor(
73+
BatchSpanProcessor(
74+
OTLPSpanExporter(
75+
endpoint=settings.OPENTELEMETRY_ENDPOINT,
76+
insecure=True,
77+
),
78+
),
79+
)
80+
81+
excluded_endpoints = [
82+
app.url_path_for("health_check"),
83+
app.url_path_for("openapi"),
84+
app.url_path_for("swagger_ui_html"),
85+
app.url_path_for("swagger_ui_redirect"),
86+
app.url_path_for("redoc_html"),
87+
"/metrics",
88+
]
89+
90+
FastAPIInstrumentor().instrument_app(
91+
app,
92+
tracer_provider=tracer_provider,
93+
excluded_urls=",".join(excluded_endpoints),
94+
)
95+
RedisInstrumentor().instrument(
96+
tracer_provider=tracer_provider,
97+
)
98+
SQLAlchemyInstrumentor().instrument(
99+
tracer_provider=tracer_provider,
100+
engine=app.state.db_engine.sync_engine,
101+
)
102+
AioPikaInstrumentor().instrument(
103+
tracer_provider=tracer_provider,
104+
)
105+
106+
set_tracer_provider(tracer_provider=tracer_provider)
107+
108+
109+
def stop_opentelemetry(app: FastAPI) -> None: # pragma: no cover
110+
"""
111+
Disables opentelemetry instrumentation.
112+
113+
:param app: current application.
114+
"""
115+
if not settings.OPENTELEMETRY_ENDPOINT:
116+
return
117+
118+
FastAPIInstrumentor().uninstrument_app(app)
119+
RedisInstrumentor().uninstrument()
120+
SQLAlchemyInstrumentor().uninstrument()
121+
AioPikaInstrumentor().uninstrument()
122+
123+
124+
def setup_prometheus(app: FastAPI) -> None: # pragma: no cover
125+
"""
126+
Enables prometheus integration.
127+
128+
:param app: current application.
129+
"""
130+
PrometheusFastApiInstrumentator(should_group_status_codes=False).instrument(
131+
app,
132+
).expose(app, should_gzip=True, name="prometheus_metrics")
133+
134+
135+
@asynccontextmanager
136+
async def lifespan_setup(
137+
app: FastAPI,
138+
) -> AsyncGenerator[None]: # pragma: no cover
139+
"""
140+
Actions to run on application startup.
141+
142+
This function uses fastAPI app to store data
143+
in the state, such as db_engine.
144+
145+
:param app: the fastAPI application.
146+
:return: function that actually performs actions.
147+
"""
148+
app.middleware_stack = None
149+
if not broker.is_worker_process:
150+
await broker.startup()
151+
_setup_db(app)
152+
setup_opentelemetry(app)
153+
init_redis(app)
154+
init_rabbit(app)
155+
await init_kafka(app)
156+
setup_prometheus(app)
157+
app.middleware_stack = app.build_middleware_stack()
158+
159+
logger.debug("Debug log")
160+
logger.info("Info log with cid + tracing")
161+
logger.warning("Warning log")
162+
logger.error("Error log")
163+
logger.critical("Critical log")
164+
165+
logger.info(os.environ)
166+
logger.info(app.routes)
167+
168+
yield
169+
if not broker.is_worker_process:
170+
await broker.shutdown()
171+
await app.state.db_engine.dispose()
172+
173+
await shutdown_redis(app)
174+
await shutdown_rabbit(app)
175+
await shutdown_kafka(app)
176+
stop_opentelemetry(app)

app/core/tkq.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,5 @@
1919

2020
taskiq_fastapi.init(
2121
broker,
22-
"app.web.application:get_app",
22+
"app.controller.application:get_app",
2323
)

app/db/base.py

Lines changed: 0 additions & 9 deletions
This file was deleted.

0 commit comments

Comments
 (0)