Skip to content

Commit bf006aa

Browse files
requests api should be traced as a whole (#203)
* requests api should be traced as a whole
1 parent 9df5f6d commit bf006aa

File tree

3 files changed

+97
-17
lines changed

3 files changed

+97
-17
lines changed

src/lumigo_tracer/spans_container.py

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -143,12 +143,14 @@ def start_timeout_timer(self, context=None) -> None:
143143
return
144144
TimeoutMechanism.start(remaining_time - buffer, self.handle_timeout)
145145

146-
def add_span(self, span: dict):
146+
def add_span(self, span: dict) -> dict:
147147
"""
148148
This function parses an request event and add it to the span.
149149
"""
150-
self.spans.append(recursive_json_join(span, self.base_msg))
150+
new_span = recursive_json_join(span, self.base_msg)
151+
self.spans.append(new_span)
151152
self.span_ids_to_send.add(span["id"])
153+
return new_span
152154

153155
def get_last_span(self) -> Optional[dict]:
154156
if not self.spans:
@@ -185,8 +187,9 @@ def update_event_times(
185187
self.spans[-1]["started"] = int(start_timestamp * 1000)
186188
self.spans[-1]["ended"] = int(end_timestamp * 1000)
187189

190+
@staticmethod
188191
def _create_exception_event(
189-
self, exc_type: str, message: str, stacktrace: str = "", frames: Optional[List[dict]] = None
192+
exc_type: str, message: str, stacktrace: str = "", frames: Optional[List[dict]] = None
190193
):
191194
return {
192195
"type": exc_type,
@@ -195,19 +198,25 @@ def _create_exception_event(
195198
"frames": frames or [],
196199
}
197200

201+
@staticmethod
202+
def add_exception_to_span(
203+
span: dict, exception: Exception, frames_infos: List[inspect.FrameInfo]
204+
):
205+
message = exception.args[0] if exception.args else None
206+
if not isinstance(message, str):
207+
message = str(message)
208+
span["error"] = SpansContainer._create_exception_event(
209+
exc_type=exception.__class__.__name__,
210+
message=message,
211+
stacktrace=get_stacktrace(exception),
212+
frames=format_frames(frames_infos) if Configuration.verbose else [],
213+
)
214+
198215
def add_exception_event(
199216
self, exception: Exception, frames_infos: List[inspect.FrameInfo]
200217
) -> None:
201218
if self.function_span:
202-
message = exception.args[0] if exception.args else None
203-
if not isinstance(message, str):
204-
message = str(message)
205-
self.function_span["error"] = self._create_exception_event(
206-
exc_type=exception.__class__.__name__,
207-
message=message,
208-
stacktrace=get_stacktrace(exception),
209-
frames=format_frames(frames_infos) if Configuration.verbose else [],
210-
)
219+
self.add_exception_to_span(self.function_span, exception, frames_infos)
211220

212221
def add_step_end_event(self, ret_val):
213222
message_id = str(uuid.uuid4())

src/lumigo_tracer/wrappers/http/sync_http_wrappers.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import http.client
33
from io import BytesIO
44
import importlib.util
5-
from typing import Optional
5+
from typing import Optional, Dict
66

77
from lumigo_tracer.libs.wrapt import wrap_function_wrapper
88
from lumigo_tracer.parsing_utils import safe_get_list, recursive_json_join
@@ -36,16 +36,17 @@ def is_lumigo_edge(host: Optional[str]) -> bool:
3636
return False
3737

3838

39-
def add_request_event(parse_params: HttpRequest):
39+
def add_request_event(parse_params: HttpRequest) -> Dict:
4040
"""
4141
This function parses an request event and add it to the span.
4242
"""
4343
if is_lumigo_edge(parse_params.host):
44-
return
44+
return {}
4545
parser = get_parser(parse_params.host)()
4646
msg = parser.parse_request(parse_params)
4747
HttpState.previous_request = parse_params
48-
SpansContainer.get_span().add_span(msg)
48+
new_span = SpansContainer.get_span().add_span(msg)
49+
return new_span
4950

5051

5152
def add_unparsed_request(parse_params: HttpRequest):
@@ -204,9 +205,29 @@ def _requests_wrapper(func, instance, args, kwargs):
204205
This is the wrapper of the function `requests.request`.
205206
This function is being wrapped specifically because it initializes the connection by itself and parses the response,
206207
which creates a gap from the traditional http.client wrapping.
208+
Moreover, these "extra" steps may raise exceptions. We should attach the error to the http span.
207209
"""
208210
start_time = datetime.now()
209-
ret_val = func(*args, **kwargs)
211+
try:
212+
ret_val = func(*args, **kwargs)
213+
except Exception as exception:
214+
with lumigo_safe_execute("requests wrapper exception occurred"):
215+
method = safe_get_list(args, 0, kwargs.get("method", "")).upper()
216+
url = safe_get_list(args, 1, kwargs.get("url"))
217+
if HttpState.previous_request and HttpState.previous_request.host in url:
218+
span = SpansContainer.get_span().get_last_span()
219+
else:
220+
span = add_request_event(
221+
HttpRequest(
222+
host=url,
223+
method=method,
224+
uri=url,
225+
body=kwargs.get("data"),
226+
headers=kwargs.get("headers"),
227+
)
228+
)
229+
SpansContainer.add_exception_to_span(span, exception, [])
230+
raise
210231
with lumigo_safe_execute("requests wrapper time updates"):
211232
SpansContainer.get_span().update_event_times(start_time=start_time)
212233
return ret_val
@@ -276,3 +297,4 @@ def wrap_http_calls():
276297
)
277298
if importlib.util.find_spec("requests"):
278299
wrap_function_wrapper("requests.api", "request", _requests_wrapper)
300+
wrap_function_wrapper("requests", "request", _requests_wrapper)

src/test/unit/wrappers/http/test_sync_http_wrappers.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,55 @@ def delayed_getaddrinfo(*args, **kwargs):
265265
assert span["started"] - start_time < 100
266266

267267

268+
@pytest.mark.parametrize(
269+
"func_to_patch",
270+
[
271+
(socket, "getaddrinfo"), # this function is being called before the request
272+
(http.client.HTTPConnection, "getresponse"), # after the request
273+
],
274+
)
275+
def test_requests_failure_before_http_call(monkeypatch, context, func_to_patch):
276+
@lumigo_tracer.lumigo_tracer()
277+
def lambda_test_function(event, context):
278+
try:
279+
requests.post("https://www.google.com", data=b"123", headers={"a": "b"})
280+
except ZeroDivisionError:
281+
return True
282+
return False
283+
284+
# requests executes this function before/after the http call
285+
monkeypatch.setattr(*func_to_patch, lambda *args, **kwargs: 1 / 0)
286+
287+
assert lambda_test_function({}, context) is True
288+
289+
assert len(SpansContainer.get_span().spans) == 1
290+
span = SpansContainer.get_span().spans[0]
291+
assert span["error"]["message"] == "division by zero"
292+
assert span["info"]["httpInfo"]["request"]["method"] == "POST"
293+
assert span["info"]["httpInfo"]["request"]["body"] == '"123"'
294+
assert span["info"]["httpInfo"]["request"]["headers"]
295+
296+
297+
def test_requests_failure_with_kwargs(monkeypatch, context):
298+
@lumigo_tracer.lumigo_tracer()
299+
def lambda_test_function(event, context):
300+
try:
301+
requests.request(
302+
method="POST", url="https://www.google.com", data=b"123", headers={"a": "b"}
303+
)
304+
except ZeroDivisionError:
305+
return True
306+
return False
307+
308+
monkeypatch.setattr(socket, "getaddrinfo", lambda *args, **kwargs: 1 / 0)
309+
310+
assert lambda_test_function({}, context) is True
311+
312+
assert len(SpansContainer.get_span().spans) == 1
313+
span = SpansContainer.get_span().spans[0]
314+
assert span["info"]["httpInfo"]["request"]["method"] == "POST"
315+
316+
268317
def test_wrapping_with_tags_for_api_gw_headers(monkeypatch, context):
269318
monkeypatch.setattr(auto_tag_event, "AUTO_TAG_API_GW_HEADERS", ["Accept"])
270319

0 commit comments

Comments
 (0)