diff --git a/poetry.lock b/poetry.lock index 4d9b64d..8efbb41 100644 --- a/poetry.lock +++ b/poetry.lock @@ -101,6 +101,23 @@ cffi = ">=1.0.1" dev = ["cogapp", "pre-commit", "pytest", "wheel"] tests = ["pytest"] +[[package]] +name = "asgiref" +version = "3.7.2" +description = "ASGI specs, helper code, and adapters" +optional = false +python-versions = ">=3.7" +files = [ + {file = "asgiref-3.7.2-py3-none-any.whl", hash = "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e"}, + {file = "asgiref-3.7.2.tar.gz", hash = "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} + +[package.extras] +tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] + [[package]] name = "astroid" version = "3.0.2" @@ -1287,6 +1304,50 @@ opentelemetry-api = ">=1.4,<2.0" setuptools = ">=16.0" wrapt = ">=1.0.0,<2.0.0" +[[package]] +name = "opentelemetry-instrumentation-asgi" +version = "0.43b0" +description = "ASGI instrumentation for OpenTelemetry" +optional = false +python-versions = ">=3.7" +files = [ + {file = "opentelemetry_instrumentation_asgi-0.43b0-py3-none-any.whl", hash = "sha256:1f593829fa039e9367820736fb063e92acd15c25b53d7bcb5d319971b8e93fd7"}, + {file = "opentelemetry_instrumentation_asgi-0.43b0.tar.gz", hash = "sha256:3f6f19333dca31ef696672e4e36cb1c2613c71dc7e847c11ff36a37e1130dadc"}, +] + +[package.dependencies] +asgiref = ">=3.0,<4.0" +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.43b0" +opentelemetry-semantic-conventions = "0.43b0" +opentelemetry-util-http = "0.43b0" + +[package.extras] +instruments = ["asgiref (>=3.0,<4.0)"] +test = ["opentelemetry-instrumentation-asgi[instruments]", "opentelemetry-test-utils (==0.43b0)"] + +[[package]] +name = "opentelemetry-instrumentation-fastapi" +version = "0.43b0" +description = "OpenTelemetry FastAPI Instrumentation" +optional = false +python-versions = ">=3.7" +files = [ + {file = "opentelemetry_instrumentation_fastapi-0.43b0-py3-none-any.whl", hash = "sha256:b79c044df68a52e07b35fa12a424e7cc0dd27ff0a171c5fdcc41dea9de8fc938"}, + {file = "opentelemetry_instrumentation_fastapi-0.43b0.tar.gz", hash = "sha256:2afaaf470622e1a2732182c68f6d2431ffe5e026a7edacd0f83605632b66347f"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.43b0" +opentelemetry-instrumentation-asgi = "0.43b0" +opentelemetry-semantic-conventions = "0.43b0" +opentelemetry-util-http = "0.43b0" + +[package.extras] +instruments = ["fastapi (>=0.58,<1.0)"] +test = ["httpx (>=0.22,<1.0)", "opentelemetry-instrumentation-fastapi[instruments]", "opentelemetry-test-utils (==0.43b0)", "requests (>=2.23,<3.0)"] + [[package]] name = "opentelemetry-instrumentation-flask" version = "0.43b0" @@ -2470,4 +2531,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.11" -content-hash = "1b4d543363bcda2ec51a2e29e849ceac983fa0e7933de58ec6bf881614bb939f" +content-hash = "2e56e0e3c159f5d8fcc95b8623d56b1dbf94318230f5550a4c5aa15c288dfc84" diff --git a/pyinfra/utils/opentelemetry.py b/pyinfra/utils/opentelemetry.py index 3c8f46e..c96d8e5 100644 --- a/pyinfra/utils/opentelemetry.py +++ b/pyinfra/utils/opentelemetry.py @@ -1,8 +1,10 @@ import json from dynaconf import Dynaconf +from fastapi import FastAPI from opentelemetry import trace from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter +from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor from opentelemetry.instrumentation.pika import PikaInstrumentor from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider @@ -37,6 +39,9 @@ def setup_trace(settings: Dynaconf, service_name: str = None, exporter: SpanExpo provider.add_span_processor(processor) + # TODO: This produces a warning if trying to set the provider twice. + # "WARNING opentelemetry.trace:__init__.py:521 Overriding of current TracerProvider is not allowed" + # This doesn't affect our current usage but should be fixed eventually. trace.set_tracer_provider(provider) @@ -60,5 +65,5 @@ def instrument_pika(): PikaInstrumentor().instrument() -# def instrument_app(app: FastAPI): -# FastAPIInstrumentor().instrument_app(app) +def instrument_app(app: FastAPI): + FastAPIInstrumentor().instrument_app(app) diff --git a/pyproject.toml b/pyproject.toml index 46761c0..55644d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ opentelemetry-sdk = "^1.22.0" opentelemetry-exporter-otlp-proto-http = "^1.22.0" opentelemetry-instrumentation-flask = "^0.43b0" opentelemetry-instrumentation-requests = "^0.43b0" +opentelemetry-instrumentation-fastapi = "^0.43b0" [tool.pytest.ini_options] minversion = "6.0" diff --git a/tests/unit_test/opentelemetry_test.py b/tests/unit_test/opentelemetry_test.py index a1c2599..a38dbbc 100644 --- a/tests/unit_test/opentelemetry_test.py +++ b/tests/unit_test/opentelemetry_test.py @@ -1,13 +1,39 @@ from time import sleep -from pyinfra.utils.opentelemetry import get_exporter, setup_trace, instrument_pika +import pytest +import requests +from fastapi import FastAPI + +from pyinfra.utils.opentelemetry import get_exporter, setup_trace, instrument_pika, instrument_app +from pyinfra.webserver.utils import create_webserver_thread_from_settings + + +@pytest.fixture(scope="class") +def app_with_tracing(settings): + app = FastAPI() + + @app.get("/test") + def test(): + return {"flat": "earth"} + + instrument_app(app) + + thread = create_webserver_thread_from_settings(app, settings) + thread.start() + sleep(1) + yield + thread.join(timeout=1) + + +@pytest.fixture(scope="session") +def exporter(settings): + settings.tracing.opentelemetry.exporter = "json" + return get_exporter(settings) class TestOpenTelemetry: - def test_queue_messages_are_traced(self, queue_manager, input_message, stop_message, settings): - settings.tracing.opentelemetry.exporter = "json" + def test_queue_messages_are_traced(self, queue_manager, input_message, stop_message, settings, exporter): - exporter = get_exporter(settings) setup_trace(settings, exporter=exporter) instrument_pika() @@ -26,3 +52,12 @@ class TestOpenTelemetry: assert ( exported_trace["resource"]["attributes"]["service.name"] == settings.tracing.opentelemetry.service_name ) + + def test_webserver_requests_are_traced(self, settings, app_with_tracing, exporter): + settings.tracing.opentelemetry.exporter = "json" + + setup_trace(settings, exporter=exporter) + + requests.get(f"http://{settings.webserver.host}:{settings.webserver.port}/test") + + print(exporter.traces)