Skip to content

fix(config): recognize duration keys at their default in the config classifier#1958

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 2 commits into
mainfrom
jszwedko/fix-config-classifier-duration-default
Jul 1, 2026
Merged

fix(config): recognize duration keys at their default in the config classifier#1958
gh-worker-dd-mergequeue-cf854d[bot] merged 2 commits into
mainfrom
jszwedko/fix-config-classifier-duration-default

Conversation

@jszwedko

@jszwedko jszwedko commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

The nightly integration suite (which runs against the latest Datadog Agent master rather than the pinned release) started failing because ADP emitted a spurious Unsupported configuration key warning for tls_handshake_timeout. The key was at its default the whole time — the classifier just couldn't tell, because it compares the Agent-supplied value against the schema default with exact JSON equality, and duration keys don't survive that comparison: the schema expresses their default as a Go duration string (10s) while the Agent transmits durations over the config stream as integer nanoseconds (10000000000). The result is a false "override" for any duration key the Agent populates at its default. This restores the intended behavior — a key sitting at its default is never treated as an incompatible override — by teaching the classifier that duration defaults and duration values are the same quantity in two encodings.

flowchart LR
    A["Agent config stream<br/>tls_handshake_timeout = 10000000000 (ns)"] --> C{is_default?}
    S["Schema default<br/>10s (Go duration string)"] --> C
    C -- "before: String != Number<br/>=> false => WARN" --> W["Unsupported configuration key"]
    C -- "after: normalize both to ns<br/>10e9 == 10e9 => true" --> OK["Recognized as default (no warning)"]
Loading

The fix threads the schema's format: duration marker through codegen into each ClassifierEntry (is_duration), and is_default_value normalizes both sides to nanoseconds before comparing. It applies to every duration-typed key in the classifier, not just tls_handshake_timeout. No schema/overlay edits were required.

Test plan

  • New unit tests in classifier.rs: a duration key sent as nanoseconds is recognized as its default; a non-default nanosecond value is not; and parse_go_duration_nanos covers the common Go duration forms (10s, 500ms, 1h30m0s, 1.5h, 250µs, -5s, bare 0, and malformed input).
  • Reproduced the nightly failure locally against Agent master-py3-full, then confirmed adp-config-check-no-warn and adp-config-stream pass with this change.

Note

This addresses one of two independent regressions surfaced by the nightly. The remaining failures (*-api-endpoints, adp-config-dynamic-basic, dogstatsd-forwarding) are a separate issue — under the new Agent, ADP's API binds default ports instead of honoring DD_DATA_PLANE__API_LISTEN_ADDRESS — and will be handled separately.

🤖 Generated with Claude Code

…ifier

The config classifier compared a key's incoming value against its schema
default using exact JSON equality. Duration keys broke this: the schema
default is a Go duration string (e.g. `10s`) while the Datadog Agent
transmits durations over the config stream as integer nanoseconds, so a
value at its default (e.g. `tls_handshake_timeout` = 10000000000) never
matched and was flagged as an override, emitting a spurious "Unsupported
configuration key" warning.

Carry the schema's `format: duration` marker through to each classifier
entry and normalize both sides to nanoseconds before comparing, so any
duration-typed key at its default is correctly recognized.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@datadog-prod-us1-4

This comment has been minimized.

@pr-commenter

pr-commenter Bot commented Jul 1, 2026

Copy link
Copy Markdown

Binary Size Analysis (Agent Data Plane)

Baseline: 9b8116a · Comparison: 44b4303 · diff
Analysis Configuration: stripped binaries · Pass/Fail Threshold: +5%
Sizes: 40.93 MiB (baseline) vs 40.93 MiB (comparison)
Size Change: +4.02 KiB (+0.01%)

✅ Binary size difference within threshold

Changes by Module
Module File Size Symbols
anon.0dc1de58c4eb14eb4a57fa004575eaa2.87.llvm.11975713051037423158 +6.60 KiB 1
anon.8235e1dce8eb18556833a2e87e74b44e.87.llvm.11559150647779385104 -6.60 KiB 1
core +3.33 KiB 180
datadog_agent_config::classifier::classifier +3.12 KiB 2
anon.62853794e13ae541c6d112711acb4464.32.llvm.5906959730437014482 +1.87 KiB 1
anon.07c4bb9ce5ce61a5c762f558e396468b.71.llvm.14285062802314426476 -1.87 KiB 1
[Unmapped] -1.44 KiB 1
[sections] -1.32 KiB 4
anon.77c732748a8e3ba7b49ad472e119a379.11.llvm.7277500116383046631 -1.22 KiB 1
anon.85166eb3b0aaad226210b48fb953f8a4.32.llvm.9366429370825567082 +1.22 KiB 1
anon.191002047911fb2fa1e2b77a05ef7373.97.llvm.12837151388450619170 +1.16 KiB 1
anon.ed68a82a9d28fb52a623006f1bfcf487.455.llvm.838646141026691578 -1.16 KiB 1
_RNvXs3_NtNtCs1rCenOSxXMM_4core3str4iterNtB5_11CharIndicesNtNtNtNtB9_4iter6traits8iterator8Iterator4next +1007 B 1
anon.2a38498d16f9782c273333dbc54e6655.107.llvm.10979839591502906197 -747 B 1
anon.85166eb3b0aaad226210b48fb953f8a4.256.llvm.9366429370825567082 +745 B 1
anon.2b8a59614595c3e3b68e70a2a9b31565.807.llvm.10324895239243302904 -640 B 1
anon.2b8a59614595c3e3b68e70a2a9b31565.807.llvm.9611478839992256081 +639 B 1
anon.e827d7478b466ec5bd3d15a8bf14bf2f.35.llvm.5804879572110709646 +635 B 1
anon.ed68a82a9d28fb52a623006f1bfcf487.601.llvm.838646141026691578 -633 B 1
_RNvXsX_NtNtCs1rCenOSxXMM_4core3fmt3numyNtB7_5Debug3fmt.llvm.15678430660458215697 +579 B 1
Detailed Symbol Changes
    FILE SIZE        VM SIZE    
 --------------  -------------- 
  [NEW] +6.60Ki  [NEW] +6.52Ki    anon.0dc1de58c4eb14eb4a57fa004575eaa2.87.llvm.11975713051037423158
  [NEW] +2.74Ki  [NEW] +2.64Ki    hashbrown::raw::RawTable<T,A>::reserve_rehash::h68aaef8921c981d1
  [NEW] +2.71Ki  [NEW] +2.59Ki    datadog_agent_config::classifier::classifier::duration_value_as_nanos::h63118431e8d84c57
  [NEW] +2.31Ki  [NEW]     +47    _<http_body_util::combinators::map_err::MapErr<B,F> as http_body::Body>::size_hint::h0cd930040729ea65
  +0.1% +2.08Ki  +0.1% +2.27Ki    [1485 Others]
  [NEW] +2.05Ki  [NEW] +1.96Ki    core::str::_<impl str>::trim_matches::h7023e7ed91ef5914
  [NEW] +1.87Ki  [NEW]     +90    anon.62853794e13ae541c6d112711acb4464.32.llvm.5906959730437014482
  [NEW] +1.31Ki  [NEW]    +592    core::ptr::drop_in_place<$LP$axum::routing::RouteId,axum::routing::Endpoint<saluki_components::sources::dogstatsd::replay::capture::DogStatsDCaptureControl>$RP$>::h39dbeb2a71bb9f03
  [NEW] +1.25Ki  [NEW] +1.15Ki    hashbrown::map::HashMap<K,V,S,A>::insert::h365adb0d6d06524a
  [NEW] +1.22Ki  [NEW]     +79    anon.85166eb3b0aaad226210b48fb953f8a4.32.llvm.9366429370825567082
  [NEW] +1.16Ki  [NEW]     +95    anon.191002047911fb2fa1e2b77a05ef7373.97.llvm.12837151388450619170
  [DEL] -1.16Ki  [DEL]     -95    anon.ed68a82a9d28fb52a623006f1bfcf487.455.llvm.838646141026691578
  [DEL] -1.22Ki  [DEL]     -79    anon.77c732748a8e3ba7b49ad472e119a379.11.llvm.7277500116383046631
  [DEL] -1.25Ki  [DEL] -1.15Ki    hashbrown::map::HashMap<K,V,S,A>::insert::h3954043cda08b562
  [DEL] -1.30Ki  [DEL]    -592    core::ptr::drop_in_place<$LP$axum::routing::RouteId,axum::routing::Endpoint<saluki_components::sources::dogstatsd::replay::replay_control::DogStatsDReplayControl>$RP$>::hef767393d081a1d7
  -0.8% -1.38Ki  -0.8% -1.38Ki    [section .rodata]
 -38.9% -1.44Ki  [ = ]       0    [Unmapped]
  [DEL] -1.87Ki  [DEL]     -90    anon.07c4bb9ce5ce61a5c762f558e396468b.71.llvm.14285062802314426476
  [DEL] -2.32Ki  [DEL]     -47    _<http_body_util::combinators::map_err::MapErr<B,F> as http_body::Body>::size_hint::hde577ad7eeb4834b
  [DEL] -2.74Ki  [DEL] -2.64Ki    hashbrown::raw::RawTable<T,A>::reserve_rehash::h38091f78138b6b58
  [DEL] -6.60Ki  [DEL] -6.52Ki    anon.8235e1dce8eb18556833a2e87e74b44e.87.llvm.11559150647779385104
  +0.0% +4.02Ki  +0.0% +5.45Ki    TOTAL

@pr-commenter

pr-commenter Bot commented Jul 1, 2026

Copy link
Copy Markdown

Regression Detector (Agent Data Plane)

Run ID: 09e134a5-880b-4745-9344-3f6df53efda1
Baseline: 9b8116a1 · Comparison: 44b4303e · diff

Optimization Goals: ✅ No significant changes detected

Fine details of change detection per experiment (5)

Experiments configured erratic: true are tagged (ignored) and skipped when determining which experiments regressed or improved. Experiments which are detected as erratic at runtime are tagged (erratic) to flag that the run's sample dispersion was high, but their regression / improvement signal still counts.

experiment goal Δ mean % links
quality_gates_rss_dsd_medium memory ⚪ +0.09 metrics profiles logs
quality_gates_rss_dsd_ultraheavy memory ⚪ +0.04 metrics profiles logs
quality_gates_rss_idle memory ⚪ -0.01 metrics profiles logs
quality_gates_rss_dsd_low memory ⚪ -0.21 metrics profiles logs
quality_gates_rss_dsd_heavy memory ⚪ -0.25 metrics profiles logs
Bounds Checks: ✅ Passed (5)
experiment check replicates observed links
quality_gates_rss_dsd_heavy memory_usage 10/10 ✅ 139 MiB ≤ 140 MiB metrics profiles logs
quality_gates_rss_dsd_low memory_usage 10/10 ✅ 42.7 MiB ≤ 50 MiB metrics profiles logs
quality_gates_rss_dsd_medium memory_usage 10/10 ✅ 66.1 MiB ≤ 75 MiB metrics profiles logs
quality_gates_rss_dsd_ultraheavy memory_usage 10/10 ✅ 191 MiB ≤ 200 MiB metrics profiles logs
quality_gates_rss_idle memory_usage 10/10 ✅ 29 MiB ≤ 40 MiB metrics profiles logs
Explanation

A change is flagged as a regression when |Δ mean %| > 5.00% in the regressing direction for its optimization goal AND SMP marks the experiment as a regression (is_regression: true). Improvements use the matching criteria for the improving direction. Experiments configured erratic: true (tagged (ignored)) are skipped outright; experiments detected as erratic at runtime (tagged (erratic)) still count, since that flag describes sample dispersion rather than directional certainty. The Δ mean % cell is colored accordingly: 🟢 = improvement, 🔴 = regression, ⚪ = neutral. Reduction in CPU or memory is an improvement; reduction in ingress throughput is a regression.

/// Supports the unit suffixes Go's `time.ParseDuration` accepts (`ns`, `us`/`µs`/`μs`, `ms`, `s`,
/// `m`, `h`), an optional leading sign, and fractional components. Returns `None` if the string is
/// not a well-formed duration.
fn parse_go_duration_nanos(input: &str) -> Option<i128> {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This can be refactored out to share the implementation from saluki-config.

@webern webern 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.

Approved as a quick fix, with a recommended follow-up (see long comment).

Basically, I think this is closer to right than #1959, but the design I pasted in (even if it's not 100% right as written, not sure, but...) gets us even closer than this to actually dealing with this datatype.

/// Whether this key holds a `format: duration` value. The Agent sends durations as integer
/// nanoseconds while `default` is a Go duration string, so the default check normalizes both
/// sides before comparing.
pub is_duration: bool,

@webern webern Jul 1, 2026

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.

I spent some time on designing what I think is a proper fix and that design is below. It solves a few problems and models the actual data type correctly. You can merge your fix as is, but if you do I might suggest posting this as a follow up issue (which I can take a stab at). It might have enough fidelity to nearly one-shot it as is from the write-up, though there's a decision buried in there about how we stop rewriting the "Go duration" algorithm over and over again 😅

The major benefits of the design below

  • We catch this at compile time. If the format is duration and default is not parsable as a duration, our build breaks.
  • We properly model the specified datatype at the source and all downstream codegen can do the right thing™ with it.

1. Model format: duration as an effective field type

The vendored Agent schema is JSON Schema in YAML form. I checked the resolved schema, and the only format value we currently see is duration. Those entries are all type: number, format: duration. (Well, actually on main it is now corrected to type: string, format: duration)

Given that, I think is_duration is modeling this at the wrong level. The source schema has type + format, but our generated FieldType is already the effective type used by codegen/smoke tests/classification. So I would rather fold format: duration into FieldType::Duration than carry FieldType::Float plus an is_duration side channel.

lib/datadog-agent/config-overlay-model/src/schema_gen.rs

pub enum FieldType {
    String,
    Bool,
    Integer,
    Float,
    StringList,
    Duration,
    Unknown,
}

lib/datadog-agent/config-overlay-model/src/schema_gen.rs

fn parse_value_type(value: &Value) -> FieldType {
    match (
        value.get("type").and_then(|v| v.as_str()),
        value.get("format").and_then(|v| v.as_str()),
    ) {
        (Some("string"), Some("duration")) => FieldType::Duration,
        // Should we break the build on nonsense like this?
        (_, Some("duration")) => panic!("illogical duration field {show_helpful_stuff}"),
        (Some("string"), _) => FieldType::String,
        (Some("boolean"), _) => FieldType::Bool,
        (Some("integer"), _) => FieldType::Integer,
        (Some("number"), _) => FieldType::Float,
        (Some("array"), _) => {
            let item_type = value.get("items").and_then(|v| v.get("type")).and_then(|v| v.as_str());
            if item_type == Some("string") {
                FieldType::StringList
            } else {
                FieldType::Unknown
            }
        }
        _ => FieldType::Unknown,
    }
}

If we later want raw JSON Schema fidelity, I would model that explicitly as something like json_type + format. But FieldType + is_duration is neither the raw schema model nor a clean effective-type model; it is a one-off side channel that future schema gotchas are likely to copy.

2. Teach smoke tests about duration too

I would also make the config-testing registry understand this as a real value type. That avoids needing bespoke test_json values for every future duration key.

lib/datadog-agent/config-testing/src/config_registry/mod.rs

pub enum ValueType {
    Bool,
    String,
    Integer,
    Float,
    StringList,
    Duration,
}

lib/datadog-agent/config-testing/src/smoke_test.rs

fn test_json_value(value_type: ValueType) -> serde_json::Value {
    match value_type {
        ValueType::String => json!(TEST_STRING_VALUE),
        ValueType::Bool => json!(TEST_BOOL_VALUE),
        ValueType::StringList => json!(TEST_STRING_LIST_VALUE),
        ValueType::Integer => json!(42i64),
        ValueType::Float => json!(1.5f64),
        ValueType::Duration => json!("42s"),
    }
}

lib/datadog-agent/config-testing/src/smoke_test.rs

fn json_value_to_env_string(value: &serde_json::Value, value_type: ValueType) -> String {
    match value_type {
        ValueType::Bool => value
            .as_bool()
            .map(|b| b.to_string())
            .unwrap_or_else(|| "true".to_string()),
        ValueType::Integer => value
            .as_i64()
            .map(|n| n.to_string())
            .unwrap_or_else(|| "42".to_string()),
        ValueType::Float => value
            .as_f64()
            .map(|f| f.to_string())
            .unwrap_or_else(|| "1.5".to_string()),
        ValueType::String => value.as_str().unwrap_or(TEST_STRING_VALUE).to_string(),
        ValueType::StringList => value
            .as_array()
            .map(|arr| arr.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>().join(" "))
            .unwrap_or_else(|| TEST_STRING_LIST_VALUE.join(" ")),
        ValueType::Duration => value.as_str().unwrap_or("42s").to_string(),
    }
}

Individual overlay entries should only need test_json: '"20s"' or similar when the generic duration test value is bad for that specific key.

3. Store parsed duration defaults, not parse schema defaults during classification

The other thing I would change is the default representation. Duration defaults are static generated metadata. I do not think the classifier should parse the schema default on every classification. Codegen should parse and canonicalize the duration default once, and fail the build if the schema default is invalid.

For example:

lib/datadog-agent/config/src/classifier/mod.rs

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DefaultValue {
    Missing,
    Json(&'static str),
    DurationNanos(u64),
}

Then generated classifier data can carry a typed default:

lib/datadog-agent/config/src/classifier/classifier_data.rs

ClassifierEntry {
    yaml_path: "tls_handshake_timeout",
    aliases: &[],
    support_level: SupportLevel::Incompatible(Severity::Medium),
    pipeline_affinity: PipelineAffinity::CrossCutting,
    default: DefaultValue::DurationNanos(10_000_000_000),
}

Non-duration defaults can stay JSON-backed:

lib/datadog-agent/config/src/classifier/classifier_data.rs

ClassifierEntry {
    yaml_path: "min_tls_version",
    aliases: &[],
    support_level: SupportLevel::Partial,
    pipeline_affinity: PipelineAffinity::CrossCutting,
    default: DefaultValue::Json("\"tlsv1.2\""),
}

Then the runtime default check switches on the already-typed default:

lib/datadog-agent/config/src/classifier/classifier.rs

fn is_default_value(default: DefaultValue, value: &Value) -> bool {
    match default {
        DefaultValue::Json(default_str) => serde_json::from_str::<Value>(default_str)
            .map(|default_value| *value == default_value)
            .unwrap_or(false),

        DefaultValue::DurationNanos(default_ns) => duration_value_as_nanos(value)
            .map(|value_ns| value_ns == default_ns)
            .unwrap_or(false),

        DefaultValue::Missing => match value {
            Value::Null => true,
            Value::String(s) => s.is_empty(),
            _ => false,
        },
    }
}

Runtime still needs to normalize the incoming value because the Agent config stream may send durations as integer nanoseconds:

lib/datadog-agent/config/src/classifier/classifier.rs

fn duration_value_as_nanos(value: &Value) -> Option<u64> {
    match value {
        Value::Number(n) => n.as_u64(),
        Value::String(s) => parse_duration(s).ok().map(|d| d.as_nanos() as u64),
        _ => None,
    }
}

4. Avoid duplicating DurationString

This PR's duration parser duplicates the existing saluki-config::DurationString / parse_duration behavior. I agree datadog-agent-config should not depend on saluki-config; that would pull a leaf-ish Datadog config crate toward a much heavier, higher-level crate.

There are also existing crates that may solve the leaf-crate problem without making our crates depend on each other:

If one of these is acceptable after careful vetting, I would prefer taking that dependency over continuing to rewrite this parser in multiple places. The vetting should include at least: license, maintenance/activity, dependency footprint, exact Go time.ParseDuration compatibility, overflow behavior, negative duration behavior, and whether the API returns the unit we want without lossy conversion.

This is especially attractive because duration parsing has a pile of small edge cases (µs vs μs, fractional units, signs, overflow, bare 0, malformed input). The existence of this PR is a good signal that we should stop locally reimplementing that algorithm.

One possible shape if we pick a crate:

lib/datadog-agent/config/Cargo.toml

[dependencies]
go-parse-duration = "0.1"

lib/saluki-config/Cargo.toml

[dependencies]
go-parse-duration = "0.1"

Then saluki-config::DurationString can remain our domain type, but delegate parsing to the vetted implementation:

lib/saluki-config/src/duration_string.rs

fn parse_string(s: &str) -> Result<Duration, ParseDurationError> {
    let nanos = go_parse_duration::parse_duration(s).map_err(|err| ...)?;
    Duration::from_nanos(nanos.try_into().map_err(|_| ParseDurationError::Negative)?).into()
}

And classifier codegen can use the same parser for schema defaults without depending on saluki-config:

lib/datadog-agent/config/build/classifier_gen.rs

fn schema_default_expr(yaml_path: &str, schema_map: &IndexMap<String, FieldInfo>) -> String {
    let Some(info) = schema_map.get(yaml_path) else {
        return "DefaultValue::Missing".to_string();
    };

    match (&info.value_type, &info.default) {
        (FieldType::Duration, Some(default)) => {
            let default_value: serde_json::Value =
                serde_json::from_str(default).expect("schema duration default must be JSON");
            let default_str = default_value
                .as_str()
                .expect("schema duration default must be a string");

            let nanos = go_parse_duration::parse_duration(default_str)
                .expect("schema duration default must be a valid Go duration");

            format!("DefaultValue::DurationNanos({})", nanos)
        }
        (_, Some(default)) => {
            format!("DefaultValue::Json(\"{}\")", escape_str(default))
        }
        (_, None) => "DefaultValue::Missing".to_string(),
    }
}

If neither crate passes vetting, then I think the fallback should be to move our existing duration parsing primitive into a smaller lower-level shared crate/module and have both saluki-config and datadog-agent-config use it. The important part is that the parser has a single owner.

The main point: I like the bugfix direction, but I would rather put the duration-specific behavior into the generated type/default model than add is_duration and reparsing logic to the classifier path.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

👍 I like the explicit modeling of duration types. Started a draft follow-up with this direction here: #1966

@jszwedko jszwedko marked this pull request as ready for review July 1, 2026 19:00
@jszwedko jszwedko requested a review from a team as a code owner July 1, 2026 19:00
…lassifier-duration-default

# Conflicts:
#	lib/datadog-agent/config/src/classifier/classifier_data.rs
@gh-worker-dd-mergequeue-cf854d gh-worker-dd-mergequeue-cf854d Bot merged commit 94ecf81 into main Jul 1, 2026
85 checks passed
@gh-worker-dd-mergequeue-cf854d gh-worker-dd-mergequeue-cf854d Bot deleted the jszwedko/fix-config-classifier-duration-default branch July 1, 2026 19:49
dd-octo-sts Bot pushed a commit that referenced this pull request Jul 1, 2026
…lassifier (#1958)

## Summary

The nightly integration suite (which runs against the latest Datadog Agent master rather than the pinned release) started failing because ADP emitted a spurious `Unsupported configuration key` warning for `tls_handshake_timeout`. The key was at its default the whole time — the classifier just couldn't tell, because it compares the Agent-supplied value against the schema default with exact JSON equality, and duration keys don't survive that comparison: the schema expresses their default as a Go duration string (`10s`) while the Agent transmits durations over the config stream as integer nanoseconds (`10000000000`). The result is a false "override" for any duration key the Agent populates at its default. This restores the intended behavior — a key sitting at its default is never treated as an incompatible override — by teaching the classifier that duration defaults and duration values are the same quantity in two encodings.

```mermaid
flowchart LR
    A["Agent config stream<br/>tls_handshake_timeout = 10000000000 (ns)"] --> C{is_default?}
    S["Schema default<br/>10s (Go duration string)"] --> C
    C -- "before: String != Number<br/>=> false => WARN" --> W["Unsupported configuration key"]
    C -- "after: normalize both to ns<br/>10e9 == 10e9 => true" --> OK["Recognized as default (no warning)"]
```

The fix threads the schema's `format: duration` marker through codegen into each `ClassifierEntry` (`is_duration`), and `is_default_value` normalizes both sides to nanoseconds before comparing. It applies to every duration-typed key in the classifier, not just `tls_handshake_timeout`. No schema/overlay edits were required.

## Test plan

- [x] New unit tests in `classifier.rs`: a duration key sent as nanoseconds is recognized as its default; a non-default nanosecond value is not; and `parse_go_duration_nanos` covers the common Go duration forms (`10s`, `500ms`, `1h30m0s`, `1.5h`, `250µs`, `-5s`, bare `0`, and malformed input).
- [x] Reproduced the nightly failure locally against Agent `master-py3-full`, then confirmed `adp-config-check-no-warn` and `adp-config-stream` pass with this change.

> [!NOTE]
> This addresses one of two independent regressions surfaced by the nightly. The remaining failures (`*-api-endpoints`, `adp-config-dynamic-basic`, `dogstatsd-forwarding`) are a separate issue — under the new Agent, ADP's API binds default ports instead of honoring `DD_DATA_PLANE__API_LISTEN_ADDRESS` — and will be handled separately.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: jesse.szwedko <jesse.szwedko@datadoghq.com> 94ecf81
gh-worker-dd-mergequeue-cf854d Bot pushed a commit that referenced this pull request Jul 2, 2026
## Summary

Follow-up to #1958 (now merged), taking the alternative design proposed in review (#1958 (comment)): model `format: duration` as a first-class type and store **parsed** duration defaults in the generated classifier data, instead of carrying an `is_duration` side channel and reparsing a schema string on every classification. The base type is modeled correctly, and an invalid duration default now breaks the build rather than misbehaving at runtime.

Key changes:

- **New `go-duration` leaf crate** owns Go `time.ParseDuration` parsing. `saluki-config`'s `DurationString` delegates to it, and the classifier build script uses it too — so the algorithm has a single owner instead of being reimplemented (the parser #1958 added to the classifier is removed). An upstream crate (for example `go-parse-duration`) can replace this later after vetting; keeping it in-tree for now avoids taking an unvetted external dependency.
- **`FieldType::Duration`** (plus a matching `ValueType::Duration` for the config-testing smoke tests) replaces the `is_duration` flag. `parse_value_type` folds `format: duration` in and panics on an illogical base type. Smoke tests use a generic `"42s"` value so future duration keys need no bespoke `test_json`.
- **Typed `DefaultValue`** (`Missing` / `Json(&'static str)` / `DurationNanos(u64)`) replaces `Option<&str>` + `is_duration` on `ClassifierEntry`. Codegen parses duration defaults to nanoseconds via `go-duration` and **fails the build** if a `format: duration` default isn't a valid Go duration. The runtime default check simply switches on the typed default; it never parses a schema default.

Behavior matches #1958 for the `tls_handshake_timeout` bug (a default-valued duration is recognized as default whether sent as nanoseconds or a Go duration string), but the model is cleaner and the failure mode moves to compile time.

## Test plan

- [x] `go-duration` unit tests (Go-style units, signs/fractions/zero, largest value, invalid/overflow, error messages).
- [x] `saluki-config` duration tests pass against the delegated parser.
- [x] Classifier unit tests updated for `DefaultValue` (per-variant `is_default_value` coverage; duration matched both as nanoseconds and as a Go duration string).
- [x] `cargo check --workspace --tests` and `cargo clippy` clean; config-testing smoke tests pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: jesse.szwedko <jesse.szwedko@datadoghq.com>
dd-octo-sts Bot pushed a commit that referenced this pull request Jul 2, 2026
## Summary

Follow-up to #1958 (now merged), taking the alternative design proposed in review (#1958 (comment)): model `format: duration` as a first-class type and store **parsed** duration defaults in the generated classifier data, instead of carrying an `is_duration` side channel and reparsing a schema string on every classification. The base type is modeled correctly, and an invalid duration default now breaks the build rather than misbehaving at runtime.

Key changes:

- **New `go-duration` leaf crate** owns Go `time.ParseDuration` parsing. `saluki-config`'s `DurationString` delegates to it, and the classifier build script uses it too — so the algorithm has a single owner instead of being reimplemented (the parser #1958 added to the classifier is removed). An upstream crate (for example `go-parse-duration`) can replace this later after vetting; keeping it in-tree for now avoids taking an unvetted external dependency.
- **`FieldType::Duration`** (plus a matching `ValueType::Duration` for the config-testing smoke tests) replaces the `is_duration` flag. `parse_value_type` folds `format: duration` in and panics on an illogical base type. Smoke tests use a generic `"42s"` value so future duration keys need no bespoke `test_json`.
- **Typed `DefaultValue`** (`Missing` / `Json(&'static str)` / `DurationNanos(u64)`) replaces `Option<&str>` + `is_duration` on `ClassifierEntry`. Codegen parses duration defaults to nanoseconds via `go-duration` and **fails the build** if a `format: duration` default isn't a valid Go duration. The runtime default check simply switches on the typed default; it never parses a schema default.

Behavior matches #1958 for the `tls_handshake_timeout` bug (a default-valued duration is recognized as default whether sent as nanoseconds or a Go duration string), but the model is cleaner and the failure mode moves to compile time.

## Test plan

- [x] `go-duration` unit tests (Go-style units, signs/fractions/zero, largest value, invalid/overflow, error messages).
- [x] `saluki-config` duration tests pass against the delegated parser.
- [x] Classifier unit tests updated for `DefaultValue` (per-variant `is_default_value` coverage; duration matched both as nanoseconds and as a Go duration string).
- [x] `cargo check --workspace --tests` and `cargo clippy` clean; config-testing smoke tests pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: jesse.szwedko <jesse.szwedko@datadoghq.com> 7b98c6f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants