Skip to content

feat(agent-config): allow extensible configuration via ConfigExtension trait#111

Merged
duncanista merged 6 commits into
mainfrom
jordan.gonzalez/config/allow-extensible-configuration
Apr 9, 2026
Merged

feat(agent-config): allow extensible configuration via ConfigExtension trait#111
duncanista merged 6 commits into
mainfrom
jordan.gonzalez/config/allow-extensible-configuration

Conversation

@duncanista

Copy link
Copy Markdown
Contributor

Summary

  • Introduces a generic Config<E: ConfigExtension = NoExtension> type that lets consumers define additional configuration fields without modifying or copy-pasting the core crate
  • Adds a unified Source type for dual extraction from both env vars and YAML, and a merge_fields! macro to reduce merge boilerplate
  • Moves Lambda-specific fields out of the core Config struct so non-Lambda consumers don't carry unused fields
  • Restructures the crate to use a conventional src/ layout and adds a README documenting the extension API

Test plan

  • All 71 existing + new unit tests pass (cargo test -p datadog-agent-config)
  • Clippy passes with -D warnings on lib and tests
  • Full workspace build passes (cargo build --workspace)
  • Extension mechanism tested: NoExtension works, extension receives env/yaml fields, env overrides yaml, defaults when not set, extension does not interfere with core fields

Copilot AI review requested due to automatic review settings April 2, 2026 20:00
@duncanista duncanista requested review from a team as code owners April 2, 2026 20:00
@duncanista duncanista requested review from Lewis-E and jchrostek-dd and removed request for a team April 2, 2026 20:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot reviewed 5 out of 12 changed files in this pull request and generated no comments.

@duncanista duncanista force-pushed the jordan.gonzalez/config/allow-extensible-configuration branch from 9fe85fb to 7c6eb51 Compare April 2, 2026 20:26
@Lewis-E Lewis-E requested a review from duncanpharvey April 2, 2026 21:12
@duncanpharvey

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7c6eb51564

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/datadog-agent-config/src/lib.rs Outdated
…n 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.
…s/ 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.
…zers/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.
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.
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.
@duncanista duncanista force-pushed the jordan.gonzalez/config/allow-extensible-configuration branch from 3c8af4d to e8ba905 Compare April 7, 2026 14:22

@duncanpharvey duncanpharvey left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved with a few notes!

Comment thread crates/datadog-agent-config/README.md Outdated
Comment on lines +91 to +93
### Flat fields only

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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would having separate merge_from_env and merge_from_yaml resolve this issue for flat fields?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really — the flat-field limitation isn't about the merge step, it's about the deserialization step. Figment uses a single key-value namespace per provider, so a flat Source struct works naturally for both env vars (DD_CUSTOM_FLAG=true) and YAML (custom_flag: true). Splitting into merge_from_env/merge_from_yaml wouldn't change the deserialization model.

The issue only shows up if you need nested YAML (e.g., lambda: { enhanced_metrics: true }) that maps to a flat env var (DD_ENHANCED_METRICS). For that case you'd need two different Source structs — one flat for env, one nested for YAML — which would mean two associated types on the trait. That's extra complexity we don't need yet since all current extension fields are flat.

If we hit that case later, we can add EnvSource/YamlSource associated types in a backward-compatible way. For now I'll update the doc to make this clearer.

Comment thread crates/datadog-agent-config/README.md Outdated
}

/// Source struct for deserialization. Must use #[serde(default)] and
/// graceful deserializers so one bad field doesn't fail the whole extraction.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there warning if #[serde(default)] or graceful deserializers aren't used? How can we make this clear to someone using the config extension in case they miss this note in the readme?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call — there are runtime warnings but no compile-time enforcement. Here's what happens:

  • Missing #[serde(default)]: figment's extract() fails because serde won't fill in defaults for absent fields → hits the tracing::warn! branch → extension falls back to E::default(). All extension fields silently get defaults.
  • Missing graceful deserializers: one malformed value (e.g., "yes" for a bool) causes the whole extraction to fail → same warn + fallback behavior.

Both cases are silent in the sense that the extension consumer gets defaults without error, just a warning log. The README note is easy to miss.

I'll improve discoverability by:

  1. Adding a prominent # Safety / requirements section to the Source associated type doc (which IDEs surface on hover)
  2. Explaining the failure mode directly ("extraction will fall back to defaults with a warning")

We can't enforce #[serde(default)] at compile time, but making the consequence clear in the type-level doc should help.


#[allow(clippy::too_many_lines)]
fn merge_config(config: &mut Config, yaml_config: &YamlConfig) {
fn merge_config<E: ConfigExtension>(config: &mut Config<E>, yaml_config: &YamlConfig) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any consequences to having the same field defined in the extension as one that is already in the core config?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No harmful consequences — the core and extension are extracted independently from the same figment, so both get their own copy of the value. The core field drives behavior as usual, and the extension field just has a duplicate. No errors, no data loss, no interference.

The main risk is confusion: someone might shadow api_key in their extension and expect it to override the core, but it wouldn't — config.api_key and config.ext.api_key would be independent copies.

I'll add a note about this in the trait doc.

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.
@duncanista duncanista merged commit 1eb4556 into main Apr 9, 2026
27 checks passed
@duncanista duncanista deleted the jordan.gonzalez/config/allow-extensible-configuration branch April 9, 2026 20:18
duncanista added a commit to DataDog/datadog-lambda-extension that referenced this pull request Jun 18, 2026
…nt-config (#1249)

## Overview

First step of migrating bottlecap's in-tree configuration module onto
the shared `datadog-agent-config` crate (lives in
[DataDog/serverless-components](https://github.com/DataDog/serverless-components/tree/main/crates/datadog-agent-config)).

This PR adds the foundation only — it introduces a `LambdaConfig`
extension struct (in `bottlecap/src/config/mod.rs`, alongside the
existing legacy code) that implements the upstream `ConfigExtension`
trait, plus a `LambdaConfigSource` deserialization shape that figment
uses for both env-var and YAML loading (dual extraction).

Nothing in bottlecap consumes the extension yet — the existing
`bottlecap::config::Config` struct is untouched. The follow-up #1251
(stacked on this) replaces it with `Config<LambdaConfig>`, deletes the
duplicated env.rs / yaml.rs / deserializer modules, and removes the
legacy `#[macro_export]` `merge_*` macros at the top of `mod.rs`
(`LambdaConfig::merge_from` deliberately uses fully-qualified upstream
macro paths today to coexist with them).

### Why this shape

The upstream crate is purpose-built for this migration — see PR
DataDog/serverless-components#111 ("ConfigExtension trait") which landed
exactly the extensibility hook we need. Each consumer (bottlecap here,
future serverless agents elsewhere) supplies its own extension type for
fields that don't generalize, while sharing one canonical implementation
for all the core agent fields (site, api_key, logs/APM/OTLP/proxy/trace
propagation, etc.).

### Fields in the extension

The 19 Lambda-specific fields that have no upstream equivalent:

- `api_key_secret_arn`, `kms_api_key`, `api_key_ssm_arn`,
`api_key_secret_reload_interval`
- `serverless_logs_enabled` (OR-merged with the `DD_LOGS_ENABLED` alias)
- `serverless_flush_strategy` (custom `FlushStrategy` deserializer for
`"end" | "end,N" | "periodically,N" | "continuously,N"`)
- `enhanced_metrics`, `lambda_proc_enhanced_metrics`
- `capture_lambda_payload`, `capture_lambda_payload_max_depth`
- `compute_trace_stats_on_extension`, `span_dedup_timeout`
- `dd_org_uuid` (sourced from `DD_ORG_UUID`, source field `org_uuid` →
config field `dd_org_uuid`)
- `serverless_appsec_enabled`, `appsec_rules`, `appsec_waf_timeout`
- `api_security_enabled`, `api_security_sample_delay`
- `custom_metrics_exclude_tags` (sourced from
`DD_LAMBDA_CUSTOMER_METRICS_EXCLUDE_TAGS` /
`lambda_customer_metrics_exclude_tags:` in YAML)

Kept in the extension rather than upstreamed because they're
Lambda-runtime concerns. `custom_metrics_exclude_tags` is arguably
generalizable to other serverless targets but stays here for now; we can
migrate it upstream when another consumer needs it.

### Pre-requisites that already merged

- DataDog/serverless-components#135 — bumps libdatadog rev across all
serverless-components crates from `0a3304c` to `48da0d8` to match
bottlecap.
- DataDog/serverless-components#136 — switches `datadog-agent-config`'s
libdd transitive deps to `default-features = false` and exposes `https`
/ `fips` opt-in features. Without this, the FIPS dep tree was poisoned
by an implicit `https` (ring) path.

Bottlecap pins all three serverless-components crates (`dogstatsd`,
`datadog-fips`, `datadog-agent-config`) to the merge SHA `bb4dedee` so
cargo deduplicates `dogstatsd`.

## Testing

37 new tests in `bottlecap/src/config/mod.rs::lambda_config_tests` cover
each extension field from both env vars and YAML where applicable, plus:

- Per-field env round-trip (`api_key_secret_arn`, `kms_api_key`,
`api_key_ssm_arn`, …)
- YAML coverage for the two source-to-config renames: `org_uuid` →
`dd_org_uuid` and `lambda_customer_metrics_exclude_tags` →
`custom_metrics_exclude_tags`
- `DD_LOGS_ENABLED` ↔ `DD_SERVERLESS_LOGS_ENABLED` OR-merge semantics
- `FlushStrategy` — `"end"`, `"end,N"` (`EndPeriodically`),
`"periodically,N"`, `"continuously,N"`, invalid → `Default`
- Duration parsing — seconds, microseconds, `_ignore_zero` variant
- env precedence over YAML
- Forgiving fallback when a single field is malformed (default preserved
instead of failing the whole extraction)

```
$ cargo test --lib config::lambda_config_tests
test result: ok. 37 passed; 0 failed; 0 ignored
```

Existing bottlecap config behavior is unchanged (this PR adds; it
doesn't replace yet).

## Follow-up

- #1251 — stacked on this branch — does the actual wire-in: replaces
`bottlecap::config::Config` with `Config<LambdaConfig>`, deletes the
duplicated config files, and removes the legacy `#[macro_export]` macros
at the top of `mod.rs`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants