From 1bb0016a6315c61413a4b97db0579e675e5b9067 Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Tue, 29 Sep 2020 19:09:43 +0530 Subject: [PATCH] Added ability to extract span attributes from Tonado request objects. 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. --- .../instrumentation/django/middleware.py | 8 ++++---- .../README.rst | 12 ++++++++++++ .../instrumentation/tornado/__init__.py | 5 ++++- .../tests/test_instrumentation.py | 15 +++++++++++++++ .../src/opentelemetry/instrumentation/utils.py | 15 +++++++++++++++ 5 files changed, 50 insertions(+), 5 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py index f468053168d..72e9f810b3f 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py @@ -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, @@ -102,10 +103,9 @@ def process_request(self, request): tracer = get_tracer(__name__, __version__) 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 + ) span = tracer.start_span( self._get_span_name(request), diff --git a/instrumentation/opentelemetry-instrumentation-tornado/README.rst b/instrumentation/opentelemetry-instrumentation-tornado/README.rst index d84fbd0412c..65e8b982421 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/README.rst +++ b/instrumentation/opentelemetry-instrumentation-tornado/README.rst @@ -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_TONADO_TRACED_REQUEST_ATTRS='uri,query' + +will extract path_info and content_type attributes from every traced request and add them as span attritbues. References ---------- diff --git a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py index a66f355359a..5357be6d0fc 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py @@ -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, ) @@ -79,6 +80,7 @@ def get_traced_request_attrs(): attrs = [] return attrs + _excluded_urls = get_excluded_urls() _traced_attrs = get_traced_request_attrs() @@ -205,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): @@ -220,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, diff --git a/instrumentation/opentelemetry-instrumentation-tornado/tests/test_instrumentation.py b/instrumentation/opentelemetry-instrumentation-tornado/tests/test_instrumentation.py index 2d5ffd00f78..9c08aa01e28 100644 --- a/instrumentation/opentelemetry-instrumentation-tornado/tests/test_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-tornado/tests/test_instrumentation.py @@ -339,6 +339,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 = spans[0] + self.assertEqual(server.kind, SpanKind.SERVER) + self.assert_span_has_attributes( + server, {"uri": "/ping?q=abc&b=123", "query": "q=abc&b=123",} + ) + self.memory_exporter.clear() + class TestTornadoUninstrument(TornadoTest): def test_uninstrument(self): diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py index 8553b1bc63a..6220854ad59 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py @@ -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: