Skip to content

Commit e4869ac

Browse files
committed
feat(config): support libdatadog's library_config for config through file
1 parent a8dfadf commit e4869ac

File tree

8 files changed

+272
-4
lines changed

8 files changed

+272
-4
lines changed

ddtrace/__init__.py

+5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55

66
LOADED_MODULES = frozenset(sys.modules.keys())
77

8+
# Configuration for the whole tracer from file. Do it before anything else happens.
9+
from ddtrace.internal.native import _apply_configuration_from_disk
10+
11+
_apply_configuration_from_disk()
12+
813
from ddtrace.internal.module import ModuleWatchdog
914

1015

ddtrace/internal/native/__init__.py

+37
Original file line numberDiff line numberDiff line change
@@ -1 +1,38 @@
1+
import os
2+
from typing import Dict
3+
14
from ._native import DDSketch # noqa: F401
5+
from ._native import PyConfigurator
6+
7+
8+
def get_configuration_from_disk(debug_logs: bool = False, file_override="") -> Dict[str, str]:
9+
"""
10+
Retrieves the tracer configuration from disk. Calls the PyConfigurator object
11+
to read the configuration from the disk using the libdatadog shared library
12+
and returns the corresponding configuration
13+
See https://github.com/DataDog/libdatadog/blob/06d2b6a19d7ec9f41b3bfd4ddf521585c55298f6/library-config/src/lib.rs
14+
for more information on how the configuration is read from disk
15+
"""
16+
configurator = PyConfigurator(debug_logs)
17+
18+
# Set the file override if provided. Only used for testing purposes.
19+
if file_override:
20+
configurator.set_local_file_override(file_override)
21+
22+
return configurator.get_configuration()
23+
24+
25+
def _apply_configuration_from_disk():
26+
"""
27+
Sets the configuration from disk as environment variables.
28+
This is not ideal and we should consider a better mechanism to
29+
apply this configuration to the tracer.
30+
Currently here is the order of precedence (higher takes precedence):
31+
1. Dynamic remote configuration
32+
2. Runtime configuration (ie fields set manually by customers / from the ddtrace code)
33+
3. Configuration from disk
34+
4. Environment variables
35+
5. Default values
36+
"""
37+
for key, value in get_configuration_from_disk().items():
38+
os.environ[key] = str(value).lower()

ddtrace/internal/native/_native.pyi

+28
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,34 @@
1+
from typing import Dict
2+
13
class DDSketch:
24
def __init__(self): ...
35
def add(self, value: float) -> None: ...
46
def to_proto(self) -> bytes: ...
57
@property
68
def count(self) -> float: ...
9+
10+
class PyConfigurator:
11+
"""
12+
PyConfigurator is a class responsible for configuring the Python environment
13+
for the application. It allows setting environment variables, command-line
14+
arguments, and file overrides, and retrieving the current configuration.
15+
"""
16+
17+
def __init__(self, debug_logs: bool):
18+
"""
19+
Initialize the PyConfigurator.
20+
:param debug_logs: A boolean indicating whether debug logs should be enabled.
21+
"""
22+
...
23+
def set_local_file_override(self, file: str) -> None:
24+
"""
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.
27+
"""
28+
...
29+
def get_configuration(self) -> Dict[str, str]:
30+
"""
31+
Retrieve the on-disk configuration.
32+
:return: A dictionary containing the current configuration of the form {key: value}.
33+
"""
34+
...

src/native/Cargo.lock

+87-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/native/Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ opt-level = 3
1010

1111
[dependencies]
1212
pyo3 = { version = "0.22.3", features = ["extension-module"] }
13-
datadog-ddsketch = { git = "https://github.com/DataDog/libdatadog", rev = "v15.0.0" }
13+
datadog-ddsketch = { git = "https://github.com/DataDog/libdatadog", rev = "v16.0.1" }
14+
datadog-library-config = { git = "https://github.com/DataDog/libdatadog", rev = "v16.0.1" }
1415

1516
[build-dependencies]
1617
pyo3-build-config = "0.21.2"

src/native/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
mod ddsketch;
2+
mod library_config;
23

34
use pyo3::prelude::*;
45

56
#[pymodule]
67
fn _native(m: &Bound<'_, PyModule>) -> PyResult<()> {
78
m.add_class::<ddsketch::DDSketchPy>()?;
9+
m.add_class::<library_config::PyConfigurator>()?;
810
Ok(())
911
}

src/native/library_config.rs

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
use datadog_library_config::{Configurator, ProcessInfo};
2+
use pyo3::exceptions::PyException;
3+
use pyo3::prelude::*;
4+
use pyo3::types::PyDict;
5+
6+
#[pyclass(name = "PyConfigurator", module = "ddtrace.internal._native")]
7+
pub struct PyConfigurator {
8+
configurator: Box<Configurator>,
9+
local_file: String,
10+
fleet_file: String,
11+
}
12+
13+
#[pymethods]
14+
impl PyConfigurator {
15+
#[new]
16+
pub fn new(debug_logs: bool) -> Self {
17+
PyConfigurator {
18+
configurator: Box::new(Configurator::new(debug_logs)),
19+
fleet_file: Configurator::FLEET_STABLE_CONFIGURATION_PATH.to_string(),
20+
local_file: Configurator::LOCAL_STABLE_CONFIGURATION_PATH.to_string(),
21+
}
22+
}
23+
24+
pub fn set_local_file_override(&mut self, file: String) -> PyResult<()> {
25+
self.local_file = file;
26+
Ok(())
27+
}
28+
29+
pub fn get_configuration(&self, py: Python<'_>) -> PyResult<PyObject> {
30+
let res_config = self
31+
.configurator
32+
.get_config_from_file(
33+
self.local_file.as_ref(),
34+
self.fleet_file.as_ref(),
35+
ProcessInfo::detect_global("python".to_string()),
36+
);
37+
match res_config {
38+
Ok(config) => {
39+
let dict = PyDict::new_bound(py);
40+
for c in config.iter() {
41+
let key = c.name.to_str().to_owned();
42+
let _ = dict.set_item(key, c.value.clone());
43+
}
44+
Ok(dict.into())
45+
}
46+
Err(e) => {
47+
let err_msg = format!("Failed to get configuration: {:?}", e);
48+
Err(PyException::new_err(err_msg))
49+
}
50+
}
51+
}
52+
}

tests/internal/test_native.py

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from ddtrace.internal.native import get_configuration_from_disk
2+
3+
4+
def test_get_configuration_from_disk(tmp_path):
5+
# First test -- config matches & should be returned
6+
config_1 = tmp_path / "config_1.yaml"
7+
config_1.write_text(
8+
"""
9+
rules:
10+
- selectors:
11+
- origin: language
12+
matches:
13+
- python
14+
operator: equals
15+
configuration:
16+
DD_SERVICE: my-service
17+
""",
18+
encoding="utf-8",
19+
)
20+
21+
config = get_configuration_from_disk(file_override=str(config_1))
22+
assert config == {"DD_SERVICE": "my-service"}
23+
24+
# Second test -- config does not match & should not be returned
25+
config_2 = tmp_path / "config_2.yaml"
26+
config_2.write_text(
27+
"""
28+
rules:
29+
- selectors:
30+
- origin: language
31+
matches:
32+
- nodejs
33+
operator: equals
34+
configuration:
35+
DD_SERVICE: my-service
36+
""",
37+
encoding="utf-8",
38+
)
39+
40+
config = get_configuration_from_disk(file_override=str(config_2))
41+
assert config == {}
42+
43+
44+
def is_imported_before(module_a, module_b):
45+
import sys
46+
47+
# Check if both modules are in sys.modules
48+
if module_a in sys.modules and module_b in sys.modules:
49+
# Get the position of the modules in sys.modules (which is an OrderedDict in Python 3.7+)
50+
modules = list(sys.modules.keys())
51+
return modules.index(module_a) < modules.index(module_b)
52+
return False # If one or both modules are not imported, return False
53+
54+
55+
def test_native_before_settings():
56+
# Ensure that the native module is imported before the settings module
57+
import ddtrace # noqa: F401
58+
59+
assert is_imported_before("ddtrace.internal.native", "ddtrace.settings")

0 commit comments

Comments
 (0)