From abab89135fbcfdf968ff3ec94918e3434f97227c Mon Sep 17 00:00:00 2001 From: Pavel Kirilin Date: Fri, 28 Nov 2025 01:59:51 +0100 Subject: [PATCH 1/4] Fixed otlp for taskiq. --- .../{{cookiecutter.project_name}}/web/lifespan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/lifespan.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/lifespan.py index 7844e90..4223d2f 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/lifespan.py +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/lifespan.py @@ -228,7 +228,7 @@ def setup_opentelemetry(app: FastAPI) -> None: # pragma: no cover ) {%- endif %} {%- if cookiecutter.enable_taskiq == "True" %} - TaskiqInstrumentor.instrument_broker( + TaskiqInstrumentor().instrument_broker( broker, tracer_provider=tracer_provider, ) From 67e506e8028acf4c653b2707d08e92a1e9bbb59b Mon Sep 17 00:00:00 2001 From: Pavel Kirilin Date: Fri, 28 Nov 2025 18:16:17 +0100 Subject: [PATCH 2/4] Added logs and metrics otel integration. --- .../conditional_files.json | 3 +- .../deploy/docker-compose.otlp.yml | 30 +++++----- .../deploy/otel-collector-config.yml | 33 ---------- .../pyproject.toml | 21 +++---- .../web/lifespan.py | 60 ++++++++++++------- 5 files changed, 65 insertions(+), 82 deletions(-) delete mode 100644 fastapi_template/template/{{cookiecutter.project_name}}/deploy/otel-collector-config.yml diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/conditional_files.json b/fastapi_template/template/{{cookiecutter.project_name}}/conditional_files.json index f84901d..e49e9cc 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/conditional_files.json +++ b/fastapi_template/template/{{cookiecutter.project_name}}/conditional_files.json @@ -205,8 +205,7 @@ "Opentelemetry support": { "enabled": "{{cookiecutter.otlp_enabled}}", "resources": [ - "deploy/docker-compose.otlp.yml", - "deploy/otel-collector-config.yml" + "deploy/docker-compose.otlp.yml" ] }, "SQLite DB": { diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/deploy/docker-compose.otlp.yml b/fastapi_template/template/{{cookiecutter.project_name}}/deploy/docker-compose.otlp.yml index c5a9515..3fd56d6 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/deploy/docker-compose.otlp.yml +++ b/fastapi_template/template/{{cookiecutter.project_name}}/deploy/docker-compose.otlp.yml @@ -4,20 +4,20 @@ services: # Adds opentelemetry endpoint. {{cookiecutter.project_name | upper}}_OPENTELEMETRY_ENDPOINT: "http://otel-collector:4317" - otel-collector: - image: otel/opentelemetry-collector-contrib:0.53.0 - volumes: - # Adds config for opentelemetry. - - ./deploy/otel-collector-config.yml:/config.yml - command: --config config.yml - ports: - # Collector's endpoint - - "4317:4317" + {%- if cookiecutter.enable_taskiq == "True" %} + taskiq-worker: + environment: + # Adds opentelemetry endpoint. + {{cookiecutter.project_name | upper}}_OPENTELEMETRY_ENDPOINT: "http://otel-collector:4317" + {%- endif %} - jaeger: - image: jaegertracing/all-in-one:1.35 - hostname: jaeger + otel-stack: + image: grafana/otel-lgtm + hostname: "otel-collector" ports: - # Jaeger UI - - 16686:16686 - + # Collector's GRPC endpoint + - "4317:4317" + # Collector's HTTP endpoint + - "4318:4318" + # Grafana UI + - "3000:3000" diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/deploy/otel-collector-config.yml b/fastapi_template/template/{{cookiecutter.project_name}}/deploy/otel-collector-config.yml deleted file mode 100644 index 6220935..0000000 --- a/fastapi_template/template/{{cookiecutter.project_name}}/deploy/otel-collector-config.yml +++ /dev/null @@ -1,33 +0,0 @@ -# Receives all info with OpenTelemetry protocol. -receivers: - otlp: - protocols: - grpc: - http: - -# Batch all spans. -processors: - batch: - -exporters: - # Exports spans to log. - logging: - logLevel: info - - # Exports spans to jaeger. - 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] diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/pyproject.toml b/fastapi_template/template/{{cookiecutter.project_name}}/pyproject.toml index 15f2c01..4d86386 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/pyproject.toml +++ b/fastapi_template/template/{{cookiecutter.project_name}}/pyproject.toml @@ -108,25 +108,22 @@ dependencies = [ "sentry-sdk>=2.19.2,<3", {%- endif %} {%- if cookiecutter.otlp_enabled == "True" %} - "opentelemetry-api>=1.29.0,<2", - "opentelemetry-sdk>=1.29.0,<2", - "opentelemetry-exporter-otlp>=1.29.0,<2", - "opentelemetry-instrumentation>=0.50b0,<1", - "opentelemetry-instrumentation-fastapi>=0.50b0,<1", -{%- if cookiecutter.enable_loguru != "True" %} - "opentelemetry-instrumentation-logging>=0.50b0,<1", -{%- endif %} + "opentelemetry-api>=1.38.0,<2", + "opentelemetry-sdk>=1.38.0,<2", + "opentelemetry-exporter-otlp>=1.38.0,<2", + "opentelemetry-instrumentation>=0.59b0,<1", + "opentelemetry-instrumentation-fastapi>=0.59b0,<1", {%- if cookiecutter.enable_redis == "True" %} - "opentelemetry-instrumentation-redis>=0.50b0,<1", + "opentelemetry-instrumentation-redis>=0.59b0,<1", {%- endif %} {%- if cookiecutter.db_info.name == "postgresql" and cookiecutter.orm in ["ormar", "tortoise"] %} - "opentelemetry-instrumentation-asyncpg>=0.50b0,<1", + "opentelemetry-instrumentation-asyncpg>=0.59b0,<1", {%- endif %} {%- if cookiecutter.orm == "sqlalchemy" %} - "opentelemetry-instrumentation-sqlalchemy>=0.50b0,<1", + "opentelemetry-instrumentation-sqlalchemy>=0.59b0,<1", {%- endif %} {%- if cookiecutter.enable_rmq == "True" %} - "opentelemetry-instrumentation-aio-pika>=0.50b0,<1", + "opentelemetry-instrumentation-aio-pika>=0.59b0,<1", {%- endif %} {%- endif %} {%- if cookiecutter.enable_loguru == "True" %} diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/lifespan.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/lifespan.py index 4223d2f..cf27818 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/lifespan.py +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/lifespan.py @@ -48,13 +48,19 @@ {%- endif %} {%- if cookiecutter.otlp_enabled == "True" %} +from opentelemetry import metrics, trace +from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter +from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor from opentelemetry.sdk.resources import (DEPLOYMENT_ENVIRONMENT, SERVICE_NAME, TELEMETRY_SDK_LANGUAGE, Resource) from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor -from opentelemetry.trace import set_tracer_provider +from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler +from opentelemetry.sdk._logs.export import BatchLogRecordProcessor {%- if cookiecutter.enable_redis == "True" %} from opentelemetry.instrumentation.redis import RedisInstrumentor @@ -144,7 +150,7 @@ async def _create_tables() -> None: # pragma: no cover engine = create_engine(str(settings.db_url)) with engine.connect() as connection: meta.create_all(connection) - engine.dispose() +% engine.dispose() {%- elif cookiecutter.orm == "sqlalchemy" %} engine = create_async_engine(str(settings.db_url)) async with engine.begin() as connection: @@ -164,24 +170,44 @@ def setup_opentelemetry(app: FastAPI) -> None: # pragma: no cover 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, - } - ) + otlp_resource = Resource( + attributes={ + SERVICE_NAME: "{{cookiecutter.project_name}}", + TELEMETRY_SDK_LANGUAGE: "python", + DEPLOYMENT_ENVIRONMENT: settings.environment, + } ) + + tracer_provider = TracerProvider(resource=otlp_resource) + tracer_provider.add_span_processor( BatchSpanProcessor( OTLPSpanExporter( endpoint=settings.opentelemetry_endpoint, - insecure=True, ) ) ) + trace.set_tracer_provider(tracer_provider=tracer_provider) + + meter_provider = MeterProvider( + resource=otlp_resource, + metric_readers=[ + (PeriodicExportingMetricReader(OTLPMetricExporter(endpoint=settings.opentelemetry_endpoint))), + ], + ) + metrics.set_meter_provider(meter_provider) + + logger_provider = LoggerProvider(resource=otlp_resource) + logger_provider.add_log_record_processor( + BatchLogRecordProcessor(OTLPLogExporter(endpoint=settings.opentelemetry_endpoint)), + ) + logging.getLogger().addHandler( + LoggingHandler( + level=logging.NOTSET, + logger_provider=logger_provider, + ), + ) excluded_endpoints = [ app.url_path_for('health_check'), @@ -220,13 +246,6 @@ def setup_opentelemetry(app: FastAPI) -> None: # pragma: no cover tracer_provider=tracer_provider, ) {%- endif %} - {%- if cookiecutter.enable_loguru != "True" %} - LoggingInstrumentor().instrument( - tracer_provider=tracer_provider, - set_logging_format=True, - log_level=logging.getLevelName(settings.log_level.value), - ) - {%- endif %} {%- if cookiecutter.enable_taskiq == "True" %} TaskiqInstrumentor().instrument_broker( broker, @@ -234,8 +253,6 @@ def setup_opentelemetry(app: FastAPI) -> None: # pragma: no cover ) {%- endif %} - set_tracer_provider(tracer_provider=tracer_provider) - def stop_opentelemetry(app: FastAPI) -> None: # pragma: no cover """ @@ -259,6 +276,9 @@ def stop_opentelemetry(app: FastAPI) -> None: # pragma: no cover {%- if cookiecutter.enable_rmq == "True" %} AioPikaInstrumentor().uninstrument() {%- endif %} + {%- if cookiecutter.enable_taskiq == "True" %} + TaskiqInstrumentor().uninstrument_broker(broker) + {%- endif %} {%- endif %} From 3289dd1060f0bb6496c398919265d6d17d8c2f04 Mon Sep 17 00:00:00 2001 From: Pavel Kirilin Date: Fri, 28 Nov 2025 18:26:01 +0100 Subject: [PATCH 3/4] Fixed error. --- .../{{cookiecutter.project_name}}/web/lifespan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/lifespan.py b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/lifespan.py index cf27818..75ee399 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/lifespan.py +++ b/fastapi_template/template/{{cookiecutter.project_name}}/{{cookiecutter.project_name}}/web/lifespan.py @@ -150,7 +150,7 @@ async def _create_tables() -> None: # pragma: no cover engine = create_engine(str(settings.db_url)) with engine.connect() as connection: meta.create_all(connection) -% engine.dispose() + engine.dispose() {%- elif cookiecutter.orm == "sqlalchemy" %} engine = create_async_engine(str(settings.db_url)) async with engine.begin() as connection: From c5d74693fd40a5e395ad559d1baa5bc015b2ea49 Mon Sep 17 00:00:00 2001 From: Pavel Kirilin Date: Fri, 28 Nov 2025 19:01:18 +0100 Subject: [PATCH 4/4] Fixed mypy tests. --- .../template/{{cookiecutter.project_name}}/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi_template/template/{{cookiecutter.project_name}}/pyproject.toml b/fastapi_template/template/{{cookiecutter.project_name}}/pyproject.toml index 4d86386..457e3e8 100644 --- a/fastapi_template/template/{{cookiecutter.project_name}}/pyproject.toml +++ b/fastapi_template/template/{{cookiecutter.project_name}}/pyproject.toml @@ -194,7 +194,7 @@ warn_unused_ignores = false warn_return_any = false namespace_packages = true {%- if cookiecutter.api_type == "graphql" %} -plugins = ["strawberry.ext.mypy_plugin"] +plugins = [] {%- endif %} {%- if cookiecutter.enable_redis == "True" %}