Skip to content

Commit 7bec76a

Browse files
toumorokoshilzchen
andauthored
fastapi instrumentation (#890)
Co-authored-by: Leighton Chen <[email protected]>
1 parent 545068d commit 7bec76a

File tree

13 files changed

+358
-7
lines changed

13 files changed

+358
-7
lines changed

docs-requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,4 @@ google-cloud-trace >=0.23.0
2929
google-cloud-monitoring>=0.36.0
3030
botocore~=1.0
3131
starlette~=0.13
32+
fastapi~=0.58.1

docs/ext/fastapi/fastapi.rst

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.. include:: ../../../ext/opentelemetry-instrumentation-fastapi/README.rst
2+
3+
API
4+
---
5+
6+
.. automodule:: opentelemetry.instrumentation.fastapi
7+
:members:
8+
:undoc-members:
9+
:show-inheritance:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Changelog
2+
3+
## Unreleased
4+
5+
- Initial release ([#890](https://github.com/open-telemetry/opentelemetry-python/pull/890))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
OpenTelemetry FastAPI Instrumentation
2+
=======================================
3+
4+
|pypi|
5+
6+
.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-fastapi.svg
7+
:target: https://pypi.org/project/opentelemetry-instrumentation-fastapi/
8+
9+
10+
This library provides automatic and manual instrumentation of FastAPI web frameworks,
11+
instrumenting http requests served by applications utilizing the framework.
12+
13+
auto-instrumentation using the opentelemetry-instrumentation package is also supported.
14+
15+
Installation
16+
------------
17+
18+
::
19+
20+
pip install opentelemetry-instrumentation-fastapi
21+
22+
23+
Usage
24+
-----
25+
26+
.. code-block:: python
27+
28+
import fastapi
29+
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
30+
31+
app = fastapi.FastAPI()
32+
33+
@app.get("/foobar")
34+
async def foobar():
35+
return {"message": "hello world"}
36+
37+
FastAPIInstrumentor.instrument_app(app)
38+
39+
40+
References
41+
----------
42+
43+
* `OpenTelemetry Project <https://opentelemetry.io/>`_
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
[metadata]
16+
name = opentelemetry-instrumentation-fastapi
17+
description = OpenTelemetry FastAPI Instrumentation
18+
long_description = file: README.rst
19+
long_description_content_type = text/x-rst
20+
author = OpenTelemetry Authors
21+
author_email = [email protected]
22+
url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-instrumentation-fastapi
23+
platforms = any
24+
license = Apache-2.0
25+
classifiers =
26+
Development Status :: 4 - Beta
27+
Intended Audience :: Developers
28+
License :: OSI Approved :: Apache Software License
29+
Programming Language :: Python
30+
Programming Language :: Python :: 3
31+
Programming Language :: Python :: 3.6
32+
Programming Language :: Python :: 3.7
33+
Programming Language :: Python :: 3.8
34+
35+
[options]
36+
python_requires = >=3.6
37+
package_dir=
38+
=src
39+
packages=find_namespace:
40+
install_requires =
41+
opentelemetry-api == 0.11.dev0
42+
opentelemetry-ext-asgi == 0.11.dev0
43+
44+
[options.entry_points]
45+
opentelemetry_instrumentor =
46+
fastapi = opentelemetry.instrumentation.fastapi:FastAPIInstrumentor
47+
48+
[options.extras_require]
49+
test =
50+
opentelemetry-test == 0.11.dev0
51+
fastapi ~= 0.58.1
52+
requests ~= 2.23.0 # needed for testclient
53+
54+
[options.packages.find]
55+
where = src
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
import os
15+
16+
import setuptools
17+
18+
BASE_DIR = os.path.dirname(__file__)
19+
VERSION_FILENAME = os.path.join(
20+
BASE_DIR,
21+
"src",
22+
"opentelemetry",
23+
"instrumentation",
24+
"fastapi",
25+
"version.py",
26+
)
27+
PACKAGE_INFO = {}
28+
with open(VERSION_FILENAME) as f:
29+
exec(f.read(), PACKAGE_INFO)
30+
31+
setuptools.setup(version=PACKAGE_INFO["__version__"])
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
from typing import Optional
15+
16+
import fastapi
17+
from starlette.routing import Match
18+
19+
from opentelemetry.ext.asgi import OpenTelemetryMiddleware
20+
from opentelemetry.instrumentation.fastapi.version import __version__ # noqa
21+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
22+
23+
24+
class FastAPIInstrumentor(BaseInstrumentor):
25+
"""An instrumentor for FastAPI
26+
27+
See `BaseInstrumentor`
28+
"""
29+
30+
_original_fastapi = None
31+
32+
@staticmethod
33+
def instrument_app(app: fastapi.FastAPI):
34+
"""Instrument an uninstrumented FastAPI application.
35+
"""
36+
if not getattr(app, "is_instrumented_by_opentelemetry", False):
37+
app.add_middleware(
38+
OpenTelemetryMiddleware,
39+
span_details_callback=_get_route_details,
40+
)
41+
app.is_instrumented_by_opentelemetry = True
42+
43+
def _instrument(self, **kwargs):
44+
self._original_fastapi = fastapi.FastAPI
45+
fastapi.FastAPI = _InstrumentedFastAPI
46+
47+
def _uninstrument(self, **kwargs):
48+
fastapi.FastAPI = self._original_fastapi
49+
50+
51+
class _InstrumentedFastAPI(fastapi.FastAPI):
52+
def __init__(self, *args, **kwargs):
53+
super().__init__(*args, **kwargs)
54+
self.add_middleware(
55+
OpenTelemetryMiddleware, span_details_callback=_get_route_details
56+
)
57+
58+
59+
def _get_route_details(scope):
60+
"""Callback to retrieve the fastapi route being served.
61+
62+
TODO: there is currently no way to retrieve http.route from
63+
a starlette application from scope.
64+
65+
See: https://github.com/encode/starlette/pull/804
66+
"""
67+
app = scope["app"]
68+
route = None
69+
for starlette_route in app.routes:
70+
match, _ = starlette_route.matches(scope)
71+
if match == Match.FULL:
72+
route = starlette_route.path
73+
break
74+
if match == Match.PARTIAL:
75+
route = starlette_route.path
76+
# method only exists for http, if websocket
77+
# leave it blank.
78+
span_name = route or scope.get("method", "")
79+
attributes = {}
80+
if route:
81+
attributes["http.route"] = route
82+
return span_name, attributes
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
__version__ = "0.11.dev0"

ext/opentelemetry-instrumentation-fastapi/tests/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import unittest
16+
17+
import fastapi
18+
from fastapi.testclient import TestClient
19+
20+
import opentelemetry.instrumentation.fastapi as otel_fastapi
21+
from opentelemetry.test.test_base import TestBase
22+
23+
24+
class TestFastAPIManualInstrumentation(TestBase):
25+
def _create_app(self):
26+
app = self._create_fastapi_app()
27+
self._instrumentor.instrument_app(app)
28+
return app
29+
30+
def setUp(self):
31+
super().setUp()
32+
self._instrumentor = otel_fastapi.FastAPIInstrumentor()
33+
self._app = self._create_app()
34+
self._client = TestClient(self._app)
35+
36+
def test_basic_fastapi_call(self):
37+
self._client.get("/foobar")
38+
spans = self.memory_exporter.get_finished_spans()
39+
self.assertEqual(len(spans), 3)
40+
for span in spans:
41+
self.assertIn("/foobar", span.name)
42+
43+
def test_fastapi_route_attribute_added(self):
44+
"""Ensure that fastapi routes are used as the span name."""
45+
self._client.get("/user/123")
46+
spans = self.memory_exporter.get_finished_spans()
47+
self.assertEqual(len(spans), 3)
48+
for span in spans:
49+
self.assertIn("/user/{username}", span.name)
50+
self.assertEqual(
51+
spans[-1].attributes["http.route"], "/user/{username}"
52+
)
53+
# ensure that at least one attribute that is populated by
54+
# the asgi instrumentation is successfully feeding though.
55+
self.assertEqual(spans[-1].attributes["http.flavor"], "1.1")
56+
57+
@staticmethod
58+
def _create_fastapi_app():
59+
app = fastapi.FastAPI()
60+
61+
@app.get("/foobar")
62+
async def _():
63+
return {"message": "hello world"}
64+
65+
@app.get("/user/{username}")
66+
async def _(username: str):
67+
return {"message": username}
68+
69+
return app
70+
71+
72+
class TestAutoInstrumentation(TestFastAPIManualInstrumentation):
73+
"""Test the auto-instrumented variant
74+
75+
Extending the manual instrumentation as most test cases apply
76+
to both.
77+
"""
78+
79+
def _create_app(self):
80+
# instrumentation is handled by the instrument call
81+
self._instrumentor.instrument()
82+
return self._create_fastapi_app()
83+
84+
def tearDown(self):
85+
self._instrumentor.uninstrument()
86+
super().tearDown()
87+
88+
89+
class TestAutoInstrumentationLogic(unittest.TestCase):
90+
def test_instrumentation(self):
91+
"""Verify that instrumentation methods are instrumenting and
92+
removing as expected.
93+
"""
94+
instrumentor = otel_fastapi.FastAPIInstrumentor()
95+
original = fastapi.FastAPI
96+
instrumentor.instrument()
97+
try:
98+
instrumented = fastapi.FastAPI
99+
self.assertIsNot(original, instrumented)
100+
finally:
101+
instrumentor.uninstrument()
102+
103+
should_be_original = fastapi.FastAPI
104+
self.assertIs(original, should_be_original)

ext/opentelemetry-instrumentation-starlette/setup.cfg

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ long_description = file: README.rst
1919
long_description_content_type = text/x-rst
2020
author = OpenTelemetry Authors
2121
author_email = [email protected]
22-
url = https://github.com/open-telemetry/opentelemetry-python/ext/opentelemetry-instrumentation-starlette
22+
url = https://github.com/open-telemetry/opentelemetry-python/tree/master/ext/opentelemetry-instrumentation-starlette
2323
platforms = any
2424
license = Apache-2.0
2525
classifiers =

ext/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class StarletteInstrumentor(BaseInstrumentor):
3131

3232
@staticmethod
3333
def instrument_app(app: applications.Starlette):
34-
"""Instrument a previously instrumented Starlette application.
34+
"""Instrument an uninstrumented Starlette application.
3535
"""
3636
if not getattr(app, "is_instrumented_by_opentelemetry", False):
3737
app.add_middleware(

0 commit comments

Comments
 (0)