Rename web framework packages from "ext" to "instrumentation" (#961)
This commit is contained in:
commit
f3dc0f00f0
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
## Version 0.10b0
|
||||||
|
|
||||||
|
Released 2020-06-23
|
||||||
|
|
||||||
|
- Initial release ([#777](https://github.com/open-telemetry/opentelemetry-python/pull/777))
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
OpenTelemetry Starlette Instrumentation
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
|pypi|
|
||||||
|
|
||||||
|
.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-starlette.svg
|
||||||
|
:target: https://pypi.org/project/opentelemetry-instrumentation-starlette/
|
||||||
|
|
||||||
|
|
||||||
|
This library provides automatic and manual instrumentation of Starlette 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-starlette
|
||||||
|
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from opentelemetry.instrumentation.starlette import StarletteInstrumentor
|
||||||
|
from starlette import applications
|
||||||
|
from starlette.responses import PlainTextResponse
|
||||||
|
from starlette.routing import Route
|
||||||
|
|
||||||
|
def home(request):
|
||||||
|
return PlainTextResponse("hi")
|
||||||
|
|
||||||
|
app = applications.Starlette(
|
||||||
|
routes=[Route("/foobar", home)]
|
||||||
|
)
|
||||||
|
StarletteInstrumentor.instrument_app(app)
|
||||||
|
|
||||||
|
|
||||||
|
References
|
||||||
|
----------
|
||||||
|
|
||||||
|
* `OpenTelemetry Project <https://opentelemetry.io/>`_
|
||||||
|
|
@ -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-starlette
|
||||||
|
description = OpenTelemetry Starlette 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-starlette
|
||||||
|
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 =
|
||||||
|
starlette = opentelemetry.instrumentation.starlette:StarletteInstrumentor
|
||||||
|
|
||||||
|
[options.extras_require]
|
||||||
|
test =
|
||||||
|
opentelemetry-test == 0.12.dev0
|
||||||
|
starlette ~= 0.13.0
|
||||||
|
requests ~= 2.23.0 # needed for testclient
|
||||||
|
|
||||||
|
[options.packages.find]
|
||||||
|
where = src
|
||||||
|
|
@ -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",
|
||||||
|
"starlette",
|
||||||
|
"version.py",
|
||||||
|
)
|
||||||
|
PACKAGE_INFO = {}
|
||||||
|
with open(VERSION_FILENAME) as f:
|
||||||
|
exec(f.read(), PACKAGE_INFO)
|
||||||
|
|
||||||
|
setuptools.setup(version=PACKAGE_INFO["__version__"])
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
from starlette import applications
|
||||||
|
from starlette.routing import Match
|
||||||
|
|
||||||
|
from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
|
||||||
|
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
||||||
|
from opentelemetry.instrumentation.starlette.version import __version__ # noqa
|
||||||
|
|
||||||
|
|
||||||
|
class StarletteInstrumentor(BaseInstrumentor):
|
||||||
|
"""An instrumentor for starlette
|
||||||
|
|
||||||
|
See `BaseInstrumentor`
|
||||||
|
"""
|
||||||
|
|
||||||
|
_original_starlette = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def instrument_app(app: applications.Starlette):
|
||||||
|
"""Instrument an uninstrumented Starlette 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_starlette = applications.Starlette
|
||||||
|
applications.Starlette = _InstrumentedStarlette
|
||||||
|
|
||||||
|
def _uninstrument(self, **kwargs):
|
||||||
|
applications.Starlette = self._original_starlette
|
||||||
|
|
||||||
|
|
||||||
|
class _InstrumentedStarlette(applications.Starlette):
|
||||||
|
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 starlette 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
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
# 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
|
||||||
|
|
||||||
|
from starlette import applications
|
||||||
|
from starlette.responses import PlainTextResponse
|
||||||
|
from starlette.routing import Route
|
||||||
|
from starlette.testclient import TestClient
|
||||||
|
|
||||||
|
import opentelemetry.instrumentation.starlette as otel_starlette
|
||||||
|
from opentelemetry.test.test_base import TestBase
|
||||||
|
|
||||||
|
|
||||||
|
class TestStarletteManualInstrumentation(TestBase):
|
||||||
|
def _create_app(self):
|
||||||
|
app = self._create_starlette_app()
|
||||||
|
self._instrumentor.instrument_app(app)
|
||||||
|
return app
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self._instrumentor = otel_starlette.StarletteInstrumentor()
|
||||||
|
self._app = self._create_app()
|
||||||
|
self._client = TestClient(self._app)
|
||||||
|
|
||||||
|
def test_basic_starlette_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_starlette_route_attribute_added(self):
|
||||||
|
"""Ensure that starlette 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_starlette_app():
|
||||||
|
def home(_):
|
||||||
|
return PlainTextResponse("hi")
|
||||||
|
|
||||||
|
app = applications.Starlette(
|
||||||
|
routes=[Route("/foobar", home), Route("/user/{username}", home)]
|
||||||
|
)
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
class TestAutoInstrumentation(TestStarletteManualInstrumentation):
|
||||||
|
"""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_starlette_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_starlette.StarletteInstrumentor()
|
||||||
|
original = applications.Starlette
|
||||||
|
instrumentor.instrument()
|
||||||
|
try:
|
||||||
|
instrumented = applications.Starlette
|
||||||
|
self.assertIsNot(original, instrumented)
|
||||||
|
finally:
|
||||||
|
instrumentor.uninstrument()
|
||||||
|
|
||||||
|
should_be_original = applications.Starlette
|
||||||
|
self.assertIs(original, should_be_original)
|
||||||
Loading…
Reference in New Issue