# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
"""
This library uses OpenTelemetry to track web requests in Tornado applications.
Usage
-----
.. code-block:: python
import tornado.web
from opentelemetry.instrumentation.tornado import TornadoInstrumentor
# apply tornado instrumentation
TornadoInstrumentor().instrument()
class Handler(tornado.web.RequestHandler):
def get(self):
self.set_status(200)
app = tornado.web.Application([(r"/", Handler)])
app.listen(8080)
tornado.ioloop.IOLoop.current().start()
Configuration
-------------
The following environment variables are supported as configuration options:
- ``OTEL_PYTHON_TORNADO_EXCLUDED_URLS`` (or ``OTEL_PYTHON_EXCLUDED_URLS`` to cover all instrumentations)
A comma separated list of paths that should not be automatically traced. For example, if this is set to
::
export OTEL_PYTHON_TORNADO_EXCLUDED_URLS='/healthz,/ping'
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.
Request/Response hooks
**********************
Tornado instrumentation supports extending tracing behaviour with the help of hooks.
Its ``instrument()`` method accepts three optional functions that get called back with the
created span and some other contextual information. Example:
.. code-block:: python
from opentelemetry.instrumentation.tornado import TornadoInstrumentor
# will be called for each incoming request to Tornado
# web server. `handler` is an instance of
# `tornado.web.RequestHandler`.
def server_request_hook(span, handler):
pass
# will be called just before sending out a request with
# `tornado.httpclient.AsyncHTTPClient.fetch`.
# `request` is an instance of ``tornado.httpclient.HTTPRequest`.
def client_request_hook(span, request):
pass
# will be called after a outgoing request made with
# `tornado.httpclient.AsyncHTTPClient.fetch` finishes.
# `response`` is an instance of ``Future[tornado.httpclient.HTTPResponse]`.
def client_response_hook(span, future):
pass
# apply tornado instrumentation with hooks
TornadoInstrumentor().instrument(
server_request_hook=server_request_hook,
client_request_hook=client_request_hook,
client_response_hook=client_response_hook,
)
Capture HTTP request and response headers
*****************************************
You can configure the agent to capture predefined HTTP headers as span attributes, according to the `semantic conventions <https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#http-server-span>`_.
Request headers
***************
To capture predefined HTTP request headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST``
to a comma-separated list of HTTP header names.
For example,
::
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content-type,custom_request_header"
will extract ``content-type`` and ``custom_request_header`` from request headers and add them as span attributes.
It is recommended that you should give the correct names of the headers to be captured in the environment variable.
Request header names in tornado are case insensitive. So, giving header name as ``CUStomHeader`` in environment variable will be able capture header with name ``customheader``.
The name of the added span attribute will follow the format ``http.request.header.<header_name>`` where ``<header_name>`` being the normalized HTTP header name (lowercase, with - characters replaced by _ ).
The value of the attribute will be single item list containing all the header values.
Example of the added span attribute,
``http.request.header.custom_request_header = ["<value1>,<value2>"]``
Response headers
****************
To capture predefined HTTP response headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE``
to a comma-separated list of HTTP header names.
For example,
::
export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content-type,custom_response_header"
will extract ``content-type`` and ``custom_response_header`` from response headers and add them as span attributes.
It is recommended that you should give the correct names of the headers to be captured in the environment variable.
Response header names captured in tornado are case insensitive. So, giving header name as ``CUStomHeader`` in environment variable will be able capture header with name ``customheader``.
The name of the added span attribute will follow the format ``http.response.header.<header_name>`` where ``<header_name>`` being the normalized HTTP header name (lowercase, with - characters replaced by _ ).
The value of the attribute will be single item list containing all the header values.
Example of the added span attribute,
``http.response.header.custom_response_header = ["<value1>,<value2>"]``
Note:
Environment variable names to capture http headers are still experimental, and thus are subject to change.
API
---
"""
from collections import namedtuple
from functools import partial
from logging import getLogger
from time import time_ns
from timeit import default_timer
from typing import Collection, Dict
import tornado.web
import tornado.websocket
import wrapt
from wrapt import wrap_function_wrapper
from opentelemetry import context, trace
from opentelemetry.instrumentation._semconv import (
HTTP_DURATION_HISTOGRAM_BUCKETS_NEW,
_get_schema_url,
_OpenTelemetrySemanticConventionStability,
_OpenTelemetryStabilitySignalType,
_report_new,
_report_old,
_set_http_flavor_version,
_set_http_host_server,
_set_http_method,
_set_http_scheme,
_set_http_target,
_set_http_url,
_set_http_user_agent,
_set_status,
_StabilityMode,
)
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
from opentelemetry.instrumentation.propagators import (
FuncSetter,
get_global_response_propagator,
)
from opentelemetry.instrumentation.tornado._utils import (
find_matched_rule,
route_from_rule,
)
from opentelemetry.instrumentation.tornado.package import _instruments
from opentelemetry.instrumentation.tornado.version import __version__
from opentelemetry.instrumentation.utils import (
_start_internal_or_server_span,
extract_attributes_from_object,
unwrap,
)
from opentelemetry.metrics import get_meter
from opentelemetry.metrics._internal.instrument import Histogram
from opentelemetry.propagators import textmap
from opentelemetry.semconv._incubating.attributes.http_attributes import (
HTTP_CLIENT_IP,
HTTP_FLAVOR,
HTTP_HOST,
HTTP_METHOD,
HTTP_ROUTE,
HTTP_SCHEME,
HTTP_STATUS_CODE,
HTTP_TARGET,
)
from opentelemetry.semconv._incubating.attributes.net_attributes import (
NET_PEER_IP,
)
from opentelemetry.semconv.attributes.client_attributes import (
CLIENT_ADDRESS,
)
from opentelemetry.semconv.attributes.http_attributes import (
HTTP_REQUEST_METHOD,
HTTP_RESPONSE_STATUS_CODE,
)
from opentelemetry.semconv.attributes.network_attributes import (
NETWORK_PEER_ADDRESS,
NETWORK_PROTOCOL_VERSION,
)
from opentelemetry.semconv.attributes.url_attributes import (
URL_SCHEME,
)
from opentelemetry.semconv.metrics import MetricInstruments
from opentelemetry.util.http import (
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST,
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE,
_parse_url_query,
get_custom_headers,
get_excluded_urls,
get_traced_request_attrs,
normalise_request_header_name,
normalise_response_header_name,
normalize_user_agent,
redact_url,
sanitize_method,
)
from .client import fetch_async # pylint: disable=E0401
_logger = getLogger(__name__)
_TraceContext = namedtuple("TraceContext", ["activation", "span", "token"])
_HANDLER_STATE_KEY = "_otel_state_key"
_HANDLER_CONTEXT_KEY = "_otel_trace_context_key"
_OTEL_PATCHED_KEY = "_otel_patched_key"
_START_TIME = "start_time"
_excluded_urls = get_excluded_urls("TORNADO")
_traced_request_attrs = get_traced_request_attrs("TORNADO")
response_propagation_setter = FuncSetter(tornado.web.RequestHandler.add_header)
[docs]class TornadoInstrumentor(BaseInstrumentor):
patched_handlers = []
original_handler_new = None
def __init__(self):
super().__init__()
self._sem_conv_opt_in_mode = _StabilityMode.DEFAULT
[docs] def instrumentation_dependencies(self) -> Collection[str]:
return _instruments
def _instrument(self, **kwargs): # pylint: disable=too-many-locals
"""
_instrument patches tornado.web.RequestHandler and tornado.httpclient.AsyncHTTPClient classes
to automatically instrument requests both received and sent by Tornado.
We don't patch RequestHandler._execute as it causes some issues with contextvars based context.
Mainly the context fails to detach from within RequestHandler.on_finish() if it is attached inside
RequestHandler._execute. Same issue plagues RequestHandler.initialize. RequestHandler.prepare works
perfectly on the other hand as it executes in the same context as on_finish and log_exection which
are patched to finish a span after a request is served.
However, we cannot just patch RequestHandler's prepare method because it is supposed to be overridden
by sub-classes and since the parent prepare method does not do anything special, sub-classes don't
have to call super() when overriding the method.
In order to work around this, we patch the __init__ method of RequestHandler and then dynamically patch
the prepare, on_finish and log_exception methods of the derived classes _only_ the first time we see them.
Note that the patch does not apply on every single __init__ call, only the first one for the entire
process lifetime.
"""
# Initialize semantic conventions opt-in mode
_OpenTelemetrySemanticConventionStability._initialize()
sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
_OpenTelemetryStabilitySignalType.HTTP,
)
self._sem_conv_opt_in_mode = sem_conv_opt_in_mode
tracer_provider = kwargs.get("tracer_provider")
tracer = trace.get_tracer(
__name__,
__version__,
tracer_provider,
schema_url=_get_schema_url(sem_conv_opt_in_mode),
)
meter_provider = kwargs.get("meter_provider")
# Create meters for old and new semconv based on opt-in mode
meter_old = None
meter_new = None
if _report_old(sem_conv_opt_in_mode):
meter_old = get_meter(
__name__,
__version__,
meter_provider,
schema_url=_get_schema_url(_StabilityMode.DEFAULT),
)
if _report_new(sem_conv_opt_in_mode):
meter_new = get_meter(
__name__,
__version__,
meter_provider,
schema_url=_get_schema_url(_StabilityMode.HTTP),
)
client_histograms = _create_client_histograms(
meter_old, meter_new, sem_conv_opt_in_mode
)
server_histograms = _create_server_histograms(
meter_old, meter_new, sem_conv_opt_in_mode
)
client_request_hook = kwargs.get("client_request_hook", None)
client_response_hook = kwargs.get("client_response_hook", None)
server_request_hook = kwargs.get("server_request_hook", None)
def handler_init(init, handler, args, kwargs):
cls = handler.__class__
if patch_handler_class(
tracer,
server_histograms,
cls,
server_request_hook,
sem_conv_opt_in_mode,
):
self.patched_handlers.append(cls)
return init(*args, **kwargs)
wrap_function_wrapper(
"tornado.web", "RequestHandler.__init__", handler_init
)
duration_old = client_histograms.get("old_duration")
duration_new = client_histograms.get("new_duration")
request_size_old = client_histograms.get("old_request_size")
request_size_new = client_histograms.get("new_request_size")
response_size_old = client_histograms.get("old_response_size")
response_size_new = client_histograms.get("new_response_size")
wrap_function_wrapper(
"tornado.httpclient",
"AsyncHTTPClient.fetch",
partial(
fetch_async,
tracer,
client_request_hook,
client_response_hook,
duration_old,
duration_new,
request_size_old,
request_size_new,
response_size_old,
response_size_new,
sem_conv_opt_in_mode,
),
)
def _uninstrument(self, **kwargs):
self._sem_conv_opt_in_mode = _StabilityMode.DEFAULT
unwrap(tornado.web.RequestHandler, "__init__")
unwrap(tornado.httpclient.AsyncHTTPClient, "fetch")
for handler in self.patched_handlers:
unpatch_handler_class(handler)
self.patched_handlers = []
def _create_server_histograms(
meter_old, meter_new, sem_conv_opt_in_mode
) -> Dict[str, Histogram]:
histograms = {}
# Create old semconv metrics
if _report_old(sem_conv_opt_in_mode):
histograms["old_duration"] = meter_old.create_histogram(
name=MetricInstruments.HTTP_SERVER_DURATION,
unit="ms",
description="measures the duration of inbound HTTP requests",
)
histograms["old_request_size"] = meter_old.create_histogram(
name=MetricInstruments.HTTP_SERVER_REQUEST_SIZE,
unit="By",
description="measures the size of HTTP request messages (compressed)",
)
histograms["old_response_size"] = meter_old.create_histogram(
name=MetricInstruments.HTTP_SERVER_RESPONSE_SIZE,
unit="By",
description="measures the size of HTTP response messages (compressed)",
)
# Create new semconv metrics
if _report_new(sem_conv_opt_in_mode):
histograms["new_duration"] = meter_new.create_histogram(
name="http.server.request.duration",
unit="s",
description="Duration of HTTP server requests.",
explicit_bucket_boundaries_advisory=HTTP_DURATION_HISTOGRAM_BUCKETS_NEW,
)
histograms["new_request_size"] = meter_new.create_histogram(
name="http.server.request.body.size",
unit="By",
description="Size of HTTP server request bodies.",
)
histograms["new_response_size"] = meter_new.create_histogram(
name="http.server.response.body.size",
unit="By",
description="Size of HTTP server response bodies.",
)
# Active request counter for old/new semantic conventions same
# because the attributes are the same for both
# Use meter_old if available, otherwise meter_new
active_meter = meter_old if meter_old is not None else meter_new
if active_meter is not None:
histograms["active_requests"] = active_meter.create_up_down_counter(
name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS,
unit="requests",
description="measures the number of concurrent HTTP requests that are currently in-flight",
)
return histograms
def _create_client_histograms(
meter_old, meter_new, sem_conv_opt_in_mode
) -> Dict[str, Histogram]:
histograms = {}
# Create old semconv metrics
if _report_old(sem_conv_opt_in_mode):
histograms["old_duration"] = meter_old.create_histogram(
name=MetricInstruments.HTTP_CLIENT_DURATION,
unit="ms",
description="measures the duration outbound HTTP requests",
)
histograms["old_request_size"] = meter_old.create_histogram(
name=MetricInstruments.HTTP_CLIENT_REQUEST_SIZE,
unit="By",
description="measures the size of HTTP request messages (compressed)",
)
histograms["old_response_size"] = meter_old.create_histogram(
name=MetricInstruments.HTTP_CLIENT_RESPONSE_SIZE,
unit="By",
description="measures the size of HTTP response messages (compressed)",
)
# Create new semconv metrics
if _report_new(sem_conv_opt_in_mode):
histograms["new_duration"] = meter_new.create_histogram(
name="http.client.request.duration",
unit="s",
description="Duration of HTTP client requests.",
explicit_bucket_boundaries_advisory=HTTP_DURATION_HISTOGRAM_BUCKETS_NEW,
)
histograms["new_request_size"] = meter_new.create_histogram(
name="http.client.request.body.size",
unit="By",
description="Size of HTTP client request bodies.",
)
histograms["new_response_size"] = meter_new.create_histogram(
name="http.client.response.body.size",
unit="By",
description="Size of HTTP client response bodies.",
)
return histograms
[docs]def patch_handler_class(
tracer,
server_histograms,
cls,
request_hook=None,
sem_conv_opt_in_mode=_StabilityMode.DEFAULT,
):
if getattr(cls, _OTEL_PATCHED_KEY, False):
return False
setattr(cls, _OTEL_PATCHED_KEY, True)
_wrap(
cls,
"prepare",
partial(
_prepare,
tracer,
server_histograms,
request_hook,
sem_conv_opt_in_mode,
),
)
_wrap(
cls,
"log_exception",
partial(
_log_exception, tracer, server_histograms, sem_conv_opt_in_mode
),
)
if issubclass(cls, tornado.websocket.WebSocketHandler):
_wrap(
cls,
"on_close",
partial(
_websockethandler_on_close,
tracer,
server_histograms,
sem_conv_opt_in_mode,
),
)
else:
_wrap(
cls,
"on_finish",
partial(
_on_finish, tracer, server_histograms, sem_conv_opt_in_mode
),
)
return True
[docs]def unpatch_handler_class(cls):
if not getattr(cls, _OTEL_PATCHED_KEY, False):
return
unwrap(cls, "prepare")
unwrap(cls, "log_exception")
if issubclass(cls, tornado.websocket.WebSocketHandler):
unwrap(cls, "on_close")
else:
unwrap(cls, "on_finish")
delattr(cls, _OTEL_PATCHED_KEY)
def _wrap(cls, method_name, wrapper):
original = getattr(cls, method_name)
wrapper = wrapt.FunctionWrapper(original, wrapper)
wrapt.apply_patch(cls, method_name, wrapper)
def _prepare(
tracer,
server_histograms,
request_hook,
sem_conv_opt_in_mode,
func,
handler,
args,
kwargs,
):
request = handler.request
otel_handler_state = {
_START_TIME: default_timer(),
"exclude_request": _excluded_urls.url_disabled(request.uri),
}
setattr(handler, _HANDLER_STATE_KEY, otel_handler_state)
if otel_handler_state["exclude_request"]:
return func(*args, **kwargs)
_record_prepare_metrics(server_histograms, handler, sem_conv_opt_in_mode)
ctx = _start_span(tracer, handler, sem_conv_opt_in_mode)
if request_hook:
request_hook(ctx.span, handler)
return func(*args, **kwargs)
def _on_finish(
tracer,
server_histograms,
sem_conv_opt_in_mode,
func,
handler,
args,
kwargs,
):
try:
return func(*args, **kwargs)
finally:
_record_on_finish_metrics(
server_histograms, handler, None, sem_conv_opt_in_mode
)
_finish_span(tracer, handler, None, sem_conv_opt_in_mode)
def _websockethandler_on_close(
tracer,
server_histograms,
sem_conv_opt_in_mode,
func,
handler,
args,
kwargs,
):
try:
func()
finally:
_record_on_finish_metrics(
server_histograms, handler, None, sem_conv_opt_in_mode
)
_finish_span(tracer, handler, None, sem_conv_opt_in_mode)
def _log_exception(
tracer,
server_histograms,
sem_conv_opt_in_mode,
func,
handler,
args,
kwargs,
):
error = None
if len(args) == 3:
error = args[1]
_record_on_finish_metrics(
server_histograms, handler, error, sem_conv_opt_in_mode
)
_finish_span(tracer, handler, error, sem_conv_opt_in_mode)
return func(*args, **kwargs)
def _collect_custom_request_headers_attributes(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]
return attributes
def _collect_custom_response_headers_attributes(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]
return attributes
def _get_attributes_from_request(request, sem_conv_opt_in_mode):
attrs = {}
# Set attributes based on semconv mode
_set_http_method(
attrs,
request.method,
sanitize_method(request.method),
sem_conv_opt_in_mode,
)
_set_http_scheme(attrs, request.protocol, sem_conv_opt_in_mode)
_set_http_host_server(attrs, request.host, sem_conv_opt_in_mode)
uri = redact_url(request.uri)
_, query = _parse_url_query(uri)
_set_http_target(
attrs,
uri,
request.path,
query,
sem_conv_opt_in_mode,
)
_set_http_url(attrs, redact_url(request.uri), sem_conv_opt_in_mode)
user_agent = request.headers.get("user-agent")
if user_agent:
_set_http_user_agent(
attrs,
normalize_user_agent(user_agent),
sem_conv_opt_in_mode,
)
# HTTP version
if request.version:
_set_http_flavor_version(attrs, request.version, sem_conv_opt_in_mode)
if request.remote_ip:
# Client IP address
# e.g. if Tornado is set to trust X-Forwarded-For headers (xheaders=True)
if _report_old(sem_conv_opt_in_mode):
attrs[HTTP_CLIENT_IP] = request.remote_ip
if _report_new(sem_conv_opt_in_mode):
attrs[CLIENT_ADDRESS] = request.remote_ip
# Network peer IP if different from remote_ip
if hasattr(request.connection, "context") and getattr(
request.connection.context, "_orig_remote_ip", None
):
if _report_old(sem_conv_opt_in_mode):
attrs[NET_PEER_IP] = request.connection.context._orig_remote_ip
if _report_new(sem_conv_opt_in_mode):
attrs[NETWORK_PEER_ADDRESS] = (
request.connection.context._orig_remote_ip
)
return extract_attributes_from_object(
request, _traced_request_attrs, attrs
)
def _get_default_span_name(handler):
"""
Default span name is the HTTP method and route, or just the method.
https://github.com/open-telemetry/opentelemetry-specification/pull/3165
https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/http/#name
Args:
handler: Tornado handler object.
Returns:
Default span name.
"""
method = handler.request.method
rule = find_matched_rule(handler)
# if there's no rule, like for 404 just return the method
route = route_from_rule(rule, handler) if rule else None
if method and route:
return f"{method} {route}"
return f"{method}"
def _get_full_handler_name(handler):
klass = type(handler)
return f"{klass.__module__}.{klass.__qualname__}"
def _start_span(tracer, handler, sem_conv_opt_in_mode) -> _TraceContext:
attributes = _get_attributes_from_request(
handler.request, sem_conv_opt_in_mode
)
span, token = _start_internal_or_server_span(
tracer=tracer,
span_name=_get_default_span_name(handler),
start_time=time_ns(),
context_carrier=handler.request.headers,
context_getter=textmap.default_getter,
attributes=attributes,
)
if span.is_recording():
span.set_attribute("tornado.handler", _get_full_handler_name(handler))
if span.kind == trace.SpanKind.SERVER:
custom_attributes = _collect_custom_request_headers_attributes(
handler.request.headers
)
if len(custom_attributes) > 0:
span.set_attributes(custom_attributes)
activation = trace.use_span(span, end_on_exit=True)
activation.__enter__() # pylint: disable=unnecessary-dunder-call
ctx = _TraceContext(activation, span, token)
setattr(handler, _HANDLER_CONTEXT_KEY, ctx)
# finish handler is called after the response is sent back to
# the client so it is too late to inject trace response headers
# there.
propagator = get_global_response_propagator()
if propagator:
propagator.inject(handler, setter=response_propagation_setter)
return ctx
def _finish_span(tracer, handler, error, sem_conv_opt_in_mode):
status_code = handler.get_status()
finish_args = (None, None, None)
ctx = getattr(handler, _HANDLER_CONTEXT_KEY, None)
if error:
if isinstance(error, tornado.web.HTTPError):
status_code = error.status_code
if not ctx and status_code == 404:
ctx = _start_span(tracer, handler, sem_conv_opt_in_mode)
else:
status_code = 500
if status_code >= 500:
finish_args = (
type(error),
error,
getattr(error, "__traceback__", None),
)
if not ctx:
return
if ctx.span.is_recording():
metric_attributes = {}
_set_status(
ctx.span,
metric_attributes,
status_code,
str(status_code) if status_code else None,
server_span=True,
sem_conv_opt_in_mode=sem_conv_opt_in_mode,
)
if ctx.span.is_recording() and ctx.span.kind == trace.SpanKind.SERVER:
custom_attributes = _collect_custom_response_headers_attributes(
handler._headers
)
if len(custom_attributes) > 0:
ctx.span.set_attributes(custom_attributes)
ctx.activation.__exit__(*finish_args) # pylint: disable=unnecessary-dunder-call
if ctx.token:
context.detach(ctx.token)
delattr(handler, _HANDLER_CONTEXT_KEY)
def _record_prepare_metrics(server_histograms, handler, sem_conv_opt_in_mode):
request_size = int(handler.request.headers.get("Content-Length", 0))
# Record old semconv metrics
if _report_old(sem_conv_opt_in_mode):
metric_attributes_old = _create_metric_attributes_old(handler)
server_histograms["old_request_size"].record(
request_size, attributes=metric_attributes_old
)
active_requests_attributes_old = (
_create_active_requests_attributes_old(handler.request)
)
server_histograms["active_requests"].add(
1, attributes=active_requests_attributes_old
)
# Record new semconv metrics
if _report_new(sem_conv_opt_in_mode):
metric_attributes_new = _create_metric_attributes_new(handler)
server_histograms["new_request_size"].record(
request_size, attributes=metric_attributes_new
)
# Don't add to active_requests again if already added in old mode
if not _report_old(sem_conv_opt_in_mode):
active_requests_attributes_new = (
_create_active_requests_attributes_new(handler.request)
)
server_histograms["active_requests"].add(
1, attributes=active_requests_attributes_new
)
def _record_on_finish_metrics(
server_histograms, handler, error, sem_conv_opt_in_mode
):
otel_handler_state = getattr(handler, _HANDLER_STATE_KEY, None) or {}
if otel_handler_state.get("exclude_request"):
return
start_time = otel_handler_state.get(_START_TIME, None) or default_timer()
elapsed_time_s = default_timer() - start_time
elapsed_time_ms = round(elapsed_time_s * 1000)
response_size = int(handler._headers.get("Content-Length", 0))
status_code = handler.get_status()
if isinstance(error, tornado.web.HTTPError):
status_code = error.status_code
# Record old semconv metrics
if _report_old(sem_conv_opt_in_mode):
metric_attributes_old = _create_metric_attributes_old(handler)
if isinstance(error, tornado.web.HTTPError):
metric_attributes_old[HTTP_STATUS_CODE] = status_code
server_histograms["old_response_size"].record(
response_size, attributes=metric_attributes_old
)
server_histograms["old_duration"].record(
elapsed_time_ms, attributes=metric_attributes_old
)
active_requests_attributes_old = (
_create_active_requests_attributes_old(handler.request)
)
server_histograms["active_requests"].add(
-1, attributes=active_requests_attributes_old
)
# Record new semconv metrics
if _report_new(sem_conv_opt_in_mode):
metric_attributes_new = _create_metric_attributes_new(handler)
if isinstance(error, tornado.web.HTTPError):
metric_attributes_new[HTTP_RESPONSE_STATUS_CODE] = status_code
server_histograms["new_response_size"].record(
response_size, attributes=metric_attributes_new
)
server_histograms["new_duration"].record(
elapsed_time_s, attributes=metric_attributes_new
)
# Don't subtract from active_requests again if already done in old mode
if not _report_old(sem_conv_opt_in_mode):
active_requests_attributes_new = (
_create_active_requests_attributes_new(handler.request)
)
server_histograms["active_requests"].add(
-1, attributes=active_requests_attributes_new
)
def _create_active_requests_attributes_old(request):
"""Create metric attributes for active requests using old semconv."""
metric_attributes = {
HTTP_METHOD: request.method,
HTTP_SCHEME: request.protocol,
HTTP_FLAVOR: request.version,
HTTP_HOST: request.host,
}
metric_attributes[HTTP_TARGET] = request.path
return metric_attributes
def _create_active_requests_attributes_new(request):
"""Create metric attributes for active requests using new semconv."""
metric_attributes = {
HTTP_REQUEST_METHOD: request.method,
URL_SCHEME: request.protocol,
}
if request.version:
metric_attributes[NETWORK_PROTOCOL_VERSION] = request.version
return metric_attributes
def _create_metric_attributes_old(handler):
"""Create metric attributes using old semconv."""
metric_attributes = _create_active_requests_attributes_old(handler.request)
metric_attributes[HTTP_STATUS_CODE] = handler.get_status()
return metric_attributes
def _create_metric_attributes_new(handler):
"""Create metric attributes using new semconv."""
metric_attributes = _create_active_requests_attributes_new(handler.request)
metric_attributes[HTTP_RESPONSE_STATUS_CODE] = handler.get_status()
if handler.request.path:
rule = find_matched_rule(handler)
if rule:
route = route_from_rule(rule, handler)
if route is not None:
metric_attributes[HTTP_ROUTE] = route
return metric_attributes