Rename web framework packages from "ext" to "instrumentation" (#961)

This commit is contained in:
Leighton Chen
2020-08-03 10:10:45 -07:00
committed by alrex
parent c440250a7c
commit c05e1798b7
8 changed files with 339 additions and 0 deletions

View File

@ -0,0 +1,9 @@
# Changelog
## Unreleased
## Version 0.11b0
Released 2020-07-28
- Initial release ([#890](https://github.com/open-telemetry/opentelemetry-python/pull/890))

View File

@ -0,0 +1,43 @@
OpenTelemetry FastAPI Instrumentation
=======================================
|pypi|
.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-fastapi.svg
:target: https://pypi.org/project/opentelemetry-instrumentation-fastapi/
This library provides automatic and manual instrumentation of FastAPI web frameworks,
instrumenting http requests served by applications utilizing the framework.
auto-instrumentation using the opentelemetry-instrumentation package is also supported.
Installation
------------
::
pip install opentelemetry-instrumentation-fastapi
Usage
-----
.. code-block:: python
import fastapi
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
app = fastapi.FastAPI()
@app.get("/foobar")
async def foobar():
return {"message": "hello world"}
FastAPIInstrumentor.instrument_app(app)
References
----------
* `OpenTelemetry Project <https://opentelemetry.io/>`_

View File

@ -0,0 +1,55 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
[metadata]
name = opentelemetry-instrumentation-fastapi
description = OpenTelemetry FastAPI Instrumentation
long_description = file: README.rst
long_description_content_type = text/x-rst
author = OpenTelemetry Authors
author_email = cncf-opentelemetry-contributors@lists.cncf.io
url = https://github.com/open-telemetry/opentelemetry-python/tree/master/instrumentation/opentelemetry-instrumentation-fastapi
platforms = any
license = Apache-2.0
classifiers =
Development Status :: 4 - Beta
Intended Audience :: Developers
License :: OSI Approved :: Apache Software License
Programming Language :: Python
Programming Language :: Python :: 3
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
[options]
python_requires = >=3.6
package_dir=
=src
packages=find_namespace:
install_requires =
opentelemetry-api == 0.12.dev0
opentelemetry-instrumentation-asgi == 0.12.dev0
[options.entry_points]
opentelemetry_instrumentor =
fastapi = opentelemetry.instrumentation.fastapi:FastAPIInstrumentor
[options.extras_require]
test =
opentelemetry-test == 0.12.dev0
fastapi ~= 0.58.1
requests ~= 2.23.0 # needed for testclient
[options.packages.find]
where = src

View File

@ -0,0 +1,31 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import setuptools
BASE_DIR = os.path.dirname(__file__)
VERSION_FILENAME = os.path.join(
BASE_DIR,
"src",
"opentelemetry",
"instrumentation",
"fastapi",
"version.py",
)
PACKAGE_INFO = {}
with open(VERSION_FILENAME) as f:
exec(f.read(), PACKAGE_INFO)
setuptools.setup(version=PACKAGE_INFO["__version__"])

View File

@ -0,0 +1,82 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Optional
import fastapi
from starlette.routing import Match
from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
from opentelemetry.instrumentation.fastapi.version import __version__ # noqa
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
class FastAPIInstrumentor(BaseInstrumentor):
"""An instrumentor for FastAPI
See `BaseInstrumentor`
"""
_original_fastapi = None
@staticmethod
def instrument_app(app: fastapi.FastAPI):
"""Instrument an uninstrumented FastAPI application.
"""
if not getattr(app, "is_instrumented_by_opentelemetry", False):
app.add_middleware(
OpenTelemetryMiddleware,
span_details_callback=_get_route_details,
)
app.is_instrumented_by_opentelemetry = True
def _instrument(self, **kwargs):
self._original_fastapi = fastapi.FastAPI
fastapi.FastAPI = _InstrumentedFastAPI
def _uninstrument(self, **kwargs):
fastapi.FastAPI = self._original_fastapi
class _InstrumentedFastAPI(fastapi.FastAPI):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.add_middleware(
OpenTelemetryMiddleware, span_details_callback=_get_route_details
)
def _get_route_details(scope):
"""Callback to retrieve the fastapi route being served.
TODO: there is currently no way to retrieve http.route from
a starlette application from scope.
See: https://github.com/encode/starlette/pull/804
"""
app = scope["app"]
route = None
for starlette_route in app.routes:
match, _ = starlette_route.matches(scope)
if match == Match.FULL:
route = starlette_route.path
break
if match == Match.PARTIAL:
route = starlette_route.path
# method only exists for http, if websocket
# leave it blank.
span_name = route or scope.get("method", "")
attributes = {}
if route:
attributes["http.route"] = route
return span_name, attributes

View File

@ -0,0 +1,15 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
__version__ = "0.12.dev0"

View File

@ -0,0 +1,104 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import unittest
import fastapi
from fastapi.testclient import TestClient
import opentelemetry.instrumentation.fastapi as otel_fastapi
from opentelemetry.test.test_base import TestBase
class TestFastAPIManualInstrumentation(TestBase):
def _create_app(self):
app = self._create_fastapi_app()
self._instrumentor.instrument_app(app)
return app
def setUp(self):
super().setUp()
self._instrumentor = otel_fastapi.FastAPIInstrumentor()
self._app = self._create_app()
self._client = TestClient(self._app)
def test_basic_fastapi_call(self):
self._client.get("/foobar")
spans = self.memory_exporter.get_finished_spans()
self.assertEqual(len(spans), 3)
for span in spans:
self.assertIn("/foobar", span.name)
def test_fastapi_route_attribute_added(self):
"""Ensure that fastapi routes are used as the span name."""
self._client.get("/user/123")
spans = self.memory_exporter.get_finished_spans()
self.assertEqual(len(spans), 3)
for span in spans:
self.assertIn("/user/{username}", span.name)
self.assertEqual(
spans[-1].attributes["http.route"], "/user/{username}"
)
# ensure that at least one attribute that is populated by
# the asgi instrumentation is successfully feeding though.
self.assertEqual(spans[-1].attributes["http.flavor"], "1.1")
@staticmethod
def _create_fastapi_app():
app = fastapi.FastAPI()
@app.get("/foobar")
async def _():
return {"message": "hello world"}
@app.get("/user/{username}")
async def _(username: str):
return {"message": username}
return app
class TestAutoInstrumentation(TestFastAPIManualInstrumentation):
"""Test the auto-instrumented variant
Extending the manual instrumentation as most test cases apply
to both.
"""
def _create_app(self):
# instrumentation is handled by the instrument call
self._instrumentor.instrument()
return self._create_fastapi_app()
def tearDown(self):
self._instrumentor.uninstrument()
super().tearDown()
class TestAutoInstrumentationLogic(unittest.TestCase):
def test_instrumentation(self):
"""Verify that instrumentation methods are instrumenting and
removing as expected.
"""
instrumentor = otel_fastapi.FastAPIInstrumentor()
original = fastapi.FastAPI
instrumentor.instrument()
try:
instrumented = fastapi.FastAPI
self.assertIsNot(original, instrumented)
finally:
instrumentor.uninstrument()
should_be_original = fastapi.FastAPI
self.assertIs(original, should_be_original)