Skip to content

Commit ef43486

Browse files
authored
Merge pull request #559 from newrelic/develop-logging
APM Log Forwarding
2 parents a472c54 + 84749b9 commit ef43486

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+2711
-363
lines changed

newrelic/api/application.py

+8-4
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class Application(object):
2929
_delayed_callables = {}
3030

3131
@staticmethod
32-
def _instance(name):
32+
def _instance(name, activate=True):
3333
if name is None:
3434
name = newrelic.core.config.global_settings().app_name
3535

@@ -44,7 +44,7 @@ def _instance(name):
4444

4545
instance = Application._instances.get(name, None)
4646

47-
if not instance:
47+
if not instance and activate:
4848
with Application._lock:
4949
# Now try again with lock so that only one gets
5050
# to create and add it.
@@ -150,6 +150,10 @@ def record_transaction(self, data):
150150
if self.active:
151151
self._agent.record_transaction(self._name, data)
152152

153+
def record_log_event(self, message, level=None, timestamp=None, priority=None):
154+
if self.active:
155+
self._agent.record_log_event(self._name, message, level, timestamp, priority=priority)
156+
153157
def normalize_name(self, name, rule_type="url"):
154158
if self.active:
155159
return self._agent.normalize_name(self._name, name, rule_type)
@@ -162,8 +166,8 @@ def compute_sampled(self):
162166
return self._agent.compute_sampled(self._name)
163167

164168

165-
def application_instance(name=None):
166-
return Application._instance(name)
169+
def application_instance(name=None, activate=True):
170+
return Application._instance(name, activate=activate)
167171

168172

169173
def register_application(name=None, timeout=None):

newrelic/api/import_hook.py

+1
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/api/log.py

+27-2
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@
1515
import json
1616
import logging
1717
import re
18+
import warnings
1819
from logging import Formatter, LogRecord
1920

2021
from newrelic.api.time_trace import get_linking_metadata
21-
from newrelic.api.transaction import current_transaction
22+
from newrelic.api.transaction import current_transaction, record_log_event
2223
from newrelic.common import agent_http
2324
from newrelic.common.object_names import parse_exc_info
2425
from newrelic.core.attribute import truncate
@@ -85,8 +86,25 @@ def safe_str(object, *args, **kwargs):
8586
return json.dumps(self.log_record_to_dict(record), default=safe_str, separators=(",", ":"))
8687

8788

89+
class NewRelicLogForwardingHandler(logging.Handler):
90+
def emit(self, record):
91+
try:
92+
# Avoid getting local log decorated message
93+
if hasattr(record, "_nr_original_message"):
94+
message = record._nr_original_message()
95+
else:
96+
message = record.getMessage()
97+
98+
record_log_event(message, record.levelname, int(record.created * 1000))
99+
except Exception:
100+
self.handleError(record)
101+
102+
88103
class NewRelicLogHandler(logging.Handler):
89-
"""This is an experimental log handler provided by the community. Use with caution."""
104+
"""
105+
Deprecated: Please use NewRelicLogForwardingHandler instead.
106+
This is an experimental log handler provided by the community. Use with caution.
107+
"""
90108

91109
PATH = "/log/v1"
92110

@@ -104,6 +122,13 @@ def __init__(
104122
ca_bundle_path=None,
105123
disable_certificate_validation=False,
106124
):
125+
warnings.warn(
126+
"The contributed NewRelicLogHandler has been superseded by automatic instrumentation for "
127+
"logging in the standard lib. If for some reason you need to manually configure a handler, "
128+
"please use newrelic.api.log.NewRelicLogForwardingHandler to take advantage of all the "
129+
"features included in application log forwarding such as proper batching.",
130+
DeprecationWarning
131+
)
107132
super(NewRelicLogHandler, self).__init__(level=level)
108133
self.license_key = license_key or self.settings.license_key
109134
self.host = host or self.settings.host or self.default_host(self.license_key)

newrelic/api/time_trace.py

+57-15
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
import logging
16+
import platform
1617
import random
1718
import sys
1819
import time
@@ -574,20 +575,26 @@ def increment_child_count(self):
574575
else:
575576
self.has_async_children = False
576577

577-
def get_linking_metadata(self):
578-
metadata = {
579-
"entity.type": "SERVICE",
580-
}
578+
def _get_service_linking_metadata(self, application=None):
579+
if application is not None:
580+
return get_service_linking_metadata(application)
581+
elif self.transaction is not None:
582+
return get_service_linking_metadata(settings=self.transaction.settings)
583+
else:
584+
return get_service_linking_metadata()
585+
586+
def _get_trace_linking_metadata(self):
587+
metadata = {}
581588
txn = self.transaction
582589
if txn:
583590
metadata["span.id"] = self.guid
584591
metadata["trace.id"] = txn.trace_id
585-
settings = txn.settings
586-
if settings:
587-
metadata["entity.name"] = settings.app_name
588-
entity_guid = settings.entity_guid
589-
if entity_guid:
590-
metadata["entity.guid"] = entity_guid
592+
593+
return metadata
594+
595+
def get_linking_metadata(self, application=None):
596+
metadata = self._get_service_linking_metadata(application)
597+
metadata.update(self._get_trace_linking_metadata())
591598
return metadata
592599

593600

@@ -601,14 +608,49 @@ def current_trace():
601608
return trace_cache().current_trace()
602609

603610

604-
def get_linking_metadata():
611+
def get_trace_linking_metadata():
605612
trace = current_trace()
606613
if trace:
607-
return trace.get_linking_metadata()
614+
return trace._get_trace_linking_metadata()
608615
else:
609-
return {
610-
"entity.type": "SERVICE",
611-
}
616+
return {}
617+
618+
619+
def get_service_linking_metadata(application=None, settings=None):
620+
metadata = {
621+
"entity.type": "SERVICE",
622+
}
623+
624+
trace = current_trace()
625+
if settings is None and trace:
626+
txn = trace.transaction
627+
if txn:
628+
settings = txn.settings
629+
630+
if not settings:
631+
if application is None:
632+
from newrelic.api.application import application_instance
633+
application = application_instance(activate=False)
634+
635+
if application is not None:
636+
settings = application.settings
637+
638+
if settings:
639+
metadata["entity.name"] = settings.app_name
640+
entity_guid = settings.entity_guid
641+
if entity_guid:
642+
metadata["entity.guid"] = entity_guid
643+
metadata["hostname"] = platform.uname()[1]
644+
645+
return metadata
646+
647+
648+
def get_linking_metadata(application=None):
649+
metadata = get_service_linking_metadata()
650+
trace = current_trace()
651+
if trace:
652+
metadata.update(trace._get_trace_linking_metadata())
653+
return metadata
612654

613655

614656
def record_exception(exc=None, value=None, tb=None, params=None, ignore_errors=None, application=None):

newrelic/api/transaction.py

+67-3
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,14 @@
2525
import weakref
2626
from collections import OrderedDict
2727

28+
from newrelic.api.application import application_instance
2829
import newrelic.core.database_node
2930
import newrelic.core.error_node
31+
from newrelic.core.log_event_node import LogEventNode
3032
import newrelic.core.root_node
3133
import newrelic.core.transaction_node
3234
import newrelic.packages.six as six
33-
from newrelic.api.time_trace import TimeTrace
35+
from newrelic.api.time_trace import TimeTrace, get_linking_metadata
3436
from newrelic.common.encoding_utils import (
3537
DistributedTracePayload,
3638
NrTraceState,
@@ -46,18 +48,20 @@
4648
obfuscate,
4749
)
4850
from newrelic.core.attribute import (
51+
MAX_LOG_MESSAGE_LENGTH,
4952
MAX_NUM_USER_ATTRIBUTES,
5053
create_agent_attributes,
5154
create_attributes,
5255
create_user_attributes,
5356
process_user_attribute,
57+
truncate,
5458
)
5559
from newrelic.core.attribute_filter import (
5660
DST_ERROR_COLLECTOR,
5761
DST_NONE,
5862
DST_TRANSACTION_TRACER,
5963
)
60-
from newrelic.core.config import DEFAULT_RESERVOIR_SIZE
64+
from newrelic.core.config import DEFAULT_RESERVOIR_SIZE, LOG_EVENT_RESERVOIR_SIZE
6165
from newrelic.core.custom_event import create_custom_event
6266
from newrelic.core.stack_trace import exception_stack
6367
from newrelic.core.stats_engine import CustomMetrics, SampledDataSet
@@ -204,7 +208,6 @@ def __init__(self, application, enabled=None, source=None):
204208

205209
self._errors = []
206210
self._slow_sql = []
207-
self._custom_events = SampledDataSet(capacity=DEFAULT_RESERVOIR_SIZE)
208211

209212
self._stack_trace_count = 0
210213
self._explain_plan_count = 0
@@ -320,6 +323,13 @@ def __init__(self, application, enabled=None, source=None):
320323
if self._settings:
321324
self.enabled = True
322325

326+
if self._settings:
327+
self._custom_events = SampledDataSet(capacity=self._settings.event_harvest_config.harvest_limits.custom_event_data)
328+
self._log_events = SampledDataSet(capacity=self._settings.event_harvest_config.harvest_limits.log_event_data)
329+
else:
330+
self._custom_events = SampledDataSet(capacity=DEFAULT_RESERVOIR_SIZE)
331+
self._log_events = SampledDataSet(capacity=LOG_EVENT_RESERVOIR_SIZE)
332+
323333
def __del__(self):
324334
self._dead = True
325335
if self._state == self.STATE_RUNNING:
@@ -562,6 +572,7 @@ def __exit__(self, exc, value, tb):
562572
errors=tuple(self._errors),
563573
slow_sql=tuple(self._slow_sql),
564574
custom_events=self._custom_events,
575+
log_events=self._log_events,
565576
apdex_t=self.apdex,
566577
suppress_apdex=self.suppress_apdex,
567578
custom_metrics=self._custom_metrics,
@@ -1465,6 +1476,31 @@ def set_transaction_name(self, name, group=None, priority=None):
14651476
self._group = group
14661477
self._name = name
14671478

1479+
1480+
def record_log_event(self, message, level=None, timestamp=None, priority=None):
1481+
settings = self.settings
1482+
if not (settings and settings.application_logging and settings.application_logging.enabled and settings.application_logging.forwarding and settings.application_logging.forwarding.enabled):
1483+
return
1484+
1485+
timestamp = timestamp if timestamp is not None else time.time()
1486+
level = str(level) if level is not None else "UNKNOWN"
1487+
1488+
if not message or message.isspace():
1489+
_logger.debug("record_log_event called where message was missing. No log event will be sent.")
1490+
return
1491+
1492+
message = truncate(message, MAX_LOG_MESSAGE_LENGTH)
1493+
1494+
event = LogEventNode(
1495+
timestamp=timestamp,
1496+
level=level,
1497+
message=message,
1498+
attributes=get_linking_metadata(),
1499+
)
1500+
1501+
self._log_events.add(event, priority=priority)
1502+
1503+
14681504
def record_exception(self, exc=None, value=None, tb=None, params=None, ignore_errors=None):
14691505
# Deprecation Warning
14701506
warnings.warn(
@@ -1814,6 +1850,34 @@ def record_custom_event(event_type, params, application=None):
18141850
application.record_custom_event(event_type, params)
18151851

18161852

1853+
def record_log_event(message, level=None, timestamp=None, application=None, priority=None):
1854+
"""Record a log event.
1855+
1856+
Args:
1857+
record (logging.Record):
1858+
application (newrelic.api.Application): Application instance.
1859+
"""
1860+
1861+
if application is None:
1862+
transaction = current_transaction()
1863+
if transaction:
1864+
transaction.record_log_event(message, level, timestamp)
1865+
else:
1866+
application = application_instance(activate=False)
1867+
1868+
if application and application.enabled:
1869+
application.record_log_event(message, level, timestamp, priority=priority)
1870+
else:
1871+
_logger.debug(
1872+
"record_log_event has been called but no transaction or application was running. As a result, "
1873+
"the following event has not been recorded. message: %r level: %r timestamp %r. To correct "
1874+
"this problem, supply an application object as a parameter to this record_log_event call.",
1875+
message, level, timestamp,
1876+
)
1877+
elif application.enabled:
1878+
application.record_log_event(message, level, timestamp, priority=priority)
1879+
1880+
18171881
def accept_distributed_trace_payload(payload, transport_type="HTTP"):
18181882
transaction = current_transaction()
18191883
if transaction:

newrelic/common/agent_http.py

+1
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,7 @@ class DeveloperModeClient(SupportabilityMixin, BaseClient):
609609
"error_event_data": None,
610610
"span_event_data": None,
611611
"custom_event_data": None,
612+
"log_event_data": None,
612613
"shutdown": [],
613614
}
614615

0 commit comments

Comments
 (0)