@@ -157,7 +157,8 @@ def client_resposne_hook(span, future):
157157from functools import partial
158158from logging import getLogger
159159from time import time_ns
160- from typing import Collection
160+ from timeit import default_timer
161+ from typing import Collection , Dict
161162
162163import tornado .web
163164import 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
180183from opentelemetry .propagators import textmap
181184from opentelemetry .semconv .trace import SpanAttributes
182185from 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
0 commit comments