Skip to content

Commit 86fc423

Browse files
committed
feat(agent-config): allow extensible configuration via ConfigExtension trait
Introduces a generic `Config<E: ConfigExtension>` type that lets consumers define additional configuration fields without modifying or copy-pasting the core crate. Includes a unified `Source` type for dual extraction from both env vars and YAML, a `merge_fields!` macro to reduce merge boilerplate, and moves Lambda-specific fields out of the core Config struct. Also restructures the crate to use a conventional `src/` layout and adds a README documenting the extension API.
1 parent bb2532a commit 86fc423

12 files changed

Lines changed: 415 additions & 631 deletions

crates/datadog-agent-config/Cargo.toml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@ version = "0.1.0"
44
edition.workspace = true
55
license.workspace = true
66

7-
[lib]
8-
path = "mod.rs"
9-
107
[dependencies]
118
figment = { version = "0.10", default-features = false, features = ["yaml", "env"] }
129
libdd-trace-obfuscation = { git = "https://github.com/DataDog/libdatadog", rev = "8c88979985154d6d97c0fc2ca9039682981eacad" }
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# datadog-agent-config
2+
3+
Shared configuration crate for Datadog serverless agents. Provides a typed `Config` struct with built-in loading from environment variables (`DD_*`) and YAML files (`datadog.yaml`), with environment variables taking precedence.
4+
5+
## Core features
6+
7+
- **Typed config struct** with fields for site, API key, proxy, logs, APM, metrics, DogStatsD, OTLP, and trace propagation
8+
- **Two built-in sources**: `EnvConfigSource` (reads `DD_*` / `DATADOG_*` env vars) and `YamlConfigSource` (reads `datadog.yaml`)
9+
- **Graceful deserialization**: every field uses forgiving deserializers that fall back to defaults on bad input, so one misconfigured value never crashes the whole config
10+
- **Extensible via `ConfigExtension`**: consumers can define additional configuration fields without modifying this crate
11+
12+
## Quick start
13+
14+
```rust
15+
use std::path::Path;
16+
use datadog_agent_config::get_config;
17+
18+
let config = get_config(Path::new("/var/task"));
19+
println!("site: {}", config.site);
20+
println!("api_key: {}", config.api_key);
21+
```
22+
23+
## Extensible configuration
24+
25+
Consumers that need additional fields (e.g., Lambda-specific settings) implement the `ConfigExtension` trait instead of forking or copy-pasting the crate.
26+
27+
### 1. Define the extension and its source
28+
29+
```rust
30+
use datadog_agent_config::{
31+
ConfigExtension, merge_fields,
32+
deserialize_optional_string, deserialize_optional_bool_from_anything,
33+
};
34+
use serde::Deserialize;
35+
36+
#[derive(Debug, PartialEq, Clone)]
37+
pub struct MyExtension {
38+
pub custom_flag: bool,
39+
pub custom_name: String,
40+
}
41+
42+
impl Default for MyExtension {
43+
fn default() -> Self {
44+
Self { custom_flag: false, custom_name: String::new() }
45+
}
46+
}
47+
48+
/// Source struct for deserialization. Must use #[serde(default)] and
49+
/// graceful deserializers so one bad field doesn't fail the whole extraction.
50+
#[derive(Debug, Clone, Default, Deserialize)]
51+
#[serde(default)]
52+
pub struct MySource {
53+
#[serde(deserialize_with = "deserialize_optional_bool_from_anything")]
54+
pub custom_flag: Option<bool>,
55+
#[serde(deserialize_with = "deserialize_optional_string")]
56+
pub custom_name: Option<String>,
57+
}
58+
59+
impl ConfigExtension for MyExtension {
60+
type Source = MySource;
61+
62+
fn merge_from(&mut self, source: &MySource) {
63+
merge_fields!(self, source,
64+
string: [custom_name],
65+
value: [custom_flag],
66+
);
67+
}
68+
}
69+
```
70+
71+
### 2. Load config with the extension
72+
73+
```rust
74+
use std::path::Path;
75+
use datadog_agent_config::{Config, get_config_with_extension};
76+
77+
type MyConfig = Config<MyExtension>;
78+
79+
let config: MyConfig = get_config_with_extension(Path::new("/var/task"));
80+
81+
// Core fields
82+
println!("site: {}", config.site);
83+
84+
// Extension fields
85+
println!("custom_flag: {}", config.ext.custom_flag);
86+
println!("custom_name: {}", config.ext.custom_name);
87+
```
88+
89+
Extension fields are populated from both `DD_*` environment variables and `datadog.yaml` using dual extraction: the core fields and extension fields are extracted independently from the same figment instance, so they don't interfere with each other.
90+
91+
### Flat fields only
92+
93+
The single `Source` type is used for both env var and YAML extraction. This works when extension fields are top-level (flat) in the YAML file, which is the common case. If you need nested YAML structures that differ from the flat env var layout, implement `merge_from` with a nested source struct and handle the mapping manually.
94+
95+
### merge_fields! macro
96+
97+
The `merge_fields!` macro reduces boilerplate in `merge_from` by batching fields by merge strategy:
98+
99+
- `string`: merges `Option<String>` into `String` (sets value if `Some`)
100+
- `value`: merges `Option<T>` into `T` (sets value if `Some`)
101+
- `option`: merges `Option<T>` into `Option<T>` (overwrites if `Some`)
102+
103+
Custom merge logic (e.g., OR-ing two boolean fields together) goes after the macro call in the same method.
104+
105+
## Config loading precedence
106+
107+
1. `Config::default()` (hardcoded defaults)
108+
2. `datadog.yaml` values (lower priority)
109+
3. `DD_*` environment variables (highest priority)
110+
4. Post-processing defaults (site, proxy, logs/APM URL construction)
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)