Skip to content

Commit 1eb4556

Browse files
authored
feat(agent-config): allow extensible configuration via ConfigExtension trait (#111)
* 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. * refactor(agent-config): organize crate into sources/ and deserializers/ modules Move config source implementations (env, yaml) into `src/sources/` and type definitions with custom deserialization into `src/deserializers/`. Re-exports at the crate root preserve all existing import paths. * refactor(agent-config): move inline deserializer helpers to deserializers/helpers.rs Extracts all generic deserializer functions (deserialize_optional_string, deserialize_with_default, duration parsers, key-value parsers, etc.) from lib.rs into src/deserializers/helpers.rs. Re-exported at the crate root so all existing import paths continue to work. * refactor(agent-config): reorder lib.rs so Config struct is visible first Reorganize lib.rs so an engineer opening the file immediately sees the Config struct and its fields, followed by the loading entry points, then the extension trait, builder, and macros. Sections are separated with headers for quick scanning. * fix(agent-config): make NoExtensionSource deserialize from map data Change NoExtensionSource from a unit struct to an empty struct so serde accepts map-shaped data from figment (env vars / YAML) instead of expecting null/unit. Prevents spurious warning logs on every get_config() call when no extension is used. * docs(agent-config): improve ConfigExtension trait and Source docs Address reviewer feedback: document field name collision behavior, clarify Source type requirements and their runtime failure modes, and expand the README with collision and flat-field explanations.
1 parent 96ab942 commit 1eb4556

File tree

15 files changed

+1024
-1164
lines changed

15 files changed

+1024
-1164
lines changed

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: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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.
49+
///
50+
/// REQUIRED: `#[serde(default)]` on the struct + graceful deserializers on each
51+
/// field. Without these, a missing or malformed value fails the entire extension
52+
/// extraction — fields silently fall back to defaults with a warning log.
53+
#[derive(Debug, Clone, Default, Deserialize)]
54+
#[serde(default)]
55+
pub struct MySource {
56+
#[serde(deserialize_with = "deserialize_optional_bool_from_anything")]
57+
pub custom_flag: Option<bool>,
58+
#[serde(deserialize_with = "deserialize_optional_string")]
59+
pub custom_name: Option<String>,
60+
}
61+
62+
impl ConfigExtension for MyExtension {
63+
type Source = MySource;
64+
65+
fn merge_from(&mut self, source: &MySource) {
66+
merge_fields!(self, source,
67+
string: [custom_name],
68+
value: [custom_flag],
69+
);
70+
}
71+
}
72+
```
73+
74+
### 2. Load config with the extension
75+
76+
```rust
77+
use std::path::Path;
78+
use datadog_agent_config::{Config, get_config_with_extension};
79+
80+
type MyConfig = Config<MyExtension>;
81+
82+
let config: MyConfig = get_config_with_extension(Path::new("/var/task"));
83+
84+
// Core fields
85+
println!("site: {}", config.site);
86+
87+
// Extension fields
88+
println!("custom_flag: {}", config.ext.custom_flag);
89+
println!("custom_name: {}", config.ext.custom_name);
90+
```
91+
92+
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.
93+
94+
### Flat fields only
95+
96+
The single `Source` type is used for both env var and YAML extraction. This works because Figment uses a single key-value namespace per provider, so flat fields map naturally to both `DD_*` env vars and top-level YAML keys. If you need nested YAML structures (e.g., `lambda: { enhanced_metrics: true }`) that differ from the flat env var layout, you'd need separate source structs — implement `merge_from` with a nested source struct and handle the mapping manually.
97+
98+
### Field name collisions
99+
100+
Extension fields are extracted independently from the same figment as core fields. If an extension defines a field with the same name as a core field (e.g., `api_key`), both get their own copy — they don't interfere, but the extension copy does **not** override the core value. Avoid shadowing core field names to prevent confusion.
101+
102+
### merge_fields! macro
103+
104+
The `merge_fields!` macro reduces boilerplate in `merge_from` by batching fields by merge strategy:
105+
106+
- `string`: merges `Option<String>` into `String` (sets value if `Some`)
107+
- `value`: merges `Option<T>` into `T` (sets value if `Some`)
108+
- `option`: merges `Option<T>` into `Option<T>` (overwrites if `Some`)
109+
110+
Custom merge logic (e.g., OR-ing two boolean fields together) goes after the macro call in the same method.
111+
112+
## Config loading precedence
113+
114+
1. `Config::default()` (hardcoded defaults)
115+
2. `datadog.yaml` values (lower priority)
116+
3. `DD_*` environment variables (highest priority)
117+
4. Post-processing defaults (site, proxy, logs/APM URL construction)

crates/datadog-agent-config/additional_endpoints.rs renamed to crates/datadog-agent-config/src/deserializers/additional_endpoints.rs

File renamed without changes.

crates/datadog-agent-config/apm_replace_rule.rs renamed to crates/datadog-agent-config/src/deserializers/apm_replace_rule.rs

File renamed without changes.

crates/datadog-agent-config/flush_strategy.rs renamed to crates/datadog-agent-config/src/deserializers/flush_strategy.rs

File renamed without changes.

0 commit comments

Comments
 (0)