Skip to content

Commit e981bbc

Browse files
committed
Add events support
1 parent f359cd5 commit e981bbc

File tree

2 files changed

+157
-3
lines changed

2 files changed

+157
-3
lines changed

newrelic/api/opentelemetry.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
from newrelic.api.message_trace import MessageTrace
3333
from newrelic.api.message_transaction import MessageTransaction
3434
from newrelic.api.time_trace import current_trace, notice_error
35-
from newrelic.api.transaction import Sentinel, current_transaction
35+
from newrelic.api.transaction import Sentinel, current_transaction, record_log_event, record_custom_event
3636
from newrelic.api.web_transaction import WebTransaction, WSGIWebTransaction
3737
from newrelic.core.database_utils import generate_dynamodb_arn, get_database_operation_target_from_statement
3838
from newrelic.core.otlp_utils import create_resource
@@ -225,8 +225,32 @@ def _set_attributes_in_nr(self, otel_attributes=None):
225225
self.nr_trace.add_custom_attribute(key, value)
226226

227227
def add_event(self, name, attributes=None, timestamp=None):
228-
# TODO: Not implemented yet.
229-
raise NotImplementedError("Events are not implemented yet.")
228+
if not name or not isinstance(name, str):
229+
raise ValueError("Event name is required and must be a string.")
230+
231+
log_kwargs = {"message": name}
232+
event_kwargs = {"event_type": name}
233+
234+
# Not sure if we reach this point without a transaction
235+
if not self.nr_transaction:
236+
application = application_instance(activate=False)
237+
event_kwargs["application"] = application
238+
if timestamp and isinstance(timestamp, (int, float)):
239+
# Convert OTel timestamp (ns) to NR timestamp (ms)
240+
# If not valid timestamp, ignore it, and NR will
241+
# use its own timestamp.
242+
log_kwargs["timestamp"] = int(timestamp * 1e6)
243+
244+
if not attributes:
245+
# Log event
246+
record_log_event(**log_kwargs)
247+
elif isinstance(attributes, dict):
248+
# Custom event
249+
event_kwargs["params"] = attributes
250+
record_custom_event(**event_kwargs)
251+
else:
252+
raise ValueError("Event attributes must be a dictionary.")
253+
230254

231255
def add_link(self, context=None, attributes=None):
232256
# TODO: Not implemented yet.
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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 pytest
16+
from testing_support.fixtures import override_application_settings, reset_core_stats_engine
17+
from testing_support.validators.validate_error_event_attributes import validate_error_event_attributes
18+
from testing_support.validators.validate_log_events import validate_log_events
19+
from testing_support.validators.validate_log_event_count import validate_log_event_count
20+
from testing_support.validators.validate_custom_event import validate_custom_event_count
21+
from testing_support.validators.validate_custom_events import validate_custom_events
22+
23+
from newrelic.api.background_task import background_task
24+
25+
# Log Event Tests
26+
27+
@override_application_settings({"application_logging.forwarding.enabled": True})
28+
@validate_log_events([{"message": "otel_event"}])
29+
def test_events_as_logs(tracer):
30+
@background_task()
31+
def _test():
32+
with tracer.start_as_current_span("otelspan") as otel_span:
33+
otel_span.add_event("otel_event")
34+
_test()
35+
36+
37+
@pytest.mark.parametrize("name", [{"dict": "value"}, ["list", "of", "values"], 42, None])
38+
@override_application_settings({"application_logging.forwarding.enabled": True})
39+
@validate_log_event_count(0)
40+
def test_events_as_logs_with_bad_name_argument(tracer, name):
41+
_exact_attrs = {
42+
"agent": {},
43+
"user": {'exception.escaped': False},
44+
"intrinsic": {
45+
"error.class": "builtins:ValueError",
46+
"error.message": "Event name is required and must be a string.",
47+
"error.expected": False,
48+
"transactionName": "OtherTransaction/Function/test_events:test_events_as_logs_with_bad_name_argument.<locals>._test",
49+
},
50+
}
51+
52+
@validate_error_event_attributes(exact_attrs=_exact_attrs)
53+
@background_task()
54+
def _test():
55+
with pytest.raises(ValueError):
56+
with tracer.start_as_current_span("otelspan") as otel_span:
57+
otel_span.add_event(name)
58+
_test()
59+
60+
61+
# While NR has the ability to record log events outside of
62+
# a transaction, the Hybrid Agent does not. The span created
63+
# here will be a NonRecordingSpan and thus the event will
64+
# not be recorded.
65+
@override_application_settings({"application_logging.forwarding.enabled": True})
66+
@validate_log_event_count(0)
67+
def test_events_as_logs_outside_transaction(tracer):
68+
def _test():
69+
with tracer.start_as_current_span("otelspan") as otel_span:
70+
otel_span.add_event("otel_event")
71+
_test()
72+
73+
# Custom Event Tests
74+
75+
_event_attributes = {
76+
"key1": "value1",
77+
"key2": 42,
78+
}
79+
80+
@reset_core_stats_engine()
81+
@validate_custom_events([({"type": "otel_event"}, _event_attributes)])
82+
def test_events_as_custom_events(tracer):
83+
@background_task()
84+
def _test():
85+
with tracer.start_as_current_span("otelspan") as otel_span:
86+
otel_span.add_event("otel_event", attributes=_event_attributes)
87+
_test()
88+
89+
90+
# Instead of falling back to recording a log event when the
91+
# attributes argument is invalid, a ValueError is raised.
92+
# This is to ensure that the developer is aware that their
93+
# event is not being recorded as intended.
94+
@pytest.mark.parametrize("attributes", ["string", ["list", "of", "values"], 42])
95+
@reset_core_stats_engine()
96+
@validate_log_event_count(0)
97+
@validate_custom_event_count(0)
98+
def test_events_as_custom_events_with_bad_attributes_argument(tracer, attributes):
99+
_exact_attrs = {
100+
"agent": {},
101+
"user": {'exception.escaped': False},
102+
"intrinsic": {
103+
"error.class": "builtins:ValueError",
104+
"error.message": "Event attributes must be a dictionary.",
105+
"error.expected": False,
106+
"transactionName": "OtherTransaction/Function/test_events:test_events_as_custom_events_with_bad_attributes_argument.<locals>._test",
107+
},
108+
}
109+
110+
@validate_error_event_attributes(exact_attrs=_exact_attrs)
111+
@background_task()
112+
def _test():
113+
with pytest.raises(ValueError):
114+
with tracer.start_as_current_span("otelspan") as otel_span:
115+
otel_span.add_event("otel_event", attributes=attributes)
116+
_test()
117+
118+
119+
# While NR has the ability to record custom events outside of
120+
# a transaction, the Hybrid Agent does not. The span created
121+
# here will be a NonRecordingSpan and thus the event will
122+
# not be recorded.
123+
@reset_core_stats_engine()
124+
@validate_custom_event_count(0)
125+
def test_events_as_custom_events_outside_transaction(tracer):
126+
def _test():
127+
with tracer.start_as_current_span("otelspan") as otel_span:
128+
otel_span.add_event("otel_event", attributes=_event_attributes)
129+
_test()
130+

0 commit comments

Comments
 (0)