Skip to content

Commit 62df9c7

Browse files
author
michael.yak
committed
Http-traces's timeTaken should be millis between request and response
When http-traces is capturing a request and response, the `timeTaken` field should be millis between the time the request was received and the time the response was sent. Both `fastapi_pyctuator` and `flask_pyctuator` used the `microseconds` of the `timedelta` between the request time and the response time. Unfortunately, this only returns the microseconds part of the time delta. Instead, should calculate the difference between the two timestamps. Fix #13
1 parent 3fc7437 commit 62df9c7

File tree

5 files changed

+50
-26
lines changed

5 files changed

+50
-26
lines changed

pyctuator/impl/fastapi_pyctuator.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,13 +159,12 @@ def _create_record(
159159
request_time: datetime,
160160
response_time: datetime,
161161
) -> TraceRecord:
162-
response_delta_time = response_time - request_time
163162
new_record: TraceRecord = TraceRecord(
164163
request_time,
165164
None,
166165
None,
167166
TraceRequest(request.method, str(request.url), self._create_headers_dictionary(request.headers)),
168167
TraceResponse(response.status_code, self._create_headers_dictionary(response.headers)),
169-
int(response_delta_time.microseconds / 1000),
168+
int((response_time.timestamp() - request_time.timestamp()) * 1000),
170169
)
171170
return new_record

pyctuator/impl/flask_pyctuator.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def get_logfile() -> Tuple[Response, int]:
120120
range_header: str = request.headers.environ.get('HTTP_RANGE')
121121
if not range_header:
122122
response: Response = make_response(pyctuator_impl.logfile.log_messages.get_range())
123-
return response, HTTPStatus.OK.value
123+
return response, HTTPStatus.OK
124124

125125
str_res, start, end = pyctuator_impl.logfile.get_logfile(range_header)
126126

@@ -129,7 +129,7 @@ def get_logfile() -> Tuple[Response, int]:
129129
resp.headers["Accept-Ranges"] = "bytes"
130130
resp.headers["Content-Range"] = f"bytes {start}-{end}/{end}"
131131

132-
return resp, HTTPStatus.PARTIAL_CONTENT.value
132+
return resp, HTTPStatus.PARTIAL_CONTENT
133133

134134
@flask_blueprint.route("/trace")
135135
@flask_blueprint.route("/httptrace")
@@ -150,13 +150,12 @@ def record_request_and_response(
150150
request_time: datetime,
151151
response_time: datetime,
152152
) -> None:
153-
response_delta_time = response_time - request_time
154153
new_record = TraceRecord(
155154
request_time,
156155
None,
157156
None,
158157
TraceRequest(request.method, str(request.url), self._create_headers_dictionary_flask(request.headers)),
159158
TraceResponse(response.status_code, self._create_headers_dictionary_flask(response.headers)),
160-
int(response_delta_time.microseconds / 1000),
159+
int((response_time.timestamp() - request_time.timestamp()) * 1000),
161160
)
162161
self.pyctuator_impl.http_tracer.add_record(record=new_record)

tests/fast_api_test_server.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22
import threading
33
import time
4+
from typing import Optional
45

56
from fastapi import FastAPI
67
from starlette.requests import Request
@@ -39,7 +40,13 @@ def logfile_test_repeater(repeated_string: str) -> str:
3940

4041
@self.app.get("/httptrace_test_url")
4142
# pylint: disable=unused-variable
42-
def get_httptrace_test_url(request: Request) -> Response:
43+
def get_httptrace_test_url(request: Request, sleep_sec: Optional[int]) -> Response:
44+
# Sleep if requested to sleep - used for asserting httptraces timing
45+
if sleep_sec:
46+
logging.info("Sleeping %s seconds before replying", sleep_sec)
47+
time.sleep(sleep_sec)
48+
49+
# Echo 'User-Data' header as 'resp-data' - used for asserting headers are captured properly
4350
return Response(headers={"resp-data": str(request.headers.get("User-Data"))}, content="my content")
4451

4552
def start(self) -> None:

tests/flask_test_server.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,22 @@ def shutdown() -> str:
3636
@self.app.route("/logfile_test_repeater")
3737
# pylint: disable=unused-variable
3838
def logfile_test_repeater() -> str:
39-
repeated_string: str = str(request.args.get('repeated_string'))
39+
repeated_string: str = str(request.args.get("repeated_string"))
4040
logging.error(repeated_string)
4141
return repeated_string
4242

4343
@self.app.route("/httptrace_test_url", methods=["GET"])
4444
# pylint: disable=unused-variable
4545
def get_httptrace_test_url() -> Response:
46+
# Sleep if requested to sleep - used for asserting httptraces timing
47+
sleep_sec = request.args.get("sleep_sec")
48+
if sleep_sec:
49+
logging.info("Sleeping %s seconds before replying", sleep_sec)
50+
time.sleep(int(sleep_sec))
51+
52+
# Echo 'User-Data' header as 'resp-data' - used for asserting headers are captured properly
4653
resp = Response()
47-
resp.headers["resp-data"] = str(request.headers.get('User-Data'))
54+
resp.headers["resp-data"] = str(request.headers.get("User-Data"))
4855
return resp
4956

5057
def start(self) -> None:

tests/test_pyctuator_e2e.py

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def test_response_content_type(endpoints: Endpoints, registration_tracker: Regis
4242
actuator_endpoint_url = asdict(endpoints)[actuator_endpoint]
4343
logging.info("Testing content type of %s (%s)", actuator_endpoint, actuator_endpoint_url)
4444
response = requests.get(actuator_endpoint_url)
45-
assert response.status_code == HTTPStatus.OK.value
45+
assert response.status_code == HTTPStatus.OK
4646
assert response.headers.get("Content-Type", response.headers.get("content-type")) == SBA_V2_CONTENT_TYPE
4747

4848
# Issue requests to non-actuator endpoints and verify the correct content-type is returned
@@ -73,7 +73,7 @@ def test_response_content_type(endpoints: Endpoints, registration_tracker: Regis
7373
@pytest.mark.mark_self_endpoint
7474
def test_self_endpoint(endpoints: Endpoints) -> None:
7575
response = requests.get(endpoints.pyctuator)
76-
assert response.status_code == HTTPStatus.OK.value
76+
assert response.status_code == HTTPStatus.OK
7777
assert response.json()["_links"] is not None
7878

7979

@@ -82,15 +82,15 @@ def test_self_endpoint(endpoints: Endpoints) -> None:
8282
def test_env_endpoint(endpoints: Endpoints) -> None:
8383
actual_key, actual_value = list(os.environ.items())[3]
8484
response = requests.get(endpoints.env)
85-
assert response.status_code == HTTPStatus.OK.value
85+
assert response.status_code == HTTPStatus.OK
8686
property_sources = response.json()["propertySources"]
8787
assert property_sources
8888
system_properties = [source for source in property_sources if source["name"] == "systemEnvironment"]
8989
assert system_properties
9090
assert system_properties[0]["properties"][actual_key]["value"] == actual_value
9191

9292
response = requests.get(endpoints.info)
93-
assert response.status_code == HTTPStatus.OK.value
93+
assert response.status_code == HTTPStatus.OK
9494
assert response.json()["app"] is not None
9595

9696

@@ -102,7 +102,7 @@ def test_health_endpoint_with_psutil(endpoints: Endpoints, monkeypatch: MonkeyPa
102102

103103
# Verify that the diskSpace health check is returning some reasonable values
104104
response = requests.get(endpoints.health)
105-
assert response.status_code == HTTPStatus.OK.value
105+
assert response.status_code == HTTPStatus.OK
106106
assert response.json()["status"] == "UP"
107107
disk_space_health = response.json()["details"]["diskSpace"]
108108
assert disk_space_health["status"] == "UP"
@@ -120,7 +120,7 @@ def mock_disk_usage(path: str) -> MockDiskUsage:
120120

121121
monkeypatch.setattr(psutil, "disk_usage", mock_disk_usage)
122122
response = requests.get(endpoints.health)
123-
assert response.status_code == HTTPStatus.OK.value
123+
assert response.status_code == HTTPStatus.OK
124124
assert response.json()["status"] == "DOWN"
125125
disk_space_health = response.json()["details"]["diskSpace"]
126126
assert disk_space_health["status"] == "DOWN"
@@ -137,7 +137,7 @@ def test_diskspace_no_psutil(endpoints: Endpoints) -> None:
137137

138138
# Verify that the diskSpace health check is returning some reasonable values
139139
response = requests.get(endpoints.health)
140-
assert response.status_code == HTTPStatus.OK.value
140+
assert response.status_code == HTTPStatus.OK
141141
assert response.json()["status"] == "UP"
142142
assert "diskSpace" not in response.json()["details"]
143143

@@ -149,20 +149,20 @@ def test_metrics_endpoint(endpoints: Endpoints) -> None:
149149
pytest.importorskip("psutil")
150150

151151
response = requests.get(endpoints.metrics)
152-
assert response.status_code == HTTPStatus.OK.value
152+
assert response.status_code == HTTPStatus.OK
153153
metric_names = response.json()["names"]
154154
assert "memory.rss" in metric_names
155155
assert "thread.count" in metric_names
156156

157157
response = requests.get(f"{endpoints.metrics}/memory.rss")
158-
assert response.status_code == HTTPStatus.OK.value
158+
assert response.status_code == HTTPStatus.OK
159159
metric_json = response.json()
160160
assert metric_json["name"] == "memory.rss"
161161
assert metric_json["measurements"][0]["statistic"] == "VALUE"
162162
assert metric_json["measurements"][0]["value"] > 10000
163163

164164
response = requests.get(f"{endpoints.metrics}/thread.count")
165-
assert response.status_code == HTTPStatus.OK.value
165+
assert response.status_code == HTTPStatus.OK
166166
metric_json = response.json()
167167
assert metric_json["name"] == "thread.count"
168168
assert metric_json["measurements"][0]["statistic"] == "COUNT"
@@ -210,7 +210,7 @@ def test_threads_endpoint(endpoints: Endpoints) -> None:
210210
@pytest.mark.mark_loggers_endpoint
211211
def test_loggers_endpoint(endpoints: Endpoints) -> None:
212212
response = requests.get(endpoints.loggers)
213-
assert response.status_code == HTTPStatus.OK.value
213+
assert response.status_code == HTTPStatus.OK
214214

215215
# levels section
216216
loggers_levels = response.json()["levels"]
@@ -226,7 +226,7 @@ def test_loggers_endpoint(endpoints: Endpoints) -> None:
226226
assert hasattr(logger_obj, "level")
227227
# Individual Get logger route
228228
response = requests.get(f"{endpoints.loggers}/{logger}")
229-
assert response.status_code == HTTPStatus.OK.value
229+
assert response.status_code == HTTPStatus.OK
230230
assert "configuredLevel" in json.loads(response.content)
231231
assert "effectiveLevel" in json.loads(response.content)
232232
# Set logger level
@@ -240,7 +240,7 @@ def test_loggers_endpoint(endpoints: Endpoints) -> None:
240240

241241
response = requests.post(f"{endpoints.loggers}/{logger}",
242242
data=post_data)
243-
assert response.status_code == HTTPStatus.OK.value
243+
assert response.status_code == HTTPStatus.OK
244244
# Perform get logger level to Validate set logger level succeeded
245245
response = requests.get(f"{endpoints.loggers}/{logger}")
246246
assert json.loads(response.content)["configuredLevel"] == random_log_level
@@ -254,14 +254,14 @@ def test_logfile_endpoint(endpoints: Endpoints) -> None:
254254
endpoints.root + "logfile_test_repeater",
255255
params={"repeated_string": thirsty_str}
256256
)
257-
assert response.status_code == HTTPStatus.OK.value
257+
assert response.status_code == HTTPStatus.OK
258258

259259
response = requests.get(endpoints.logfile)
260-
assert response.status_code == HTTPStatus.OK.value
260+
assert response.status_code == HTTPStatus.OK
261261
assert response.text.find(thirsty_str) >= 0
262262

263263
response = requests.get(endpoints.logfile, headers={"Range": "bytes=-307200"}) # Immitate SBA's 1st request
264-
assert response.status_code == HTTPStatus.PARTIAL_CONTENT.value
264+
assert response.status_code == HTTPStatus.PARTIAL_CONTENT
265265

266266

267267
@pytest.mark.usefixtures("boot_admin_server", "pyctuator_server")
@@ -270,9 +270,11 @@ def test_traces_endpoint(endpoints: Endpoints) -> None:
270270
response = requests.get(endpoints.httptrace)
271271
assert response.status_code == 200
272272

273-
# Create request with header
273+
# Create a request with header
274274
user_header = "my header test"
275275
requests.get(endpoints.root + "httptrace_test_url", headers={"User-Data": user_header})
276+
277+
# Get the captured httptraces
276278
response = requests.get(endpoints.httptrace)
277279
response_traces = response.json()["traces"]
278280
trace = next(x for x in response_traces if x["request"]["uri"].endswith("httptrace_test_url"))
@@ -283,3 +285,13 @@ def test_traces_endpoint(endpoints: Endpoints) -> None:
283285

284286
# Assert timestamp is formatted in ISO format
285287
datetime.fromisoformat(trace["timestamp"])
288+
289+
# Assert that the "time taken" (i.e. the time the server spent processing the request) is less than 100ms
290+
assert int(trace["timeTaken"]) < 100
291+
292+
# Issue the same request asking the server to sleep for a sec, than assert request timing is at least 1s
293+
requests.get(endpoints.root + "httptrace_test_url", params={"sleep_sec": 1}, headers={"User-Data": user_header})
294+
response = requests.get(endpoints.httptrace)
295+
response_traces = response.json()["traces"]
296+
trace = next(x for x in response_traces if "httptrace_test_url?sleep_sec" in x["request"]["uri"])
297+
assert int(trace["timeTaken"]) > 1000

0 commit comments

Comments
 (0)