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
8 changes: 4 additions & 4 deletions src/instana/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,10 +197,10 @@ def boot_agent():
# pubsub, # noqa: F401
# storage, # noqa: F401
# )
# from instana.instrumentation.tornado import (
# client, # noqa: F401
# server, # noqa: F401
# )
from instana.instrumentation.tornado import (
client, # noqa: F401
server, # noqa: F401
)

# Hooks
# from instana.hooks import hook_uwsgi # noqa: F401
Expand Down
59 changes: 27 additions & 32 deletions src/instana/instrumentation/tornado/client.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,27 @@
# (c) Copyright IBM Corp. 2021
# (c) Copyright Instana Inc. 2019


import opentracing
import wrapt
import functools

from ...log import logger
from ...singletons import agent, setup_tornado_tracer, tornado_tracer
from ...util.secrets import strip_secrets_from_query

try:
import tornado

# Tornado >=6.0 switched to contextvars for context management. This requires changes to the opentracing
# scope managers which we will tackle soon.
# Limit Tornado version for the time being.
if not (hasattr(tornado, 'version') and tornado.version[0] < '6'):
logger.debug('Instana supports Tornado package versions < 6.0. Skipping.')
raise ImportError
import wrapt
import functools

setup_tornado_tracer()
from opentelemetry.semconv.trace import SpanAttributes

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

@wrapt.patch_function_wrapper('tornado.httpclient', 'AsyncHTTPClient.fetch')
def fetch_with_instana(wrapped, instance, argv, kwargs):
try:
parent_span = tornado_tracer.active_span
parent_span = get_current_span()

# If we're not tracing, just return
if (parent_span is None) or (parent_span.operation_name == "tornado-client"):
if (not parent_span.is_recording()) or (parent_span.name == "tornado-client"):
return wrapped(*argv, **kwargs)

request = argv[0]
Expand All @@ -45,41 +38,43 @@ def fetch_with_instana(wrapped, instance, argv, kwargs):
new_kwargs[param] = kwargs.pop(param)
kwargs = new_kwargs

scope = tornado_tracer.start_active_span('tornado-client', child_of=parent_span)
tornado_tracer.inject(scope.span.context, opentracing.Format.HTTP_HEADERS, request.headers)
parent_context = parent_span.get_span_context() if parent_span else None

span = tracer.start_span("tornado-client", span_context=parent_context)
tracer.inject(span.context, Format.HTTP_HEADERS, request.headers)

# Query param scrubbing
parts = request.url.split('?')
if len(parts) > 1:
cleaned_qp = strip_secrets_from_query(parts[1], agent.options.secrets_matcher,
agent.options.secrets_list)
scope.span.set_tag("http.params", cleaned_qp)
span.set_attribute("http.params", cleaned_qp)

scope.span.set_tag("http.url", parts[0])
scope.span.set_tag("http.method", request.method)
span.set_attribute(SpanAttributes.HTTP_URL, parts[0])
span.set_attribute(SpanAttributes.HTTP_METHOD, request.method)

future = wrapped(request, **kwargs)

if future is not None:
cb = functools.partial(finish_tracing, scope=scope)
cb = functools.partial(finish_tracing, span=span)
future.add_done_callback(cb)

return future
except Exception:
logger.debug("tornado fetch", exc_info=True)
raise
logger.debug("Tornado fetch_with_instana: ", exc_info=True)


def finish_tracing(future, scope):
def finish_tracing(future, span):
try:
response = future.result()
scope.span.set_tag("http.status_code", response.code)
span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, response.code)
except tornado.httpclient.HTTPClientError as e:
scope.span.set_tag("http.status_code", e.code)
scope.span.log_exception(e)
raise
span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, e.code)
span.record_exception(e)
logger.debug("Tornado finish_tracing HTTPClientError: ", exc_info=True)
finally:
scope.close()
if span.is_recording():
span.end()


logger.debug("Instrumenting tornado client")
Expand Down
97 changes: 45 additions & 52 deletions src/instana/instrumentation/tornado/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,25 @@
# (c) Copyright Instana Inc. 2019


import opentracing
import wrapt

from ...log import logger
from ...singletons import agent, setup_tornado_tracer, tornado_tracer
from ...util.secrets import strip_secrets_from_query

try:
import tornado

# Tornado >=6.0 switched to contextvars for context management. This requires changes to the opentracing
# scope managers which we will tackle soon.
# Limit Tornado version for the time being.
if not (hasattr(tornado, 'version') and tornado.version[0] < '6'):
logger.debug('Instana supports Tornado package versions < 6.0. Skipping.')
raise ImportError
import wrapt

from opentracing.scope_managers.tornado import tracer_stack_context
from opentelemetry.semconv.trace import SpanAttributes

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

def extract_custom_headers(span, headers):
if not agent.options.extra_http_headers or not headers:
return
try:
for custom_header in agent.options.extra_http_headers:
if custom_header in headers:
span.set_tag("http.header.%s" % custom_header, headers[custom_header])
span.set_attribute("http.header.%s" % custom_header, headers[custom_header])

except Exception:
logger.debug("extract_custom_headers: ", exc_info=True)
Expand All @@ -38,36 +29,36 @@ def extract_custom_headers(span, headers):
@wrapt.patch_function_wrapper('tornado.web', 'RequestHandler._execute')
def execute_with_instana(wrapped, instance, argv, kwargs):
try:
with tracer_stack_context():
ctx = None
if hasattr(instance.request.headers, '__dict__') and '_dict' in instance.request.headers.__dict__:
ctx = tornado_tracer.extract(opentracing.Format.HTTP_HEADERS,
instance.request.headers.__dict__['_dict'])
scope = tornado_tracer.start_active_span('tornado-server', child_of=ctx)
span_context = None
if hasattr(instance.request.headers, '__dict__') and '_dict' in instance.request.headers.__dict__:
span_context = tracer.extract(Format.HTTP_HEADERS,
instance.request.headers.__dict__['_dict'])

# Query param scrubbing
if instance.request.query is not None and len(instance.request.query) > 0:
cleaned_qp = strip_secrets_from_query(instance.request.query, agent.options.secrets_matcher,
agent.options.secrets_list)
scope.span.set_tag("http.params", cleaned_qp)
span = tracer.start_span("tornado-server", span_context=span_context)

url = "%s://%s%s" % (instance.request.protocol, instance.request.host, instance.request.path)
scope.span.set_tag("http.url", url)
scope.span.set_tag("http.method", instance.request.method)
# Query param scrubbing
if instance.request.query is not None and len(instance.request.query) > 0:
cleaned_qp = strip_secrets_from_query(instance.request.query, agent.options.secrets_matcher,
agent.options.secrets_list)
span.set_attribute("http.params", cleaned_qp)

url = f"{instance.request.protocol}://{instance.request.host}{instance.request.path}"
span.set_attribute(SpanAttributes.HTTP_URL, url)
span.set_attribute(SpanAttributes.HTTP_METHOD, instance.request.method)

scope.span.set_tag("handler", instance.__class__.__name__)
span.set_attribute("handler", instance.__class__.__name__)

# Request header tracking support
extract_custom_headers(scope.span, instance.request.headers)
# Request header tracking support
extract_custom_headers(span, instance.request.headers)

setattr(instance.request, "_instana", scope)
setattr(instance.request, "_instana", span)

# Set the context response headers now because tornado doesn't give us a better option to do so
# later for this request.
tornado_tracer.inject(scope.span.context, opentracing.Format.HTTP_HEADERS, instance._headers)
instance.set_header(name='Server-Timing', value="intid;desc=%s" % scope.span.context.trace_id)
# Set the context response headers now because tornado doesn't give us a better option to do so
# later for this request.
tracer.inject(span.context, Format.HTTP_HEADERS, instance._headers)
instance.set_header(name='Server-Timing', value=f"intid;desc={span.context.trace_id}")

return wrapped(*argv, **kwargs)
return wrapped(*argv, **kwargs)
except Exception:
logger.debug("tornado execute", exc_info=True)

Expand All @@ -77,9 +68,9 @@ def set_default_headers_with_instana(wrapped, instance, argv, kwargs):
if not hasattr(instance.request, '_instana'):
return wrapped(*argv, **kwargs)

scope = instance.request._instana
tornado_tracer.inject(scope.span.context, opentracing.Format.HTTP_HEADERS, instance._headers)
instance.set_header(name='Server-Timing', value="intid;desc=%s" % scope.span.context.trace_id)
span = instance.request._instana
tracer.inject(span.context, Format.HTTP_HEADERS, instance._headers)
instance.set_header(name='Server-Timing', value=f"intid;desc={span.context.trace_id}")


@wrapt.patch_function_wrapper('tornado.web', 'RequestHandler.on_finish')
Expand All @@ -88,17 +79,19 @@ def on_finish_with_instana(wrapped, instance, argv, kwargs):
if not hasattr(instance.request, '_instana'):
return wrapped(*argv, **kwargs)

with instance.request._instana as scope:
# Response header tracking support
extract_custom_headers(scope.span, instance._headers)
span = instance.request._instana
# Response header tracking support
extract_custom_headers(span, instance._headers)

status_code = instance.get_status()
status_code = instance.get_status()

# Mark 500 responses as errored
if 500 <= status_code:
scope.span.mark_as_errored()
# Mark 500 responses as errored
if 500 <= status_code:
span.mark_as_errored()

scope.span.set_tag("http.status_code", status_code)
span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code)
if span.is_recording():
span.end()

return wrapped(*argv, **kwargs)
except Exception:
Expand All @@ -112,8 +105,8 @@ def log_exception_with_instana(wrapped, instance, argv, kwargs):
return wrapped(*argv, **kwargs)

if not isinstance(argv[1], tornado.web.HTTPError):
scope = instance.request._instana
scope.span.log_exception(argv[0])
span = instance.request._instana
span.record_exception(argv[0])

return wrapped(*argv, **kwargs)
except Exception:
Expand Down
2 changes: 1 addition & 1 deletion tests/apps/tornado_server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import asyncio

from ...helpers import testenv
from tests.helpers import testenv


class Application(tornado.web.Application):
Expand Down
10 changes: 1 addition & 9 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
collect_ignore_glob.append("*frameworks/test_celery*")
collect_ignore_glob.append("*frameworks/test_gevent*")
collect_ignore_glob.append("*frameworks/test_grpcio*")
collect_ignore_glob.append("*frameworks/test_tornado*")

# # Cassandra and gevent tests are run in dedicated jobs on CircleCI and will
# # be run explicitly. (So always exclude them here)
Expand All @@ -48,14 +47,6 @@
# collect_ignore_glob.append("*test_gevent*")
# collect_ignore_glob.append("*test_starlette*")

# Python 3.10 support is incomplete yet
# TODO: Remove this once we start supporting Tornado >= 6.0
if sys.version_info >= (3, 10):
collect_ignore_glob.append("*test_tornado*")
# Furthermore on Python 3.11 the above TC is skipped:
# tests/opentracing/test_ot_span.py::TestOTSpan::test_stacks
# TODO: Remove that once we find a workaround or DROP opentracing!

if sys.version_info >= (3, 11):
if not os.environ.get("GOOGLE_CLOUD_TEST"):
collect_ignore_glob.append("*test_google-cloud*")
Expand All @@ -64,6 +55,7 @@
# TODO: Test Case failures for unknown reason:
collect_ignore_glob.append("*test_aiohttp_server*")
collect_ignore_glob.append("*test_celery*")
collect_ignore_glob.append("*frameworks/test_tornado_server*")

# Currently there is a runtime incompatibility caused by the library:
# `undefined symbol: _PyErr_WriteUnraisableMsg`
Expand Down
Loading