Skip to content

Commit

Permalink
add django and modernize flask with content (#489)
Browse files Browse the repository at this point in the history
* add django, and modernize flask with content
  • Loading branch information
saartochner-lumigo authored Sep 20, 2023
1 parent d96da7d commit 4d4bda6
Show file tree
Hide file tree
Showing 24 changed files with 389 additions and 15 deletions.
50 changes: 50 additions & 0 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,56 @@ def integration_tests_flask(session, flask_version):
kill_process_and_clean_outputs(temp_file, "flask", session)


@nox.session(python=python_versions())
@nox.parametrize(
"django_version",
dependency_versions_to_be_tested(
directory="django",
dependency_name="django",
test_untested_versions=should_test_only_untested_versions(),
),
)
def integration_tests_django(session, django_version):
with TestedVersions.save_tests_result("django", "django", django_version):
install_package("django", django_version, session)

session.install(".")

temp_file = create_it_tempfile("django")
with session.chdir("src/test/integration/django"):
session.install("-r", OTHER_REQUIREMENTS)

try:
session.run(
"sh",
"./scripts/start_django",
env={
"LUMIGO_DEBUG_SPANDUMP": temp_file,
"OTEL_SERVICE_NAME": "app",
},
external=True,
) # One happy day we will have https://github.com/wntrblm/nox/issues/198

# TODO Make this deterministic
# Wait 1s to give time for app to start
time.sleep(8)

session.run(
"pytest",
"--tb",
"native",
"--log-cli-level=INFO",
"--color=yes",
"-v",
"./tests/test_django.py",
env={
"LUMIGO_DEBUG_SPANDUMP": temp_file,
},
)
finally:
kill_process_and_clean_outputs(temp_file, "django", session)


@nox.session(python=python_versions())
@nox.parametrize(
"grpcio_version",
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,6 @@
"opentelemetry-instrumentation-pymysql==0.36b0",
"opentelemetry-instrumentation-requests==0.36b0",
"opentelemetry-instrumentation-redis==0.36b0",
"opentelemetry-instrumentation-django==0.36b0",
],
)
45 changes: 45 additions & 0 deletions src/lumigo_opentelemetry/instrumentations/django/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from opentelemetry.trace.span import Span

from lumigo_opentelemetry.instrumentations import AbstractInstrumentor
from lumigo_opentelemetry.libs.general_utils import lumigo_safe_execute
from lumigo_opentelemetry.instrumentations.instrumentation_utils import (
add_body_attribute,
)
from lumigo_opentelemetry.libs.json_utils import dump_with_context


class DjangoInstrumentorWrapper(AbstractInstrumentor):
def __init__(self) -> None:
super().__init__("django")

def check_if_applicable(self) -> None:
import django # noqa

def install_instrumentation(self) -> None:
from opentelemetry.instrumentation.django import DjangoInstrumentor
from django.http import HttpRequest, HttpResponse

def request_hook(span: Span, request: HttpRequest) -> None:
with lumigo_safe_execute("django request_hook"):
span.set_attribute(
"http.request.headers",
dump_with_context("requestHeaders", request.headers),
)
add_body_attribute(span, request.body, "http.request.body")

def response_hook(
span: Span, request: HttpRequest, response: HttpResponse
) -> None:
with lumigo_safe_execute("django response_hook"):
span.set_attribute(
"http.response.headers",
dump_with_context("responseHeaders", response.headers),
)
add_body_attribute(span, response.content, "http.response.body")

DjangoInstrumentor().instrument(
request_hook=request_hook, response_hook=response_hook
)


instrumentor: AbstractInstrumentor = DjangoInstrumentorWrapper()
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
4.2.5
40 changes: 27 additions & 13 deletions src/lumigo_opentelemetry/instrumentations/flask/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
from typing import Any, Dict

from opentelemetry.trace.span import Span

from lumigo_opentelemetry.instrumentations import AbstractInstrumentor
from lumigo_opentelemetry.libs.general_utils import lumigo_safe_execute
from lumigo_opentelemetry.libs.json_utils import dump_with_context


class FlaskInstrumentorWrapper(AbstractInstrumentor):
Expand All @@ -9,19 +15,27 @@ def check_if_applicable(self) -> None:
import flask # noqa

def install_instrumentation(self) -> None:
import wrapt
from lumigo_opentelemetry import logger

@wrapt.patch_function_wrapper("flask", "Flask.__init__")
def init_otel_flask_instrumentation(wrapped, instance, args, kwargs): # type: ignore
try:
from opentelemetry.instrumentation.flask import FlaskInstrumentor

return_value = wrapped(*args, **kwargs)
FlaskInstrumentor().instrument_app(instance)
return return_value
except Exception as e:
logger.exception("failed instrumenting Flask", exc_info=e)
from opentelemetry.instrumentation.flask import FlaskInstrumentor

def request_hook(span: Span, flask_request_environ: Dict[str, Any]) -> None:
with lumigo_safe_execute("flask_request_hook"):
span.set_attribute(
"http.request.headers",
dump_with_context("requestHeaders", flask_request_environ),
)

def response_hook(
span: Span, status: int, response_headers: Dict[str, Any]
) -> None:
with lumigo_safe_execute("flask_response_hook"):
span.set_attribute(
"http.response.headers",
dump_with_context("responseHeaders", response_headers),
)

FlaskInstrumentor().instrument(
request_hook=request_hook, response_hook=response_hook
)


instrumentor: AbstractInstrumentor = FlaskInstrumentorWrapper()
4 changes: 4 additions & 0 deletions src/lumigo_opentelemetry/instrumentations/instrumentations.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .botocore import instrumentor as botocore_instrumentor
from .fastapi import instrumentor as fastapi_instrumentor
from .flask import instrumentor as flask_instrumentor
from .django import instrumentor as django_instrumentor
from .grpcio import instrumentor as grpc_instrumentor
from .kafka_python import instrumentor as kafka_python_instrumentor
from .pika import instrumentor as pika_instrumentor
Expand All @@ -23,6 +24,7 @@
botocore_instrumentor,
fastapi_instrumentor,
flask_instrumentor,
django_instrumentor,
grpc_instrumentor,
kafka_python_instrumentor,
pika_instrumentor,
Expand All @@ -46,6 +48,7 @@
"An error occurred while applying the '%s' instrumentation: %s",
instrumentor.instrumentation_id,
str(e),
exc_info=True,
)

logger.debug(
Expand All @@ -58,6 +61,7 @@
in [
fastapi_instrumentor.instrumentation_id,
flask_instrumentor.instrumentation_id,
django_instrumentor.instrumentation_id,
],
installed_instrumentations,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def request_hook(
pass

def response_hook(span: Span, instance: Connection, response: Any) -> None:
add_body_attribute(span, response, "redis.response.body")
add_body_attribute(span, response, "db.response.body")

RedisInstrumentor().instrument(
request_hook=request_hook,
Expand Down
Empty file.
4 changes: 4 additions & 0 deletions src/test/integration/django/app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
THIS DIRECTORY WAS CREATED BY THE DJANGO TEST APP GENERATOR:
```shell
django-admin startproject app
```
Empty file.
Empty file.
16 changes: 16 additions & 0 deletions src/test/integration/django/app/app/asgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
ASGI config for app 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/4.2/howto/deployment/asgi/
"""

import os

from django.core.asgi import get_asgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings")

application = get_asgi_application()
123 changes: 123 additions & 0 deletions src/test/integration/django/app/app/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"""
Django settings for app project.
Generated by 'django-admin startproject' using Django 4.2.4.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""

from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "django-insecure-(^(jp%=9%fu8o5_u1w(!a))bwy1uls0=v@cchpze%_fgur_ac2"

# 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 = "app.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 = "app.wsgi.application"


# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases

DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}


# Password validation
# https://docs.djangoproject.com/en/4.2/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/4.2/topics/i18n/

LANGUAGE_CODE = "en-us"

TIME_ZONE = "UTC"

USE_I18N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/

STATIC_URL = "static/"

# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
22 changes: 22 additions & 0 deletions src/test/integration/django/app/app/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""
URL configuration for app project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.2/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 path

urlpatterns = [
path("admin/", admin.site.urls),
]
16 changes: 16 additions & 0 deletions src/test/integration/django/app/app/wsgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
WSGI config for app 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/4.2/howto/deployment/wsgi/
"""

import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings")

application = get_wsgi_application()
Loading

0 comments on commit 4d4bda6

Please sign in to comment.