Skip to content

Commit 399c81f

Browse files
umaannamalaiTimPansinolrafeeihmstepanekmergify[bot]
authored
Update structlog instrumentation. (#865)
* Add structlog instrumentation. (#685) * Add initial structlog instrumentation. * Cleanup. * Add processor filtering and attribute testing. * Add more filtering tests. * Add co-authors. Co-authored-by: Tim Pansino <[email protected]> Co-authored-by: Lalleh Rafeei <[email protected]> * Remove pylint codes from flake8 config (#701) * Create pytest fixtures and cleanup tests. Co-authored-by: Tim Pansino <[email protected]> Co-authored-by: Lalleh Rafeei <[email protected]> Co-authored-by: Timothy Pansino <[email protected]> Co-authored-by: Hannah Stepanek <[email protected]> * Updates to release structlog instrumentation. * Update pypy testing versions. * Update from pypy37 to pypy38 for structlog. --------- Co-authored-by: Tim Pansino <[email protected]> Co-authored-by: Lalleh Rafeei <[email protected]> Co-authored-by: Timothy Pansino <[email protected]> Co-authored-by: Hannah Stepanek <[email protected]> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent 62abb45 commit 399c81f

8 files changed

+501
-0
lines changed

newrelic/config.py

+5
Original file line numberDiff line numberDiff line change
@@ -2454,6 +2454,11 @@ def _process_module_builtin_defaults():
24542454
"newrelic.hooks.logger_loguru",
24552455
"instrument_loguru_logger",
24562456
)
2457+
_process_module_definition(
2458+
"structlog._base",
2459+
"newrelic.hooks.logger_structlog",
2460+
"instrument_structlog__base",
2461+
)
24572462

24582463
_process_module_definition(
24592464
"paste.httpserver",

newrelic/hooks/logger_structlog.py

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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+
from newrelic.common.object_wrapper import wrap_function_wrapper
16+
from newrelic.api.transaction import current_transaction, record_log_event
17+
from newrelic.core.config import global_settings
18+
from newrelic.api.application import application_instance
19+
from newrelic.hooks.logger_logging import add_nr_linking_metadata
20+
21+
22+
def normalize_level_name(method_name):
23+
# Look up level number for method name, using result to look up level name for that level number.
24+
# Convert result to upper case, and default to UNKNOWN in case of errors or missing values.
25+
try:
26+
from structlog._log_levels import _LEVEL_TO_NAME, _NAME_TO_LEVEL
27+
return _LEVEL_TO_NAME[_NAME_TO_LEVEL[method_name]].upper()
28+
except Exception:
29+
return "UNKNOWN"
30+
31+
32+
def bind_process_event(method_name, event, event_kw):
33+
return method_name, event, event_kw
34+
35+
36+
def wrap__process_event(wrapped, instance, args, kwargs):
37+
try:
38+
method_name, event, event_kw = bind_process_event(*args, **kwargs)
39+
except TypeError:
40+
return wrapped(*args, **kwargs)
41+
42+
original_message = event # Save original undecorated message
43+
44+
transaction = current_transaction()
45+
46+
if transaction:
47+
settings = transaction.settings
48+
else:
49+
settings = global_settings()
50+
51+
# Return early if application logging not enabled
52+
if settings and settings.application_logging and settings.application_logging.enabled:
53+
if settings.application_logging.local_decorating and settings.application_logging.local_decorating.enabled:
54+
event = add_nr_linking_metadata(event)
55+
56+
# Send log to processors for filtering, allowing any DropEvent exceptions that occur to prevent instrumentation from recording the log event.
57+
result = wrapped(method_name, event, event_kw)
58+
59+
level_name = normalize_level_name(method_name)
60+
61+
if settings.application_logging.metrics and settings.application_logging.metrics.enabled:
62+
if transaction:
63+
transaction.record_custom_metric("Logging/lines", {"count": 1})
64+
transaction.record_custom_metric("Logging/lines/%s" % level_name, {"count": 1})
65+
else:
66+
application = application_instance(activate=False)
67+
if application and application.enabled:
68+
application.record_custom_metric("Logging/lines", {"count": 1})
69+
application.record_custom_metric("Logging/lines/%s" % level_name, {"count": 1})
70+
71+
if settings.application_logging.forwarding and settings.application_logging.forwarding.enabled:
72+
try:
73+
record_log_event(original_message, level_name)
74+
75+
except Exception:
76+
pass
77+
78+
# Return the result from wrapped after we've recorded the resulting log event.
79+
return result
80+
81+
return wrapped(*args, **kwargs)
82+
83+
84+
def instrument_structlog__base(module):
85+
if hasattr(module, "BoundLoggerBase") and hasattr(module.BoundLoggerBase, "_process_event"):
86+
wrap_function_wrapper(module, "BoundLoggerBase._process_event", wrap__process_event)

tests/logger_structlog/conftest.py

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
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 logging
16+
import pytest
17+
from structlog import DropEvent, PrintLogger
18+
from newrelic.api.time_trace import current_trace
19+
from newrelic.api.transaction import current_transaction
20+
from testing_support.fixtures import (
21+
collector_agent_registration_fixture,
22+
collector_available_fixture,
23+
)
24+
25+
_default_settings = {
26+
"transaction_tracer.explain_threshold": 0.0,
27+
"transaction_tracer.transaction_threshold": 0.0,
28+
"transaction_tracer.stack_trace_threshold": 0.0,
29+
"debug.log_data_collector_payloads": True,
30+
"debug.record_transaction_failure": True,
31+
"application_logging.enabled": True,
32+
"application_logging.forwarding.enabled": True,
33+
"application_logging.metrics.enabled": True,
34+
"application_logging.local_decorating.enabled": True,
35+
"event_harvest_config.harvest_limits.log_event_data": 100000,
36+
}
37+
38+
collector_agent_registration = collector_agent_registration_fixture(
39+
app_name="Python Agent Test (logger_structlog)",
40+
default_settings=_default_settings,
41+
)
42+
43+
44+
class StructLogCapLog(PrintLogger):
45+
def __init__(self, caplog):
46+
self.caplog = caplog if caplog is not None else []
47+
48+
def msg(self, event, **kwargs):
49+
self.caplog.append(event)
50+
return
51+
52+
log = debug = info = warn = warning = msg
53+
fatal = failure = err = error = critical = exception = msg
54+
55+
def __repr__(self):
56+
return "<StructLogCapLog %s>" % str(id(self))
57+
58+
__str__ = __repr__
59+
60+
61+
@pytest.fixture
62+
def set_trace_ids():
63+
def _set():
64+
txn = current_transaction()
65+
if txn:
66+
txn._trace_id = "abcdefgh12345678"
67+
trace = current_trace()
68+
if trace:
69+
trace.guid = "abcdefgh"
70+
return _set
71+
72+
def drop_event_processor(logger, method_name, event_dict):
73+
if method_name == "info":
74+
raise DropEvent
75+
else:
76+
return event_dict
77+
78+
79+
@pytest.fixture(scope="function")
80+
def structlog_caplog():
81+
return list()
82+
83+
84+
@pytest.fixture(scope="function")
85+
def logger(structlog_caplog):
86+
import structlog
87+
structlog.configure(processors=[], logger_factory=lambda *args, **kwargs: StructLogCapLog(structlog_caplog))
88+
_logger = structlog.get_logger()
89+
return _logger
90+
91+
92+
@pytest.fixture(scope="function")
93+
def filtering_logger(structlog_caplog):
94+
import structlog
95+
structlog.configure(processors=[drop_event_processor], logger_factory=lambda *args, **kwargs: StructLogCapLog(structlog_caplog))
96+
_filtering_logger = structlog.get_logger()
97+
return _filtering_logger
98+
99+
100+
@pytest.fixture
101+
def exercise_logging_multiple_lines(set_trace_ids, logger, structlog_caplog):
102+
def _exercise():
103+
set_trace_ids()
104+
105+
logger.msg("Cat", a=42)
106+
logger.error("Dog")
107+
logger.critical("Elephant")
108+
109+
assert len(structlog_caplog) == 3
110+
111+
assert "Cat" in structlog_caplog[0]
112+
assert "Dog" in structlog_caplog[1]
113+
assert "Elephant" in structlog_caplog[2]
114+
115+
return _exercise
116+
117+
118+
@pytest.fixture
119+
def exercise_filtering_logging_multiple_lines(set_trace_ids, filtering_logger, structlog_caplog):
120+
def _exercise():
121+
set_trace_ids()
122+
123+
filtering_logger.msg("Cat", a=42)
124+
filtering_logger.error("Dog")
125+
filtering_logger.critical("Elephant")
126+
127+
assert len(structlog_caplog) == 2
128+
129+
assert "Cat" not in structlog_caplog[0]
130+
assert "Dog" in structlog_caplog[0]
131+
assert "Elephant" in structlog_caplog[1]
132+
133+
return _exercise
134+
135+
136+
@pytest.fixture
137+
def exercise_logging_single_line(set_trace_ids, logger, structlog_caplog):
138+
def _exercise():
139+
set_trace_ids()
140+
logger.error("A", key="value")
141+
assert len(structlog_caplog) == 1
142+
143+
return _exercise
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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+
from newrelic.api.background_task import background_task
16+
from testing_support.fixtures import override_application_settings, reset_core_stats_engine
17+
from testing_support.validators.validate_log_event_count import validate_log_event_count
18+
from testing_support.validators.validate_log_event_count_outside_transaction import validate_log_event_count_outside_transaction
19+
from testing_support.validators.validate_log_events import validate_log_events
20+
from testing_support.validators.validate_log_events_outside_transaction import validate_log_events_outside_transaction
21+
22+
23+
_event_attributes = {"message": "A"}
24+
25+
26+
@override_application_settings({
27+
"application_logging.forwarding.context_data.enabled": True,
28+
})
29+
def test_attributes_inside_transaction(exercise_logging_single_line):
30+
@validate_log_events([_event_attributes])
31+
@validate_log_event_count(1)
32+
@background_task()
33+
def test():
34+
exercise_logging_single_line()
35+
36+
test()
37+
38+
39+
@reset_core_stats_engine()
40+
@override_application_settings({
41+
"application_logging.forwarding.context_data.enabled": True,
42+
})
43+
def test_attributes_outside_transaction(exercise_logging_single_line):
44+
@validate_log_events_outside_transaction([_event_attributes])
45+
@validate_log_event_count_outside_transaction(1)
46+
def test():
47+
exercise_logging_single_line()
48+
49+
test()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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+
from newrelic.api.application import application_settings
18+
from newrelic.api.background_task import background_task
19+
from testing_support.fixtures import reset_core_stats_engine
20+
from testing_support.validators.validate_log_event_count import validate_log_event_count
21+
from testing_support.validators.validate_log_event_count_outside_transaction import validate_log_event_count_outside_transaction
22+
23+
24+
def get_metadata_string(log_message, is_txn):
25+
host = platform.uname()[1]
26+
assert host
27+
entity_guid = application_settings().entity_guid
28+
if is_txn:
29+
metadata_string = "".join(('NR-LINKING|', entity_guid, '|', host, '|abcdefgh12345678|abcdefgh|Python%20Agent%20Test%20%28logger_structlog%29|'))
30+
else:
31+
metadata_string = "".join(('NR-LINKING|', entity_guid, '|', host, '|||Python%20Agent%20Test%20%28logger_structlog%29|'))
32+
formatted_string = log_message + " " + metadata_string
33+
return formatted_string
34+
35+
36+
@reset_core_stats_engine()
37+
def test_local_log_decoration_inside_transaction(exercise_logging_single_line, structlog_caplog):
38+
@validate_log_event_count(1)
39+
@background_task()
40+
def test():
41+
exercise_logging_single_line()
42+
assert get_metadata_string('A', True) in structlog_caplog[0]
43+
44+
test()
45+
46+
47+
@reset_core_stats_engine()
48+
def test_local_log_decoration_outside_transaction(exercise_logging_single_line, structlog_caplog):
49+
@validate_log_event_count_outside_transaction(1)
50+
def test():
51+
exercise_logging_single_line()
52+
assert get_metadata_string('A', False) in structlog_caplog[0]
53+
54+
test()

0 commit comments

Comments
 (0)