Skip to content

Commit b004ead

Browse files
authored
Loguru Instrumentation (#552)
* Add loguru instrumentation * Add more loguru versions to test * Correct tox ini file * Fix missing parenthesis * Fix loguru v4 and add patching test * Patch loguru v3 support
1 parent dead2dd commit b004ead

File tree

8 files changed

+585
-1
lines changed

8 files changed

+585
-1
lines changed

newrelic/config.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2313,6 +2313,17 @@ def _process_module_builtin_defaults():
23132313
"instrument_logging",
23142314
)
23152315

2316+
_process_module_definition(
2317+
"loguru",
2318+
"newrelic.hooks.logger_loguru",
2319+
"instrument_loguru",
2320+
)
2321+
_process_module_definition(
2322+
"loguru._logger",
2323+
"newrelic.hooks.logger_loguru",
2324+
"instrument_loguru_logger",
2325+
)
2326+
23162327
_process_module_definition(
23172328
"paste.httpserver",
23182329
"newrelic.hooks.adapter_paste",

newrelic/hooks/logger_loguru.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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+
17+
from newrelic.api.application import application_instance
18+
from newrelic.api.transaction import current_transaction, record_log_event
19+
from newrelic.common.object_wrapper import wrap_function_wrapper
20+
from newrelic.core.config import global_settings
21+
from newrelic.hooks.logger_logging import add_nr_linking_metadata
22+
from newrelic.packages import six
23+
24+
_logger = logging.getLogger(__name__)
25+
26+
def loguru_version():
27+
from loguru import __version__
28+
return tuple(int(x) for x in __version__.split("."))
29+
30+
31+
def _nr_log_forwarder(message_instance):
32+
transaction = current_transaction()
33+
record = message_instance.record
34+
message = record.get("_nr_original_message", record["message"])
35+
36+
if transaction:
37+
settings = transaction.settings
38+
else:
39+
settings = global_settings()
40+
41+
# Return early if application logging not enabled
42+
if settings and settings.application_logging and settings.application_logging.enabled:
43+
level = record["level"]
44+
level_name = "UNKNOWN" if not level else (level.name or "UNKNOWN")
45+
46+
if settings.application_logging.metrics and settings.application_logging.metrics.enabled:
47+
if transaction:
48+
transaction.record_custom_metric("Logging/lines", {"count": 1})
49+
transaction.record_custom_metric("Logging/lines/%s" % level_name, {"count": 1})
50+
else:
51+
application = application_instance(activate=False)
52+
if application and application.enabled:
53+
application.record_custom_metric("Logging/lines", {"count": 1})
54+
application.record_custom_metric("Logging/lines/%s" % level_name, {"count": 1})
55+
56+
if settings.application_logging.forwarding and settings.application_logging.forwarding.enabled:
57+
try:
58+
record_log_event(message, level_name, int(record["time"].timestamp()))
59+
except Exception:
60+
pass
61+
62+
63+
ALLOWED_LOGURU_OPTIONS_LENGTHS = frozenset((8, 9))
64+
65+
def bind_log(level_id, static_level_no, from_decorator, options, message, args, kwargs):
66+
assert len(options) in ALLOWED_LOGURU_OPTIONS_LENGTHS # Assert the options signature we expect
67+
return level_id, static_level_no, from_decorator, list(options), message, args, kwargs
68+
69+
70+
def wrap_log(wrapped, instance, args, kwargs):
71+
try:
72+
level_id, static_level_no, from_decorator, options, message, subargs, subkwargs = bind_log(*args, **kwargs)
73+
options[-2] = nr_log_patcher(options[-2])
74+
except Exception as e:
75+
_logger.debug("Exception in loguru handling: %s" % str(e))
76+
return wrapped(*args, **kwargs)
77+
else:
78+
return wrapped(level_id, static_level_no, from_decorator, options, message, subargs, subkwargs)
79+
80+
81+
def nr_log_patcher(original_patcher=None):
82+
def _nr_log_patcher(record):
83+
if original_patcher:
84+
record = original_patcher(record)
85+
86+
transaction = current_transaction()
87+
88+
if transaction:
89+
settings = transaction.settings
90+
else:
91+
settings = global_settings()
92+
93+
if settings and settings.application_logging and settings.application_logging.enabled:
94+
if settings.application_logging.local_decorating and settings.application_logging.local_decorating.enabled:
95+
record["_nr_original_message"] = message = record["message"]
96+
record["message"] = add_nr_linking_metadata(message)
97+
98+
if loguru_version() > (0, 6, 0):
99+
if original_patcher is not None:
100+
patchers = [p for p in original_patcher] # Consumer iterable into list so we can modify
101+
# Wipe out reference so patchers aren't called twice, as the framework will handle calling other patchers.
102+
original_patcher = None
103+
else:
104+
patchers = []
105+
106+
patchers.append(_nr_log_patcher)
107+
return patchers
108+
else:
109+
return _nr_log_patcher
110+
111+
112+
def wrap_Logger_init(wrapped, instance, args, kwargs):
113+
result = wrapped(*args, **kwargs)
114+
patch_loguru_logger(instance)
115+
return result
116+
117+
118+
def patch_loguru_logger(logger):
119+
if hasattr(logger, "_core"):
120+
if not hasattr(logger._core, "_nr_instrumented"):
121+
logger.add(_nr_log_forwarder, format="{message}")
122+
logger._core._nr_instrumented = True
123+
elif not hasattr(logger, "_nr_instrumented"):
124+
for _, handler in six.iteritems(logger._handlers):
125+
if handler._writer is _nr_log_forwarder:
126+
logger._nr_instrumented = True
127+
return
128+
129+
logger.add(_nr_log_forwarder, format="{message}")
130+
logger._nr_instrumented = True
131+
132+
133+
def instrument_loguru_logger(module):
134+
if hasattr(module, "Logger"):
135+
wrap_function_wrapper(module, "Logger.__init__", wrap_Logger_init)
136+
if hasattr(module.Logger, "_log"):
137+
wrap_function_wrapper(module, "Logger._log", wrap_log)
138+
139+
140+
def instrument_loguru(module):
141+
if hasattr(module, "logger"):
142+
patch_loguru_logger(module.logger)

tests/logger_loguru/conftest.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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+
18+
from testing_support.fixtures import (
19+
code_coverage_fixture,
20+
collector_agent_registration_fixture,
21+
collector_available_fixture,
22+
)
23+
24+
_coverage_source = [
25+
"newrelic.hooks.logger_loguru",
26+
]
27+
28+
code_coverage = code_coverage_fixture(source=_coverage_source)
29+
30+
_default_settings = {
31+
"transaction_tracer.explain_threshold": 0.0,
32+
"transaction_tracer.transaction_threshold": 0.0,
33+
"transaction_tracer.stack_trace_threshold": 0.0,
34+
"debug.log_data_collector_payloads": True,
35+
"debug.record_transaction_failure": True,
36+
"application_logging.enabled": True,
37+
"application_logging.forwarding.enabled": True,
38+
"application_logging.metrics.enabled": True,
39+
"application_logging.local_decorating.enabled": True,
40+
"event_harvest_config.harvest_limits.log_event_data": 100000,
41+
}
42+
43+
collector_agent_registration = collector_agent_registration_fixture(
44+
app_name="Python Agent Test (logger_loguru)",
45+
default_settings=_default_settings,
46+
)
47+
48+
49+
class CaplogHandler(logging.StreamHandler):
50+
"""
51+
To prevent possible issues with pytest's monkey patching
52+
use a custom Caplog handler to capture all records
53+
"""
54+
def __init__(self, *args, **kwargs):
55+
self.records = []
56+
super(CaplogHandler, self).__init__(*args, **kwargs)
57+
58+
def emit(self, record):
59+
self.records.append(self.format(record))
60+
61+
62+
@pytest.fixture(scope="function")
63+
def logger():
64+
import loguru
65+
_logger = loguru.logger
66+
caplog = CaplogHandler()
67+
handler_id = _logger.add(caplog, level="WARNING", format="{message}")
68+
_logger.caplog = caplog
69+
yield _logger
70+
del caplog.records[:]
71+
_logger.remove(handler_id)
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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 newrelic.api.time_trace import current_trace
20+
from newrelic.api.transaction import current_transaction
21+
from testing_support.fixtures import reset_core_stats_engine
22+
from testing_support.validators.validate_log_event_count import validate_log_event_count
23+
from testing_support.validators.validate_log_event_count_outside_transaction import validate_log_event_count_outside_transaction
24+
from testing_support.fixtures import (
25+
validate_transaction_metrics,
26+
)
27+
28+
29+
def set_trace_ids():
30+
txn = current_transaction()
31+
if txn:
32+
txn._trace_id = "abcdefgh12345678"
33+
trace = current_trace()
34+
if trace:
35+
trace.guid = "abcdefgh"
36+
37+
def exercise_logging(logger):
38+
set_trace_ids()
39+
40+
logger.warning("C")
41+
42+
43+
def get_metadata_string(log_message, is_txn):
44+
host = platform.uname().node
45+
assert host
46+
entity_guid = application_settings().entity_guid
47+
if is_txn:
48+
metadata_string = "".join(('NR-LINKING|', entity_guid, '|', host, '|abcdefgh12345678|abcdefgh|Python%20Agent%20Test%20%28logger_loguru%29|'))
49+
else:
50+
metadata_string = "".join(('NR-LINKING|', entity_guid, '|', host, '|||Python%20Agent%20Test%20%28logger_loguru%29|'))
51+
formatted_string = log_message + " " + metadata_string
52+
return formatted_string
53+
54+
55+
@reset_core_stats_engine()
56+
def test_local_log_decoration_inside_transaction(logger):
57+
@validate_log_event_count(1)
58+
@background_task()
59+
def test():
60+
exercise_logging(logger)
61+
assert logger.caplog.records[0] == get_metadata_string('C', True)
62+
63+
test()
64+
65+
66+
@reset_core_stats_engine()
67+
def test_local_log_decoration_outside_transaction(logger):
68+
@validate_log_event_count_outside_transaction(1)
69+
def test():
70+
exercise_logging(logger)
71+
assert logger.caplog.records[0] == get_metadata_string('C', False)
72+
73+
test()
74+
75+
76+
@reset_core_stats_engine()
77+
def test_patcher_application_order(logger):
78+
def patch(record):
79+
record["message"] += "-PATCH"
80+
return record
81+
82+
@validate_log_event_count_outside_transaction(1)
83+
def test():
84+
patch_logger = logger.patch(patch)
85+
exercise_logging(patch_logger)
86+
assert logger.caplog.records[0] == get_metadata_string('C-PATCH', False)
87+
88+
test()

0 commit comments

Comments
 (0)