Skip to content

Commit 1ebacb5

Browse files
committed
fix(library_config): Support order of precedence
1 parent 6671f3c commit 1ebacb5

File tree

4 files changed

+123
-23
lines changed

4 files changed

+123
-23
lines changed

ddtrace/internal/native/__init__.py

+29-8
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import os
2+
import sys
23
from typing import Dict
4+
from typing import List
5+
6+
from ddtrace.internal.utils.formats import asbool
37

48
from ._native import DDSketch # noqa: F401
59
from ._native import PyConfigurator
610

711

8-
def get_configuration_from_disk(debug_logs: bool = False, file_override="") -> Dict[str, str]:
12+
def get_configuration_from_disk(
13+
debug_logs: bool = False, local_file_override="", managed_file_override=""
14+
) -> List[Dict[str, str]]:
915
"""
1016
Retrieves the tracer configuration from disk. Calls the PyConfigurator object
1117
to read the configuration from the disk using the libdatadog shared library
@@ -16,23 +22,38 @@ def get_configuration_from_disk(debug_logs: bool = False, file_override="") -> D
1622
configurator = PyConfigurator(debug_logs)
1723

1824
# Set the file override if provided. Only used for testing purposes.
19-
if file_override:
20-
configurator.set_local_file_override(file_override)
25+
if local_file_override:
26+
configurator.set_local_file_override(local_file_override)
27+
if managed_file_override:
28+
configurator.set_managed_file_override(managed_file_override)
2129

22-
return configurator.get_configuration()
30+
config = []
31+
try:
32+
config = configurator.get_configuration()
33+
except Exception as e:
34+
# No logger at this point, so we rely on good old print
35+
if asbool(os.environ.get("DD_TRACE_DEBUG", "false")):
36+
print("Error reading configuration from disk, skipping: %s" % e, file=sys.stderr)
37+
return config
2338

2439

25-
def _apply_configuration_from_disk():
40+
def _apply_configuration_from_disk(debug_logs: bool = False, local_file_override="", managed_file_override=""):
2641
"""
2742
Sets the configuration from disk as environment variables.
2843
This is not ideal and we should consider a better mechanism to
2944
apply this configuration to the tracer.
3045
Currently here is the order of precedence (higher takes precedence):
3146
1. Dynamic remote configuration
3247
2. Runtime configuration (ie fields set manually by customers / from the ddtrace code)
33-
3. Configuration from disk
48+
3. Managed configuration from disk ("fleet stable config")
3449
4. Environment variables
50+
5. Local configuration from disk ("local stable config")
3551
5. Default values
3652
"""
37-
for key, value in get_configuration_from_disk().items():
38-
os.environ[key] = str(value).lower()
53+
for entry in get_configuration_from_disk(
54+
debug_logs=debug_logs, local_file_override=local_file_override, managed_file_override=managed_file_override
55+
):
56+
if entry["source"] == "fleet_stable_config" or (
57+
entry["source"] == "local_stable_config" and entry["name"] not in list(os.environ.keys())
58+
):
59+
os.environ[entry["name"]] = str(entry["value"])

ddtrace/internal/native/_native.pyi

+26-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Dict
1+
from typing import Dict, List
22

33
class DDSketch:
44
def __init__(self): ...
@@ -22,13 +22,34 @@ class PyConfigurator:
2222
...
2323
def set_local_file_override(self, file: str) -> None:
2424
"""
25-
Overrides the file path for the configuration. Should not be used outside of tests.
26-
:param file: The path to the file to override.
25+
Overrides the local file path for the configuration. Should not be used outside of tests.
26+
:param file: The path to the local file to override.
2727
"""
2828
...
29-
def get_configuration(self) -> Dict[str, str]:
29+
def set_managed_file_override(self, file: str) -> None:
30+
"""
31+
Overrides the managed file path for the configuration. Should not be used outside of tests.
32+
:param file: The path to the managed file to override.
33+
"""
34+
...
35+
def get_configuration(self) -> List[Dict[str, str]]:
3036
"""
3137
Retrieve the on-disk configuration.
32-
:return: A dictionary containing the current configuration of the form {key: value}.
38+
:return: A list of dictionaries containing the configuration:
39+
[{"source": ..., "key": ..., "value": ..., "config_id": ...}]
40+
"""
41+
...
42+
@property
43+
def local_stable_config_type(self) -> str:
44+
"""
45+
Retrieve the local stable configuration type.
46+
:return: A string representing the local stable configuration type.
47+
"""
48+
...
49+
@property
50+
def fleet_stable_config_type(self) -> str:
51+
"""
52+
Retrieve the fleet stable configuration type.
53+
:return: A string representing the fleet stable configuration type.
3354
"""
3455
...

src/native/library_config.rs

+14-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use datadog_library_config::{Configurator, ProcessInfo};
22
use pyo3::exceptions::PyException;
33
use pyo3::prelude::*;
44
use pyo3::types::PyDict;
5+
use pyo3::types::PyList;
56

67
#[pyclass(name = "PyConfigurator", module = "ddtrace.internal._native")]
78
pub struct PyConfigurator {
@@ -26,6 +27,11 @@ impl PyConfigurator {
2627
Ok(())
2728
}
2829

30+
pub fn set_managed_file_override(&mut self, file: String) -> PyResult<()> {
31+
self.fleet_file = file;
32+
Ok(())
33+
}
34+
2935
pub fn get_configuration(&self, py: Python<'_>) -> PyResult<PyObject> {
3036
let res_config = self.configurator.get_config_from_file(
3137
self.local_file.as_ref(),
@@ -34,12 +40,16 @@ impl PyConfigurator {
3440
);
3541
match res_config {
3642
Ok(config) => {
37-
let dict = PyDict::new_bound(py);
43+
let list = PyList::empty_bound(py);
3844
for c in config.iter() {
39-
let key = c.name.to_str().to_owned();
40-
let _ = dict.set_item(key, c.value.clone());
45+
let dict = PyDict::new_bound(py);
46+
dict.set_item("name", c.name.to_str().to_owned())?;
47+
dict.set_item("value", c.value.clone())?;
48+
dict.set_item("source", c.source.to_str().to_owned())?;
49+
dict.set_item("config_id", c.config_id.as_deref().unwrap_or("").to_owned())?;
50+
list.append(dict)?;
4151
}
42-
Ok(dict.into())
52+
Ok(list.into())
4353
}
4454
Err(e) => {
4555
let err_msg = format!("Failed to get configuration: {:?}", e);

tests/internal/test_native.py

+54-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,50 @@
1+
import os
2+
3+
from ddtrace.internal.native import _apply_configuration_from_disk
14
from ddtrace.internal.native import get_configuration_from_disk
25

36

7+
def test_get_configuration_from_disk__priority(tmp_path):
8+
"""
9+
Verify the order:
10+
local stable config < environment variables < managed stable config
11+
"""
12+
13+
local_config = tmp_path / "local_config.yaml"
14+
local_config.write_text(
15+
"""
16+
apm_configuration_default:
17+
DD_SERVICE: "a"
18+
""",
19+
encoding="utf-8",
20+
)
21+
22+
managed_config = tmp_path / "managed_config.yaml"
23+
managed_config.write_text(
24+
"""
25+
apm_configuration_default:
26+
DD_SERVICE: "c"
27+
""",
28+
encoding="utf-8",
29+
)
30+
31+
# First test, local config
32+
_apply_configuration_from_disk(local_file_override=str(local_config))
33+
assert os.environ["DD_SERVICE"] == "a"
34+
del os.environ["DD_SERVICE"]
35+
36+
# Second test, local config + environment variable
37+
os.environ["DD_SERVICE"] = "b"
38+
_apply_configuration_from_disk(local_file_override=str(local_config))
39+
assert os.environ["DD_SERVICE"] == "b"
40+
del os.environ["DD_SERVICE"]
41+
42+
# Third test, local config + environment variable + managed config
43+
_apply_configuration_from_disk(local_file_override=str(local_config), managed_file_override=str(managed_config))
44+
assert os.environ["DD_SERVICE"] == "c"
45+
del os.environ["DD_SERVICE"]
46+
47+
448
def test_get_configuration_from_disk__host_selector(tmp_path):
549
# First test -- config matches & should be returned
650
config_1 = tmp_path / "config_1.yaml"
@@ -12,8 +56,10 @@ def test_get_configuration_from_disk__host_selector(tmp_path):
1256
encoding="utf-8",
1357
)
1458

15-
config = get_configuration_from_disk(file_override=str(config_1))
16-
assert config == {"DD_RUNTIME_METRICS_ENABLED": "true"}
59+
config = get_configuration_from_disk(local_file_override=str(config_1))
60+
assert len(config) == 1
61+
assert config[0]["name"] == "DD_RUNTIME_METRICS_ENABLED"
62+
assert config[0]["value"] == "true"
1763

1864

1965
def test_get_configuration_from_disk__service_selector(tmp_path):
@@ -33,8 +79,10 @@ def test_get_configuration_from_disk__service_selector(tmp_path):
3379
encoding="utf-8",
3480
)
3581

36-
config = get_configuration_from_disk(file_override=str(config_1))
37-
assert config == {"DD_SERVICE": "my-service"}
82+
config = get_configuration_from_disk(local_file_override=str(config_1))
83+
assert len(config) == 1
84+
assert config[0]["name"] == "DD_SERVICE"
85+
assert config[0]["value"] == "my-service"
3886

3987
# Second test -- config does not match & should not be returned
4088
config_2 = tmp_path / "config_2.yaml"
@@ -52,8 +100,8 @@ def test_get_configuration_from_disk__service_selector(tmp_path):
52100
encoding="utf-8",
53101
)
54102

55-
config = get_configuration_from_disk(file_override=str(config_2))
56-
assert config == {}
103+
config = get_configuration_from_disk(local_file_override=str(config_2))
104+
assert len(config) == 0
57105

58106

59107
def is_imported_before(module_a, module_b):

0 commit comments

Comments
 (0)