Skip to content

Commit

Permalink
Tornado: Capture custom request/response headers as span attributes (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
ashu658 authored Mar 11, 2022
1 parent 5539d1f commit d86f164
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 3 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `opentelemetry-instrumentation-flask` Flask: Capture custom request/response headers in span attributes
([#952])(https://github.com/open-telemetry/opentelemetry-python-contrib/pull/952)

- `opentelemetry-instrumentation-tornado` Tornado: Capture custom request/response headers in span attributes
([#950])(https://github.com/open-telemetry/opentelemetry-python-contrib/pull/950)

### Added

- `opentelemetry-instrumentation-aws-lambda` `SpanKind.SERVER` by default, add more cases for `SpanKind.CONSUMER` services. ([#926](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/926))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,15 @@ def client_resposne_hook(span, future):
from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.trace.status import Status, StatusCode
from opentelemetry.util._time import _time_ns
from opentelemetry.util.http import get_excluded_urls, get_traced_request_attrs
from opentelemetry.util.http import (
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST,
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE,
get_custom_headers,
get_excluded_urls,
get_traced_request_attrs,
normalise_request_header_name,
normalise_response_header_name,
)

from .client import fetch_async # pylint: disable=E0401

Expand All @@ -141,7 +149,6 @@ def client_resposne_hook(span, future):

_excluded_urls = get_excluded_urls("TORNADO")
_traced_request_attrs = get_traced_request_attrs("TORNADO")

response_propagation_setter = FuncSetter(tornado.web.RequestHandler.add_header)


Expand Down Expand Up @@ -257,6 +264,32 @@ def _log_exception(tracer, func, handler, args, kwargs):
return func(*args, **kwargs)


def _add_custom_request_headers(span, request_headers):
custom_request_headers_name = get_custom_headers(
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST
)
attributes = {}
for header_name in custom_request_headers_name:
header_values = request_headers.get(header_name)
if header_values:
key = normalise_request_header_name(header_name.lower())
attributes[key] = [header_values]
span.set_attributes(attributes)


def _add_custom_response_headers(span, response_headers):
custom_response_headers_name = get_custom_headers(
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE
)
attributes = {}
for header_name in custom_response_headers_name:
header_values = response_headers.get(header_name)
if header_values:
key = normalise_response_header_name(header_name.lower())
attributes[key] = [header_values]
span.set_attributes(attributes)


def _get_attributes_from_request(request):
attrs = {
SpanAttributes.HTTP_METHOD: request.method,
Expand Down Expand Up @@ -307,6 +340,8 @@ def _start_span(tracer, handler, start_time) -> _TraceContext:
for key, value in attributes.items():
span.set_attribute(key, value)
span.set_attribute("tornado.handler", _get_full_handler_name(handler))
if span.kind == trace.SpanKind.SERVER:
_add_custom_request_headers(span, handler.request.headers)

activation = trace.use_span(span, end_on_exit=True)
activation.__enter__() # pylint: disable=E1101
Expand Down Expand Up @@ -360,6 +395,8 @@ def _finish_span(tracer, handler, error=None):
description=otel_status_description,
)
)
if ctx.span.kind == trace.SpanKind.SERVER:
_add_custom_response_headers(ctx.span, handler._headers)

ctx.activation.__exit__(*finish_args) # pylint: disable=E1101
if ctx.token:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@
from opentelemetry.test.test_base import TestBase
from opentelemetry.test.wsgitestutil import WsgiTestBase
from opentelemetry.trace import SpanKind
from opentelemetry.util.http import get_excluded_urls, get_traced_request_attrs
from opentelemetry.util.http import (
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST,
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE,
get_excluded_urls,
get_traced_request_attrs,
)

from .tornado_test_app import (
AsyncHandler,
Expand Down Expand Up @@ -604,3 +609,122 @@ def test_mark_span_internal_in_presence_of_another_span(self):
self.assertEqual(
test_span.context.span_id, tornado_handler_span.parent.span_id
)


class TestTornadoCustomRequestResponseHeadersAddedWithServerSpan(TornadoTest):
@patch.dict(
"os.environ",
{
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3"
},
)
def test_custom_request_headers_added_in_server_span(self):
headers = {
"Custom-Test-Header-1": "Test Value 1",
"Custom-Test-Header-2": "TestValue2,TestValue3",
}
response = self.fetch("/", headers=headers)
self.assertEqual(response.code, 201)
_, tornado_span, _ = self.sorted_spans(
self.memory_exporter.get_finished_spans()
)
expected = {
"http.request.header.custom_test_header_1": ("Test Value 1",),
"http.request.header.custom_test_header_2": (
"TestValue2,TestValue3",
),
}
self.assertEqual(tornado_span.kind, trace.SpanKind.SERVER)
self.assertSpanHasAttributes(tornado_span, expected)

@patch.dict(
"os.environ",
{
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "content-type,content-length,my-custom-header,invalid-header"
},
)
def test_custom_response_headers_added_in_server_span(self):
response = self.fetch("/test_custom_response_headers")
self.assertEqual(response.code, 200)
tornado_span, _ = self.sorted_spans(
self.memory_exporter.get_finished_spans()
)
expected = {
"http.response.header.content_type": (
"text/plain; charset=utf-8",
),
"http.response.header.content_length": ("0",),
"http.response.header.my_custom_header": (
"my-custom-value-1,my-custom-header-2",
),
}
self.assertEqual(tornado_span.kind, trace.SpanKind.SERVER)
self.assertSpanHasAttributes(tornado_span, expected)


class TestTornadoCustomRequestResponseHeadersNotAddedWithInternalSpan(
TornadoTest
):
def get_app(self):
tracer = trace.get_tracer(__name__)
app = make_app(tracer)

def middleware(request):
"""Wraps the request with a server span"""
with tracer.start_as_current_span(
"test", kind=trace.SpanKind.SERVER
):
app(request)

return middleware

@patch.dict(
"os.environ",
{
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3"
},
)
def test_custom_request_headers_not_added_in_internal_span(self):
headers = {
"Custom-Test-Header-1": "Test Value 1",
"Custom-Test-Header-2": "TestValue2,TestValue3",
}
response = self.fetch("/", headers=headers)
self.assertEqual(response.code, 201)
_, tornado_span, _, _ = self.sorted_spans(
self.memory_exporter.get_finished_spans()
)
not_expected = {
"http.request.header.custom_test_header_1": ("Test Value 1",),
"http.request.header.custom_test_header_2": (
"TestValue2,TestValue3",
),
}
self.assertEqual(tornado_span.kind, trace.SpanKind.INTERNAL)
for key, _ in not_expected.items():
self.assertNotIn(key, tornado_span.attributes)

@patch.dict(
"os.environ",
{
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "content-type,content-length,my-custom-header,invalid-header"
},
)
def test_custom_response_headers_not_added_in_internal_span(self):
response = self.fetch("/test_custom_response_headers")
self.assertEqual(response.code, 200)
tornado_span, _, _ = self.sorted_spans(
self.memory_exporter.get_finished_spans()
)
not_expected = {
"http.response.header.content_type": (
"text/plain; charset=utf-8",
),
"http.response.header.content_length": ("0",),
"http.response.header.my_custom_header": (
"my-custom-value-1,my-custom-header-2",
),
}
self.assertEqual(tornado_span.kind, trace.SpanKind.INTERNAL)
for key, _ in not_expected.items():
self.assertNotIn(key, tornado_span.attributes)
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ def get(self):
self.set_status(200)


class CustomResponseHeaderHandler(tornado.web.RequestHandler):
def get(self):
self.set_header("content-type", "text/plain; charset=utf-8")
self.set_header("content-length", "0")
self.set_header(
"my-custom-header", "my-custom-value-1,my-custom-header-2"
)
self.set_status(200)


def make_app(tracer):
app = tornado.web.Application(
[
Expand All @@ -105,6 +115,7 @@ def make_app(tracer):
(r"/on_finish", FinishedHandler),
(r"/healthz", HealthCheckHandler),
(r"/ping", HealthCheckHandler),
(r"/test_custom_response_headers", CustomResponseHeaderHandler),
]
)
app.tracer = tracer
Expand Down

0 comments on commit d86f164

Please sign in to comment.