Skip to content

Commit 744851b

Browse files
authored
metric instrumentation Tornado (#1252)
1 parent c615fa7 commit 744851b

File tree

7 files changed

+464
-23
lines changed

7 files changed

+464
-23
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.13.0-0.34b0...HEAD)
9+
- Add metric instrumentation for tornado
10+
([#1252](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1252))
11+
912

1013
### Added
1114

instrumentation/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
| [opentelemetry-instrumentation-sqlite3](./opentelemetry-instrumentation-sqlite3) | sqlite3 | No
3939
| [opentelemetry-instrumentation-starlette](./opentelemetry-instrumentation-starlette) | starlette ~= 0.13.0 | Yes
4040
| [opentelemetry-instrumentation-system-metrics](./opentelemetry-instrumentation-system-metrics) | psutil >= 5 | No
41-
| [opentelemetry-instrumentation-tornado](./opentelemetry-instrumentation-tornado) | tornado >= 5.1.1 | No
41+
| [opentelemetry-instrumentation-tornado](./opentelemetry-instrumentation-tornado) | tornado >= 5.1.1 | Yes
4242
| [opentelemetry-instrumentation-urllib](./opentelemetry-instrumentation-urllib) | urllib | No
4343
| [opentelemetry-instrumentation-urllib3](./opentelemetry-instrumentation-urllib3) | urllib3 >= 1.0.0, < 2.0.0 | Yes
4444
| [opentelemetry-instrumentation-wsgi](./opentelemetry-instrumentation-wsgi) | wsgi | Yes

instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py

+171-15
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,8 @@ def client_resposne_hook(span, future):
157157
from functools import partial
158158
from logging import getLogger
159159
from time import time_ns
160-
from typing import Collection
160+
from timeit import default_timer
161+
from typing import Collection, Dict
161162

162163
import tornado.web
163164
import wrapt
@@ -177,6 +178,8 @@ def client_resposne_hook(span, future):
177178
http_status_to_status_code,
178179
unwrap,
179180
)
181+
from opentelemetry.metrics import get_meter
182+
from opentelemetry.metrics._internal.instrument import Histogram
180183
from opentelemetry.propagators import textmap
181184
from opentelemetry.semconv.trace import SpanAttributes
182185
from opentelemetry.trace.status import Status, StatusCode
@@ -197,6 +200,14 @@ def client_resposne_hook(span, future):
197200
_HANDLER_CONTEXT_KEY = "_otel_trace_context_key"
198201
_OTEL_PATCHED_KEY = "_otel_patched_key"
199202

203+
_START_TIME = "start_time"
204+
_CLIENT_DURATION_HISTOGRAM = "http.client.duration"
205+
_CLIENT_REQUEST_SIZE_HISTOGRAM = "http.client.request.size"
206+
_CLIENT_RESPONSE_SIZE_HISTOGRAM = "http.client.response.size"
207+
_SERVER_DURATION_HISTOGRAM = "http.server.duration"
208+
_SERVER_REQUEST_SIZE_HISTOGRAM = "http.server.request.size"
209+
_SERVER_RESPONSE_SIZE_HISTOGRAM = "http.server.response.size"
210+
_SERVER_ACTIVE_REQUESTS_HISTOGRAM = "http.server.active_requests"
200211

201212
_excluded_urls = get_excluded_urls("TORNADO")
202213
_traced_request_attrs = get_traced_request_attrs("TORNADO")
@@ -233,13 +244,21 @@ def _instrument(self, **kwargs):
233244
tracer_provider = kwargs.get("tracer_provider")
234245
tracer = trace.get_tracer(__name__, __version__, tracer_provider)
235246

247+
meter_provider = kwargs.get("meter_provider")
248+
meter = get_meter(__name__, __version__, meter_provider)
249+
250+
client_histograms = _create_client_histograms(meter)
251+
server_histograms = _create_server_histograms(meter)
252+
236253
client_request_hook = kwargs.get("client_request_hook", None)
237254
client_response_hook = kwargs.get("client_response_hook", None)
238255
server_request_hook = kwargs.get("server_request_hook", None)
239256

240257
def handler_init(init, handler, args, kwargs):
241258
cls = handler.__class__
242-
if patch_handler_class(tracer, cls, server_request_hook):
259+
if patch_handler_class(
260+
tracer, server_histograms, cls, server_request_hook
261+
):
243262
self.patched_handlers.append(cls)
244263
return init(*args, **kwargs)
245264

@@ -250,7 +269,13 @@ def handler_init(init, handler, args, kwargs):
250269
"tornado.httpclient",
251270
"AsyncHTTPClient.fetch",
252271
partial(
253-
fetch_async, tracer, client_request_hook, client_response_hook
272+
fetch_async,
273+
tracer,
274+
client_request_hook,
275+
client_response_hook,
276+
client_histograms[_CLIENT_DURATION_HISTOGRAM],
277+
client_histograms[_CLIENT_REQUEST_SIZE_HISTOGRAM],
278+
client_histograms[_CLIENT_RESPONSE_SIZE_HISTOGRAM],
254279
),
255280
)
256281

@@ -262,14 +287,71 @@ def _uninstrument(self, **kwargs):
262287
self.patched_handlers = []
263288

264289

265-
def patch_handler_class(tracer, cls, request_hook=None):
290+
def _create_server_histograms(meter) -> Dict[str, Histogram]:
291+
histograms = {
292+
_SERVER_DURATION_HISTOGRAM: meter.create_histogram(
293+
name="http.server.duration",
294+
unit="ms",
295+
description="measures the duration outbound HTTP requests",
296+
),
297+
_SERVER_REQUEST_SIZE_HISTOGRAM: meter.create_histogram(
298+
name="http.server.request.size",
299+
unit="By",
300+
description="measures the size of HTTP request messages (compressed)",
301+
),
302+
_SERVER_RESPONSE_SIZE_HISTOGRAM: meter.create_histogram(
303+
name="http.server.response.size",
304+
unit="By",
305+
description="measures the size of HTTP response messages (compressed)",
306+
),
307+
_SERVER_ACTIVE_REQUESTS_HISTOGRAM: meter.create_up_down_counter(
308+
name="http.server.active_requests",
309+
unit="requests",
310+
description="measures the number of concurrent HTTP requests that are currently in-flight",
311+
),
312+
}
313+
314+
return histograms
315+
316+
317+
def _create_client_histograms(meter) -> Dict[str, Histogram]:
318+
histograms = {
319+
_CLIENT_DURATION_HISTOGRAM: meter.create_histogram(
320+
name="http.client.duration",
321+
unit="ms",
322+
description="measures the duration outbound HTTP requests",
323+
),
324+
_CLIENT_REQUEST_SIZE_HISTOGRAM: meter.create_histogram(
325+
name="http.client.request.size",
326+
unit="By",
327+
description="measures the size of HTTP request messages (compressed)",
328+
),
329+
_CLIENT_RESPONSE_SIZE_HISTOGRAM: meter.create_histogram(
330+
name="http.client.response.size",
331+
unit="By",
332+
description="measures the size of HTTP response messages (compressed)",
333+
),
334+
}
335+
336+
return histograms
337+
338+
339+
def patch_handler_class(tracer, server_histograms, cls, request_hook=None):
266340
if getattr(cls, _OTEL_PATCHED_KEY, False):
267341
return False
268342

269343
setattr(cls, _OTEL_PATCHED_KEY, True)
270-
_wrap(cls, "prepare", partial(_prepare, tracer, request_hook))
271-
_wrap(cls, "on_finish", partial(_on_finish, tracer))
272-
_wrap(cls, "log_exception", partial(_log_exception, tracer))
344+
_wrap(
345+
cls,
346+
"prepare",
347+
partial(_prepare, tracer, server_histograms, request_hook),
348+
)
349+
_wrap(cls, "on_finish", partial(_on_finish, tracer, server_histograms))
350+
_wrap(
351+
cls,
352+
"log_exception",
353+
partial(_log_exception, tracer, server_histograms),
354+
)
273355
return True
274356

275357

@@ -289,28 +371,40 @@ def _wrap(cls, method_name, wrapper):
289371
wrapt.apply_patch(cls, method_name, wrapper)
290372

291373

292-
def _prepare(tracer, request_hook, func, handler, args, kwargs):
293-
start_time = time_ns()
374+
def _prepare(
375+
tracer, server_histograms, request_hook, func, handler, args, kwargs
376+
):
377+
server_histograms[_START_TIME] = default_timer()
378+
294379
request = handler.request
295380
if _excluded_urls.url_disabled(request.uri):
296381
return func(*args, **kwargs)
297-
ctx = _start_span(tracer, handler, start_time)
382+
383+
_record_prepare_metrics(server_histograms, handler)
384+
385+
ctx = _start_span(tracer, handler)
298386
if request_hook:
299387
request_hook(ctx.span, handler)
300388
return func(*args, **kwargs)
301389

302390

303-
def _on_finish(tracer, func, handler, args, kwargs):
391+
def _on_finish(tracer, server_histograms, func, handler, args, kwargs):
304392
response = func(*args, **kwargs)
393+
394+
_record_on_finish_metrics(server_histograms, handler)
395+
305396
_finish_span(tracer, handler)
397+
306398
return response
307399

308400

309-
def _log_exception(tracer, func, handler, args, kwargs):
401+
def _log_exception(tracer, server_histograms, func, handler, args, kwargs):
310402
error = None
311403
if len(args) == 3:
312404
error = args[1]
313405

406+
_record_on_finish_metrics(server_histograms, handler, error)
407+
314408
_finish_span(tracer, handler, error)
315409
return func(*args, **kwargs)
316410

@@ -377,11 +471,11 @@ def _get_full_handler_name(handler):
377471
return f"{klass.__module__}.{klass.__qualname__}"
378472

379473

380-
def _start_span(tracer, handler, start_time) -> _TraceContext:
474+
def _start_span(tracer, handler) -> _TraceContext:
381475
span, token = _start_internal_or_server_span(
382476
tracer=tracer,
383477
span_name=_get_operation_name(handler, handler.request),
384-
start_time=start_time,
478+
start_time=time_ns(),
385479
context_carrier=handler.request.headers,
386480
context_getter=textmap.default_getter,
387481
)
@@ -423,7 +517,7 @@ def _finish_span(tracer, handler, error=None):
423517
if isinstance(error, tornado.web.HTTPError):
424518
status_code = error.status_code
425519
if not ctx and status_code == 404:
426-
ctx = _start_span(tracer, handler, time_ns())
520+
ctx = _start_span(tracer, handler)
427521
else:
428522
status_code = 500
429523
reason = None
@@ -462,3 +556,65 @@ def _finish_span(tracer, handler, error=None):
462556
if ctx.token:
463557
context.detach(ctx.token)
464558
delattr(handler, _HANDLER_CONTEXT_KEY)
559+
560+
561+
def _record_prepare_metrics(server_histograms, handler):
562+
request_size = int(handler.request.headers.get("Content-Length", 0))
563+
metric_attributes = _create_metric_attributes(handler)
564+
565+
server_histograms[_SERVER_REQUEST_SIZE_HISTOGRAM].record(
566+
request_size, attributes=metric_attributes
567+
)
568+
569+
active_requests_attributes = _create_active_requests_attributes(
570+
handler.request
571+
)
572+
server_histograms[_SERVER_ACTIVE_REQUESTS_HISTOGRAM].add(
573+
1, attributes=active_requests_attributes
574+
)
575+
576+
577+
def _record_on_finish_metrics(server_histograms, handler, error=None):
578+
elapsed_time = round(
579+
(default_timer() - server_histograms[_START_TIME]) * 1000
580+
)
581+
582+
response_size = int(handler._headers.get("Content-Length", 0))
583+
metric_attributes = _create_metric_attributes(handler)
584+
585+
if isinstance(error, tornado.web.HTTPError):
586+
metric_attributes[SpanAttributes.HTTP_STATUS_CODE] = error.status_code
587+
588+
server_histograms[_SERVER_RESPONSE_SIZE_HISTOGRAM].record(
589+
response_size, attributes=metric_attributes
590+
)
591+
592+
server_histograms[_SERVER_DURATION_HISTOGRAM].record(
593+
elapsed_time, attributes=metric_attributes
594+
)
595+
596+
active_requests_attributes = _create_active_requests_attributes(
597+
handler.request
598+
)
599+
server_histograms[_SERVER_ACTIVE_REQUESTS_HISTOGRAM].add(
600+
-1, attributes=active_requests_attributes
601+
)
602+
603+
604+
def _create_active_requests_attributes(request):
605+
metric_attributes = {
606+
SpanAttributes.HTTP_METHOD: request.method,
607+
SpanAttributes.HTTP_SCHEME: request.protocol,
608+
SpanAttributes.HTTP_FLAVOR: request.version,
609+
SpanAttributes.HTTP_HOST: request.host,
610+
SpanAttributes.HTTP_TARGET: request.path,
611+
}
612+
613+
return metric_attributes
614+
615+
616+
def _create_metric_attributes(handler):
617+
metric_attributes = _create_active_requests_attributes(handler.request)
618+
metric_attributes[SpanAttributes.HTTP_STATUS_CODE] = handler.get_status()
619+
620+
return metric_attributes

instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/client.py

+48-3
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,18 @@ def _normalize_request(args, kwargs):
4141
return (new_args, new_kwargs)
4242

4343

44-
def fetch_async(tracer, request_hook, response_hook, func, _, args, kwargs):
44+
def fetch_async(
45+
tracer,
46+
request_hook,
47+
response_hook,
48+
duration_histogram,
49+
request_size_histogram,
50+
response_size_histogram,
51+
func,
52+
_,
53+
args,
54+
kwargs,
55+
):
4556
start_time = time_ns()
4657

4758
# Return immediately if no args were provided (error)
@@ -78,21 +89,34 @@ def fetch_async(tracer, request_hook, response_hook, func, _, args, kwargs):
7889
_finish_tracing_callback,
7990
span=span,
8091
response_hook=response_hook,
92+
duration_histogram=duration_histogram,
93+
request_size_histogram=request_size_histogram,
94+
response_size_histogram=response_size_histogram,
8195
)
8296
)
8397
return future
8498

8599

86-
def _finish_tracing_callback(future, span, response_hook):
100+
def _finish_tracing_callback(
101+
future,
102+
span,
103+
response_hook,
104+
duration_histogram,
105+
request_size_histogram,
106+
response_size_histogram,
107+
):
87108
status_code = None
88109
description = None
89110
exc = future.exception()
111+
112+
response = future.result()
113+
90114
if span.is_recording() and exc:
91115
if isinstance(exc, HTTPError):
92116
status_code = exc.code
93117
description = f"{type(exc).__name__}: {exc}"
94118
else:
95-
status_code = future.result().code
119+
status_code = response.code
96120

97121
if status_code is not None:
98122
span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code)
@@ -102,6 +126,27 @@ def _finish_tracing_callback(future, span, response_hook):
102126
description=description,
103127
)
104128
)
129+
130+
metric_attributes = _create_metric_attributes(response)
131+
request_size = int(response.request.headers.get("Content-Length", 0))
132+
response_size = int(response.headers.get("Content-Length", 0))
133+
134+
duration_histogram.record(
135+
response.request_time, attributes=metric_attributes
136+
)
137+
request_size_histogram.record(request_size, attributes=metric_attributes)
138+
response_size_histogram.record(response_size, attributes=metric_attributes)
139+
105140
if response_hook:
106141
response_hook(span, future)
107142
span.end()
143+
144+
145+
def _create_metric_attributes(response):
146+
metric_attributes = {
147+
SpanAttributes.HTTP_STATUS_CODE: response.code,
148+
SpanAttributes.HTTP_URL: remove_url_credentials(response.request.url),
149+
SpanAttributes.HTTP_METHOD: response.request.method,
150+
}
151+
152+
return metric_attributes

instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/package.py

+2
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@
1414

1515

1616
_instruments = ("tornado >= 5.1.1",)
17+
18+
_supports_metrics = True

0 commit comments

Comments
 (0)