Skip to content

Commit 39fa078

Browse files
toumorokoshilzchenalrex
authored
starlette instrumentation (#777)
adding an initial starlette instrumentation. tox does exact match on fields delimited by a dash. Thus, any instrumentation that includes "instrumentation" in the name would collide with testing of the "opentelemetry-instrumentation" package. Renaming opentelemetry-instrumentation to opentelemetry-instrumentation-base to fix that. Co-authored-by: Leighton Chen <[email protected]> Co-authored-by: alrex <[email protected]>
1 parent b70450e commit 39fa078

File tree

21 files changed

+423
-101
lines changed

21 files changed

+423
-101
lines changed

Diff for: docs-requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ boto~=2.0
2626
google-cloud-trace >=0.23.0
2727
google-cloud-monitoring>=0.36.0
2828
botocore~=1.0
29+
starlette~=0.13

Diff for: docs/examples/opentelemetry-example-app/src/opentelemetry_example_app/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
__version__ = "0.9.dev0"
15+
__version__ = "0.10.dev0"

Diff for: docs/ext/starlette/starlette.rst

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.. include:: ../../../ext/opentelemetry-instrumentation-starlette/README.rst
2+
3+
API
4+
---
5+
6+
.. automodule:: opentelemetry.instrumentation.starlette
7+
:members:
8+
:undoc-members:
9+
:show-inheritance:

Diff for: ext/opentelemetry-ext-asgi/setup.cfg

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ package_dir=
4040
packages=find_namespace:
4141
install_requires =
4242
opentelemetry-api == 0.10.dev0
43+
opentelemetry-instrumentation == 0.10.dev0
4344
asgiref ~= 3.0
4445

4546
[options.extras_require]

Diff for: ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/__init__.py

+25-41
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@
2222
import typing
2323
import urllib
2424
from functools import wraps
25+
from typing import Tuple
2526

2627
from asgiref.compatibility import guarantee_single_callable
2728

2829
from opentelemetry import context, propagators, trace
2930
from opentelemetry.ext.asgi.version import __version__ # noqa
31+
from opentelemetry.instrumentation.utils import http_status_to_canonical_code
3032
from opentelemetry.trace.status import Status, StatusCanonicalCode
3133

3234

@@ -44,37 +46,6 @@ def get_header_from_scope(scope: dict, header_name: str) -> typing.List[str]:
4446
]
4547

4648

47-
def http_status_to_canonical_code(code: int, allow_redirect: bool = True):
48-
# pylint:disable=too-many-branches,too-many-return-statements
49-
if code < 100:
50-
return StatusCanonicalCode.UNKNOWN
51-
if code <= 299:
52-
return StatusCanonicalCode.OK
53-
if code <= 399:
54-
if allow_redirect:
55-
return StatusCanonicalCode.OK
56-
return StatusCanonicalCode.DEADLINE_EXCEEDED
57-
if code <= 499:
58-
if code == 401: # HTTPStatus.UNAUTHORIZED:
59-
return StatusCanonicalCode.UNAUTHENTICATED
60-
if code == 403: # HTTPStatus.FORBIDDEN:
61-
return StatusCanonicalCode.PERMISSION_DENIED
62-
if code == 404: # HTTPStatus.NOT_FOUND:
63-
return StatusCanonicalCode.NOT_FOUND
64-
if code == 429: # HTTPStatus.TOO_MANY_REQUESTS:
65-
return StatusCanonicalCode.RESOURCE_EXHAUSTED
66-
return StatusCanonicalCode.INVALID_ARGUMENT
67-
if code <= 599:
68-
if code == 501: # HTTPStatus.NOT_IMPLEMENTED:
69-
return StatusCanonicalCode.UNIMPLEMENTED
70-
if code == 503: # HTTPStatus.SERVICE_UNAVAILABLE:
71-
return StatusCanonicalCode.UNAVAILABLE
72-
if code == 504: # HTTPStatus.GATEWAY_TIMEOUT:
73-
return StatusCanonicalCode.DEADLINE_EXCEEDED
74-
return StatusCanonicalCode.INTERNAL
75-
return StatusCanonicalCode.UNKNOWN
76-
77-
7849
def collect_request_attributes(scope):
7950
"""Collects HTTP request attributes from the ASGI scope and returns a
8051
dictionary to be used as span creation attributes."""
@@ -134,11 +105,19 @@ def set_status_code(span, status_code):
134105
span.set_status(Status(http_status_to_canonical_code(status_code)))
135106

136107

137-
def get_default_span_name(scope):
138-
"""Default implementation for name_callback"""
108+
def get_default_span_details(scope: dict) -> Tuple[str, dict]:
109+
"""Default implementation for span_details_callback
110+
111+
Args:
112+
scope: the asgi scope dictionary
113+
114+
Returns:
115+
a tuple of the span, and any attributes to attach to the
116+
span.
117+
"""
139118
method_or_path = scope.get("method") or scope.get("path")
140119

141-
return method_or_path
120+
return method_or_path, {}
142121

143122

144123
class OpenTelemetryMiddleware:
@@ -149,15 +128,18 @@ class OpenTelemetryMiddleware:
149128
150129
Args:
151130
app: The ASGI application callable to forward requests to.
152-
name_callback: Callback which calculates a generic span name for an
153-
incoming HTTP request based on the ASGI scope.
154-
Optional: Defaults to get_default_span_name.
131+
span_details_callback: Callback which should return a string
132+
and a tuple, representing the desired span name and a
133+
dictionary with any additional span attributes to set.
134+
Optional: Defaults to get_default_span_details.
155135
"""
156136

157-
def __init__(self, app, name_callback=None):
137+
def __init__(self, app, span_details_callback=None):
158138
self.app = guarantee_single_callable(app)
159139
self.tracer = trace.get_tracer(__name__, __version__)
160-
self.name_callback = name_callback or get_default_span_name
140+
self.span_details_callback = (
141+
span_details_callback or get_default_span_details
142+
)
161143

162144
async def __call__(self, scope, receive, send):
163145
"""The ASGI application
@@ -173,13 +155,15 @@ async def __call__(self, scope, receive, send):
173155
token = context.attach(
174156
propagators.extract(get_header_from_scope, scope)
175157
)
176-
span_name = self.name_callback(scope)
158+
span_name, additional_attributes = self.span_details_callback(scope)
159+
attributes = collect_request_attributes(scope)
160+
attributes.update(additional_attributes)
177161

178162
try:
179163
with self.tracer.start_as_current_span(
180164
span_name + " asgi",
181165
kind=trace.SpanKind.SERVER,
182-
attributes=collect_request_attributes(scope),
166+
attributes=attributes,
183167
):
184168

185169
@wraps(receive)

Diff for: ext/opentelemetry-ext-asgi/tests/test_asgi_middleware.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -176,9 +176,8 @@ def test_override_span_name(self):
176176
"""Test that span_names can be overwritten by our callback function."""
177177
span_name = "Dymaxion"
178178

179-
# pylint:disable=unused-argument
180-
def get_predefined_span_name(scope):
181-
return span_name
179+
def get_predefined_span_details(_):
180+
return span_name, {}
182181

183182
def update_expected_span_name(expected):
184183
for entry in expected:
@@ -188,7 +187,7 @@ def update_expected_span_name(expected):
188187
return expected
189188

190189
app = otel_asgi.OpenTelemetryMiddleware(
191-
simple_asgi, name_callback=get_predefined_span_name
190+
simple_asgi, span_details_callback=get_predefined_span_details
192191
)
193192
self.seed_app(app)
194193
self.send_default_request()

Diff for: ext/opentelemetry-ext-boto/tests/test_boto_instrumentation.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@
1919
import boto.elasticache
2020
import boto.s3
2121
import boto.sts
22-
2322
from moto import ( # pylint: disable=import-error
2423
mock_ec2_deprecated,
2524
mock_lambda_deprecated,
2625
mock_s3_deprecated,
2726
mock_sts_deprecated,
2827
)
28+
2929
from opentelemetry.ext.boto import BotoInstrumentor
3030
from opentelemetry.test.test_base import TestBase
3131

Diff for: ext/opentelemetry-ext-botocore/tests/test_botocore_instrumentation.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import botocore.session
22
from botocore.exceptions import ParamValidationError
3-
43
from moto import ( # pylint: disable=import-error
54
mock_ec2,
65
mock_kinesis,
@@ -9,6 +8,7 @@
98
mock_s3,
109
mock_sqs,
1110
)
11+
1212
from opentelemetry.ext.botocore import BotocoreInstrumentor
1313
from opentelemetry.test.test_base import TestBase
1414

Diff for: ext/opentelemetry-ext-datadog/src/opentelemetry/ext/datadog/exporter.py

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
DEFAULT_AGENT_URL = "http://localhost:8126"
3434
_INSTRUMENTATION_SPAN_TYPES = {
3535
"opentelemetry.ext.aiohttp-client": DatadogSpanTypes.HTTP,
36+
"opentelemetry.ext.asgi": DatadogSpanTypes.WEB,
3637
"opentelemetry.ext.dbapi": DatadogSpanTypes.SQL,
3738
"opentelemetry.ext.django": DatadogSpanTypes.WEB,
3839
"opentelemetry.ext.flask": DatadogSpanTypes.WEB,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Changelog
2+
3+
## Unreleased
4+
5+
- Initial release ([#777](https://github.com/open-telemetry/opentelemetry-python/pull/777))
+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
OpenTelemetry Starlette Instrumentation
2+
=======================================
3+
4+
|pypi|
5+
6+
.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-starlette.svg
7+
:target: https://pypi.org/project/opentelemetry-instrumentation-starlette/
8+
9+
10+
This library provides automatic and manual instrumentation of Starlette 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-starlette
21+
22+
23+
Usage
24+
-----
25+
26+
.. code-block:: python
27+
28+
from opentelemetry.instrumentation.starlette import StarletteInstrumentor
29+
from starlette import applications
30+
from starlette.responses import PlainTextResponse
31+
from starlette.routing import Route
32+
33+
def home(request):
34+
return PlainTextResponse("hi")
35+
36+
app = applications.Starlette(
37+
routes=[Route("/foobar", home)]
38+
)
39+
StarletteInstrumentor.instrument_app(app)
40+
41+
42+
References
43+
----------
44+
45+
* `OpenTelemetry Project <https://opentelemetry.io/>`_
+55
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-starlette
17+
description = OpenTelemetry Starlette 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/ext/opentelemetry-instrumentation-starlette
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.10.dev0
42+
opentelemetry-ext-asgi == 0.10.dev0
43+
44+
[options.entry_points]
45+
opentelemetry_instrumentor =
46+
starlette = opentelemetry.instrumentation.starlette:StarletteInstrumentor
47+
48+
[options.extras_require]
49+
test =
50+
opentelemetry-test == 0.10.dev0
51+
starlette ~= 0.13.0
52+
requests ~= 2.23.0 # needed for testclient
53+
54+
[options.packages.find]
55+
where = src

Diff for: ext/opentelemetry-instrumentation-starlette/setup.py

+31
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+
"starlette",
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__"])

0 commit comments

Comments
 (0)