Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added ability to extract span attributes from tornado request objects #1178

Merged
merged 8 commits into from
Oct 8, 2020
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
Comment on lines +75 to +81
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it worth moving this code into the utils package as well since it seems likely to be repeated?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree. I think it might be better to have convenience methods on the configuration class itself for common config items. Something like,

cfg = configuration.Configuration()
cfg.traced_methods("django")
cfg.excluded_urls("django")

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created an issue for it. #1217



_excluded_urls = get_excluded_urls()
_traced_attrs = get_traced_request_attrs()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about this to not have the function above?

Suggested change
_traced_attrs = get_traced_request_attrs()
_traced_attrs = [attr.strip() for attr in configuration.Configuration().TORNADO_TRACED_REQUEST_ATTRS.split(",")] if configuration.Configuration().TORNADO_TRACED_REQUEST_ATTRS else []

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very similar functionality above uses a function so I think it's better to be consistent with existing code.



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