Skip to content

Commit dead2dd

Browse files
Add builtin logging framework instrumentation. (#549)
* Add logging instrumentation Uma Annamalai [email protected] * Address code review comments. * Fix testing failures Co-authored-by: Timothy Pansino <[email protected]> Co-authored-by: Tim Pansino <[email protected]>
1 parent 2913cc4 commit dead2dd

File tree

13 files changed

+679
-0
lines changed

13 files changed

+679
-0
lines changed

newrelic/api/import_hook.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
# These modules should not be added to the _uninstrumented_modules set
4141
# because they have been deemed okay to import before initialization by
4242
# the customer.
43+
"logging",
4344
"gunicorn.app.base",
4445
"wsgiref.simple_server",
4546
"gevent.wsgi",

newrelic/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2307,6 +2307,12 @@ def _process_module_builtin_defaults():
23072307
"instrument_cherrypy__cptree",
23082308
)
23092309

2310+
_process_module_definition(
2311+
"logging",
2312+
"newrelic.hooks.logger_logging",
2313+
"instrument_logging",
2314+
)
2315+
23102316
_process_module_definition(
23112317
"paste.httpserver",
23122318
"newrelic.hooks.adapter_paste",

newrelic/hooks/logger_logging.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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.application import application_instance
16+
from newrelic.api.time_trace import get_linking_metadata
17+
from newrelic.api.transaction import current_transaction, record_log_event
18+
from newrelic.common.object_wrapper import wrap_function_wrapper, function_wrapper
19+
from newrelic.core.config import global_settings
20+
21+
22+
try:
23+
from urllib import quote
24+
except ImportError:
25+
from urllib.parse import quote
26+
27+
28+
def add_nr_linking_metadata(message):
29+
available_metadata = get_linking_metadata()
30+
entity_name = quote(available_metadata.get("entity.name", ""))
31+
entity_guid = available_metadata.get("entity.guid", "")
32+
span_id = available_metadata.get("span.id", "")
33+
trace_id = available_metadata.get("trace.id", "")
34+
hostname = available_metadata.get("hostname", "")
35+
36+
nr_linking_str = "|".join(("NR-LINKING", entity_guid, hostname, trace_id, span_id, entity_name))
37+
return "%s %s|" % (message, nr_linking_str)
38+
39+
40+
@function_wrapper
41+
def wrap_getMessage(wrapped, instance, args, kwargs):
42+
message = wrapped(*args, **kwargs)
43+
return add_nr_linking_metadata(message)
44+
45+
46+
def bind_callHandlers(record):
47+
return record
48+
49+
50+
def wrap_callHandlers(wrapped, instance, args, kwargs):
51+
transaction = current_transaction()
52+
record = bind_callHandlers(*args, **kwargs)
53+
54+
logger_name = getattr(instance, "name", None)
55+
if logger_name and logger_name.split(".")[0] == "newrelic":
56+
return wrapped(*args, **kwargs)
57+
58+
if transaction:
59+
settings = transaction.settings
60+
else:
61+
settings = global_settings()
62+
63+
# Return early if application logging not enabled
64+
if settings and settings.application_logging and settings.application_logging.enabled:
65+
level_name = str(getattr(record, "levelname", "UNKNOWN"))
66+
if settings.application_logging.metrics and settings.application_logging.metrics.enabled:
67+
if transaction:
68+
transaction.record_custom_metric("Logging/lines", {"count": 1})
69+
transaction.record_custom_metric("Logging/lines/%s" % level_name, {"count": 1})
70+
else:
71+
application = application_instance(activate=False)
72+
if application and application.enabled:
73+
application.record_custom_metric("Logging/lines", {"count": 1})
74+
application.record_custom_metric("Logging/lines/%s" % level_name, {"count": 1})
75+
76+
if settings.application_logging.forwarding and settings.application_logging.forwarding.enabled:
77+
try:
78+
message = record.getMessage()
79+
record_log_event(message, level_name, int(record.created * 1000))
80+
except Exception:
81+
pass
82+
83+
if settings.application_logging.local_decorating and settings.application_logging.local_decorating.enabled:
84+
record._nr_original_message = record.getMessage
85+
record.getMessage = wrap_getMessage(record.getMessage)
86+
87+
return wrapped(*args, **kwargs)
88+
89+
90+
def instrument_logging(module):
91+
if hasattr(module, "Logger"):
92+
if hasattr(module.Logger, "callHandlers"):
93+
wrap_function_wrapper(module, "Logger.callHandlers", wrap_callHandlers)

tests/agent_features/test_collector_payloads.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import pytest
1516
import webtest
1617

1718
from testing_support.fixtures import (validate_error_trace_collector_json,
@@ -22,6 +23,8 @@
2223
from testing_support.sample_applications import (simple_app,
2324
simple_exceptional_app, simple_custom_event_app)
2425

26+
from testing_support.validators.validate_log_event_collector_json import validate_log_event_collector_json
27+
2528

2629
exceptional_application = webtest.TestApp(simple_exceptional_app)
2730
normal_application = webtest.TestApp(simple_app)
@@ -63,3 +66,10 @@ def test_transaction_event_json():
6366
@validate_custom_event_collector_json()
6467
def test_custom_event_json():
6568
custom_event_application.get('/')
69+
70+
71+
@pytest.mark.xfail(reason="Unwritten validator")
72+
@validate_log_event_collector_json
73+
def test_log_event_json():
74+
normal_application.get('/')
75+
raise NotImplementedError("Fix my validator")

tests/logger_logging/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_logging",
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_logging)",
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+
_logger = logging.getLogger("my_app")
65+
caplog = CaplogHandler()
66+
_logger.addHandler(caplog)
67+
_logger.caplog = caplog
68+
_logger.setLevel(logging.WARNING)
69+
yield _logger
70+
del caplog.records[:]
71+
_logger.removeHandler(caplog)
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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+
25+
26+
def set_trace_ids():
27+
txn = current_transaction()
28+
if txn:
29+
txn._trace_id = "abcdefgh12345678"
30+
trace = current_trace()
31+
if trace:
32+
trace.guid = "abcdefgh"
33+
34+
def exercise_logging(logger):
35+
set_trace_ids()
36+
37+
logger.warning("C")
38+
39+
40+
def get_metadata_string(log_message, is_txn):
41+
host = platform.uname()[1]
42+
assert host
43+
entity_guid = application_settings().entity_guid
44+
if is_txn:
45+
metadata_string = "".join(('NR-LINKING|', entity_guid, '|', host, '|abcdefgh12345678|abcdefgh|Python%20Agent%20Test%20%28logger_logging%29|'))
46+
else:
47+
metadata_string = "".join(('NR-LINKING|', entity_guid, '|', host, '|||Python%20Agent%20Test%20%28logger_logging%29|'))
48+
formatted_string = log_message + " " + metadata_string
49+
return formatted_string
50+
51+
52+
@reset_core_stats_engine()
53+
def test_local_log_decoration_inside_transaction(logger):
54+
@validate_log_event_count(1)
55+
@background_task()
56+
def test():
57+
exercise_logging(logger)
58+
assert logger.caplog.records[0] == get_metadata_string('C', True)
59+
60+
test()
61+
62+
63+
@reset_core_stats_engine()
64+
def test_local_log_decoration_outside_transaction(logger):
65+
@validate_log_event_count_outside_transaction(1)
66+
def test():
67+
exercise_logging(logger)
68+
assert logger.caplog.records[0] == get_metadata_string('C', False)
69+
70+
test()
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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.background_task import background_task
18+
from newrelic.api.time_trace import current_trace
19+
from newrelic.api.transaction import current_transaction
20+
from testing_support.fixtures import reset_core_stats_engine
21+
from testing_support.validators.validate_log_event_count import validate_log_event_count
22+
from testing_support.validators.validate_log_event_count_outside_transaction import validate_log_event_count_outside_transaction
23+
from testing_support.validators.validate_log_events import validate_log_events
24+
from testing_support.validators.validate_log_events_outside_transaction import validate_log_events_outside_transaction
25+
26+
27+
def set_trace_ids():
28+
txn = current_transaction()
29+
if txn:
30+
txn._trace_id = "abcdefgh12345678"
31+
trace = current_trace()
32+
if trace:
33+
trace.guid = "abcdefgh"
34+
35+
def exercise_logging(logger):
36+
set_trace_ids()
37+
38+
logger.debug("A")
39+
logger.info("B")
40+
logger.warning("C")
41+
logger.error("D")
42+
logger.critical("E")
43+
44+
assert len(logger.caplog.records) == 3
45+
46+
def update_all(events, attrs):
47+
for event in events:
48+
event.update(attrs)
49+
50+
51+
_common_attributes_service_linking = {"timestamp": None, "hostname": None, "entity.name": "Python Agent Test (logger_logging)", "entity.guid": None}
52+
_common_attributes_trace_linking = {"span.id": "abcdefgh", "trace.id": "abcdefgh12345678"}
53+
_common_attributes_trace_linking.update(_common_attributes_service_linking)
54+
55+
_test_logging_inside_transaction_events = [
56+
{"message": "C", "level": "WARNING"},
57+
{"message": "D", "level": "ERROR"},
58+
{"message": "E", "level": "CRITICAL"},
59+
]
60+
update_all(_test_logging_inside_transaction_events, _common_attributes_trace_linking)
61+
62+
63+
def test_logging_inside_transaction(logger):
64+
@validate_log_events(_test_logging_inside_transaction_events)
65+
@validate_log_event_count(3)
66+
@background_task()
67+
def test():
68+
exercise_logging(logger)
69+
70+
test()
71+
72+
73+
_test_logging_outside_transaction_events = [
74+
{"message": "C", "level": "WARNING"},
75+
{"message": "D", "level": "ERROR"},
76+
{"message": "E", "level": "CRITICAL"},
77+
]
78+
update_all(_test_logging_outside_transaction_events, _common_attributes_service_linking)
79+
80+
81+
@reset_core_stats_engine()
82+
def test_logging_outside_transaction(logger):
83+
@validate_log_events_outside_transaction(_test_logging_outside_transaction_events)
84+
@validate_log_event_count_outside_transaction(3)
85+
def test():
86+
exercise_logging(logger)
87+
88+
test()
89+
90+
91+
@reset_core_stats_engine()
92+
def test_logging_newrelic_logs_not_forwarded(logger):
93+
@validate_log_event_count(0)
94+
@background_task()
95+
def test():
96+
nr_logger = logging.getLogger("newrelic")
97+
nr_logger.addHandler(logger.caplog)
98+
nr_logger.error("A")
99+
assert len(logger.caplog.records) == 1
100+
101+
test()

0 commit comments

Comments
 (0)