Skip to content

Commit d7d34a0

Browse files
Rd 3761 support pymongo (#152)
* support pymongo
1 parent 3c27adb commit d7d34a0

File tree

8 files changed

+196
-0
lines changed

8 files changed

+196
-0
lines changed

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ pytest-cov==2.6.1
66
capturer==2.4
77
attrs==19.1.0
88
requests==2.24.0
9+
pymongo==3.11.0

src/lumigo_tracer/spans_container.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,12 @@ def get_last_span(self) -> Optional[dict]:
144144
return None
145145
return self.spans[-1]
146146

147+
def get_span_by_id(self, span_id: str) -> Optional[dict]:
148+
for span in self.spans:
149+
if span.get("id") == span_id:
150+
return span
151+
return None
152+
147153
def remove_last_span(self) -> Optional[dict]:
148154
return self.spans.pop() if self.spans else None
149155

src/lumigo_tracer/wrappers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from .http.sync_http_wrappers import wrap_http_calls
2+
from .pymongo.pymongo_wrapper import wrap_pymongo
23

34

45
already_wrapped = False
@@ -8,4 +9,5 @@ def wrap():
89
global already_wrapped
910
if not already_wrapped:
1011
wrap_http_calls()
12+
wrap_pymongo()
1113
already_wrapped = True

src/lumigo_tracer/wrappers/pymongo/__init__.py

Whitespace-only changes.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from typing import Dict
2+
3+
import time
4+
import uuid
5+
6+
from lumigo_tracer.lumigo_utils import lumigo_safe_execute, get_logger, lumigo_dumps
7+
from lumigo_tracer.spans_container import SpansContainer
8+
9+
try:
10+
from pymongo import monitoring
11+
except Exception:
12+
monitoring = None
13+
14+
if not monitoring:
15+
LumigoMongoMonitoring = None
16+
else:
17+
18+
class LumigoMongoMonitoring(monitoring.CommandListener): # type: ignore
19+
request_to_span_id: Dict[str, str] = {}
20+
MONGO_SPAN = "mongoDb"
21+
22+
def started(self, event):
23+
with lumigo_safe_execute("pymongo started"):
24+
span_id = str(uuid.uuid4())
25+
LumigoMongoMonitoring.request_to_span_id[event.request_id] = span_id
26+
SpansContainer.get_span().add_span(
27+
{
28+
"id": span_id,
29+
"type": self.MONGO_SPAN,
30+
"started": int(time.time() * 1000),
31+
"databaseName": event.database_name,
32+
"commandName": event.command_name,
33+
"request": lumigo_dumps(event.command),
34+
"mongoRequestId": event.request_id,
35+
"mongoOperationId": event.operation_id,
36+
"mongoConnectionId": event.connection_id,
37+
}
38+
)
39+
40+
def succeeded(self, event):
41+
with lumigo_safe_execute("pymongo succeed"):
42+
if event.request_id not in LumigoMongoMonitoring.request_to_span_id:
43+
get_logger().warning("Mongo span ended without a record on its start")
44+
return
45+
span_id = LumigoMongoMonitoring.request_to_span_id.pop(event.request_id)
46+
span = SpansContainer.get_span().get_span_by_id(span_id)
47+
span.update(
48+
{
49+
"ended": span["started"] + (event.duration_micros / 1000),
50+
"response": lumigo_dumps(event.reply),
51+
}
52+
)
53+
54+
def failed(self, event):
55+
with lumigo_safe_execute("pymongo failed"):
56+
if event.request_id not in LumigoMongoMonitoring.request_to_span_id:
57+
get_logger().warning("Mongo span ended without a record on its start")
58+
return
59+
span_id = LumigoMongoMonitoring.request_to_span_id.pop(event.request_id)
60+
span = SpansContainer.get_span().get_span_by_id(span_id)
61+
span.update(
62+
{
63+
"ended": span["started"] + (event.duration_micros / 1000),
64+
"error": lumigo_dumps(event.failure),
65+
}
66+
)
67+
68+
69+
def wrap_pymongo():
70+
with lumigo_safe_execute("wrap pymogno"):
71+
if monitoring:
72+
get_logger().debug("wrapping pymongo")
73+
monitoring.register(LumigoMongoMonitoring())

src/test/unit/test_spans_container.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,12 @@ def test_get_tags_len():
170170
SpansContainer.get_span().add_tag("k0", "v0")
171171
SpansContainer.get_span().add_tag("k1", "v1")
172172
assert SpansContainer.get_span().get_tags_len() == 2
173+
174+
175+
def test_get_span_by_id():
176+
container = SpansContainer.get_span()
177+
container.add_span({"id": 1, "extra": "a"})
178+
container.add_span({"id": 2, "extra": "b"})
179+
container.add_span({"id": 3, "extra": "c"})
180+
assert SpansContainer.get_span().get_span_by_id(2)["extra"] == "b"
181+
assert SpansContainer.get_span().get_span_by_id(5) is None
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from types import SimpleNamespace
2+
3+
import pytest
4+
5+
from lumigo_tracer.spans_container import SpansContainer
6+
from lumigo_tracer.wrappers.pymongo.pymongo_wrapper import LumigoMongoMonitoring
7+
8+
9+
@pytest.fixture
10+
def start_event():
11+
return SimpleNamespace(
12+
database_name="dname",
13+
command_name="cname",
14+
command="cmd",
15+
request_id="rid",
16+
operation_id="oid",
17+
connection_id="cid",
18+
)
19+
20+
21+
@pytest.fixture
22+
def success_event():
23+
return SimpleNamespace(duration_micros=5000, reply={"code": 200}, request_id="rid")
24+
25+
26+
@pytest.fixture
27+
def fail_event():
28+
return SimpleNamespace(duration_micros=5000, failure={"code": 500}, request_id="rid")
29+
30+
31+
def test_pymongo_happy_flow(monkeypatch, start_event, success_event):
32+
monitor = LumigoMongoMonitoring()
33+
monitor.started(start_event)
34+
monitor.succeeded(success_event)
35+
36+
spans = SpansContainer.get_span().spans
37+
assert len(spans) == 1
38+
assert spans[0]["request"] == '"cmd"'
39+
assert spans[0]["ended"] > spans[0]["started"]
40+
assert spans[0]["response"] == '{"code": 200}'
41+
assert "error" not in spans[0]
42+
43+
44+
def test_pymongo_only_start(monkeypatch, start_event):
45+
monitor = LumigoMongoMonitoring()
46+
monitor.started(start_event)
47+
48+
spans = SpansContainer.get_span().spans
49+
assert len(spans) == 1
50+
assert spans[0]["request"] == '"cmd"'
51+
assert "duration" not in spans[0]
52+
53+
54+
def test_pymongo_error(monkeypatch, start_event, fail_event):
55+
monitor = LumigoMongoMonitoring()
56+
monitor.started(start_event)
57+
monitor.failed(fail_event)
58+
59+
spans = SpansContainer.get_span().spans
60+
assert len(spans) == 1
61+
assert spans[0]["request"] == '"cmd"'
62+
assert spans[0]["ended"] > spans[0]["started"]
63+
assert spans[0]["error"] == '{"code": 500}'
64+
assert "response" not in spans[0]
65+
66+
67+
def test_pymongo_concurrent_events(monkeypatch, start_event, success_event):
68+
monitor = LumigoMongoMonitoring()
69+
monitor.started(start_event)
70+
monitor.started(
71+
SimpleNamespace(
72+
database_name="dname",
73+
command_name="cname",
74+
command="cmd",
75+
request_id="rid2",
76+
operation_id="oid",
77+
connection_id="cid",
78+
)
79+
)
80+
monitor.succeeded(success_event)
81+
82+
spans = SpansContainer.get_span().spans
83+
assert len(spans) == 2
84+
assert spans[0]["mongoRequestId"] == "rid"
85+
assert spans[0]["ended"] > spans[0]["started"]
86+
assert spans[0]["response"] == '{"code": 200}'
87+
88+
assert spans[1]["mongoRequestId"] == "rid2"
89+
assert "ended" not in spans[1]
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import importlib
2+
import sys
3+
4+
import lumigo_tracer
5+
6+
7+
def test_wrapping_without_libraries(monkeypatch):
8+
# remove library from path and reload module
9+
monkeypatch.setitem(sys.modules, "pymongo", None)
10+
wrapper = importlib.reload(lumigo_tracer.wrappers.pymongo.pymongo_wrapper)
11+
assert wrapper.LumigoMongoMonitoring is None
12+
13+
lumigo_tracer.wrappers.wrap() # should succeed
14+
15+
monkeypatch.undo()
16+
importlib.reload(lumigo_tracer.wrappers.pymongo.pymongo_wrapper)

0 commit comments

Comments
 (0)