diff --git a/.flake8 b/.flake8
index 5922f31d8f3..f511c4c3e01 100644
--- a/.flake8
+++ b/.flake8
@@ -2,7 +2,8 @@
ignore =
E501 # line too long, defer to black
F401 # unused import, defer to pylint
- W503 # allow line breaks after binary ops, not after
+ W503 # allow line breaks before binary ops
+ W504 # allow line breaks after binary ops
E203 # allow whitespace before ':' (https://github.com/psf/black#slices)
exclude =
.bzr
diff --git a/.gitignore b/.gitignore
index 42e6d0bf043..75cdf092930 100644
--- a/.gitignore
+++ b/.gitignore
@@ -56,3 +56,7 @@ _build/
# mypy
.mypy_cache/
target
+
+# Django example
+
+docs/examples/django/db.sqlite3
diff --git a/docs/examples/django/README.rst b/docs/examples/django/README.rst
new file mode 100644
index 00000000000..2b2f158e7a6
--- /dev/null
+++ b/docs/examples/django/README.rst
@@ -0,0 +1,108 @@
+OpenTelemetry Django Instrumentation Example
+============================================
+
+This shows how to use `opentelemetry-ext-django` to automatically instrument a
+Django app.
+
+For more user convenience, a Django app is already provided in this directory.
+
+Preparation
+-----------
+
+This example will be executed in a separate virtual environment:
+
+.. code-block::
+
+ $ mkdir django_auto_instrumentation
+ $ virtualenv django_auto_instrumentation
+ $ source django_auto_instrumentation/bin/activate
+
+
+Installation
+------------
+
+.. code-block::
+
+ $ pip install opentelemetry-sdk
+ $ pip install opentelemetry-ext-django
+ $ pip install requests
+
+
+Execution
+---------
+
+Execution of the Django app
+...........................
+
+Set these environment variables first:
+
+#. `export OPENTELEMETRY_PYTHON_DJANGO_INSTRUMENT=True`
+#. `export DJANGO_SETTINGS_MODULE=instrumentation_example.settings`
+
+The way to achieve OpenTelemetry instrumentation for your Django app is to use
+an `opentelemetry.ext.django.DjangoInstrumentor` to instrument the app.
+
+Clone the `opentelemetry-python` repository and go to `opentelemetry-python/docs/examples/django`.
+
+Once there, open the `manage.py` file. The call to `DjangoInstrumentor().instrument()`
+in `main` is all that is needed to make the app be instrumented.
+
+Run the Django app with `python manage.py runserver`.
+
+Execution of the client
+.......................
+
+Open up a new console and activate the previous virtual environment there too:
+
+`source django_auto_instrumentation/bin/activate`
+
+Go to `opentelemetry-python/ext/opentelemetry-ext-django/example`, once there
+run the client with:
+
+`python client.py hello`
+
+Go to the previous console, where the Django app is running. You should see
+output similar to this one:
+
+.. code-block::
+
+ {
+ "name": "home_page_view",
+ "context": {
+ "trace_id": "0xed88755c56d95d05a506f5f70e7849b9",
+ "span_id": "0x0a94c7a60e0650d5",
+ "trace_state": "{}"
+ },
+ "kind": "SpanKind.SERVER",
+ "parent_id": "0x3096ef92e621c22d",
+ "start_time": "2020-04-26T01:49:57.205833Z",
+ "end_time": "2020-04-26T01:49:57.206214Z",
+ "status": {
+ "canonical_code": "OK"
+ },
+ "attributes": {
+ "component": "http",
+ "http.method": "GET",
+ "http.server_name": "localhost",
+ "http.scheme": "http",
+ "host.port": 8000,
+ "http.host": "localhost:8000",
+ "http.url": "http://localhost:8000/?param=hello",
+ "net.peer.ip": "127.0.0.1",
+ "http.flavor": "1.1",
+ "http.status_text": "OK",
+ "http.status_code": 200
+ },
+ "events": [],
+ "links": []
+ }
+
+The last output shows spans automatically generated by the OpenTelemetry Django
+Instrumentation package.
+
+References
+----------
+
+* `Django `_
+* `OpenTelemetry Project `_
+* `OpenTelemetry Django extension `_
diff --git a/docs/examples/django/client.py b/docs/examples/django/client.py
new file mode 100644
index 00000000000..e65285c35d2
--- /dev/null
+++ b/docs/examples/django/client.py
@@ -0,0 +1,45 @@
+# 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 sys import argv
+
+from requests import get
+
+from opentelemetry import propagators, trace
+from opentelemetry.sdk.trace import TracerProvider
+from opentelemetry.sdk.trace.export import (
+ ConsoleSpanExporter,
+ SimpleExportSpanProcessor,
+)
+
+trace.set_tracer_provider(TracerProvider())
+tracer = trace.get_tracer_provider().get_tracer(__name__)
+
+trace.get_tracer_provider().add_span_processor(
+ SimpleExportSpanProcessor(ConsoleSpanExporter())
+)
+
+
+with tracer.start_as_current_span("client"):
+
+ with tracer.start_as_current_span("client-server"):
+ headers = {}
+ propagators.inject(dict.__setitem__, headers)
+ requested = get(
+ "http://localhost:8000",
+ params={"param": argv[1]},
+ headers=headers,
+ )
+
+ assert requested.status_code == 200
diff --git a/docs/examples/django/instrumentation_example/__init__.py b/docs/examples/django/instrumentation_example/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/docs/examples/django/instrumentation_example/asgi.py b/docs/examples/django/instrumentation_example/asgi.py
new file mode 100644
index 00000000000..dd8fb568f4a
--- /dev/null
+++ b/docs/examples/django/instrumentation_example/asgi.py
@@ -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.
+"""
+ASGI config for instrumentation_example project.
+
+It exposes the ASGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
+"""
+
+import os
+
+from django.core.asgi import get_asgi_application
+
+os.environ.setdefault(
+ "DJANGO_SETTINGS_MODULE", "instrumentation_example.settings"
+)
+
+application = get_asgi_application()
diff --git a/docs/examples/django/instrumentation_example/settings.py b/docs/examples/django/instrumentation_example/settings.py
new file mode 100644
index 00000000000..b5b8897b91b
--- /dev/null
+++ b/docs/examples/django/instrumentation_example/settings.py
@@ -0,0 +1,133 @@
+# 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.
+"""
+Django settings for instrumentation_example project.
+
+Generated by "django-admin startproject" using Django 3.0.4.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.0/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/3.0/ref/settings/
+"""
+
+import os
+
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = "it%*!=l2(fcawu=!m-06nj(iq2j#%$fu6)myi*b9i5ojk+6+"
+
+# SECURITY WARNING: don"t run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = []
+
+
+# Application definition
+
+INSTALLED_APPS = [
+ "django.contrib.admin",
+ "django.contrib.auth",
+ "django.contrib.contenttypes",
+ "django.contrib.sessions",
+ "django.contrib.messages",
+ "django.contrib.staticfiles",
+]
+
+MIDDLEWARE = [
+ "django.middleware.security.SecurityMiddleware",
+ "django.contrib.sessions.middleware.SessionMiddleware",
+ "django.middleware.common.CommonMiddleware",
+ "django.middleware.csrf.CsrfViewMiddleware",
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
+ "django.contrib.messages.middleware.MessageMiddleware",
+ "django.middleware.clickjacking.XFrameOptionsMiddleware",
+]
+
+ROOT_URLCONF = "instrumentation_example.urls"
+
+TEMPLATES = [
+ {
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
+ "DIRS": [],
+ "APP_DIRS": True,
+ "OPTIONS": {
+ "context_processors": [
+ "django.template.context_processors.debug",
+ "django.template.context_processors.request",
+ "django.contrib.auth.context_processors.auth",
+ "django.contrib.messages.context_processors.messages",
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = "instrumentation_example.wsgi.application"
+
+
+# Database
+# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
+
+DATABASES = {
+ "default": {
+ "ENGINE": "django.db.backends.sqlite3",
+ "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
+ }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
+ },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/3.0/topics/i18n/
+
+LANGUAGE_CODE = "en-us"
+
+TIME_ZONE = "UTC"
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/3.0/howto/static-files/
+
+STATIC_URL = "/static/"
diff --git a/docs/examples/django/instrumentation_example/urls.py b/docs/examples/django/instrumentation_example/urls.py
new file mode 100644
index 00000000000..292467155f8
--- /dev/null
+++ b/docs/examples/django/instrumentation_example/urls.py
@@ -0,0 +1,35 @@
+# 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.
+"""instrumentation_example URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+ https://docs.djangoproject.com/en/3.0/topics/http/urls/
+Examples:
+Function views
+ 1. Add an import: from my_app import views
+ 2. Add a URL to urlpatterns: path("", views.home, name="home")
+Class-based views
+ 1. Add an import: from other_app.views import Home
+ 2. Add a URL to urlpatterns: path("", Home.as_view(), name="home")
+Including another URLconf
+ 1. Import the include() function: from django.urls import include, path
+ 2. Add a URL to urlpatterns: path("blog/", include("blog.urls"))
+"""
+from django.contrib import admin
+from django.urls import include, path
+
+urlpatterns = [
+ path("admin/", admin.site.urls),
+ path("", include("pages.urls")),
+]
diff --git a/docs/examples/django/instrumentation_example/wsgi.py b/docs/examples/django/instrumentation_example/wsgi.py
new file mode 100644
index 00000000000..70ea9e0db56
--- /dev/null
+++ b/docs/examples/django/instrumentation_example/wsgi.py
@@ -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.
+"""
+WSGI config for instrumentation_example project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault(
+ "DJANGO_SETTINGS_MODULE", "instrumentation_example.settings"
+)
+
+application = get_wsgi_application()
diff --git a/docs/examples/django/manage.py b/docs/examples/django/manage.py
new file mode 100755
index 00000000000..fdf32287c5c
--- /dev/null
+++ b/docs/examples/django/manage.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python
+# 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.
+
+"""Django"s command-line utility for administrative tasks."""
+import os
+import sys
+
+from opentelemetry.ext.django import DjangoInstrumentor
+
+
+def main():
+
+ # This call is what makes the Django application be instrumented
+ DjangoInstrumentor().instrument()
+
+ os.environ.setdefault(
+ "DJANGO_SETTINGS_MODULE", "instrumentation_example.settings"
+ )
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError as exc:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ ) from exc
+ execute_from_command_line(sys.argv)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/docs/examples/django/pages/__init__.py b/docs/examples/django/pages/__init__.py
new file mode 100644
index 00000000000..5855e41f3a5
--- /dev/null
+++ b/docs/examples/django/pages/__init__.py
@@ -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.
+
+default_app_config = "pages.apps.PagesConfig"
diff --git a/docs/examples/django/pages/apps.py b/docs/examples/django/pages/apps.py
new file mode 100644
index 00000000000..0f12b7b66ca
--- /dev/null
+++ b/docs/examples/django/pages/apps.py
@@ -0,0 +1,18 @@
+# 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 django.apps import AppConfig
+
+
+class PagesConfig(AppConfig):
+ name = "pages"
diff --git a/docs/examples/django/pages/migrations/__init__.py b/docs/examples/django/pages/migrations/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/docs/examples/django/pages/urls.py b/docs/examples/django/pages/urls.py
new file mode 100644
index 00000000000..99c95765a42
--- /dev/null
+++ b/docs/examples/django/pages/urls.py
@@ -0,0 +1,18 @@
+# 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 django.urls import path
+
+from .views import home_page_view
+
+urlpatterns = [path("", home_page_view, name="home")]
diff --git a/docs/examples/django/pages/views.py b/docs/examples/django/pages/views.py
new file mode 100644
index 00000000000..d54633c3298
--- /dev/null
+++ b/docs/examples/django/pages/views.py
@@ -0,0 +1,32 @@
+# 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 django.http import HttpResponse
+
+from opentelemetry import trace
+from opentelemetry.sdk.trace import TracerProvider
+from opentelemetry.sdk.trace.export import (
+ ConsoleSpanExporter,
+ SimpleExportSpanProcessor,
+)
+
+trace.set_tracer_provider(TracerProvider())
+tracer = trace.get_tracer_provider().get_tracer(__name__)
+
+trace.get_tracer_provider().add_span_processor(
+ SimpleExportSpanProcessor(ConsoleSpanExporter())
+)
+
+
+def home_page_view(request):
+ return HttpResponse("Hello, world")
diff --git a/docs/ext/django/django.rst b/docs/ext/django/django.rst
new file mode 100644
index 00000000000..1a2c844e28c
--- /dev/null
+++ b/docs/ext/django/django.rst
@@ -0,0 +1,7 @@
+OpenTelemetry Django Instrumentation
+====================================
+
+.. automodule:: opentelemetry.ext.django
+ :members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/ext/opentelemetry-ext-django/CHANGELOG.md b/ext/opentelemetry-ext-django/CHANGELOG.md
new file mode 100644
index 00000000000..3e04402cea9
--- /dev/null
+++ b/ext/opentelemetry-ext-django/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Changelog
+
+## Unreleased
+
+- Initial release
diff --git a/ext/opentelemetry-ext-django/README.rst b/ext/opentelemetry-ext-django/README.rst
new file mode 100644
index 00000000000..b922046ca6f
--- /dev/null
+++ b/ext/opentelemetry-ext-django/README.rst
@@ -0,0 +1,24 @@
+OpenTelemetry Django Tracing
+============================
+
+|pypi|
+
+.. |pypi| image:: https://badge.fury.io/py/opentelemetry-ext-django.svg
+ :target: https://pypi.org/project/opentelemetry-ext-django/
+
+This library allows tracing requests for Django applications.
+
+Installation
+------------
+
+::
+
+ pip install opentelemetry-ext-django
+
+
+References
+----------
+
+* `Django `_
+* `OpenTelemetry Django Tracing `_
+* `OpenTelemetry Project `_
diff --git a/ext/opentelemetry-ext-django/setup.cfg b/ext/opentelemetry-ext-django/setup.cfg
new file mode 100644
index 00000000000..c308a6f3521
--- /dev/null
+++ b/ext/opentelemetry-ext-django/setup.cfg
@@ -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-ext-django
+description = OpenTelemetry Instrumentation for Django
+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/ext/opentelemetry-ext-django
+platforms = any
+license = Apache-2.0
+classifiers =
+ Development Status :: 3 - 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 =
+ django >= 2.2
+ opentelemetry-ext-wsgi == 0.7.dev0
+ opentelemetry-auto-instrumentation == 0.7.dev0
+ opentelemetry-api == 0.7.dev0
+
+[options.extras_require]
+test =
+ opentelemetry-test == 0.7.dev0
+
+[options.packages.find]
+where = src
+
+[options.entry_points]
+opentelemetry_instrumentor =
+ django = opentelemetry.ext.django:DjangoInstrumentor
diff --git a/ext/opentelemetry-ext-django/setup.py b/ext/opentelemetry-ext-django/setup.py
new file mode 100644
index 00000000000..45cc68c0f42
--- /dev/null
+++ b/ext/opentelemetry-ext-django/setup.py
@@ -0,0 +1,32 @@
+# 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 os.path import dirname, join
+
+from setuptools import setup
+
+PACKAGE_INFO = {}
+with open(
+ join(
+ dirname(__file__),
+ "src",
+ "opentelemetry",
+ "ext",
+ "django",
+ "version.py",
+ )
+) as f:
+ exec(f.read(), PACKAGE_INFO)
+
+setup(version=PACKAGE_INFO["__version__"])
diff --git a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/__init__.py b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/__init__.py
new file mode 100644
index 00000000000..f59d90b4903
--- /dev/null
+++ b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/__init__.py
@@ -0,0 +1,72 @@
+# 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 logging import getLogger
+
+from django.conf import settings
+
+from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor
+from opentelemetry.configuration import Configuration
+from opentelemetry.ext.django.middleware import _DjangoMiddleware
+
+_logger = getLogger(__name__)
+
+
+class DjangoInstrumentor(BaseInstrumentor):
+ """An instrumentor for Django
+
+ See `BaseInstrumentor`
+ """
+
+ _opentelemetry_middleware = ".".join(
+ [_DjangoMiddleware.__module__, _DjangoMiddleware.__qualname__]
+ )
+
+ def _instrument(self, **kwargs):
+
+ # FIXME this is probably a pattern that will show up in the rest of the
+ # ext. Find a better way of implementing this.
+ # FIXME Probably the evaluation of strings into boolean values can be
+ # built inside the Configuration class itself with the magic method
+ # __bool__
+
+ if Configuration().DJANGO_INSTRUMENT != "True":
+ return
+
+ # This can not be solved, but is an inherent problem of this approach:
+ # the order of middleware entries matters, and here you have no control
+ # on that:
+ # https://docs.djangoproject.com/en/3.0/topics/http/middleware/#activating-middleware
+ # https://docs.djangoproject.com/en/3.0/ref/middleware/#middleware-ordering
+
+ settings_middleware = getattr(settings, "MIDDLEWARE", [])
+ settings_middleware.append(self._opentelemetry_middleware)
+
+ setattr(settings, "MIDDLEWARE", settings_middleware)
+
+ def _uninstrument(self, **kwargs):
+ settings_middleware = getattr(settings, "MIDDLEWARE", None)
+
+ # FIXME This is starting to smell like trouble. We have 2 mechanisms
+ # that may make this condition be True, one implemented in
+ # BaseInstrumentor and another one implemented in _instrument. Both
+ # stop _instrument from running and thus, settings_middleware not being
+ # set.
+ if settings_middleware is None or (
+ self._opentelemetry_middleware not in settings_middleware
+ ):
+ return
+
+ settings_middleware.remove(self._opentelemetry_middleware)
+ setattr(settings, "MIDDLEWARE", settings_middleware)
diff --git a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/middleware.py b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/middleware.py
new file mode 100644
index 00000000000..5974c5e5030
--- /dev/null
+++ b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/middleware.py
@@ -0,0 +1,114 @@
+# 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 logging import getLogger
+
+from django.utils.deprecation import MiddlewareMixin
+
+from opentelemetry.context import attach, detach
+from opentelemetry.ext.django.version import __version__
+from opentelemetry.ext.wsgi import (
+ add_response_attributes,
+ collect_request_attributes,
+ get_header_from_environ,
+)
+from opentelemetry.propagators import extract
+from opentelemetry.trace import SpanKind, get_tracer
+
+_logger = getLogger(__name__)
+
+
+class _DjangoMiddleware(MiddlewareMixin):
+ """Django Middleware for OpenTelemetry
+ """
+
+ _environ_activation_key = (
+ "opentelemetry-instrumentor-django.activation_key"
+ )
+ _environ_token = "opentelemetry-instrumentor-django.token"
+ _environ_span_key = "opentelemetry-instrumentor-django.span_key"
+
+ def process_view(
+ self, request, view_func, view_args, view_kwargs
+ ): # pylint: disable=unused-argument
+ # request.META is a dictionary containing all available HTTP headers
+ # Read more about request.META here:
+ # https://docs.djangoproject.com/en/3.0/ref/request-response/#django.http.HttpRequest.META
+
+ # environ = {
+ # key.lower().replace('_', '-').replace("http-", "", 1): value
+ # for key, value in request.META.items()
+ # }
+
+ environ = request.META
+
+ token = attach(extract(get_header_from_environ, environ))
+
+ tracer = get_tracer(__name__, __version__)
+
+ attributes = collect_request_attributes(environ)
+
+ span = tracer.start_span(
+ view_func.__name__,
+ kind=SpanKind.SERVER,
+ attributes=attributes,
+ start_time=environ.get(
+ "opentelemetry-instrumentor-django.starttime_key"
+ ),
+ )
+
+ activation = tracer.use_span(span, end_on_exit=True)
+ activation.__enter__()
+
+ request.META[self._environ_activation_key] = activation
+ request.META[self._environ_span_key] = span
+ request.META[self._environ_token] = token
+
+ def process_exception(self, request, exception):
+ # Django can call this method and process_response later. In order
+ # to avoid __exit__ and detach from being called twice then, the
+ # respective keys are being removed here.
+ if self._environ_activation_key in request.META.keys():
+ request.META[self._environ_activation_key].__exit__(
+ type(exception),
+ exception,
+ getattr(exception, "__traceback__", None),
+ )
+ request.META.pop(self._environ_activation_key)
+
+ detach(request.environ[self._environ_token])
+ request.META.pop(self._environ_token, None)
+
+ def process_response(self, request, response):
+ if (
+ self._environ_activation_key in request.META.keys()
+ and self._environ_span_key in request.META.keys()
+ ):
+ add_response_attributes(
+ request.META[self._environ_span_key],
+ "{} {}".format(response.status_code, response.reason_phrase),
+ response,
+ )
+ request.META.pop(self._environ_span_key)
+
+ request.META[self._environ_activation_key].__exit__(
+ None, None, None
+ )
+ request.META.pop(self._environ_activation_key)
+
+ if self._environ_token in request.META.keys():
+ detach(request.environ.get(self._environ_token))
+ request.META.pop(self._environ_token)
+
+ return response
diff --git a/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py
new file mode 100644
index 00000000000..86c61362ab5
--- /dev/null
+++ b/ext/opentelemetry-ext-django/src/opentelemetry/ext/django/version.py
@@ -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.7.dev0"
diff --git a/ext/opentelemetry-ext-django/tests/__init__.py b/ext/opentelemetry-ext-django/tests/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/ext/opentelemetry-ext-django/tests/conftest.py b/ext/opentelemetry-ext-django/tests/conftest.py
new file mode 100644
index 00000000000..b2b39bc049a
--- /dev/null
+++ b/ext/opentelemetry-ext-django/tests/conftest.py
@@ -0,0 +1,19 @@
+# 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 os import environ
+
+
+def pytest_sessionstart(session): # pylint: disable=unused-argument
+ environ.setdefault("OPENTELEMETRY_PYTHON_DJANGO_INSTRUMENT", "True")
diff --git a/ext/opentelemetry-ext-django/tests/test_middleware.py b/ext/opentelemetry-ext-django/tests/test_middleware.py
new file mode 100644
index 00000000000..afee6acf77a
--- /dev/null
+++ b/ext/opentelemetry-ext-django/tests/test_middleware.py
@@ -0,0 +1,108 @@
+# 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 sys import modules
+
+from django.conf import settings
+from django.conf.urls import url
+from django.test import Client
+from django.test.utils import setup_test_environment, teardown_test_environment
+
+from opentelemetry.ext.django import DjangoInstrumentor
+from opentelemetry.test.wsgitestutil import WsgiTestBase
+from opentelemetry.trace import SpanKind
+from opentelemetry.trace.status import StatusCanonicalCode
+
+from .views import error, traced # pylint: disable=import-error
+
+urlpatterns = [
+ url(r"^traced/", traced),
+ url(r"^error/", error),
+]
+_django_instrumentor = DjangoInstrumentor()
+
+
+class TestMiddleware(WsgiTestBase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ settings.configure(ROOT_URLCONF=modules[__name__])
+
+ def setUp(self):
+ super().setUp()
+ setup_test_environment()
+ _django_instrumentor.instrument()
+
+ def tearDown(self):
+ super().tearDown()
+ teardown_test_environment()
+ _django_instrumentor.uninstrument()
+
+ def test_traced_get(self):
+ Client().get("/traced/")
+
+ spans = self.memory_exporter.get_finished_spans()
+ self.assertEqual(len(spans), 1)
+
+ span = spans[0]
+
+ self.assertEqual(span.name, "traced")
+ self.assertEqual(span.kind, SpanKind.SERVER)
+ self.assertEqual(span.status.canonical_code, StatusCanonicalCode.OK)
+ self.assertEqual(span.attributes["http.method"], "GET")
+ self.assertEqual(
+ span.attributes["http.url"], "http://testserver/traced/"
+ )
+ self.assertEqual(span.attributes["http.scheme"], "http")
+ self.assertEqual(span.attributes["http.status_code"], 200)
+ self.assertEqual(span.attributes["http.status_text"], "OK")
+
+ def test_traced_post(self):
+ Client().post("/traced/")
+
+ spans = self.memory_exporter.get_finished_spans()
+ self.assertEqual(len(spans), 1)
+
+ span = spans[0]
+
+ self.assertEqual(span.name, "traced")
+ self.assertEqual(span.kind, SpanKind.SERVER)
+ self.assertEqual(span.status.canonical_code, StatusCanonicalCode.OK)
+ self.assertEqual(span.attributes["http.method"], "POST")
+ self.assertEqual(
+ span.attributes["http.url"], "http://testserver/traced/"
+ )
+ self.assertEqual(span.attributes["http.scheme"], "http")
+ self.assertEqual(span.attributes["http.status_code"], 200)
+ self.assertEqual(span.attributes["http.status_text"], "OK")
+
+ def test_error(self):
+ with self.assertRaises(ValueError):
+ Client().get("/error/")
+
+ spans = self.memory_exporter.get_finished_spans()
+ self.assertEqual(len(spans), 1)
+
+ span = spans[0]
+
+ self.assertEqual(span.name, "error")
+ self.assertEqual(span.kind, SpanKind.SERVER)
+ self.assertEqual(
+ span.status.canonical_code, StatusCanonicalCode.UNKNOWN
+ )
+ self.assertEqual(span.attributes["http.method"], "GET")
+ self.assertEqual(
+ span.attributes["http.url"], "http://testserver/error/"
+ )
+ self.assertEqual(span.attributes["http.scheme"], "http")
diff --git a/ext/opentelemetry-ext-django/tests/views.py b/ext/opentelemetry-ext-django/tests/views.py
new file mode 100644
index 00000000000..498a4518eda
--- /dev/null
+++ b/ext/opentelemetry-ext-django/tests/views.py
@@ -0,0 +1,9 @@
+from django.http import HttpResponse
+
+
+def traced(request): # pylint: disable=unused-argument
+ return HttpResponse()
+
+
+def error(request): # pylint: disable=unused-argument
+ raise ValueError("error")
diff --git a/tox.ini b/tox.ini
index 1570df787c6..84dd157dbaa 100644
--- a/tox.ini
+++ b/tox.ini
@@ -28,6 +28,9 @@ envlist =
py3{4,5,6,7,8}-test-example-http
pypy3-test-example-http
+ py3{6,7,8}-test-ext-django
+ pypy3-test-ext-django
+
; opentelemetry-ext-dbapi
py3{4,5,6,7,8}-test-ext-dbapi
pypy3-test-ext-dbapi
@@ -127,6 +130,7 @@ changedir =
test-ext-requests: ext/opentelemetry-ext-requests/tests
test-ext-jaeger: ext/opentelemetry-ext-jaeger/tests
test-ext-dbapi: ext/opentelemetry-ext-dbapi/tests
+ test-ext-django: ext/opentelemetry-ext-django/tests
test-ext-mysql: ext/opentelemetry-ext-mysql/tests
test-ext-otcollector: ext/opentelemetry-ext-otcollector/tests
test-ext-prometheus: ext/opentelemetry-ext-prometheus/tests
@@ -168,13 +172,15 @@ commands_pre =
grpc: pip install {toxinidir}/ext/opentelemetry-ext-grpc[test]
- wsgi,flask: pip install {toxinidir}/ext/opentelemetry-ext-wsgi
-
- flask: pip install {toxinidir}/opentelemetry-auto-instrumentation
+ wsgi,flask,django: pip install {toxinidir}/tests/util
+ wsgi,flask,django: pip install {toxinidir}/ext/opentelemetry-ext-wsgi
+ flask,django: pip install {toxinidir}/opentelemetry-auto-instrumentation
flask: pip install {toxinidir}/ext/opentelemetry-ext-flask[test]
dbapi: pip install {toxinidir}/ext/opentelemetry-ext-dbapi[test]
+ django: pip install {toxinidir}/ext/opentelemetry-ext-django[test]
+
mysql: pip install {toxinidir}/ext/opentelemetry-ext-dbapi
mysql: pip install {toxinidir}/ext/opentelemetry-ext-mysql[test]