Skip to content

Commit 7dbb2cc

Browse files
authored
fix(loguru): ensure loguru events are automatically patched (#7995)
Resolves: #7988 ## Checklist - [x] Change(s) are motivated and described in the PR description. - [x] Testing strategy is described if automated tests are not included in the PR. - [x] Risk is outlined (performance impact, potential for breakage, maintainability, etc). - [x] Change is maintainable (easy to change, telemetry, documentation). - [x] [Library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html) are followed. If no release note is required, add label `changelog/no-changelog`. - [x] Documentation is included (in-code, generated user docs, [public corp docs](https://github.com/DataDog/documentation/)). - [x] Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)) ## Reviewer Checklist - [ ] Title is accurate. - [ ] No unnecessary changes are introduced. - [ ] Description motivates each change. - [ ] Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes unless absolutely necessary. - [ ] Testing strategy adequately addresses listed risk(s). - [ ] Change is maintainable (easy to change, telemetry, documentation). - [ ] Release note makes sense to a user of the library. - [ ] Reviewer has explicitly acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment. - [ ] Backport labels are set in a manner that is consistent with the [release branch maintenance policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting) - [ ] If this PR touches code that signs or publishes builds or packages, or handles credentials of any kind, I've requested a review from `@DataDog/security-design-and-guidance`. - [ ] This PR doesn't touch any of that.
1 parent 23c9011 commit 7dbb2cc

File tree

4 files changed

+105
-58
lines changed

4 files changed

+105
-58
lines changed

ddtrace/contrib/loguru/patch.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,18 @@ def _tracer_injection(event_dict):
4747
return event_dict
4848

4949

50-
def _w_add(func, instance, args, kwargs):
51-
# patch logger to include datadog info before logging
52-
instance.configure(patcher=lambda record: record.update(_tracer_injection(record["extra"])))
50+
def _w_configure(func, instance, args, kwargs):
51+
original_patcher = kwargs.get("patcher", None)
52+
instance._dd_original_patcher = original_patcher
53+
if not original_patcher:
54+
# no patcher, we do not need to worry about ddtrace fields being overridden
55+
return func(*args, **kwargs)
56+
57+
def _wrapped_patcher(record):
58+
original_patcher(record)
59+
record.update(_tracer_injection(record["extra"]))
60+
61+
kwargs["patcher"] = _wrapped_patcher
5362
return func(*args, **kwargs)
5463

5564

@@ -61,12 +70,16 @@ def patch():
6170
if getattr(loguru, "_datadog_patch", False):
6271
return
6372
loguru._datadog_patch = True
64-
65-
_w(loguru.logger, "add", _w_add)
73+
# Adds ddtrace fields to loguru logger
74+
loguru.logger.configure(patcher=lambda record: record.update(_tracer_injection(record["extra"])))
75+
# Ensures that calling loguru.logger.configure(..) does not overwrite ddtrace fields
76+
_w(loguru.logger, "configure", _w_configure)
6677

6778

6879
def unpatch():
6980
if getattr(loguru, "_datadog_patch", False):
7081
loguru._datadog_patch = False
7182

72-
_u(loguru.logger, "add")
83+
_u(loguru.logger, "configure")
84+
if hasattr(loguru.logger, "_dd_original_patcher"):
85+
loguru.logger.configure(patcher=loguru.logger._dd_original_patcher)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
fixes:
3+
- |
4+
loguru: Ensures log correlation is enabled when the root logger is initialized. Previously, log correlation was only enabled when a new sink was added.

tests/contrib/loguru/test_loguru_logging.py

Lines changed: 79 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import json
2+
import os
23

34
from loguru import logger
45
import pytest
@@ -13,9 +14,6 @@
1314
from tests.utils import override_global_config
1415

1516

16-
captured_logs = []
17-
18-
1917
def _test_logging(output, span, env, service, version):
2018
dd_trace_id, dd_span_id = (span.trace_id, span.span_id) if span else (0, 0)
2119

@@ -30,12 +28,13 @@ def _test_logging(output, span, env, service, version):
3028
assert json.loads(output[0])["record"]["extra"]["dd.version"] == version or ""
3129

3230

33-
@pytest.fixture(autouse=True)
34-
def patch_loguru():
31+
@pytest.fixture()
32+
def captured_logs():
33+
captured_logs = []
3534
try:
3635
patch()
3736
logger.add(captured_logs.append, serialize=True)
38-
yield
37+
yield captured_logs
3938
finally:
4039
unpatch()
4140

@@ -44,10 +43,9 @@ def patch_loguru():
4443
def global_config():
4544
with override_global_config({"service": "logging", "env": "global.env", "version": "global.version"}):
4645
yield
47-
captured_logs.clear()
4846

4947

50-
def test_log_trace_global_values():
48+
def test_log_trace_global_values(captured_logs):
5149
"""
5250
Check trace info includes global values over local span values
5351
"""
@@ -61,13 +59,68 @@ def test_log_trace_global_values():
6159
_test_logging(captured_logs, span, config.env, config.service, config.version)
6260

6361

64-
def test_log_no_trace():
62+
def test_log_no_trace(captured_logs):
6563
logger.info("Hello!")
6664

6765
_test_logging(captured_logs, None, config.env, config.service, config.version)
6866

6967

70-
@pytest.mark.subprocess(env=dict(DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED="False"))
68+
def test_log_with_default_sink(ddtrace_run_python_code_in_subprocess):
69+
code = """
70+
from loguru import logger
71+
from ddtrace import tracer
72+
73+
with tracer.trace("test.logging") as span:
74+
logger.info("Hello!")
75+
"""
76+
77+
env = os.environ.copy()
78+
env.update(dict(LOGURU_SERIALIZE="1", DD_LOGS_INJECTION="1", DD_SERVICE="dds", DD_ENV="ddenv", DD_VERSION="vv"))
79+
out, err, status, _ = ddtrace_run_python_code_in_subprocess(code, env=env)
80+
81+
assert status == 0, err + out
82+
83+
# default sink is stderr
84+
result = json.loads(err)
85+
assert "Hello!" in result["text"]
86+
assert result["record"]["extra"]["dd.trace_id"] != ""
87+
assert result["record"]["extra"]["dd.span_id"] != ""
88+
assert result["record"]["extra"]["dd.env"] == "ddenv"
89+
assert result["record"]["extra"]["dd.service"] == "dds"
90+
assert result["record"]["extra"]["dd.version"] == "vv"
91+
92+
93+
def test_log_with_default_sink_and_configure(ddtrace_run_python_code_in_subprocess):
94+
code = """
95+
from loguru import logger
96+
from ddtrace import tracer
97+
98+
logger.configure(patcher=lambda r: r.update({"extra": {"dd.new": "cc"}}))
99+
100+
with tracer.trace("test.logging") as span:
101+
logger.info("Hello!")
102+
"""
103+
104+
env = os.environ.copy()
105+
env.update(dict(LOGURU_SERIALIZE="1", DD_LOGS_INJECTION="1", DD_SERVICE="dds", DD_ENV="ddenv", DD_VERSION="vv"))
106+
out, err, status, _ = ddtrace_run_python_code_in_subprocess(code, env=env)
107+
108+
assert status == 0, err + out
109+
110+
# default sink is stderr
111+
result = json.loads(err)
112+
assert "Hello!" in result["text"]
113+
assert result["record"]["extra"]["dd.trace_id"] != ""
114+
assert result["record"]["extra"]["dd.span_id"] != ""
115+
assert result["record"]["extra"]["dd.env"] == "ddenv"
116+
assert result["record"]["extra"]["dd.service"] == "dds"
117+
assert result["record"]["extra"]["dd.version"] == "vv"
118+
assert result["record"]["extra"]["dd.new"] == "cc"
119+
120+
121+
@pytest.mark.subprocess(
122+
ddtrace_run=True, env=dict(DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED="False", DD_LOGS_INJECTION="1")
123+
)
71124
def test_log_trace():
72125
"""
73126
Check logging patched and formatter including trace info when 64bit trace ids are generated.
@@ -79,15 +132,11 @@ def test_log_trace():
79132

80133
from ddtrace import config
81134
from ddtrace import tracer
82-
from ddtrace.contrib.loguru import patch
83-
from ddtrace.contrib.loguru import unpatch
84135

85136
config.service = "logging"
86137
config.env = "global.env"
87138
config.version = "global.version"
88139

89-
patch()
90-
91140
captured_logs = []
92141
logger.remove()
93142
logger.add(captured_logs.append, serialize=True)
@@ -103,12 +152,14 @@ def test_log_trace():
103152
assert json.loads(captured_logs[0])["record"]["extra"]["dd.service"] == "logging"
104153
assert json.loads(captured_logs[0])["record"]["extra"]["dd.version"] == "global.version"
105154

106-
captured_logs.clear()
107-
unpatch()
108-
109155

110156
@pytest.mark.subprocess(
111-
env=dict(DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED="True", DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED="True")
157+
ddtrace_run=True,
158+
env=dict(
159+
DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED="True",
160+
DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED="True",
161+
DD_LOGS_INJECTION="1",
162+
),
112163
)
113164
def test_log_trace_128bit_trace_ids():
114165
"""
@@ -121,16 +172,12 @@ def test_log_trace_128bit_trace_ids():
121172

122173
from ddtrace import config
123174
from ddtrace import tracer
124-
from ddtrace.contrib.loguru import patch
125-
from ddtrace.contrib.loguru import unpatch
126175
from ddtrace.internal.constants import MAX_UINT_64BITS
127176

128177
config.service = "logging"
129178
config.env = "global.env"
130179
config.version = "global.version"
131180

132-
patch()
133-
134181
captured_logs = []
135182
logger.remove()
136183
logger.add(captured_logs.append, serialize=True)
@@ -147,12 +194,14 @@ def test_log_trace_128bit_trace_ids():
147194
assert json.loads(captured_logs[0])["record"]["extra"]["dd.service"] == "logging"
148195
assert json.loads(captured_logs[0])["record"]["extra"]["dd.version"] == "global.version"
149196

150-
captured_logs.clear()
151-
unpatch()
152-
153197

154198
@pytest.mark.subprocess(
155-
env=dict(DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED="True", DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED="False")
199+
ddtrace_run=True,
200+
env=dict(
201+
DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED="True",
202+
DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED="False",
203+
DD_LOGS_INJECTION="1",
204+
),
156205
)
157206
def test_log_trace_128bit_trace_ids_log_64bits():
158207
"""
@@ -165,16 +214,12 @@ def test_log_trace_128bit_trace_ids_log_64bits():
165214

166215
from ddtrace import config
167216
from ddtrace import tracer
168-
from ddtrace.contrib.loguru import patch
169-
from ddtrace.contrib.loguru import unpatch
170217
from ddtrace.internal.constants import MAX_UINT_64BITS
171218

172219
config.service = "logging"
173220
config.env = "global.env"
174221
config.version = "global.version"
175222

176-
patch()
177-
178223
captured_logs = []
179224
logger.remove()
180225
logger.add(captured_logs.append, serialize=True)
@@ -191,21 +236,16 @@ def test_log_trace_128bit_trace_ids_log_64bits():
191236
assert json.loads(captured_logs[0])["record"]["extra"]["dd.service"] == "logging"
192237
assert json.loads(captured_logs[0])["record"]["extra"]["dd.version"] == "global.version"
193238

194-
captured_logs.clear()
195-
unpatch()
196239

197-
198-
@pytest.mark.subprocess(env=dict(DD_TAGS="service:ddtagservice,env:ddenv,version:ddversion"))
240+
@pytest.mark.subprocess(
241+
ddtrace_run=True, env=dict(DD_TAGS="service:ddtagservice,env:ddenv,version:ddversion", DD_LOGS_INJECTION="1")
242+
)
199243
def test_log_DD_TAGS():
200244
import json
201245

202246
from loguru import logger
203247

204248
from ddtrace import tracer
205-
from ddtrace.contrib.loguru import patch
206-
from ddtrace.contrib.loguru import unpatch
207-
208-
patch()
209249

210250
captured_logs = []
211251
logger.remove()
@@ -222,11 +262,8 @@ def test_log_DD_TAGS():
222262
assert json.loads(captured_logs[0])["record"]["extra"]["dd.service"] == "ddtagservice"
223263
assert json.loads(captured_logs[0])["record"]["extra"]["dd.version"] == "ddversion"
224264

225-
captured_logs.clear()
226-
unpatch()
227-
228265

229-
@pytest.mark.subprocess
266+
@pytest.mark.subprocess(ddtrace_run=True, env=dict(DD_LOGS_INJECTION="1"))
230267
def test_configured_format():
231268
"""
232269
Ensure injection works when user configured format is used
@@ -254,15 +291,11 @@ def serialize(record):
254291

255292
from ddtrace import config
256293
from ddtrace import tracer
257-
from ddtrace.contrib.loguru import patch
258-
from ddtrace.contrib.loguru import unpatch
259294

260295
config.service = "logging"
261296
config.env = "global.env"
262297
config.version = "global.version"
263298

264-
patch()
265-
266299
captured_logs = []
267300
logger.remove()
268301
logger.add(captured_logs.append, format=log_format)
@@ -277,6 +310,3 @@ def serialize(record):
277310
assert json.loads(captured_logs[0])["dd.env"] == "global.env"
278311
assert json.loads(captured_logs[0])["dd.service"] == "logging"
279312
assert json.loads(captured_logs[0])["dd.version"] == "global.version"
280-
281-
captured_logs.clear()
282-
unpatch()

tests/contrib/loguru/test_loguru_patch.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ class TestLoguruPatch(PatchTestCase.Base):
2222
__get_version__ = get_version
2323

2424
def assert_module_patched(self, loguru):
25-
self.assert_wrapped(loguru.logger.add)
25+
self.assert_wrapped(loguru.logger.configure)
2626

2727
def assert_not_module_patched(self, loguru):
28-
self.assert_not_wrapped(loguru.logger.add)
28+
self.assert_not_wrapped(loguru.logger.configure)
2929

3030
def assert_not_module_double_patched(self, loguru):
31-
self.assert_not_double_wrapped(loguru.logger.add)
31+
self.assert_not_double_wrapped(loguru.logger.configure)

0 commit comments

Comments
 (0)