Skip to content

Commit a884f0e

Browse files
Add include/exclude settings for Hybrid Agent tracers (#1681)
* Add include/exclude settings for tracers * Fix typos * Remove duplicate test case * [MegaLinter] Apply linters fixes * [MegaLinter] Apply linters fixes * Remove redundant ENV VAR logic * [MegaLinter] Apply linters fixes * Safeguard explicit setting of exclude/include to None * Remove redundant checks * Comments to clarify settings used --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent 7343f58 commit a884f0e

File tree

20 files changed

+291
-95
lines changed

20 files changed

+291
-95
lines changed

newrelic/api/opentelemetry.py

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
import json
1616
import logging
17-
import os
1817
import sys
1918
import time
2019
from contextlib import contextmanager
@@ -46,6 +45,7 @@
4645
from newrelic.api.transaction import Sentinel, current_transaction
4746
from newrelic.api.web_transaction import WebTransaction, WSGIWebTransaction
4847
from newrelic.core.attribute import sanitize
48+
from newrelic.core.config import global_settings
4949
from newrelic.core.database_utils import (
5050
_all_literals_re,
5151
_quotes_table,
@@ -175,6 +175,9 @@ def __init__(
175175
# create another transaction or trace, but rather just
176176
# append existing attributes to the existing transaction.
177177
self.nr_trace = current_nr_trace
178+
if not self.nr_trace:
179+
return
180+
178181
# Add Instrumentation Scope Attributes
179182
self.nr_trace._add_agent_attribute("otel.scope.name", self.attributes.get("library_name"))
180183
self.nr_trace._add_agent_attribute("otel.scope.version", self.attributes.get("library_version"))
@@ -658,6 +661,7 @@ def __init__(
658661
self.schema_url = schema_url
659662
self.tracer_attributes = attributes or {}
660663
self.resource = resource
664+
self.exclude_tracer = False
661665

662666
def _create_web_transaction(self, nr_headers=None):
663667
if "nr.wsgi.environ" in self.attributes:
@@ -739,12 +743,17 @@ def start_span(
739743

740744
self._record_exception = record_exception
741745
self.set_status_on_exception = set_status_on_exception
746+
self.settings = getattr(self.nr_application, "settings", None) or global_settings()
742747

743-
if not (
744-
self.nr_application.settings and self.nr_application.settings.opentelemetry.enabled
745-
) and not os.environ.get("NEW_RELIC_OPENTELEMETRY_ENABLED"):
748+
if self.settings and not self.settings.opentelemetry.enabled:
746749
return otel_api_trace.INVALID_SPAN
747750

751+
if self.settings and (
752+
not self.settings.opentelemetry.traces.enabled
753+
or (self.instrumentation_library in self.settings.opentelemetry.traces.exclude)
754+
):
755+
self.exclude_tracer = True
756+
748757
# Retrieve parent span
749758
parent_span_context = otel_api_trace.get_current_span(context).get_span_context()
750759

@@ -757,7 +766,7 @@ def start_span(
757766

758767
parent_span_trace_id = None
759768
nr_headers = {}
760-
if parent_span_context and self.nr_application.settings.distributed_tracing.enabled:
769+
if parent_span_context and self.settings.distributed_tracing.enabled:
761770
parent_span_trace_id = parent_span_context.trace_id
762771
if len(parent_span_context.trace_state) > 0:
763772
# If headers did not propagate from an existing transaction due
@@ -770,16 +779,14 @@ def start_span(
770779
f"00-{parent_span_trace_id:032x}-{parent_span_span_id:016x}-{'01' if parent_span_trace_flag else '00'}"
771780
)
772781

773-
if not self.nr_application.settings.opentelemetry.traces.enabled:
782+
if self.exclude_tracer:
774783
create_nr_trace = False
775784

776785
# If remote_parent, transaction must be created, regardless of kind type
777786
# Make sure we transfer DT headers when we are here, if DT is enabled
778-
if parent_span_context and parent_span_context.is_remote:
787+
if parent_span_context and parent_span_context.is_remote and not self.exclude_tracer:
779788
if kind in (otel_api_trace.SpanKind.SERVER, otel_api_trace.SpanKind.CLIENT):
780789
transaction = self._create_web_transaction(nr_headers)
781-
if not self.nr_application.settings.opentelemetry.traces.enabled:
782-
transaction.ignore_transaction = True
783790
transaction.__enter__()
784791
# If a transaction was already active, we want to create
785792
# an NR trace under the existing transaction. Otherwise,
@@ -789,8 +796,6 @@ def start_span(
789796
create_nr_trace = False
790797
elif kind in (otel_api_trace.SpanKind.PRODUCER, otel_api_trace.SpanKind.INTERNAL):
791798
transaction = BackgroundTask(self.nr_application, name=self.name)
792-
if not self.nr_application.settings.opentelemetry.traces.enabled:
793-
transaction.ignore_transaction = True
794799
transaction.__enter__()
795800
# If a transaction was already active, we want to create
796801
# an NR trace under the existing transaction. Otherwise,
@@ -806,8 +811,6 @@ def start_span(
806811
application=self.nr_application,
807812
headers=nr_headers,
808813
)
809-
if not self.nr_application.settings.opentelemetry.traces.enabled:
810-
transaction.ignore_transaction = True
811814
transaction.__enter__()
812815
# In the case of a Kafka consumer span, we do not want to create
813816
# a trace regardless of whether a transaction already existed.
@@ -831,14 +834,12 @@ def start_span(
831834
if kind == otel_api_trace.SpanKind.SERVER:
832835
if transaction:
833836
nr_trace_type = FunctionTrace
834-
elif not transaction:
837+
elif not transaction and not self.exclude_tracer:
835838
transaction = self._create_web_transaction(nr_headers)
836839

837840
transaction._trace_id = (
838841
f"{parent_span_trace_id:x}" if parent_span_trace_id else transaction.trace_id
839842
)
840-
if not self.nr_application.settings.opentelemetry.traces.enabled:
841-
transaction.ignore_transaction = True
842843
transaction.__enter__()
843844
create_nr_trace = False
844845
elif kind == otel_api_trace.SpanKind.INTERNAL:
@@ -869,16 +870,14 @@ def start_span(
869870
# Note that for Kafka, this flag will not be
870871
# set, so we will not create a MessageTrace
871872
nr_trace_type = MessageTrace
872-
else:
873+
elif not self.exclude_tracer:
873874
transaction = MessageTransaction(
874875
library=self.instrumentation_library,
875876
destination_type="Exchange",
876877
destination_name=self.name,
877878
application=self.nr_application,
878879
headers=nr_headers,
879880
)
880-
if not self.nr_application.settings.opentelemetry.traces.enabled:
881-
transaction.ignore_transaction = True
882881
transaction.__enter__()
883882
# In the case of a Kafka consumer span, we do not want to create
884883
# a trace regardless of whether a transaction already existed.

newrelic/common/opentelemetry_tracers.py

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,27 @@
1717
# OpenTelemetry instrumentor and the list of targets in the
1818
# NR hooks that need to be disabled if the OpenTelemetry
1919
# instrumentor is to be used instead.
20-
HYBRID_AGENT_DEFAULT_INCLUDED_TRACERS_TO_NR_HOOKS = {
20+
OPENTELEMETRY_ONLY_TRACERS_TO_NR_HOOKS = {
2121
"aio-pika": [],
22+
"aiopg": [
23+
"psycopg2",
24+
"psycopg2._psycopg2",
25+
"psycopg2.extensions",
26+
"psycopg2._json",
27+
"psycopg2._range",
28+
"psycopg2.sql",
29+
],
30+
"aiokafka": [],
31+
"asyncclick": [],
32+
"click": [],
33+
"remoulade": [],
34+
"sqlalchemy": [],
35+
"system_metrics": [],
36+
"threading": [],
37+
"tortoiseorm": [],
38+
}
39+
40+
NEW_RELIC_AND_OPENTELEMETRY_TRACERS_TO_NR_HOOKS = {
2241
"aiohttp_client": ["aiohttp.client", "aiohttp.client_reqrep"],
2342
"aiohttp_server": [
2443
"aiohttp.web",
@@ -28,15 +47,6 @@
2847
"aiohttp.web_urldispatcher",
2948
"aiohttp.protocol",
3049
],
31-
"aiokafka": [],
32-
"aiopg": [
33-
"psycopg2",
34-
"psycopg2._psycopg2",
35-
"psycopg2.extensions",
36-
"psycopg2._json",
37-
"psycopg2._range",
38-
"psycopg2.sql",
39-
],
4050
"ariadne": [
4151
"ariadne.graphql",
4252
"graphql.graphql",
@@ -50,14 +60,12 @@
5060
"graphql.validation.validation",
5161
"graphql.type.schema",
5262
],
53-
"asyncclick": [],
5463
"asyncpg": ["asyncpg.connect_utils", "asyncpg.protocol"],
5564
# "boto": [], # this is boto3
5665
# "boto3": [], # this is boto3sqs
5766
# "botocore": [],
5867
"cassandra": ["cassandra", "cassandra.cluster"],
5968
"celery": ["celery.local", "celery.app.trace", "celery.worker", "celery.concurrency.prefork", "billiard.pool"],
60-
"click": [],
6169
"confluent_kafka": [
6270
"confluent_kafka.cimpl",
6371
"confluent_kafka.serializing_producer",
@@ -179,7 +187,6 @@
179187
"redis.commands.graph.commands",
180188
"redis.commands.vectorset.commands",
181189
],
182-
"remoulade": [],
183190
"requests": [
184191
"requests.sessions",
185192
"requests.api",
@@ -189,7 +196,6 @@
189196
"http.client",
190197
"httplib2",
191198
],
192-
"sqlalchemy": [],
193199
"sqlite3": ["sqlite3", "sqlite3.dbapi2", "pysqlite2", "pysqlite2.dbapi2"],
194200
"starlette": [
195201
"starlette.requests",
@@ -215,8 +221,6 @@
215221
"graphql.validation.validation",
216222
"graphql.type.schema",
217223
],
218-
"system_metrics": [],
219-
"threading": [],
220224
"tornado": [
221225
"tornado.httpserver",
222226
"tornado.httputil",
@@ -225,11 +229,14 @@
225229
"tornado.web",
226230
"http.client",
227231
],
228-
"tortoiseorm": [],
229232
"urllib": ["urllib.request", "http.client"],
230233
"urllib3": ["urllib3.connectionpool", "urllib3.connection", "requests.packages.urllib3.connection", "http.client"],
231234
}
232235

236+
ALL_LIBRARY_TRACERS_TO_NR_HOOKS = {
237+
**NEW_RELIC_AND_OPENTELEMETRY_TRACERS_TO_NR_HOOKS,
238+
**OPENTELEMETRY_ONLY_TRACERS_TO_NR_HOOKS,
239+
}
233240

234241
TEMPORARILY_DISABLED_OPENTELEMETRY_FRAMEWORKS = {
235242
"boto",

newrelic/config.py

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@
4444
from newrelic.common.log_file import initialize_logging
4545
from newrelic.common.object_names import callable_name, expand_builtin_exception_name
4646
from newrelic.common.opentelemetry_tracers import (
47-
HYBRID_AGENT_DEFAULT_INCLUDED_TRACERS_TO_NR_HOOKS,
47+
ALL_LIBRARY_TRACERS_TO_NR_HOOKS,
48+
OPENTELEMETRY_ONLY_TRACERS_TO_NR_HOOKS,
4849
TEMPORARILY_DISABLED_OPENTELEMETRY_FRAMEWORKS,
4950
)
5051
from newrelic.common.package_version_utils import get_package_version
@@ -219,6 +220,10 @@ def _map_split_strings(s):
219220
return s.split()
220221

221222

223+
def _map_split_string_by_comma(s):
224+
return newrelic.core.config.parse_comma_separated_into_set(s)
225+
226+
222227
def _map_console_listener_socket(s):
223228
return s % {"pid": os.getpid()}
224229

@@ -680,6 +685,8 @@ def _process_configuration(section):
680685
_process_setting(section, "instrumentation.middleware.django.include", "get", _map_inc_excl_middleware)
681686
_process_setting(section, "opentelemetry.enabled", "getboolean", None)
682687
_process_setting(section, "opentelemetry.traces.enabled", "getboolean", None)
688+
_process_setting(section, "opentelemetry.traces.exclude", "get", _map_split_string_by_comma)
689+
_process_setting(section, "opentelemetry.traces.include", "get", _map_split_string_by_comma)
683690

684691

685692
# Loading of configuration from specified file and for specified
@@ -4456,14 +4463,35 @@ def _is_installed(req):
44564463
return False
44574464

44584465

4459-
def _process_opentelemetry_instrumentation_entry_points(
4460-
final_include_dict=HYBRID_AGENT_DEFAULT_INCLUDED_TRACERS_TO_NR_HOOKS,
4461-
):
4462-
from importlib.metadata import entry_points
4466+
def _tracer_include_and_exclude_filter():
4467+
"""Uses the values in `opentelemetry.traces.include` and
4468+
`opentelemetry.traces.exclude` settings, along with the
4469+
internal included defaults, to determine which tracers
4470+
should be used.
4471+
"""
4472+
4473+
user_exclude = _settings.opentelemetry.traces.exclude or newrelic.core.config._environ_as_comma_separated_set(
4474+
"NEW_RELIC_OPENTELEMETRY_TRACES_EXCLUDE"
4475+
)
4476+
user_include = _settings.opentelemetry.traces.include or newrelic.core.config._environ_as_comma_separated_set(
4477+
"NEW_RELIC_OPENTELEMETRY_TRACES_INCLUDE"
4478+
)
44634479

4480+
tracer_include_union = {*OPENTELEMETRY_ONLY_TRACERS_TO_NR_HOOKS.keys(), *user_include}
4481+
mask = tracer_include_union & user_exclude
4482+
final_include_set = tracer_include_union ^ mask
4483+
4484+
return final_include_set
4485+
4486+
4487+
def _process_opentelemetry_instrumentation_entry_points():
44644488
if not _settings.opentelemetry.enabled or not _is_installed("opentelemetry-api"):
44654489
return
44664490

4491+
include_set = _tracer_include_and_exclude_filter()
4492+
4493+
from importlib.metadata import entry_points
4494+
44674495
group = "opentelemetry_instrumentor"
44684496

44694497
try:
@@ -4476,27 +4504,19 @@ def _process_opentelemetry_instrumentation_entry_points(
44764504
entry_points_generator = (
44774505
entrypoint
44784506
for entrypoint in _entry_points
4479-
if entrypoint.name in final_include_dict
4480-
and entrypoint.name not in TEMPORARILY_DISABLED_OPENTELEMETRY_FRAMEWORKS
4507+
if entrypoint.name in include_set and entrypoint.name not in TEMPORARILY_DISABLED_OPENTELEMETRY_FRAMEWORKS
44814508
)
44824509

44834510
for entrypoint in entry_points_generator:
44844511
opentelemetry_entrypoints.append(entrypoint)
4485-
opentelemetry_instrumentation.extend(final_include_dict[entrypoint.name])
4512+
opentelemetry_instrumentation.extend(ALL_LIBRARY_TRACERS_TO_NR_HOOKS[entrypoint.name])
44864513

44874514
# Check for native installations
4488-
# NOTE: This logic will change once enabled and disabled
4489-
# functionality is implemented for opentelemetry.traces setting.
4490-
# NOTE: elasticsearch is instrumented both with libs and natively.
4491-
# To handle this case: If lib is installed, the library itself
4492-
# will check for native instrumentation and switch to that on its
4493-
# own, but if not, native instrumentation could still be used and
4494-
# we would not know. We handle this as we are with strawberry-graphql
4495-
# and ariadne where we check to see if opentelemetry-api and the
4496-
# specific library are installed on the system.
4515+
# NOTE: for native instrumentation to work, the tracer name must be
4516+
# explicitly included in the opentelemetry.traces.include setting.
44974517
for lib in ["strawberry-graphql", "ariadne", "elasticsearch"]:
4498-
if _is_installed(lib):
4499-
opentelemetry_instrumentation.extend(final_include_dict[lib])
4518+
if _is_installed(lib) and (lib in include_set):
4519+
opentelemetry_instrumentation.extend(ALL_LIBRARY_TRACERS_TO_NR_HOOKS[lib])
45004520

45014521

45024522
def _process_opentelemetry_instrumentors():

newrelic/core/config.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@ def emit(self, record):
7272
_logger.addHandler(_NullHandler())
7373

7474

75+
def parse_comma_separated_into_set(string):
76+
if not string:
77+
return set()
78+
# Strip the string in case user has separated by string and space
79+
return {item.strip() for item in string.split(",")}
80+
81+
7582
def parse_space_separated_into_list(string):
7683
return string.split()
7784

@@ -753,6 +760,11 @@ def _environ_as_set(name, default=""):
753760
return set(value.split())
754761

755762

763+
def _environ_as_comma_separated_set(name, default=""):
764+
value = os.environ.get(name, default)
765+
return parse_comma_separated_into_set(value)
766+
767+
756768
def _environ_as_mapping(name, default=""):
757769
result = []
758770
items = os.environ.get(name, default)
@@ -1409,6 +1421,12 @@ def default_otlp_host(host):
14091421
_settings.ml_insights_events.enabled = _environ_as_bool("NEW_RELIC_ML_INSIGHTS_EVENTS_ENABLED", default=False)
14101422
_settings.opentelemetry.enabled = _environ_as_bool("NEW_RELIC_OPENTELEMETRY_ENABLED", default=False)
14111423
_settings.opentelemetry.traces.enabled = _environ_as_bool("NEW_RELIC_OPENTELEMETRY_TRACES_ENABLED", default=True)
1424+
_settings.opentelemetry.traces.exclude = _environ_as_comma_separated_set(
1425+
"NEW_RELIC_OPENTELEMETRY_TRACES_EXCLUDE", default=""
1426+
)
1427+
_settings.opentelemetry.traces.include = _environ_as_comma_separated_set(
1428+
"NEW_RELIC_OPENTELEMETRY_TRACES_INCLUDE", default=""
1429+
)
14121430

14131431

14141432
def global_settings():

0 commit comments

Comments
 (0)