Skip to content

Commit

Permalink
Merge branch 'release/3.3.5'
Browse files Browse the repository at this point in the history
s3rius committed Jun 24, 2022

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
2 parents 9aef9a5 + 8597037 commit 1568837
Showing 26 changed files with 526 additions and 82 deletions.
33 changes: 33 additions & 0 deletions fastapi_template/cli.py
Original file line number Diff line number Diff line change
@@ -130,6 +130,27 @@ def parse_args():
default=None,
dest="self_hosted_swagger",
)
parser.add_argument(
"--prometheus",
help="Add prometheus integration",
action="store_true",
default=None,
dest="prometheus_enabled",
)
parser.add_argument(
"--sentry",
help="Add sentry integration",
action="store_true",
default=None,
dest="sentry_enabled",
)
parser.add_argument(
"--opentelemetry",
help="Add opentelemetry integration",
action="store_true",
default=None,
dest="otlp_enabled",
)
parser.add_argument(
"--force",
help="Owerrite directory if it exists",
@@ -170,6 +191,18 @@ def ask_features(current_context: BuilderContext) -> BuilderContext:
"name": "enable_rmq",
"value": current_context.enable_rmq,
},
"Prometheus integration": {
"name": "prometheus_enabled",
"value": current_context.prometheus_enabled,
},
"Sentry integration": {
"name": "sentry_enabled",
"value": current_context.sentry_enabled,
},
"Opentelemetry integration": {
"name": "otlp_enabled",
"value": current_context.otlp_enabled,
},
}
if current_context.db != DatabaseType.none:
features["Migrations support"] = {
5 changes: 4 additions & 1 deletion fastapi_template/input_model.py
Original file line number Diff line number Diff line change
@@ -115,8 +115,11 @@ class BuilderContext(BaseModel):
enable_migrations: Optional[bool]
enable_kube: Optional[bool]
enable_routers: Optional[bool]
add_dummy: Optional[bool] = False
add_dummy: Optional[bool]
self_hosted_swagger: Optional[bool]
prometheus_enabled: Optional[bool]
sentry_enabled: Optional[bool]
otlp_enabled: Optional[bool]
enable_rmq: Optional[bool]
force: bool = False
quite: bool = False
107 changes: 58 additions & 49 deletions fastapi_template/template/cookiecutter.json
Original file line number Diff line number Diff line change
@@ -1,51 +1,60 @@
{
"project_name": {
"type": "string"
},
"project_description": {
"type": "string"
},
"api_type": {
"type": "dict"
},
"db_info": {
"type": "dict"
},
"enable_redis": {
"type": "bool"
},
"enable_rmq": {
"type": "bool"
},
"ci_type": {
"type": "string"
},
"enable_migrations": {
"type": "bool"
},
"enable_kube": {
"type": "bool"
},
"kube_name": {
"type": "string"
},
"enable_routers": {
"type": "bool"
},
"add_dummy": {
"type": "bool"
},
"orm": {
"type": "str"
},
"self_hosted_swagger": {
"type": "bool"
},
"_extensions": [
"cookiecutter.extensions.RandomStringExtension"
],
"_copy_without_render": [
"*.js",
"*.css"
]
"project_name": {
"type": "string"
},
"project_description": {
"type": "string"
},
"api_type": {
"type": "dict"
},
"db_info": {
"type": "dict"
},
"enable_redis": {
"type": "bool"
},
"enable_rmq": {
"type": "bool"
},
"ci_type": {
"type": "string"
},
"enable_migrations": {
"type": "bool"
},
"enable_kube": {
"type": "bool"
},
"kube_name": {
"type": "string"
},
"enable_routers": {
"type": "bool"
},
"add_dummy": {
"type": "bool"
},
"orm": {
"type": "str"
},
"self_hosted_swagger": {
"type": "bool"
},
"prometheus_enabled": {
"type": "bool"
},
"sentry_enabled": {
"type": "bool"
},
"otlp_enabled": {
"type": "bool"
},
"_extensions": [
"cookiecutter.extensions.RandomStringExtension"
],
"_copy_without_render": [
"*.js",
"*.css"
]
}
87 changes: 83 additions & 4 deletions fastapi_template/template/{{cookiecutter.project_name}}/README.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,112 @@
# {{cookiecutter.project_name}}

This project was generated using fastapi_template.

Start a project with:
## Poetry

This project uses poetry. It's a modern dependency management
tool.

To run the project use this set of commands:

```bash
docker-compose -f deploy/docker-compose.yml --project-directory . up
poetry install
poetry run python -m {{cookiecutter.project_name}}
```

If you want to develop in docker with autoreload, use this command:
This will start the server on the configured host.

You can find swagger documentation at `/api/docs`.

You can read more about poetry here: https://python-poetry.org/

## Docker

You can start the project with docker using this command:

```bash
docker-compose -f deploy/docker-compose.yml --project-directory . up --build
```

If you want to develop in docker with autoreload add `-f deploy/docker-compose.dev.yml` to your docker command.
Like this:

```bash
docker-compose -f deploy/docker-compose.yml -f deploy/docker-compose.dev.yml --project-directory . up
```

This command exposes application on port 8000, mounts current directory and enables autoreload.
This command exposes the web application on port 8000, mounts current directory and enables autoreload.

But you have to rebuild image every time you modify `poetry.lock` or `pyproject.toml` with this command:

```bash
docker-compose -f deploy/docker-compose.yml --project-directory . build
```

{%- if cookiecutter.otlp_enabled == "True" %}
## Opentelemetry

If you want to start your project with opentelemetry collector
you can add `-f ./deploy/docker-compose.otlp.yml` to your docker command.

Like this:

```bash
docker-compose -f deploy/docker-compose.yml -f deploy/docker-compose.otlp.yml --project-directory . up
```

This command will start opentelemetry collector and jaeger.
After sending a requests you can see traces in jaeger's UI
at http://localhost:16686/.

This docker configuration is not supposed to be used in production.
It's only for demo purpose.

You can read more about opentelemetry here: https://opentelemetry.io/
{%- endif %}

## Configuration

This application can be configured with environment variables.

You can create `.env` file in the root directory and place all
environment variables here.

All environment variabels should start with "{{cookiecutter.project_name | upper}}_" prefix.

For example if you see in your "{{cookiecutter.project_name}}/settings.py" a variable named like
`random_parameter`, you should provide the "{{cookiecutter.project_name | upper}}_RANDOM_PARAMETER"
variable to configure the value.

An exmaple of .env file:
```bash
{{cookiecutter.project_name | upper}}_RELOAD="True"
{{cookiecutter.project_name | upper}}_PORT="8000"
{{cookiecutter.project_name | upper}}_ENVIRONMENT="dev"
```

You can read more about BaseSettings class here: https://pydantic-docs.helpmanual.io/usage/settings/

## Pre-commit

To install pre-commit simply run inside the shell:
```bash
pre-commit install
```

pre-commit is very useful to check your code before publishing it.
It's configured using .pre-commit-config.yaml file.

By default it runs:
* black (formats your code);
* mypy (validates types);
* isort (sorts imports in all files);
* flake8 (spots possibe bugs);
* yesqa (removes useless `# noqa` comments).


You can read more about pre-commit here: https://pre-commit.com/

{%- if cookiecutter.enable_kube == 'True' %}

## Kubernetes
Original file line number Diff line number Diff line change
@@ -178,11 +178,18 @@
"{{cookiecutter.project_name}}/db_tortoise/migrations/models/1_20210928165300_init_dummy_mysql.sql"
]
},
"Opentelemetry support": {
"enabled": "{{cookiecutter.otlp_enabled}}",
"resources": [
"deploy/docker-compose.otlp.yml",
"deploy/otel-collector-config.yml"
]
},
"SQLite DB": {
"enabled": "{{cookiecutter.db_info.name == 'sqlite'}}",
"resources": [
"{{cookiecutter.project_name}}/db_tortoise/migrations/models/0_20210928165300_init_sqlite.sql",
"{{cookiecutter.project_name}}/db_tortoise/migrations/models/1_20210928165300_init_dummy_sqlite.sql"
]
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
FROM python:3.9.6-slim-buster

{% if cookiecutter.db_info.name == "mysql" -%}
{%- if cookiecutter.db_info.name == "mysql" %}
RUN apt-get update && apt-get install -y \
default-libmysqlclient-dev \
gcc \
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
services:
api:
environment:
{{cookiecutter.project_name | upper}}_OPENTELEMETRY_ENDPOINT: "http://otel-collector:4317"

otel-collector:
image: otel/opentelemetry-collector-contrib:0.53.0
volumes:
- ./deploy/otel-collector-config.yml:/config.yml
command: --config config.yml
ports:
# Collector's endpoint
- "4317:4317"

jaeger:
image: jaegertracing/all-in-one:1.35
hostname: jaeger
ports:
# Jaeger UI
- 16686:16686

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
receivers:
otlp:
protocols:
grpc:
http:

processors:
batch:

exporters:
logging:
logLevel: info

jaeger:
endpoint: "jaeger:14250"
tls:
insecure: true

extensions:
health_check:
pprof:

service:
extensions: [health_check, pprof]
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [logging, jaeger]
Original file line number Diff line number Diff line change
@@ -69,7 +69,7 @@ mysqlclient = "^2.1.0"
{%- endif %}
{%- endif %}
{%- if cookiecutter.enable_redis == "True" %}
aioredis = {version = "^2.0.1", extras = ["hiredis"]}
redis = {version = "^4.3.3", extras = ["hiredis"]}
{%- endif %}
{%- if cookiecutter.self_hosted_swagger == 'True' %}
aiofiles = "^0.8.0"
@@ -79,37 +79,64 @@ psycopg = { version = "^3.0.11", extras = ["binary", "pool"] }
{%- endif %}
httptools = "^0.3.0"
{%- if cookiecutter.api_type == "graphql" %}
strawberry-graphql = { version = "^0.106.3", extras = ["fastapi"] }
strawberry-graphql = { version = "^0.114.2", extras = ["fastapi"] }
{%- endif %}
{%- if cookiecutter.enable_rmq == "True" %}
aio-pika = "^7.2.0"
aio-pika = "^8.0.3"
{%- endif %}
{%- if cookiecutter.prometheus_enabled == "True" %}
prometheus-client = "^0.14.1"
prometheus-fastapi-instrumentator = "5.8.1"
{%- endif %}
{%- if cookiecutter.sentry_enabled == "True" %}
sentry-sdk = "^1.5.12"
{%- endif %}
{%- if cookiecutter.otlp_enabled == "True" %}
protobuf = "~3.20.0"
opentelemetry-api = {version = "^1.12.0rc1", allow-prereleases = true}
opentelemetry-sdk = {version = "^1.12.0rc1", allow-prereleases = true}
opentelemetry-exporter-otlp = {version = "^1.12.0rc1", allow-prereleases = true}
opentelemetry-instrumentation = "^0.31b0"
opentelemetry-instrumentation-logging = "^0.31b0"
opentelemetry-instrumentation-fastapi = "^0.31b0"
{%- if cookiecutter.enable_redis == "True" %}
opentelemetry-instrumentation-redis = "^0.31b0"
{%- endif %}
{%- if cookiecutter.db_info.name == "postgresql" and cookiecutter.orm in ["ormar", "tortoise"] %}
opentelemetry-instrumentation-asyncpg = "^0.31b0"
{%- endif %}
{%- if cookiecutter.orm == "sqlalchemy" %}
opentelemetry-instrumentation-sqlalchemy = "^0.31b0"
{%- endif %}
{%- endif %}

[tool.poetry.dev-dependencies]
pytest = "^7.0"
flake8 = "^4.0.1"
mypy = "^0.910"
isort = "^5.9.3"
yesqa = "^1.2.3"
pre-commit = "^2.11.0"
isort = "^5.10.1"
yesqa = "^1.3.0"
pre-commit = "^2.19.0"
wemake-python-styleguide = "^0.16.1"
black = "^22.3.0"
autoflake = "^1.4"
{%- if cookiecutter.orm == "sqlalchemy" %}
SQLAlchemy = {version = "^1.4", extras = ["mypy"]}
{%- endif %}
pytest-cov = "^3.0.0"
anyio = "^3.5.0"
anyio = "^3.6.1"
pytest-env = "^0.6.2"
{%- if cookiecutter.enable_redis == "True" %}
fakeredis = "^1.7.1"
fakeredis = "^1.8.1"
{%- endif %}
requests = "^2.26.0"
{%- if cookiecutter.orm == "tortoise" %}
asynctest = "^0.13.0"
nest-asyncio = "^1.5.5"
{%- endif %}
httpx = "^0.22.0"
{%- if cookiecutter.enable_redis == "True" %}
types-redis = "^4.2.6"
{%- endif %}


[tool.isort]
@@ -126,6 +153,7 @@ pretty = true
show_error_codes = true
implicit_reexport = true
allow_untyped_decorators = true
warn_unused_ignores = false
warn_return_any = false
{%- if cookiecutter.orm == "sqlalchemy" %}
plugins = ["sqlalchemy.ext.mypy.plugin"]
@@ -145,7 +173,10 @@ env = [
"{{cookiecutter.project_name | upper}}_DB_BASE={{cookiecutter.project_name}}_test",
{%- endif %}
{%- if cookiecutter.orm == "piccolo" %}
"PICCOLO_CONF={{cookiecutter.project_name}}.piccolo_conf"
"PICCOLO_CONF={{cookiecutter.project_name}}.piccolo_conf",
{%- endif %}
{%- if cookiecutter.sentry_enabled == "True" %}
"{{cookiecutter.project_name | upper}}_SENTRY_DSN=",
{%- endif %}
]
{%- endif %}
Original file line number Diff line number Diff line change
@@ -1,10 +1,42 @@
import uvicorn
import os
import shutil

from {{cookiecutter.project_name}}.settings import settings


{%- if cookiecutter.prometheus_enabled == "True" %}
def set_multiproc_dir() -> None:
"""
Sets mutiproc_dir env variable.
This function cleans up the multiprocess directory
and recreates it. This actions are required by prometheus-client
to share metrics between processes.
After cleanup, it sets two variables.
Uppercase and lowercase because different
versions of the prometheus-client library
depend on different environment variables,
so I've decided to export all needed variables,
to avoid undefined behaviour.
"""
shutil.rmtree(settings.prometheus_dir, ignore_errors=True)
os.makedirs(settings.prometheus_dir, exist_ok=True)
os.environ["prometheus_multiproc_dir"] = str(
settings.prometheus_dir.expanduser().absolute(),
)
os.environ["PROMETHEUS_MULTIPROC_DIR"] = str(
settings.prometheus_dir.expanduser().absolute(),
)
{%- endif %}


def main() -> None:
"""Entrypoint of the application."""
{%- if cookiecutter.prometheus_enabled == "True" %}
set_multiproc_dir()
{%- endif %}
{%- if cookiecutter.orm == "piccolo" %}
os.environ['PICCOLO_CONF'] = "{{cookiecutter.project_name}}.piccolo_conf"
{%- endif %}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import AsyncGenerator

from aioredis import Redis
from redis.asyncio import Redis
from starlette.requests import Request


Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from fastapi import FastAPI
import aioredis
from redis.asyncio import ConnectionPool
from {{cookiecutter.project_name}}.settings import settings


@@ -9,7 +9,7 @@ def init_redis(app: FastAPI) -> None:
:param app: current fastapi application.
"""
app.state.redis_pool = aioredis.ConnectionPool.from_url(
app.state.redis_pool = ConnectionPool.from_url(
str(settings.redis_url),
)

Original file line number Diff line number Diff line change
@@ -18,6 +18,9 @@ class Settings(BaseSettings):
# Enable uvicorn reloading
reload: bool = False

# Current environment
environment: str = "dev"

{%- if cookiecutter.db_info.name != "none" %}
{%- if cookiecutter.db_info.name == "sqlite" %}
db_file: Path = TEMP_DIR / "db.sqlite3"
@@ -50,6 +53,19 @@ class Settings(BaseSettings):
rabbit_channel_pool_size: int = 10
{%- endif %}

{%- if cookiecutter.prometheus_enabled == "True" %}
prometheus_dir: Path = TEMP_DIR / "prom"
{%- endif %}

{%- if cookiecutter.sentry_enabled == "True" %}
sentry_dsn: Optional[str] = None
sentry_sample_rate: float = 1.0
{%- endif %}

{%- if cookiecutter.otlp_enabled == "True" %}
opentelemetry_endpoint: Optional[str] = None
{%- endif %}

{%- if cookiecutter.db_info.name != "none" %}
@property
def db_url(self) -> URL:
Original file line number Diff line number Diff line change
@@ -26,7 +26,7 @@ async def test_creation(
{%- if cookiecutter.api_type == 'rest' %}
url = fastapi_app.url_path_for('create_dummy_model')
{%- elif cookiecutter.api_type == 'graphql' %}
url = fastapi_app.url_path_for('handle_http_query')
url = fastapi_app.url_path_for('handle_http_post')
{%- endif %}
test_name = uuid.uuid4().hex
{%- if cookiecutter.api_type == 'rest' %}
@@ -74,7 +74,7 @@ async def test_getting(
{%- if cookiecutter.api_type == 'rest' %}
url = fastapi_app.url_path_for('get_dummy_models')
{%- elif cookiecutter.api_type == 'graphql' %}
url = fastapi_app.url_path_for('handle_http_query')
url = fastapi_app.url_path_for('handle_http_post')
{%- endif %}

{%- if cookiecutter.api_type == 'rest' %}
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@ async def test_echo(fastapi_app: FastAPI, client: AsyncClient) -> None:
{%- if cookiecutter.api_type == 'rest' %}
url = fastapi_app.url_path_for('send_echo_message')
{%- elif cookiecutter.api_type == 'graphql' %}
url = fastapi_app.url_path_for('handle_http_query')
url = fastapi_app.url_path_for('handle_http_post')
{%- endif %}
message = uuid.uuid4().hex
{%- if cookiecutter.api_type == 'rest' %}
Original file line number Diff line number Diff line change
@@ -35,7 +35,7 @@ async def test_message_publishing(
},
)
{%- elif cookiecutter.api_type == 'graphql' %}
url = fastapi_app.url_path_for('handle_http_query')
url = fastapi_app.url_path_for('handle_http_post')
await client.post(
url,
json={
@@ -87,7 +87,7 @@ async def test_message_wrong_exchange(
},
)
{%- elif cookiecutter.api_type == 'graphql' %}
url = fastapi_app.url_path_for('handle_http_query')
url = fastapi_app.url_path_for('handle_http_post')
await client.post(
url,
json={
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ async def test_setting_value(
{%- if cookiecutter.api_type == 'rest' %}
url = fastapi_app.url_path_for('set_redis_value')
{%- elif cookiecutter.api_type == 'graphql' %}
url = fastapi_app.url_path_for('handle_http_query')
url = fastapi_app.url_path_for('handle_http_post')
{%- endif %}

test_key = uuid.uuid4().hex
@@ -77,7 +77,7 @@ async def test_getting_value(
{%- if cookiecutter.api_type == 'rest' %}
url = fastapi_app.url_path_for('get_redis_value')
{%- elif cookiecutter.api_type == 'graphql' %}
url = fastapi_app.url_path_for('handle_http_query')
url = fastapi_app.url_path_for('handle_http_post')
{%- endif %}

{%- if cookiecutter.api_type == 'rest' %}
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@
router = APIRouter()

@router.get("/docs", include_in_schema=False)
async def custom_swagger_ui_html(request: Request) -> HTMLResponse:
async def swagger_ui_html(request: Request) -> HTMLResponse:
"""
Swagger UI.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from aioredis import Redis
from redis.asyncio import Redis
from fastapi import APIRouter
from fastapi.param_functions import Depends

Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from fastapi import FastAPI
from fastapi.responses import UJSONResponse

import logging
from {{cookiecutter.project_name}}.web.api.router import api_router
from {{cookiecutter.project_name}}.settings import settings
{%- if cookiecutter.api_type == 'graphql' %}
from {{cookiecutter.project_name}}.web.gql.router import gql_router
{%- endif %}
@@ -13,6 +14,15 @@
from {{cookiecutter.project_name}}.db.config import TORTOISE_CONFIG
{%- endif %}

{%- if cookiecutter.sentry_enabled == "True" %}
import sentry_sdk
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
from sentry_sdk.integrations.logging import LoggingIntegration
{%- if cookiecutter.orm == "sqlalchemy" %}
from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration
{%- endif %}
{%- endif %}


{%- if cookiecutter.self_hosted_swagger == 'True' %}
from fastapi.staticfiles import StaticFiles
@@ -73,4 +83,23 @@ def get_app() -> FastAPI:
)
{%- endif %}

{%- if cookiecutter.sentry_enabled == "True" %}
if settings.sentry_dsn:
sentry_sdk.init(
dsn=settings.sentry_dsn,
traces_sample_rate=settings.sentry_sample_rate,
environment=settings.environment,
integrations=[
LoggingIntegration(
level=logging.INFO,
event_level=logging.ERROR,
),
{%- if cookiecutter.orm == "sqlalchemy" %}
SqlalchemyIntegration(),
{%- endif %}
],
)
app = SentryAsgiMiddleware(app) # type: ignore
{%- endif %}

return app
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@
from strawberry.fastapi import BaseContext

{%- if cookiecutter.enable_redis == "True" %}
from aioredis import Redis
from redis.asyncio import Redis
from {{cookiecutter.project_name}}.services.redis.dependency import get_redis_connection
{%- endif %}

Original file line number Diff line number Diff line change
@@ -4,6 +4,10 @@

from {{cookiecutter.project_name}}.settings import settings

{%- if cookiecutter.prometheus_enabled == "True" %}
from prometheus_fastapi_instrumentator.instrumentation import PrometheusFastApiInstrumentator
{%- endif %}

{%- if cookiecutter.enable_redis == "True" %}
from {{cookiecutter.project_name}}.services.redis.lifetime import init_redis, shutdown_redis
{%- endif %}
@@ -22,6 +26,31 @@
{%- endif %}
{%- endif %}

{%- if cookiecutter.otlp_enabled == "True" %}
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( # type: ignore
OTLPSpanExporter,
)
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor # type: ignore
from opentelemetry.sdk.resources import ( # type: ignore
SERVICE_NAME,
TELEMETRY_SDK_LANGUAGE,
DEPLOYMENT_ENVIRONMENT,
Resource,
)
from opentelemetry.sdk.trace import TracerProvider # type: ignore
from opentelemetry.sdk.trace.export import BatchSpanProcessor # type: ignore
from opentelemetry.trace import set_tracer_provider # type: ignore
{%- if cookiecutter.enable_redis == "True" %}
from opentelemetry.instrumentation.redis import RedisInstrumentor # type: ignore
{%- endif %}
{%- if cookiecutter.db_info.name == "postgresql" and cookiecutter.orm in ["ormar", "tortoise"] %}
from opentelemetry.instrumentation.asyncpg import AsyncPGInstrumentor # type: ignore
{%- endif %}
{%- if cookiecutter.orm == "sqlalchemy" %}
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor # type: ignore
{%- endif %}

{%- endif %}

{%- if cookiecutter.orm == "psycopg" %}
import psycopg_pool
@@ -93,6 +122,106 @@ async def _create_tables() -> None:
{%- endif %}
{%- endif %}

{%- if cookiecutter.otlp_enabled == "True" %}
def setup_opentelemetry(app: FastAPI) -> None:
"""
Enables opentelemetry instrumetnation.
:param app: current application.
"""
if not settings.opentelemetry_endpoint:
return

tracer_provider = TracerProvider(
resource=Resource(
attributes={
SERVICE_NAME: "{{cookiecutter.project_name}}",
TELEMETRY_SDK_LANGUAGE: "python",
DEPLOYMENT_ENVIRONMENT: settings.environment,
}
)
)

tracer_provider.add_span_processor(
BatchSpanProcessor(
OTLPSpanExporter(
endpoint=settings.opentelemetry_endpoint,
insecure=True,
)
)
)

excluded_endpoints = [
app.url_path_for('health_check'),
app.url_path_for('openapi'),
app.url_path_for('swagger_ui_html'),
app.url_path_for('swagger_ui_redirect'),
app.url_path_for('redoc_html'),
{%- if cookiecutter.prometheus_enabled == "True" %}
"/metrics",
{%- endif %}
]

FastAPIInstrumentor().instrument_app(
app,
tracer_provider=tracer_provider,
excluded_urls=",".join(excluded_endpoints),
)
{%- if cookiecutter.enable_redis == "True" %}
RedisInstrumentor().instrument(
tracer_provider=tracer_provider,
)
{%- endif %}
{%- if cookiecutter.db_info.name == "postgresql" and cookiecutter.orm in ["ormar", "tortoise"] %}
AsyncPGInstrumentor().instrument(
tracer_provider=tracer_provider,
)
{%- endif %}
{%- if cookiecutter.orm == "sqlalchemy" %}
SQLAlchemyInstrumentor().instrument(
tracer_provider=tracer_provider,
engine=app.state.db_engine.sync_engine,
)
{%- endif %}

set_tracer_provider(tracer_provider=tracer_provider)


def stop_opentelemetry(app: FastAPI) -> None:
"""
Disables opentelemetry instrumentation.
:param app: current application.
"""
if not settings.opentelemetry_endpoint:
return

FastAPIInstrumentor().uninstrument_app(app)
{%- if cookiecutter.enable_redis == "True" %}
RedisInstrumentor().uninstrument()
{%- endif %}
{%- if cookiecutter.db_info.name == "postgresql" and cookiecutter.orm in ["ormar", "tortoise"] %}
AsyncPGInstrumentor().uninstrument()
{%- endif %}
{%- if cookiecutter.orm == "sqlalchemy" %}
SQLAlchemyInstrumentor().uninstrument()
{%- endif %}

{%- endif %}

{%- if cookiecutter.prometheus_enabled == "True" %}
def setup_prometheus(app: FastAPI) -> None:
"""
Enables prometheus integration.
:param app: current application.
"""
PrometheusFastApiInstrumentator(should_group_status_codes=False).instrument(
app,
).expose(app, should_gzip=True, name="prometheus_metrics")
{%- endif %}


def register_startup_event(app: FastAPI) -> Callable[[], Awaitable[None]]:
"""
Actions to run on application startup.
@@ -118,12 +247,18 @@ async def _startup() -> None: # noqa: WPS430
await _create_tables()
{%- endif %}
{%- endif %}
{%- if cookiecutter.otlp_enabled == "True" %}
setup_opentelemetry(app)
{%- endif %}
{%- if cookiecutter.enable_redis == "True" %}
init_redis(app)
{%- endif %}
{%- if cookiecutter.enable_rmq == "True" %}
init_rabbit(app)
{%- endif %}
{%- if cookiecutter.prometheus_enabled == "True" %}
setup_prometheus(app)
{%- endif %}
pass # noqa: WPS420

return _startup
@@ -152,6 +287,9 @@ async def _shutdown() -> None: # noqa: WPS430
{%- if cookiecutter.enable_rmq == "True" %}
await shutdown_rabbit(app)
{%- endif %}
{%- if cookiecutter.otlp_enabled == "True" %}
stop_opentelemetry(app)
{%- endif %}
pass # noqa: WPS420

return _shutdown
4 changes: 4 additions & 0 deletions fastapi_template/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -63,6 +63,10 @@ def default_context(project_name: str) -> None:
enable_routers=True,
add_dummy=False,
self_hosted_swagger=False,
enable_rmq=False,
prometheus_enabled=False,
otlp_enabled=False,
sentry_enabled=False,
force=True,
)

9 changes: 9 additions & 0 deletions fastapi_template/tests/test_generator.py
Original file line number Diff line number Diff line change
@@ -104,3 +104,12 @@ def test_rmq(default_context: BuilderContext, api: APIType):
default_context.enable_rmq = True
default_context.api_type = api
run_default_check(default_context)


def test_telemetry_pre_commit(default_context: BuilderContext):
default_context.enable_rmq = True
default_context.enable_redis = True
default_context.prometheus_enabled = True
default_context.otlp_enabled = True
default_context.sentry_enabled = True
run_default_check(default_context, without_pytest=True)
6 changes: 5 additions & 1 deletion fastapi_template/tests/utils.py
Original file line number Diff line number Diff line change
@@ -34,9 +34,13 @@ def run_docker_compose_command(command: Optional[str] = None) -> subprocess.Comp
return subprocess.run(docker_command)


def run_default_check(context: BuilderContext):
def run_default_check(context: BuilderContext, without_pytest=False):
generate_project_and_chdir(context)
assert run_pre_commit() == 0

if without_pytest:
return

build = run_docker_compose_command("build")
assert build.returncode == 0
tests = run_docker_compose_command("run --rm api pytest -vv .")
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "fastapi_template"
version = "3.3.4"
version = "3.3.5"
description = "Feature-rich robust FastAPI template"
authors = ["Pavel Kirilin <win10@list.ru>"]
packages = [{ include = "fastapi_template" }]

0 comments on commit 1568837

Please sign in to comment.