Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/instana/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ def boot_agent():
psycopg2, # noqa: F401
pymongo, # noqa: F401
pymysql, # noqa: F401
pyramid, # noqa: F401
redis, # noqa: F401
# sqlalchemy, # noqa: F401
starlette_inst, # noqa: F401
Expand Down
144 changes: 144 additions & 0 deletions src/instana/instrumentation/pyramid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# (c) Copyright IBM Corp. 2021
# (c) Copyright Instana Inc. 2020

try:
from pyramid.httpexceptions import HTTPException
from pyramid.path import caller_package
from pyramid.settings import aslist
from pyramid.tweens import EXCVIEW
from pyramid.config import Configurator
from typing import TYPE_CHECKING, Dict, Any, Callable, Tuple
import wrapt

from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.trace import SpanKind

from instana.log import logger
from instana.singletons import tracer, agent
from instana.util.secrets import strip_secrets_from_query
from instana.propagators.format import Format

if TYPE_CHECKING:
from pyramid.request import Request
from pyramid.response import Response
from instana.span.span import InstanaSpan
from pyramid.registry import Registry

class InstanaTweenFactory(object):
"""A factory that provides Instana instrumentation tween for Pyramid apps"""

def __init__(
self, handler: Callable[["Request"], "Response"], registry: "Registry"
) -> None:
self.handler = handler

def _extract_custom_headers(
self, span: "InstanaSpan", headers: Dict[str, Any]
) -> None:
if not agent.options.extra_http_headers:
return
try:
for custom_header in agent.options.extra_http_headers:
if custom_header in headers:
span.set_attribute(
f"http.header.{custom_header}", headers[custom_header]
)

except Exception:
logger.debug("extract_custom_headers: ", exc_info=True)

def __call__(self, request: "Request") -> "Response":
ctx = tracer.extract(Format.HTTP_HEADERS, dict(request.headers))

with tracer.start_as_current_span("wsgi", span_context=ctx) as span:
span.set_attribute("span.kind", SpanKind.SERVER)
span.set_attribute("http.host", request.host)
span.set_attribute(SpanAttributes.HTTP_METHOD, request.method)
span.set_attribute(SpanAttributes.HTTP_URL, request.path)

self._extract_custom_headers(span, request.headers)

if len(request.query_string):
scrubbed_params = strip_secrets_from_query(
request.query_string,
agent.options.secrets_matcher,
agent.options.secrets_list,
)
span.set_attribute("http.params", scrubbed_params)

response = None
try:
response = self.handler(request)
if request.matched_route is not None:
span.set_attribute(
"http.path_tpl", request.matched_route.pattern
)

self._extract_custom_headers(span, response.headers)

tracer.inject(span.context, Format.HTTP_HEADERS, response.headers)
response.headers["Server-Timing"] = (
f"intid;desc={span.context.trace_id}"
)
except HTTPException as e:
response = e
logger.debug(
"Pyramid InstanaTweenFactory HTTPException: ", exc_info=True
)
except BaseException as e:
span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, 500)
span.record_exception(e)

logger.debug(
"Pyramid InstanaTweenFactory BaseException: ", exc_info=True
)
finally:
if response:
span.set_attribute(
SpanAttributes.HTTP_STATUS_CODE, response.status_int
)

if 500 <= response.status_int:
if response.exception:
span.record_exception(response.exception)
span.assure_errored()

return response

INSTANA_TWEEN = __name__ + ".InstanaTweenFactory"

# implicit tween ordering
def includeme(config: Configurator) -> None:
logger.debug("Instrumenting pyramid")
config.add_tween(INSTANA_TWEEN)

# explicit tween ordering
@wrapt.patch_function_wrapper("pyramid.config", "Configurator.__init__")
def init_with_instana(
wrapped: Callable[..., Configurator.__init__],
instance: Configurator,
args: Tuple[object, ...],
kwargs: Dict[str, Any],
):
settings = kwargs.get("settings", {})
tweens = aslist(settings.get("pyramid.tweens", []))

if tweens and INSTANA_TWEEN not in settings:
# pyramid.tweens.EXCVIEW is the name of built-in exception view provided by
# pyramid. We need our tween to be before it, otherwise unhandled
# exceptions will be caught before they reach our tween.
if EXCVIEW in tweens:
tweens = [INSTANA_TWEEN] + tweens
else:
tweens = [INSTANA_TWEEN] + tweens + [EXCVIEW]
settings["pyramid.tweens"] = "\n".join(tweens)
kwargs["settings"] = settings

if not kwargs.get("package", None):
kwargs["package"] = caller_package()

wrapped(*args, **kwargs)
instance.include(__name__)

except ImportError:
pass
Empty file.
92 changes: 0 additions & 92 deletions src/instana/instrumentation/pyramid/tweens.py

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
# (c) Copyright Instana Inc. 2020

import os
from .app import pyramid_server as server
from ..utils import launch_background_thread
from tests.apps.pyramid.pyramid_app.app import pyramid_server as server
from tests.apps.utils import launch_background_thread

app_thread = None

if not os.environ.get('CASSANDRA_TEST'):
if not os.environ.get("CASSANDRA_TEST"):
app_thread = launch_background_thread(server.serve_forever, "Pyramid")
59 changes: 59 additions & 0 deletions tests/apps/pyramid/pyramid_app/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# (c) Copyright IBM Corp. 2021
# (c) Copyright Instana Inc. 2020

from wsgiref.simple_server import make_server
from pyramid.config import Configurator
import logging

from pyramid.response import Response
import pyramid.httpexceptions as exc

from tests.helpers import testenv

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

testenv["pyramid_port"] = 10815
testenv["pyramid_server"] = "http://127.0.0.1:" + str(testenv["pyramid_port"])


def hello_world(request):
return Response("Ok")


def please_fail(request):
raise exc.HTTPInternalServerError("internal error")


def tableflip(request):
raise BaseException("fake exception")


def response_headers(request):
headers = {"X-Capture-This": "Ok", "X-Capture-That": "Ok too"}
return Response("Stan wuz here with headers!", headers=headers)


def hello_user(request):
user = request.matchdict["user"]
return Response(f"Hello {user}!")


app = None
settings = {
"pyramid.tweens": "tests.apps.pyramid.pyramid_utils.tweens.timing_tween_factory",
}
with Configurator(settings=settings) as config:
config.add_route("hello", "/")
config.add_view(hello_world, route_name="hello")
config.add_route("fail", "/500")
config.add_view(please_fail, route_name="fail")
config.add_route("crash", "/exception")
config.add_view(tableflip, route_name="crash")
config.add_route("response_headers", "/response_headers")
config.add_view(response_headers, route_name="response_headers")
config.add_route("hello_user", "/hello_user/{user}")
config.add_view(hello_user, route_name="hello_user")
app = config.make_wsgi_app()

pyramid_server = make_server("127.0.0.1", testenv["pyramid_port"], app)
16 changes: 16 additions & 0 deletions tests/apps/pyramid/pyramid_utils/tweens.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# (c) Copyright IBM Corp. 2024

import time


def timing_tween_factory(handler, registry):
def timing_tween(request):
start = time.time()
try:
response = handler(request)
finally:
end = time.time()
print(f"The request took {end - start} seconds")
return response

return timing_tween
49 changes: 0 additions & 49 deletions tests/apps/pyramid_app/app.py

This file was deleted.

Loading