Skip to content

Commit

Permalink
starlette instrumentation (#777)
Browse files Browse the repository at this point in the history
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 <lechen@microsoft.com>
Co-authored-by: alrex <aboten@lightstep.com>
  • Loading branch information
3 people authored Jun 15, 2020
1 parent b70450e commit 39fa078
Show file tree
Hide file tree
Showing 21 changed files with 423 additions and 101 deletions.
1 change: 1 addition & 0 deletions docs-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ boto~=2.0
google-cloud-trace >=0.23.0
google-cloud-monitoring>=0.36.0
botocore~=1.0
starlette~=0.13
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@
# See the License for the specific language governing permissions and
# limitations under the License.

__version__ = "0.9.dev0"
__version__ = "0.10.dev0"
9 changes: 9 additions & 0 deletions docs/ext/starlette/starlette.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.. include:: ../../../ext/opentelemetry-instrumentation-starlette/README.rst

API
---

.. automodule:: opentelemetry.instrumentation.starlette
:members:
:undoc-members:
:show-inheritance:
1 change: 1 addition & 0 deletions ext/opentelemetry-ext-asgi/setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ package_dir=
packages=find_namespace:
install_requires =
opentelemetry-api == 0.10.dev0
opentelemetry-instrumentation == 0.10.dev0
asgiref ~= 3.0

[options.extras_require]
Expand Down
66 changes: 25 additions & 41 deletions ext/opentelemetry-ext-asgi/src/opentelemetry/ext/asgi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@
import typing
import urllib
from functools import wraps
from typing import Tuple

from asgiref.compatibility import guarantee_single_callable

from opentelemetry import context, propagators, trace
from opentelemetry.ext.asgi.version import __version__ # noqa
from opentelemetry.instrumentation.utils import http_status_to_canonical_code
from opentelemetry.trace.status import Status, StatusCanonicalCode


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


def http_status_to_canonical_code(code: int, allow_redirect: bool = True):
# pylint:disable=too-many-branches,too-many-return-statements
if code < 100:
return StatusCanonicalCode.UNKNOWN
if code <= 299:
return StatusCanonicalCode.OK
if code <= 399:
if allow_redirect:
return StatusCanonicalCode.OK
return StatusCanonicalCode.DEADLINE_EXCEEDED
if code <= 499:
if code == 401: # HTTPStatus.UNAUTHORIZED:
return StatusCanonicalCode.UNAUTHENTICATED
if code == 403: # HTTPStatus.FORBIDDEN:
return StatusCanonicalCode.PERMISSION_DENIED
if code == 404: # HTTPStatus.NOT_FOUND:
return StatusCanonicalCode.NOT_FOUND
if code == 429: # HTTPStatus.TOO_MANY_REQUESTS:
return StatusCanonicalCode.RESOURCE_EXHAUSTED
return StatusCanonicalCode.INVALID_ARGUMENT
if code <= 599:
if code == 501: # HTTPStatus.NOT_IMPLEMENTED:
return StatusCanonicalCode.UNIMPLEMENTED
if code == 503: # HTTPStatus.SERVICE_UNAVAILABLE:
return StatusCanonicalCode.UNAVAILABLE
if code == 504: # HTTPStatus.GATEWAY_TIMEOUT:
return StatusCanonicalCode.DEADLINE_EXCEEDED
return StatusCanonicalCode.INTERNAL
return StatusCanonicalCode.UNKNOWN


def collect_request_attributes(scope):
"""Collects HTTP request attributes from the ASGI scope and returns a
dictionary to be used as span creation attributes."""
Expand Down Expand Up @@ -134,11 +105,19 @@ def set_status_code(span, status_code):
span.set_status(Status(http_status_to_canonical_code(status_code)))


def get_default_span_name(scope):
"""Default implementation for name_callback"""
def get_default_span_details(scope: dict) -> Tuple[str, dict]:
"""Default implementation for span_details_callback
Args:
scope: the asgi scope dictionary
Returns:
a tuple of the span, and any attributes to attach to the
span.
"""
method_or_path = scope.get("method") or scope.get("path")

return method_or_path
return method_or_path, {}


class OpenTelemetryMiddleware:
Expand All @@ -149,15 +128,18 @@ class OpenTelemetryMiddleware:
Args:
app: The ASGI application callable to forward requests to.
name_callback: Callback which calculates a generic span name for an
incoming HTTP request based on the ASGI scope.
Optional: Defaults to get_default_span_name.
span_details_callback: Callback which should return a string
and a tuple, representing the desired span name and a
dictionary with any additional span attributes to set.
Optional: Defaults to get_default_span_details.
"""

def __init__(self, app, name_callback=None):
def __init__(self, app, span_details_callback=None):
self.app = guarantee_single_callable(app)
self.tracer = trace.get_tracer(__name__, __version__)
self.name_callback = name_callback or get_default_span_name
self.span_details_callback = (
span_details_callback or get_default_span_details
)

async def __call__(self, scope, receive, send):
"""The ASGI application
Expand All @@ -173,13 +155,15 @@ async def __call__(self, scope, receive, send):
token = context.attach(
propagators.extract(get_header_from_scope, scope)
)
span_name = self.name_callback(scope)
span_name, additional_attributes = self.span_details_callback(scope)
attributes = collect_request_attributes(scope)
attributes.update(additional_attributes)

try:
with self.tracer.start_as_current_span(
span_name + " asgi",
kind=trace.SpanKind.SERVER,
attributes=collect_request_attributes(scope),
attributes=attributes,
):

@wraps(receive)
Expand Down
7 changes: 3 additions & 4 deletions ext/opentelemetry-ext-asgi/tests/test_asgi_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,8 @@ def test_override_span_name(self):
"""Test that span_names can be overwritten by our callback function."""
span_name = "Dymaxion"

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

def update_expected_span_name(expected):
for entry in expected:
Expand All @@ -188,7 +187,7 @@ def update_expected_span_name(expected):
return expected

app = otel_asgi.OpenTelemetryMiddleware(
simple_asgi, name_callback=get_predefined_span_name
simple_asgi, span_details_callback=get_predefined_span_details
)
self.seed_app(app)
self.send_default_request()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@
import boto.elasticache
import boto.s3
import boto.sts

from moto import ( # pylint: disable=import-error
mock_ec2_deprecated,
mock_lambda_deprecated,
mock_s3_deprecated,
mock_sts_deprecated,
)

from opentelemetry.ext.boto import BotoInstrumentor
from opentelemetry.test.test_base import TestBase

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import botocore.session
from botocore.exceptions import ParamValidationError

from moto import ( # pylint: disable=import-error
mock_ec2,
mock_kinesis,
Expand All @@ -9,6 +8,7 @@
mock_s3,
mock_sqs,
)

from opentelemetry.ext.botocore import BotocoreInstrumentor
from opentelemetry.test.test_base import TestBase

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
DEFAULT_AGENT_URL = "http://localhost:8126"
_INSTRUMENTATION_SPAN_TYPES = {
"opentelemetry.ext.aiohttp-client": DatadogSpanTypes.HTTP,
"opentelemetry.ext.asgi": DatadogSpanTypes.WEB,
"opentelemetry.ext.dbapi": DatadogSpanTypes.SQL,
"opentelemetry.ext.django": DatadogSpanTypes.WEB,
"opentelemetry.ext.flask": DatadogSpanTypes.WEB,
Expand Down
5 changes: 5 additions & 0 deletions ext/opentelemetry-instrumentation-starlette/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Changelog

## Unreleased

- Initial release ([#777](https://github.com/open-telemetry/opentelemetry-python/pull/777))
45 changes: 45 additions & 0 deletions ext/opentelemetry-instrumentation-starlette/README.rst
Original file line number Diff line number Diff line change
@@ -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/>`_
55 changes: 55 additions & 0 deletions ext/opentelemetry-instrumentation-starlette/setup.cfg
Original file line number Diff line number Diff line change
@@ -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/ext/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.10.dev0
opentelemetry-ext-asgi == 0.10.dev0

[options.entry_points]
opentelemetry_instrumentor =
starlette = opentelemetry.instrumentation.starlette:StarletteInstrumentor

[options.extras_require]
test =
opentelemetry-test == 0.10.dev0
starlette ~= 0.13.0
requests ~= 2.23.0 # needed for testclient

[options.packages.find]
where = src
31 changes: 31 additions & 0 deletions ext/opentelemetry-instrumentation-starlette/setup.py
Original file line number Diff line number Diff line change
@@ -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__"])
Loading

0 comments on commit 39fa078

Please sign in to comment.