Skip to content

List the documented /config and /health keys in the served order - #372

Merged
diolektor merged 1 commit into
mainfrom
fix/docs-config-samples-list-keys-in-declaration-order
Sep 20, 2026
Merged

diolektor merged 1 commit into
mainfrom
fix/docs-config-samples-list-keys-in-declaration-order

Conversation

@diolektor

Copy link
Copy Markdown
Contributor

Problem

serde_json is built without preserve_order, so serde_json::Map is a BTreeMap and every object /config and /health return goes out with its keys ascending, at every nesting level. The documented samples were written in the order the json! literals declare the fields — network, workers, timeouts.

The keys themselves were right, all thirty-six of them on /config, and so was every value. Only the position was wrong, which is enough to stop a sample being usable as the baseline for a diff against a live response, and to send a reader looking for a setting several lines from where it turns out to be.

The scope is wider than it first looks. Seven blocks across three pages:

Page /config /health
docs/features/internal-server.md 1 (36 keys) 2 (7 each)
docs/operations/health-checks.md 1 2
docs/operations/configuration.md 1, plus the nested plugins

/health is built by the same json! in build_health_json and sorts the same way. The nested plugins object in configuration.md was wrong twice over: otel came before apm, and apm's own settings were not ascending either.

Why not preserve_order

Enabling the feature and leaving the samples alone was the obvious alternative. It was rejected because it breaks the very scenario this fixes.

PluginManager::config_json() serializes config_values: HashMap<String, HashMap<String, Value>> through serde_json::to_value. Today that lands in a BTreeMap and comes out sorted, so the plugins subtree is stable across restarts. Under preserve_order it would land in an IndexMap and inherit the HashMap iteration order, which std randomizes per process via RandomStateplugins would reshuffle on every restart, making a diff against a live response worse rather than better. Avoiding that would mean converting plugin/manager.rs, plugin/context.rs and five plugins' config to BTreeMap, plus a Cargo.lock change pulling THIRD_PARTY_LICENSES.html and cargo deny behind it — and it would make the textual order of every json! literal in the project an observable contract.

Alphabetical is also not the worse order to read: finding a key by name among thirty-six is easiest alphabetically, and the grouped reference already exists as the settings table in docs/operations/configuration.md. A JSON sample's job is to show what actually arrives.

What is deliberately not touched

docs/features/profiling.md's index.json sample is correct and was left alone. RunMeta is a #[derive(Serialize)] struct written with serde_json::to_vec, and serde's text path streams fields in declaration order without ever building a Map. Worth stating precisely, because the premise is narrower than it sounds: this holds for to_string/to_vec/to_writer only — serde_json::to_value(&struct) does go through Map and sorts.

docs/getting-started/installation.md also contains listen_addr, but in a tracing-subscriber log line, not a /config body.

Guard

A unit test compares each sample's top-level key list against the body build_config_json / build_health_json actually builds — set and order in one assertion — and separately checks that nested objects ascend, which is the only reach it has into a plugins block an empty PluginManager cannot produce.

Six mutants were run to confirm the test earns its place; each reddens on its intended assertion:

Mutant Fails on
Two top-level keys swapped the key-list assertion
Two nested keys swapped inside plugins.apm the nested-order assertion
Two plugin names swapped at indent 4 the nested-order assertion
One key line deleted (order still ascending) the key-list assertion, not the sort check
A whole /health sample removed the per-page sample count
A key added to Config::to_json() the key-list assertion

The last is the drift that produced this in the first place: a key added to the code with the samples left behind.

Verification

  • cargo fmt -- --check, cargo clippy --no-default-features --all-targets -- -D warnings — clean
  • cargo test --no-default-features and with plugin-apm,plugin-async,plugin-profiler,plugin-shared — 1863 passed, 0 failed
  • scripts/gen-llms-txt.sh --check — up to date (58 pages); llms-full.txt is regenerated, not hand-edited
  • Every reordered block parses as JSON, and each one compares equal in value to its pre-change version — only the text ordering moved

Docs:
  - The `/config` and `/health` samples followed the order the settings are written in the source, while both endpoints serialize through a sorted map and hand back keys ascending. A sample was therefore unusable as the baseline for a diff against a live response, and sent a reader looking for a setting several lines from where it lands. Seven samples across three pages, including the nested plugins object, where the APM plugin's own settings were out of order too. Turning on serde_json's preserve_order was the alternative and was rejected: the plugin config is a HashMap, so that feature would give the plugins subtree the per-process random iteration order and make the same diff worse rather than better.

Tests:
  - Compare each sample's top-level key list against the body its handler actually builds, so a setting added to /config cannot land without the samples following it. The profiler's index.json sample is deliberately out of this net: RunMeta is a derived struct written on serde's text path, which keeps declaration order and never reaches the sorted map.

1863 tests (1717 unit + 146 integration).
@diolektor
diolektor merged commit 7ed753c into main Sep 20, 2026
7 checks passed
@diolektor
diolektor deleted the fix/docs-config-samples-list-keys-in-declaration-order branch September 20, 2026 07:28
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.

1 participant