Skip to content

Commit e0ecab6

Browse files
author
michael.yak
committed
E2E should include Tornado
1 parent 30f355a commit e0ecab6

File tree

6 files changed

+205
-71
lines changed

6 files changed

+205
-71
lines changed

.github/workflows/python_package_build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
- run: poetry build -vvv
2828

2929
# Install all dependencies except for psutil and run the tests with coverage - this tests handling missing psutil
30-
- run: poetry install --extras flask --extras fastapi --extras aiohttp --extras db --extras redis
30+
- run: poetry install --extras flask --extras fastapi --extras aiohttp --extras tornado --extras db --extras redis
3131
- run: make coverage
3232

3333
# Run pylint+mypy after installing psutil so they don't complain on missing dependencies

examples/tornado/tornado_example_app.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,26 @@
33
import random
44

55
from tornado import ioloop
6-
from tornado.web import Application, RequestHandler
76
from tornado.httpserver import HTTPServer
7+
from tornado.web import Application, RequestHandler
88

99
from pyctuator.pyctuator import Pyctuator
1010

1111
my_logger = logging.getLogger("example")
1212

13+
1314
class HomeHandler(RequestHandler):
1415
def get(self):
1516
my_logger.debug(f"{datetime.datetime.now()} - {str(random.randint(0, 100))}")
16-
print("Printing to STDOUT")
1717
self.write("Hello World!")
1818

19-
app = Application([
20-
(r"/", HomeHandler)
21-
], debug=False)
19+
20+
app = Application(
21+
[
22+
(r"/", HomeHandler)
23+
],
24+
debug=False
25+
)
2226

2327
example_app_address = "host.docker.internal"
2428
example_sba_address = "localhost"

pyctuator/impl/aiohttp_pyctuator.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,7 @@ async def intercept_requests_and_responses(request: web.Request, handler: Callab
122122
web.get("/pyctuator/info", get_info),
123123
web.get("/pyctuator/health", get_health),
124124
web.get("/pyctuator/metrics", get_metric_names),
125-
web.get(
126-
"/pyctuator/metrics/{metric_name}",
127-
get_metric_measurement
128-
),
125+
web.get("/pyctuator/metrics/{metric_name}", get_metric_measurement),
129126
web.get("/pyctuator/loggers", get_loggers),
130127
web.get("/pyctuator/loggers/{logger_name}", get_logger),
131128
web.post("/pyctuator/loggers/{logger_name}", set_logger_level),

pyctuator/impl/tornado_pyctuator.py

Lines changed: 105 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,110 +1,87 @@
11
import dataclasses
22
import json
3-
from datetime import datetime
3+
from datetime import datetime, timedelta
44
from functools import partial
5-
from typing import Any, Optional
5+
from http import HTTPStatus
6+
from typing import Any, Optional, Callable
67

78
from tornado.web import Application, RequestHandler
89

10+
from pyctuator.httptrace import TraceRecord, TraceRequest, TraceResponse
11+
from pyctuator.impl import SBA_V2_CONTENT_TYPE
912
from pyctuator.impl.pyctuator_impl import PyctuatorImpl
1013
from pyctuator.impl.pyctuator_router import PyctuatorRouter
1114

1215

1316
# pylint: disable=abstract-method
1417
class 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

2332
class 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
3240
class 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
4648
class 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
6056
class 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
7464
class 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}"
8872
class 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
9780
class 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):

tests/test_pyctuator_e2e.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@
2222

2323

2424
# mypy: ignore_errors
25+
from tests.tornado_test_server import TornadoPyctuatorServer
26+
27+
2528
@pytest.fixture(
26-
params=[FastApiPyctuatorServer, FlaskPyctuatorServer, AiohttpPyctuatorServer],
27-
ids=["FastAPI", "Flask", "aiohttp"]
29+
params=[FastApiPyctuatorServer, FlaskPyctuatorServer, AiohttpPyctuatorServer, TornadoPyctuatorServer],
30+
ids=["FastAPI", "Flask", "aiohttp", "Tornado"]
2831
)
2932
def pyctuator_server(request) -> Generator: # type: ignore
3033
# Start a the web-server in which the pyctuator is integrated

0 commit comments

Comments
 (0)