11import dataclasses
22import json
3- from datetime import datetime
3+ from datetime import datetime , timedelta
44from functools import partial
5- from typing import Any , Optional
5+ from http import HTTPStatus
6+ from typing import Any , Optional , Callable
67
78from tornado .web import Application , RequestHandler
89
10+ from pyctuator .httptrace import TraceRecord , TraceRequest , TraceResponse
11+ from pyctuator .impl import SBA_V2_CONTENT_TYPE
912from pyctuator .impl .pyctuator_impl import PyctuatorImpl
1013from pyctuator .impl .pyctuator_router import PyctuatorRouter
1114
1215
1316# pylint: disable=abstract-method
1417class AbstractPyctuatorHandler (RequestHandler ):
1518 pyctuator_router : Optional [PyctuatorRouter ] = None
16- dumps : Optional [partial [ str ]] = None
19+ dumps : Optional [Callable [[ Any ], str ]] = None
1720
1821 def initialize (self ) -> None :
19- self .pyctuator_router = self .application .settings .get ('pyctuator_router' )
20- self .dumps = self .application .settings .get ('custom_dumps' )
22+ self .pyctuator_router = self .application .settings .get ("pyctuator_router" )
23+ self .dumps = self .application .settings .get ("custom_dumps" )
24+ self .set_header ("Content-Type" , SBA_V2_CONTENT_TYPE )
25+
26+ def options (self ) -> None :
27+ assert self .pyctuator_router is not None
28+ assert self .dumps is not None
29+ self .write ("" )
2130
2231
2332class PyctuatorHandler (AbstractPyctuatorHandler ):
2433 def get (self ) -> None :
2534 assert self .pyctuator_router is not None
2635 assert self .dumps is not None
27- resp = self .pyctuator_router .get_endpoints_data ()
28- self .write (self .dumps (resp ))
36+ self .write (self .dumps (self .pyctuator_router .get_endpoints_data ()))
2937
3038
3139# GET /env
3240class EnvHandler (AbstractPyctuatorHandler ):
33- def options (self ) -> None :
34- assert self .pyctuator_router is not None
35- assert self .dumps is not None
36- self .write ('' )
37-
3841 def get (self ) -> None :
3942 assert self .pyctuator_router is not None
4043 assert self .dumps is not None
41- resp = self .pyctuator_router .pyctuator_impl .get_environment ()
42- self .write (self .dumps (resp ))
44+ self .write (self .dumps (self .pyctuator_router .pyctuator_impl .get_environment ()))
4345
4446
4547# GET /info
4648class InfoHandler (AbstractPyctuatorHandler ):
47- def options (self ) -> None :
48- assert self .pyctuator_router is not None
49- assert self .dumps is not None
50- self .write ('' )
51-
5249 def get (self ) -> None :
5350 assert self .pyctuator_router is not None
5451 assert self .dumps is not None
55- resp = self .pyctuator_router .pyctuator_impl .app_info
56- self .write (self .dumps (resp ))
52+ self .write (self .dumps (self .pyctuator_router .pyctuator_impl .app_info ))
5753
5854
5955# GET /health
6056class HealthHandler (AbstractPyctuatorHandler ):
61- def options (self ) -> None :
62- assert self .pyctuator_router is not None
63- assert self .dumps is not None
64- self .write ('' )
65-
6657 def get (self ) -> None :
6758 assert self .pyctuator_router is not None
6859 assert self .dumps is not None
69- resp = self .pyctuator_router .pyctuator_impl .get_health ()
70- self .write (self .dumps (resp ))
60+ self .write (self .dumps (self .pyctuator_router .pyctuator_impl .get_health ()))
7161
7262
7363# GET /metrics
7464class MetricsHandler (AbstractPyctuatorHandler ):
75- def options (self ) -> None :
76- assert self .pyctuator_router is not None
77- assert self .dumps is not None
78- self .write ('' )
79-
8065 def get (self ) -> None :
8166 assert self .pyctuator_router is not None
8267 assert self .dumps is not None
83- resp = self .pyctuator_router .pyctuator_impl .get_metric_names ()
84- self .write (self .dumps (resp ))
68+ self .write (self .dumps (self .pyctuator_router .pyctuator_impl .get_metric_names ()))
8569
8670
8771# GET "/metrics/{metric_name}"
8872class MetricsNameHandler (AbstractPyctuatorHandler ):
8973 def get (self , metric_name : str ) -> None :
9074 assert self .pyctuator_router is not None
9175 assert self .dumps is not None
92- resp = self .pyctuator_router .pyctuator_impl .get_metric_measurement (metric_name )
93- self .write (self .dumps (resp ))
76+ self .write (self .dumps (self .pyctuator_router .pyctuator_impl .get_metric_measurement (metric_name )))
9477
9578
9679# GET /loggers
9780class LoggersHandler (AbstractPyctuatorHandler ):
98- def options (self ) -> None :
99- assert self .pyctuator_router is not None
100- assert self .dumps is not None
101- self .write ('' )
102-
10381 def get (self ) -> None :
10482 assert self .pyctuator_router is not None
10583 assert self .dumps is not None
106- resp = self .pyctuator_router .pyctuator_impl .logging .get_loggers ()
107- self .write (self .dumps (resp ))
84+ self .write (self .dumps (self .pyctuator_router .pyctuator_impl .logging .get_loggers ()))
10885
10986
11087# GET /loggers/{logger_name}
@@ -113,16 +90,50 @@ class LoggersNameHandler(AbstractPyctuatorHandler):
11390 def get (self , logger_name : str ) -> None :
11491 assert self .pyctuator_router is not None
11592 assert self .dumps is not None
116- resp = self .pyctuator_router .pyctuator_impl .logging .get_logger (logger_name )
117- self .write (self .dumps (resp ))
93+ self .write (self .dumps (self .pyctuator_router .pyctuator_impl .logging .get_logger (logger_name )))
11894
11995 def post (self , logger_name : str ) -> None :
12096 assert self .pyctuator_router is not None
12197 assert self .dumps is not None
122- body_str = self .request .body .decode (' utf-8' )
98+ body_str = self .request .body .decode (" utf-8" )
12399 body = json .loads (body_str )
124- self .pyctuator_router .pyctuator_impl .logging .set_logger_level (logger_name , body .get ('configuredLevel' , None ))
125- self .write ('' )
100+ self .pyctuator_router .pyctuator_impl .logging .set_logger_level (logger_name , body .get ("configuredLevel" , None ))
101+ self .write ("" )
102+
103+
104+ # GET /threaddump
105+ class ThreadDumpHandler (AbstractPyctuatorHandler ):
106+ def get (self ) -> None :
107+ assert self .pyctuator_router is not None
108+ assert self .dumps is not None
109+ self .write (self .dumps (self .pyctuator_router .pyctuator_impl .get_thread_dump ()))
110+
111+
112+ # GET /logfile
113+ class LogFileHandler (AbstractPyctuatorHandler ):
114+ def get (self ) -> None :
115+ assert self .pyctuator_router is not None
116+ assert self .dumps is not None
117+
118+ range_header = self .request .headers .get ("range" )
119+ if not range_header :
120+ self .write (f"{ self .pyctuator_router .pyctuator_impl .logfile .log_messages .get_range ()} " )
121+
122+ else :
123+ str_res , start , end = self .pyctuator_router .pyctuator_impl .logfile .get_logfile (range_header )
124+ self .set_status (HTTPStatus .PARTIAL_CONTENT .value )
125+ self .add_header ("Content-Type" , "text/html; charset=UTF-8" )
126+ self .add_header ("Accept-Ranges" , "bytes" )
127+ self .add_header ("Content-Range" , f"bytes { start } -{ end } /{ end } " )
128+ self .write (str_res )
129+
130+
131+ # GET /httptrace
132+ class HttpTraceHandler (AbstractPyctuatorHandler ):
133+ def get (self ) -> None :
134+ assert self .pyctuator_router is not None
135+ assert self .dumps is not None
136+ self .write (self .dumps (self .pyctuator_router .pyctuator_impl .http_tracer .get_httptrace ()))
126137
127138
128139# pylint: disable=too-many-locals,unused-argument
@@ -136,16 +147,51 @@ def __init__(self, app: Application, pyctuator_impl: PyctuatorImpl) -> None:
136147
137148 app .settings .setdefault ("pyctuator_router" , self )
138149 app .settings .setdefault ("custom_dumps" , custom_dumps )
139- app .add_handlers (".*$" , [
140- (r"/pyctuator" , PyctuatorHandler ),
141- (r"/pyctuator/env" , EnvHandler ),
142- (r"/pyctuator/info" , InfoHandler ),
143- (r"/pyctuator/health" , HealthHandler ),
144- (r"/pyctuator/metrics" , MetricsHandler ),
145- (r"/pyctuator/metrics/(?P<metric_name>.*$)" , MetricsNameHandler ),
146- (r"/pyctuator/loggers" , LoggersHandler ),
147- (r"/pyctuator/loggers/(?P<logger_name>.*$)" , LoggersNameHandler ),
148- ])
150+
151+ # Register a log-function that records request and response in traces and than delegates to the original func
152+ self .delegate_log_function = app .settings .get ("log_function" )
153+ app .settings .setdefault ("log_function" , self ._intercept_request_and_response )
154+
155+ app .add_handlers (
156+ ".*$" ,
157+ [
158+ (r"/pyctuator" , PyctuatorHandler ),
159+ (r"/pyctuator/env" , EnvHandler ),
160+ (r"/pyctuator/info" , InfoHandler ),
161+ (r"/pyctuator/health" , HealthHandler ),
162+ (r"/pyctuator/metrics" , MetricsHandler ),
163+ (r"/pyctuator/metrics/(?P<metric_name>.*$)" , MetricsNameHandler ),
164+ (r"/pyctuator/loggers" , LoggersHandler ),
165+ (r"/pyctuator/loggers/(?P<logger_name>.*$)" , LoggersNameHandler ),
166+ (r"/pyctuator/dump" , ThreadDumpHandler ),
167+ (r"/pyctuator/threaddump" , ThreadDumpHandler ),
168+ (r"/pyctuator/logfile" , LogFileHandler ),
169+ (r"/pyctuator/trace" , HttpTraceHandler ),
170+ (r"/pyctuator/httptrace" , HttpTraceHandler ),
171+ ]
172+ )
173+
174+ def _intercept_request_and_response (self , handler : RequestHandler ) -> None :
175+ # Record the request and response
176+ record = TraceRecord (
177+ timestamp = datetime .now () - timedelta (seconds = handler .request .request_time ()),
178+ principal = None ,
179+ session = None ,
180+ request = TraceRequest (
181+ method = handler .request .method or "" ,
182+ uri = handler .request .full_url (),
183+ headers = {k .lower (): v for k , v in handler .request .headers .items ()}
184+ ),
185+ response = TraceResponse (
186+ status = handler .get_status (),
187+ headers = {k .lower (): [v ] for k , v in handler ._headers .items ()} # pylint: disable=protected-access
188+ ),
189+ timeTaken = int (handler .request .request_time () * 1000 ),
190+ )
191+ self .pyctuator_impl .http_tracer .add_record (record )
192+
193+ if self .delegate_log_function :
194+ self .delegate_log_function (handler )
149195
150196 def _custom_json_serializer (self , value : Any ) -> Any :
151197 if dataclasses .is_dataclass (value ):
0 commit comments