Skip to content

Commit 84a5d46

Browse files
Structlog processor formatter (#1289)
* Initial commit * Add local_log_decoration tests * Add tests for processor formatter * Add try-except clauses --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent 58363f2 commit 84a5d46

File tree

2 files changed

+289
-0
lines changed

2 files changed

+289
-0
lines changed

newrelic/hooks/logger_structlog.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,17 @@ def new_relic_event_consumer(logger, level, event):
5454
elif isinstance(event, dict):
5555
message = original_message = event.get("event", "")
5656
event_attrs = {k: v for k, v in event.items() if k != "event"}
57+
elif isinstance(event, tuple):
58+
try:
59+
# This accounts for the ProcessorFormatter format:
60+
# tuple[tuple[EventDict], dict[str, dict[str, Any]]]
61+
_event = event[0][0]
62+
message = original_message = _event.get("event")
63+
event_attrs = {k: v for k, v in _event.items() if k != "event"}
64+
except:
65+
# In the case that this is a tuple but not in the
66+
# ProcessorFormatter format. Unclear how to proceed.
67+
return event
5768
else:
5869
# Unclear how to proceed, ignore log. Avoid logging an error message or we may incur an infinite loop.
5970
return event
@@ -64,6 +75,11 @@ def new_relic_event_consumer(logger, level, event):
6475
event = message
6576
elif isinstance(event, dict) and "event" in event:
6677
event["event"] = message
78+
else:
79+
try:
80+
event[0][0]["event"] = message
81+
except:
82+
pass
6783

6884
level_name = normalize_level_name(level)
6985

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
# Copyright 2010 New Relic, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import platform
16+
17+
import pytest
18+
from testing_support.fixtures import (
19+
override_application_settings,
20+
reset_core_stats_engine,
21+
)
22+
from testing_support.validators.validate_custom_metrics_outside_transaction import (
23+
validate_custom_metrics_outside_transaction,
24+
)
25+
from testing_support.validators.validate_log_event_count import validate_log_event_count
26+
from testing_support.validators.validate_log_event_count_outside_transaction import (
27+
validate_log_event_count_outside_transaction,
28+
)
29+
from testing_support.validators.validate_log_events import validate_log_events
30+
from testing_support.validators.validate_log_events_outside_transaction import (
31+
validate_log_events_outside_transaction,
32+
)
33+
from testing_support.validators.validate_transaction_metrics import (
34+
validate_transaction_metrics,
35+
)
36+
37+
from newrelic.api.application import application_settings
38+
from newrelic.api.background_task import background_task, current_transaction
39+
40+
"""
41+
This file tests structlog's ability to render structlog-based
42+
formatters within logging through structlog's `ProcessorFormatter` as
43+
a `logging.Formatter` for both logging as well as structlog log entries.
44+
"""
45+
46+
47+
@pytest.fixture(scope="function")
48+
def structlog_formatter_within_logging(structlog_caplog):
49+
import logging
50+
51+
import structlog
52+
53+
class CaplogHandler(logging.StreamHandler):
54+
"""
55+
To prevent possible issues with pytest's monkey patching
56+
use a custom Caplog handler to capture all records
57+
"""
58+
59+
def __init__(self, *args, **kwargs):
60+
self.records = []
61+
super(CaplogHandler, self).__init__(*args, **kwargs)
62+
63+
def emit(self, record):
64+
self.records.append(self.format(record))
65+
66+
structlog.configure(
67+
processors=[
68+
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
69+
],
70+
logger_factory=lambda *args, **kwargs: structlog_caplog,
71+
)
72+
73+
handler = CaplogHandler()
74+
formatter = structlog.stdlib.ProcessorFormatter(
75+
processors=[structlog.dev.ConsoleRenderer()],
76+
)
77+
78+
handler.setFormatter(formatter)
79+
80+
logging_logger = logging.getLogger()
81+
logging_logger.addHandler(handler)
82+
logging_logger.caplog = handler
83+
logging_logger.setLevel(logging.WARNING)
84+
85+
structlog_logger = structlog.get_logger(logger_attr=2)
86+
87+
yield logging_logger, structlog_logger
88+
89+
90+
@pytest.fixture
91+
def exercise_both_loggers(set_trace_ids, structlog_formatter_within_logging, structlog_caplog):
92+
def _exercise():
93+
if current_transaction():
94+
set_trace_ids()
95+
96+
logging_logger, structlog_logger = structlog_formatter_within_logging
97+
98+
logging_logger.info("Cat", a=42)
99+
logging_logger.error("Dog")
100+
logging_logger.critical("Elephant")
101+
102+
structlog_logger.info("Bird")
103+
structlog_logger.error("Fish", events="water")
104+
structlog_logger.critical("Giraffe")
105+
106+
assert len(structlog_caplog.caplog) == 3 # set to INFO level
107+
assert len(logging_logger.caplog.records) == 2 # set to WARNING level
108+
109+
assert "Dog" in logging_logger.caplog.records[0]
110+
assert "Elephant" in logging_logger.caplog.records[1]
111+
112+
assert "Bird" in structlog_caplog.caplog[0]["event"]
113+
assert "Fish" in structlog_caplog.caplog[1]["event"]
114+
assert "Giraffe" in structlog_caplog.caplog[2]["event"]
115+
116+
return _exercise
117+
118+
119+
# Test attributes
120+
# ---------------------
121+
122+
123+
@validate_log_events(
124+
[
125+
{ # Fixed attributes
126+
"message": "context_attrs: arg1",
127+
"context.kwarg_attr": 1,
128+
"context.logger_attr": 2,
129+
}
130+
],
131+
)
132+
@validate_log_event_count(1)
133+
@background_task()
134+
def test_processor_formatter_context_attributes(structlog_formatter_within_logging):
135+
_, structlog_logger = structlog_formatter_within_logging
136+
structlog_logger.error("context_attrs: %s", "arg1", kwarg_attr=1)
137+
138+
139+
@validate_log_events([{"message": "A", "message.attr": 1}])
140+
@validate_log_event_count(1)
141+
@background_task()
142+
def test_processor_formatter_message_attributes(structlog_formatter_within_logging):
143+
_, structlog_logger = structlog_formatter_within_logging
144+
structlog_logger.error({"message": "A", "attr": 1})
145+
146+
147+
# Test local decorating
148+
# ---------------------
149+
150+
151+
def get_metadata_string(log_message, is_txn):
152+
host = platform.uname()[1]
153+
assert host
154+
entity_guid = application_settings().entity_guid
155+
if is_txn:
156+
metadata_string = (
157+
f"NR-LINKING|{entity_guid}|{host}|abcdefgh12345678|abcdefgh|Python%20Agent%20Test%20%28logger_structlog%29|"
158+
)
159+
else:
160+
metadata_string = f"NR-LINKING|{entity_guid}|{host}|||Python%20Agent%20Test%20%28logger_structlog%29|"
161+
formatted_string = f"{log_message} {metadata_string}"
162+
return formatted_string
163+
164+
165+
@reset_core_stats_engine()
166+
def test_processor_formatter_local_log_decoration_inside_transaction(exercise_both_loggers, structlog_caplog):
167+
@validate_log_event_count(5)
168+
@background_task()
169+
def test():
170+
exercise_both_loggers()
171+
assert get_metadata_string("Fish", True) in structlog_caplog.caplog[1]["event"]
172+
173+
test()
174+
175+
176+
@reset_core_stats_engine()
177+
def test_processor_formatter_local_log_decoration_outside_transaction(exercise_both_loggers, structlog_caplog):
178+
@validate_log_event_count_outside_transaction(5)
179+
def test():
180+
exercise_both_loggers()
181+
assert get_metadata_string("Fish", False) in structlog_caplog.caplog[1]["event"]
182+
183+
test()
184+
185+
186+
# Test log forwarding
187+
# ---------------------
188+
189+
_common_attributes_service_linking = {
190+
"timestamp": None,
191+
"hostname": None,
192+
"entity.name": "Python Agent Test (logger_structlog)",
193+
"entity.guid": None,
194+
}
195+
196+
_common_attributes_trace_linking = {
197+
"span.id": "abcdefgh",
198+
"trace.id": "abcdefgh12345678",
199+
**_common_attributes_service_linking,
200+
}
201+
202+
203+
@reset_core_stats_engine()
204+
@override_application_settings({"application_logging.local_decorating.enabled": False})
205+
def test_processor_formatter_logging_inside_transaction(exercise_both_loggers):
206+
@validate_log_events(
207+
[
208+
{"message": "Dog", "level": "ERROR", **_common_attributes_trace_linking},
209+
{"message": "Elephant", "level": "CRITICAL", **_common_attributes_trace_linking},
210+
{"message": "Bird", "level": "INFO", **_common_attributes_trace_linking},
211+
{"message": "Fish", "level": "ERROR", **_common_attributes_trace_linking},
212+
{"message": "Giraffe", "level": "CRITICAL", **_common_attributes_trace_linking},
213+
]
214+
)
215+
@validate_log_event_count(5)
216+
@background_task()
217+
def test():
218+
exercise_both_loggers()
219+
220+
test()
221+
222+
223+
@reset_core_stats_engine()
224+
@override_application_settings({"application_logging.local_decorating.enabled": False})
225+
def test_processor_formatter_logging_outside_transaction(exercise_both_loggers):
226+
@validate_log_events_outside_transaction(
227+
[
228+
{"message": "Dog", "level": "ERROR", **_common_attributes_service_linking},
229+
{"message": "Elephant", "level": "CRITICAL", **_common_attributes_service_linking},
230+
{"message": "Bird", "level": "INFO", **_common_attributes_service_linking},
231+
{"message": "Fish", "level": "ERROR", **_common_attributes_service_linking},
232+
{"message": "Giraffe", "level": "CRITICAL", **_common_attributes_service_linking},
233+
]
234+
)
235+
@validate_log_event_count_outside_transaction(5)
236+
def test():
237+
exercise_both_loggers()
238+
239+
test()
240+
241+
242+
# Test metrics
243+
# ---------------------
244+
245+
_test_logging_unscoped_metrics = [
246+
("Logging/lines", 5),
247+
("Logging/lines/INFO", 1),
248+
("Logging/lines/ERROR", 2),
249+
("Logging/lines/CRITICAL", 2),
250+
]
251+
252+
253+
@reset_core_stats_engine()
254+
def test_processor_formatter_metrics_inside_transaction(exercise_both_loggers):
255+
@validate_transaction_metrics(
256+
"test_processor_formatter:test_processor_formatter_metrics_inside_transaction.<locals>.test",
257+
custom_metrics=_test_logging_unscoped_metrics,
258+
background_task=True,
259+
)
260+
@background_task()
261+
def test():
262+
exercise_both_loggers()
263+
264+
test()
265+
266+
267+
@reset_core_stats_engine()
268+
def test_processor_formatter_metrics_outside_transaction(exercise_both_loggers):
269+
@validate_custom_metrics_outside_transaction(_test_logging_unscoped_metrics)
270+
def test():
271+
exercise_both_loggers()
272+
273+
test()

0 commit comments

Comments
 (0)