Skip to content

Commit

Permalink
Added ability to extract span attributes from tornado request objects (
Browse files Browse the repository at this point in the history
open-telemetry#1178)

OTEL_PYTHON_TONADO_TRACED_REQUEST_ATTRS env var can be set to a command
separated list of attributes names that will be extracted from Tornado's
request object and set as attributes on spans.

Co-authored-by: (Eliseo) Nathaniel Ruiz Nowell <enruizno@uwaterloo.ca>
  • Loading branch information
owais and NathanielRN committed Oct 8, 2020
1 parent 68b7cea commit 4a66652
Show file tree
Hide file tree
Showing 7 changed files with 70 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from opentelemetry.configuration import Configuration
from opentelemetry.context import attach, detach
from opentelemetry.instrumentation.django.version import __version__
from opentelemetry.instrumentation.utils import extract_attributes_from_object
from opentelemetry.instrumentation.wsgi import (
add_response_attributes,
collect_request_attributes,
Expand Down Expand Up @@ -111,10 +112,9 @@ def process_request(self, request):

if span.is_recording():
attributes = collect_request_attributes(environ)
for attr in self._traced_request_attrs:
value = getattr(request, attr, None)
if value is not None:
attributes[attr] = str(value)
attributes = extract_attributes_from_object(
request, self._traced_request_attrs, attributes
)
for key, value in attributes.items():
span.set_attribute(key, value)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ def on_get(self, req, resp):
from opentelemetry.configuration import Configuration
from opentelemetry.instrumentation.falcon.version import __version__
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
from opentelemetry.instrumentation.utils import http_status_to_canonical_code
from opentelemetry.instrumentation.utils import (
extract_attributes_from_object,
http_status_to_canonical_code,
)
from opentelemetry.trace.status import Status
from opentelemetry.util import ExcludeList, time_ns

Expand Down Expand Up @@ -162,10 +165,11 @@ def process_request(self, req, resp):
if not span:
return

for attr in self._traced_request_attrs:
value = getattr(req, attr, None)
if value is not None:
span.set_attribute(attr, str(value))
attributes = extract_attributes_from_object(
req, self._traced_request_attrs
)
for key, value in attributes.items():
span.set_attribute(key, value)

def process_resource(self, req, resp, resource, params):
span = req.env.get(_ENVIRON_SPAN_KEY)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- Added support for `OTEL_PYTHON_TORNADO_TRACED_REQUEST_ATTRS` ([#1178](https://github.com/open-telemetry/opentelemetry-python/pull/1178))

## Version 0.13b0

Released 2020-09-17
Expand Down
12 changes: 12 additions & 0 deletions instrumentation/opentelemetry-instrumentation-tornado/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ A comma separated list of paths that should not be automatically traced. For exa

Then any requests made to ``/healthz`` and ``/ping`` will not be automatically traced.

Request attributes
********************
To extract certain attributes from Tornado's request object and use them as span attributes, set the environment variable ``OTEL_PYTHON_TORNADO_TRACED_REQUEST_ATTRS`` to a comma
delimited list of request attribute names.

For example,

::

export OTEL_PYTHON_TORNADO_TRACED_REQUEST_ATTRS='uri,query'

will extract path_info and content_type attributes from every traced request and add them as span attributes.

References
----------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def get(self):
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
from opentelemetry.instrumentation.tornado.version import __version__
from opentelemetry.instrumentation.utils import (
extract_attributes_from_object,
http_status_to_canonical_code,
unwrap,
)
Expand All @@ -71,7 +72,17 @@ def get_excluded_urls():
return ExcludeList(urls)


def get_traced_request_attrs():
attrs = configuration.Configuration().TORNADO_TRACED_REQUEST_ATTRS or ""
if attrs:
attrs = [attr.strip() for attr in attrs.split(",")]
else:
attrs = []
return attrs


_excluded_urls = get_excluded_urls()
_traced_attrs = get_traced_request_attrs()


class TornadoInstrumentor(BaseInstrumentor):
Expand Down Expand Up @@ -196,7 +207,7 @@ def _get_attributes_from_request(request):
if request.remote_ip:
attrs["net.peer.ip"] = request.remote_ip

return attrs
return extract_attributes_from_object(request, _traced_attrs, attrs)


def _get_operation_name(handler, request):
Expand All @@ -211,6 +222,7 @@ def _start_span(tracer, handler, start_time) -> _TraceContext:
_get_header_from_request_headers, handler.request.headers,
)
)

span = tracer.start_span(
_get_operation_name(handler, handler.request),
kind=trace.SpanKind.SERVER,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,21 @@ def test_excluded(path):
test_excluded("/healthz")
test_excluded("/ping")

@patch(
"opentelemetry.instrumentation.tornado._traced_attrs",
["uri", "full_url", "query"],
)
def test_traced_attrs(self):
self.fetch("/ping?q=abc&b=123")
spans = self.sorted_spans(self.memory_exporter.get_finished_spans())
self.assertEqual(len(spans), 2)
server_span = spans[0]
self.assertEqual(server_span.kind, SpanKind.SERVER)
self.assert_span_has_attributes(
server_span, {"uri": "/ping?q=abc&b=123", "query": "q=abc&b=123"}
)
self.memory_exporter.clear()


class TestTornadoUninstrument(TornadoTest):
def test_uninstrument(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,26 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import Dict, Sequence

from wrapt import ObjectProxy

from opentelemetry.trace.status import StatusCanonicalCode


def extract_attributes_from_object(
obj: any, attributes: Sequence[str], existing: Dict[str, str] = None
) -> Dict[str, str]:
extracted = {}
if existing:
extracted.update(existing)
for attr in attributes:
value = getattr(obj, attr, None)
if value is not None:
extracted[attr] = str(value)
return extracted


def http_status_to_canonical_code(
status: int, allow_redirect: bool = True
) -> StatusCanonicalCode:
Expand Down

0 comments on commit 4a66652

Please sign in to comment.