Skip to content

Commit a5d0de3

Browse files
Package capture harvest (#1228)
* Initial commit for UI debug * Add 2 sec timer for module polling (in fast harvest) * Remove Plugin List tests * Only send loaded modules during shutdown * Add plugin test * Add plugin collection to slow harvest * Fix logic errors for conditional send * Add update_loaded_module to list of methods * Disable flaskmaster-py38 and pin coverage-py38 * Add py37 coverage version * Revert coverage version * Add more info for disabled test * Address reviewer comments * Add ignore flag for linter error * [Mega-Linter] Apply linters fixes * Trigger tests * Remove commented-out import --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Co-authored-by: lrafeei <[email protected]>
1 parent dacf155 commit a5d0de3

File tree

6 files changed

+111
-101
lines changed

6 files changed

+111
-101
lines changed

newrelic/common/agent_http.py

+8-4
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
from ssl import get_default_verify_paths
3636
except ImportError:
3737

38-
class _DEFAULT_CERT_PATH():
38+
class _DEFAULT_CERT_PATH:
3939
cafile = None
4040
capath = None
4141

@@ -73,7 +73,7 @@ def _urllib3_ssl_recursion_workaround(wrapped, instance, args, kwargs):
7373
return wrapped(*args, **kwargs)
7474

7575

76-
class BaseClient():
76+
class BaseClient:
7777
AUDIT_LOG_ID = 0
7878

7979
def __init__(
@@ -122,7 +122,10 @@ def log_request(cls, fp, method, url, params, payload, headers, body=None, compr
122122

123123
# Obfuscate license key from headers and URL params
124124
if headers:
125-
headers = {k: obfuscate_license_key(v) if k.lower() in HEADER_AUDIT_LOGGING_DENYLIST else v for k, v in headers.items()}
125+
headers = {
126+
k: obfuscate_license_key(v) if k.lower() in HEADER_AUDIT_LOGGING_DENYLIST else v
127+
for k, v in headers.items()
128+
}
126129

127130
if params and "license_key" in params:
128131
params = params.copy()
@@ -517,7 +520,7 @@ def __init__(
517520
)
518521

519522

520-
class SupportabilityMixin():
523+
class SupportabilityMixin:
521524
@staticmethod
522525
def _supportability_request(params, payload, body, compression_time):
523526
# *********
@@ -606,6 +609,7 @@ class DeveloperModeClient(SupportabilityMixin, BaseClient):
606609
"span_event_data": None,
607610
"custom_event_data": None,
608611
"log_event_data": None,
612+
"update_loaded_modules": ["Jars", [[" ", " ", {}]]],
609613
"shutdown": [],
610614
}
611615

newrelic/core/application.py

+44-2
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
from newrelic.core.custom_event import create_custom_event
3232
from newrelic.core.data_collector import create_session
3333
from newrelic.core.database_utils import SQLConnections
34-
from newrelic.core.environment import environment_settings
34+
from newrelic.core.environment import environment_settings, plugins
3535
from newrelic.core.internal_metrics import (
3636
InternalTrace,
3737
InternalTraceContext,
@@ -52,9 +52,10 @@
5252

5353
_logger = logging.getLogger(__name__)
5454

55+
MAX_PACKAGE_CAPTURE_TIME_PER_SLOW_HARVEST = 2.0
5556

56-
class Application():
5757

58+
class Application:
5859
"""Class which maintains recorded data for a single application."""
5960

6061
def __init__(self, app_name, linked_applications=None):
@@ -107,6 +108,8 @@ def __init__(self, app_name, linked_applications=None):
107108
self._data_samplers_lock = threading.Lock()
108109
self._data_samplers_started = False
109110

111+
self._remaining_plugins = True
112+
110113
# We setup empty rules engines here even though they will be
111114
# replaced when application first registered. This is done to
112115
# avoid a race condition in setting it later. Otherwise we have
@@ -120,6 +123,7 @@ def __init__(self, app_name, linked_applications=None):
120123
}
121124

122125
self._data_samplers = []
126+
self.modules = []
123127

124128
# Thread profiler and state of whether active or not.
125129

@@ -129,6 +133,8 @@ def __init__(self, app_name, linked_applications=None):
129133

130134
self.profile_manager = profile_session_manager()
131135

136+
self.plugins = plugins() # initialize the generator
137+
132138
self._uninstrumented = []
133139

134140
@property
@@ -1238,6 +1244,28 @@ def harvest(self, shutdown=False, flexible=False):
12381244
data_sampler.name,
12391245
)
12401246

1247+
# Send environment plugin list
1248+
1249+
stopwatch_start = time.time()
1250+
while (
1251+
configuration
1252+
and configuration.package_reporting.enabled
1253+
and self._remaining_plugins
1254+
and ((time.time() - stopwatch_start) < MAX_PACKAGE_CAPTURE_TIME_PER_SLOW_HARVEST)
1255+
):
1256+
try:
1257+
module_info = next(self.plugins)
1258+
self.modules.append(module_info)
1259+
except StopIteration:
1260+
self._remaining_plugins = False
1261+
1262+
# Send the accumulated environment plugin list if not empty
1263+
if self.modules:
1264+
self._active_session.send_loaded_modules(self.modules)
1265+
1266+
# Reset the modules list every harvest cycle
1267+
self.modules = []
1268+
12411269
# Add a metric we can use to track how many harvest
12421270
# periods have occurred.
12431271

@@ -1671,6 +1699,20 @@ def internal_agent_shutdown(self, restart=False):
16711699

16721700
self.stop_data_samplers()
16731701

1702+
# Finishes collecting environment plugin information
1703+
# if this has not been completed during harvest
1704+
# lifetime of the application
1705+
1706+
while self.configuration and self.configuration.package_reporting.enabled and self._remaining_plugins:
1707+
try:
1708+
module_info = next(self.plugins)
1709+
self.modules.append(module_info)
1710+
except StopIteration:
1711+
self._remaining_plugins = False
1712+
if self.modules:
1713+
self._active_session.send_loaded_modules(self.modules)
1714+
self.modules = []
1715+
16741716
# Now shutdown the actual agent session.
16751717

16761718
try:

newrelic/core/data_collector.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
_logger = logging.getLogger(__name__)
3636

3737

38-
class Session():
38+
class Session:
3939
PROTOCOL = AgentProtocol
4040
OTLP_PROTOCOL = OtlpProtocol
4141
CLIENT = ApplicationModeClient
@@ -209,6 +209,12 @@ def send_profile_data(self, profile_data):
209209
payload = (self.agent_run_id, profile_data)
210210
return self._protocol.send("profile_data", payload)
211211

212+
def send_loaded_modules(self, environment_info):
213+
"""Called to submit loaded modules."""
214+
215+
payload = ("Jars", environment_info)
216+
return self._protocol.send("update_loaded_modules", payload)
217+
212218
def shutdown_session(self):
213219
"""Called to perform orderly deregistration of agent run against
214220
the data collector, rather than simply dropping the connection and

newrelic/core/environment.py

+41-46
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
physical_processor_count,
3030
total_physical_memory,
3131
)
32-
from newrelic.core.config import global_settings
3332
from newrelic.packages.isort import stdlibs as isort_stdlibs
3433

3534
try:
@@ -198,58 +197,54 @@ def environment_settings():
198197

199198
env.extend(dispatcher)
200199

200+
return env
201+
202+
203+
def plugins():
201204
# Module information.
202205
stdlib_builtin_module_names = _get_stdlib_builtin_module_names()
203206

204-
plugins = []
205-
206-
settings = global_settings()
207-
if settings and settings.package_reporting.enabled:
208-
# Using any iterable to create a snapshot of sys.modules can occassionally
209-
# fail in a rare case when modules are imported in parallel by different
210-
# threads.
211-
#
212-
# TL;DR: Do NOT use an iterable on the original sys.modules to generate the
213-
# list
214-
for name, module in sys.modules.copy().items():
215-
# Exclude lib.sub_paths as independent modules except for newrelic.hooks.
216-
nr_hook = name.startswith("newrelic.hooks.")
217-
if "." in name and not nr_hook or name.startswith("_"):
207+
# Using any iterable to create a snapshot of sys.modules can occassionally
208+
# fail in a rare case when modules are imported in parallel by different
209+
# threads.
210+
#
211+
# TL;DR: Do NOT use an iterable on the original sys.modules to generate the
212+
# list
213+
for name, module in sys.modules.copy().items():
214+
# Exclude lib.sub_paths as independent modules except for newrelic.hooks.
215+
nr_hook = name.startswith("newrelic.hooks.")
216+
if "." in name and not nr_hook or name.startswith("_"):
217+
continue
218+
219+
# If the module isn't actually loaded (such as failed relative imports
220+
# in Python 2.7), the module will be None and should not be reported.
221+
try:
222+
if not module:
218223
continue
219-
220-
# If the module isn't actually loaded (such as failed relative imports
221-
# in Python 2.7), the module will be None and should not be reported.
224+
except Exception: # nosec B112
225+
# if the application uses generalimport to manage optional depedencies,
226+
# it's possible that generalimport.MissingOptionalDependency is raised.
227+
# In this case, we should not report the module as it is not actually loaded and
228+
# is not a runtime dependency of the application.
229+
#
230+
continue
231+
232+
# Exclude standard library/built-in modules.
233+
if name in stdlib_builtin_module_names:
234+
continue
235+
236+
# Don't attempt to look up version information for our hooks
237+
version = None
238+
if not nr_hook:
222239
try:
223-
if not module:
224-
continue
240+
version = get_package_version(name)
225241
except Exception:
226-
# if the application uses generalimport to manage optional depedencies,
227-
# it's possible that generalimport.MissingOptionalDependency is raised.
228-
# In this case, we should not report the module as it is not actually loaded and
229-
# is not a runtime dependency of the application.
230-
#
231-
continue
232-
233-
# Exclude standard library/built-in modules.
234-
if name in stdlib_builtin_module_names:
235-
continue
236-
237-
# Don't attempt to look up version information for our hooks
238-
version = None
239-
if not nr_hook:
240-
try:
241-
version = get_package_version(name)
242-
except Exception:
243-
pass
244-
245-
# If it has no version it's likely not a real package so don't report it unless
246-
# it's a new relic hook.
247-
if nr_hook or version:
248-
plugins.append(f"{name} ({version})")
249-
250-
env.append(("Plugin List", plugins))
242+
pass
251243

252-
return env
244+
# If it has no version it's likely not a real package so don't report it unless
245+
# it's a new relic hook.
246+
if nr_hook or version:
247+
yield [name, version, {}] if version else [name, " ", {}]
253248

254249

255250
def _get_stdlib_builtin_module_names():

tests/agent_unittests/test_connect_response_fields.py

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
("send_agent_command_results", ({0: {}},)),
4747
("agent_settings", ({},)),
4848
("send_span_events", (EMPTY_SAMPLES, ())),
49+
("update_loaded_modules", ("Jars", [[" ", " ", {}]])),
4950
("shutdown_session", ()),
5051
)
5152

tests/agent_unittests/test_environment.py

+10-48
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,15 @@
1515
import sys
1616

1717
import pytest
18-
from testing_support.fixtures import override_generic_settings
1918

2019
from newrelic.core.config import global_settings
21-
from newrelic.core.environment import environment_settings
20+
from newrelic.core.environment import environment_settings, plugins
2221

2322
settings = global_settings()
2423

2524

2625
def module(version):
27-
class Module():
26+
class Module:
2827
pass
2928

3029
if version:
@@ -35,40 +34,17 @@ class Module():
3534

3635
def test_plugin_list():
3736
# Let's pretend we fired an import hook
38-
import newrelic.hooks.adapter_gunicorn # noqa: F401
37+
import pytest # noqa: F401
3938

40-
environment_info = environment_settings()
41-
42-
for key, plugin_list in environment_info:
43-
if key == "Plugin List":
44-
break
45-
else:
46-
assert False, "'Plugin List' not found"
47-
48-
# Check that bogus plugins don't get reported
49-
assert "newrelic.hooks.newrelic" not in plugin_list
50-
# Check that plugin that should get reported has version info.
51-
assert f"pytest ({pytest.__version__})" in plugin_list
52-
53-
54-
@override_generic_settings(settings, {"package_reporting.enabled": False})
55-
def test_plugin_list_when_package_reporting_disabled():
56-
# Let's pretend we fired an import hook
57-
import newrelic.hooks.adapter_gunicorn # noqa: F401
58-
59-
environment_info = environment_settings()
39+
for name, version, _ in plugins():
40+
if name == "newrelic.hooks.newrelic":
41+
assert False, "Bogus plugin found"
42+
if name == "pytest":
43+
# Check that plugin that should get reported has version info.
44+
assert version == pytest.__version__
6045

61-
for key, plugin_list in environment_info:
62-
if key == "Plugin List":
63-
break
64-
else:
65-
assert False, "'Plugin List' not found"
6646

67-
# Check that bogus plugins don't get reported
68-
assert plugin_list == []
69-
70-
71-
class NoIteratorDict():
47+
class NoIteratorDict:
7248
def __init__(self, d):
7349
self.d = d
7450

@@ -85,20 +61,6 @@ def __contains__(self, *args, **kwargs):
8561
return self.d.__contains__(*args, **kwargs)
8662

8763

88-
def test_plugin_list_uses_no_sys_modules_iterator(monkeypatch):
89-
modules = NoIteratorDict(sys.modules)
90-
monkeypatch.setattr(sys, "modules", modules)
91-
92-
# If environment_settings iterates over sys.modules, an attribute error will be generated
93-
environment_info = environment_settings()
94-
95-
for key, plugin_list in environment_info:
96-
if key == "Plugin List":
97-
break
98-
else:
99-
assert False, "'Plugin List' not found"
100-
101-
10264
@pytest.mark.parametrize(
10365
"loaded_modules,dispatcher,dispatcher_version,worker_version",
10466
(

0 commit comments

Comments
 (0)