Skip to content

Commit 14a219f

Browse files
TatshTimPansinomergify[bot]
authored
Add TOML configuration file support (#1238)
* Add TOML configuration file support Layout is: [tool.newrelic] app_name = "app name" Keys with . in them are expanded to be subkeys: [tool.newrelic.browser_monitoring] enabled = true Anything that is a list in the layout is made into a string joined with a single space. import-hook: is translated as such: [tool.newrelic.import-hook.module_name] "sub.sub" = ["item1", "item2"] error_collector.ignore_errors: [tool.newrelic.error_collector] ignore_errors = ["module.name:ClassName", "module.name:ClassName2"] Environment use the tool.newrelic.env namespace: [tool.newrelic.env.production] app_name = "production name" * Correct last supported version for grpc on py37 * Drop tests for grpc0126 * Undo changes related to url_rules handling * Add fixtures to configuration tests to handle restoring state * Add test skip for older python versions --------- Co-authored-by: Tim Pansino <[email protected]> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent 791c0a7 commit 14a219f

File tree

2 files changed

+129
-2
lines changed

2 files changed

+129
-2
lines changed

newrelic/config.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -911,6 +911,29 @@ def apply_local_high_security_mode_setting(settings):
911911
return settings
912912

913913

914+
def _toml_config_to_configparser_dict(d, top=None, _path=None):
915+
top = top or {"newrelic": {}}
916+
_path = _path or ""
917+
for key, value in d.items():
918+
if isinstance(value, dict):
919+
_toml_config_to_configparser_dict(value, top, f"{_path}.{key}" if _path else key)
920+
else:
921+
fixed_value = " ".join(value) if isinstance(value, list) else value
922+
path_split = _path.split(".")
923+
# Handle environments
924+
if _path.startswith("env."):
925+
env_key = f"newrelic:{path_split[1]}"
926+
fixed_key = ".".join((*path_split[2:], key))
927+
top[env_key] = {**top.get(env_key, {}), fixed_key: fixed_value}
928+
# Handle import-hook:... configuration
929+
elif _path.startswith("import-hook."):
930+
import_hook_key = f"import-hook:{'.'.join(path_split[1:])}"
931+
top[import_hook_key] = {**top.get(import_hook_key, {}), key: fixed_value}
932+
else:
933+
top["newrelic"][f"{_path}.{key}" if _path else key] = fixed_value
934+
return top
935+
936+
914937
def _load_configuration(
915938
config_file=None,
916939
environment=None,
@@ -994,8 +1017,20 @@ def _load_configuration(
9941017

9951018
# Now read in the configuration file. Cache the config file
9961019
# name in internal settings object as indication of succeeding.
997-
998-
if not _config_object.read([config_file]):
1020+
if config_file.endswith(".toml"):
1021+
try:
1022+
import tomllib
1023+
except ImportError:
1024+
raise newrelic.api.exceptions.ConfigurationError(
1025+
"TOML configuration file can only be used if tomllib is available (Python 3.11+)."
1026+
)
1027+
with open(config_file, "rb") as f:
1028+
content = tomllib.load(f)
1029+
newrelic_section = content.get("tool", {}).get("newrelic")
1030+
if not newrelic_section:
1031+
raise newrelic.api.exceptions.ConfigurationError("New Relic configuration not found in TOML file.")
1032+
_config_object.read_dict(_toml_config_to_configparser_dict(newrelic_section))
1033+
elif not _config_object.read([config_file]):
9991034
raise newrelic.api.exceptions.ConfigurationError(f"Unable to open configuration file {config_file}.")
10001035

10011036
_settings.config_file = config_file

tests/agent_features/test_configuration.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
# limitations under the License.
1414

1515
import collections
16+
import copy
17+
import sys
1618
import tempfile
1719

1820
import urllib.parse as urlparse
@@ -44,6 +46,29 @@
4446
)
4547

4648

49+
SKIP_IF_NOT_PY311 = pytest.mark.skipif(sys.version_info < (3, 11), reason="TOML not in the standard library.")
50+
51+
52+
@pytest.fixture(scope="function")
53+
def collector_available_fixture():
54+
# Disable fixture that requires real application to exist for this file
55+
pass
56+
57+
58+
@pytest.fixture(scope="module", autouse=True)
59+
def restore_settings_fixture():
60+
# Backup settings from before this test file runs
61+
original_settings = global_settings()
62+
backup = copy.deepcopy(original_settings.__dict__)
63+
64+
# Run tests
65+
yield
66+
67+
# Restore settings after tests run
68+
original_settings.__dict__.clear()
69+
original_settings.__dict__.update(backup)
70+
71+
4772
def function_to_trace():
4873
pass
4974

@@ -949,6 +974,73 @@ def test_initialize_developer_mode(section, expect_error, logger):
949974
assert "CONFIGURATION ERROR" not in logger.caplog.records
950975

951976

977+
newrelic_toml_contents = b"""
978+
[tool.newrelic]
979+
app_name = "test11"
980+
monitor_mode = true
981+
982+
[tool.newrelic.env.development]
983+
app_name = "test11 (Development)"
984+
985+
[tool.newrelic.env.production]
986+
app_name = "test11 (Production)"
987+
log_level = "error"
988+
989+
[tool.newrelic.env.production.distributed_tracing]
990+
enabled = false
991+
992+
[tool.newrelic.error_collector]
993+
enabled = true
994+
ignore_errors = ["module:name1", "module:name"]
995+
996+
[tool.newrelic.transaction_tracer]
997+
enabled = true
998+
999+
[tool.newrelic.import-hook.django]
1000+
"instrumentation.scripts.django_admin" = ["stuff", "stuff2"]
1001+
"""
1002+
1003+
1004+
@SKIP_IF_NOT_PY311
1005+
def test_toml_parse_development():
1006+
settings = global_settings()
1007+
_reset_configuration_done()
1008+
_reset_config_parser()
1009+
_reset_instrumentation_done()
1010+
1011+
with tempfile.NamedTemporaryFile(suffix=".toml") as f:
1012+
f.write(newrelic_toml_contents)
1013+
f.seek(0)
1014+
1015+
initialize(config_file=f.name, environment="development")
1016+
value = fetch_config_setting(settings, "app_name")
1017+
assert value != "test11"
1018+
value = fetch_config_setting(settings, "monitor_mode")
1019+
assert value is True
1020+
value = fetch_config_setting(settings, "error_collector")
1021+
assert value.enabled is True
1022+
assert value.ignore_classes[0] == "module:name1"
1023+
assert value.ignore_classes[1] == "module:name"
1024+
1025+
1026+
@SKIP_IF_NOT_PY311
1027+
def test_toml_parse_production():
1028+
settings = global_settings()
1029+
_reset_configuration_done()
1030+
_reset_config_parser()
1031+
_reset_instrumentation_done()
1032+
1033+
with tempfile.NamedTemporaryFile(suffix=".toml") as f:
1034+
f.write(newrelic_toml_contents)
1035+
f.seek(0)
1036+
1037+
initialize(config_file=f.name, environment="production")
1038+
value = fetch_config_setting(settings, "app_name")
1039+
assert value == "test11 (Production)"
1040+
value = fetch_config_setting(settings, "distributed_tracing")
1041+
assert value.enabled is False
1042+
1043+
9521044
@pytest.fixture
9531045
def caplog_handler():
9541046
class CaplogHandler(logging.StreamHandler):

0 commit comments

Comments
 (0)