|
| 1 | +# Agent Config Schema Generator |
| 2 | + |
| 3 | +This directory contains the Python scripts that walk |
| 4 | +`newrelic.core.config.global_settings()` to produce a JSON Schema |
| 5 | +(`../schemas/config.json`) and to manage version bumps in |
| 6 | +`../configurationDefinitions.yml` for Fleet Control. |
| 7 | + |
| 8 | +## Files |
| 9 | + |
| 10 | +| File | Description | |
| 11 | +|------|-------------| |
| 12 | +| `generate-schema.py` | Per-push regenerator. Reads the live agent settings tree, writes `config.json`. Never touches `configurationDefinitions.yml`. | |
| 13 | +| `bump-schema-version.py` | Release-time version bumper. Compares the schema at a prior git ref to the current schema and writes a new version into `configurationDefinitions.yml`. | |
| 14 | +| `schema_diff.py` | Shared library (no `main`). Holds the diff classification (`classify_changes`), bump arithmetic (`recommend_bump`, `apply_bump`, `bump_version`), and schema loading (`load_existing`). Imported by both top-level scripts. | |
| 15 | +| `dump-settings.py` | Dev helper. Lists every leaf in `global_settings()` and how it appears in the generated schema (or why it was excluded). Not part of the workflow. | |
| 16 | +| `tests/test_generate_schema.py` | Tests for the generator (`infer_type`, `make_property`, `build_properties`, `generate_schema`, anyOf helpers). | |
| 17 | +| `tests/test_schema_diff.py` | Tests for the shared library (`classify_changes`, `recommend_bump`, `apply_bump`, `bump_version`, `load_existing`). | |
| 18 | +| `tests/test_bump_schema_version.py` | Tests for the bump script (parsing helpers + main bootstrap/happy paths with mocked `git_show`). | |
| 19 | +| `../schemas/config.json` | Generated JSON Schema (Draft 2020-12). | |
| 20 | +| `../configurationDefinitions.yml` | Fleet Control metadata, including the schema's semver version. Bumped only at release time. | |
| 21 | + |
| 22 | +## How the generator works |
| 23 | + |
| 24 | +The agent's live settings tree (`newrelic.core.config.global_settings()`) |
| 25 | +is the source of truth for which keys exist, their types, and their |
| 26 | +defaults. `newrelic/newrelic.ini` is consulted only for descriptions |
| 27 | +(comments adjacent to `key = value` lines in the `[newrelic]` section). |
| 28 | + |
| 29 | +`generate-schema.py`: |
| 30 | + |
| 31 | +1. Imports `newrelic.core.config` and walks every leaf whose containing |
| 32 | + class name ends in `Settings`. |
| 33 | +2. Loads descriptions from `newrelic/newrelic.ini`. |
| 34 | +3. For every leaf, applies (in order): `TYPE_OVERRIDES`, `ENUM_OVERRIDES`, |
| 35 | + set-typed auto-anyOf, type inference from the live value. |
| 36 | +4. Skips leaves whose path matches `EXCLUDE_KEYS` (exact or `prefix.*`). |
| 37 | +5. Validates the result against the JSON Schema Draft 2020-12 meta-schema. |
| 38 | +6. Deep-merges the freshly generated schema into the existing on-disk |
| 39 | + `config.json` so the published schema only ever grows. |
| 40 | +7. Writes `config.json` and prints a classified diff summary. |
| 41 | + |
| 42 | +The generator does **not** touch `configurationDefinitions.yml` -- |
| 43 | +version bumps live in the next section. |
| 44 | + |
| 45 | +## How versioning works |
| 46 | + |
| 47 | +Schema regeneration runs **per push** on feature branches via |
| 48 | +`.github/workflows/fleet-control-schema.yml`. It writes `config.json` |
| 49 | +and nothing else. Reviewers see schema diffs in PRs. |
| 50 | + |
| 51 | +Version bumps run **manually before each release** via |
| 52 | +`.github/workflows/fleet-control-schema-bump.yml`, which is |
| 53 | +`workflow_dispatch`-only. The bump workflow: |
| 54 | + |
| 55 | +1. Finds the latest `v*` tag on `main` (overridable via the |
| 56 | + `since_ref` workflow input). |
| 57 | +2. Reads the historical `configurationDefinitions.yml` from that tag -- |
| 58 | + the version stored there is the **starter version** for the bump. |
| 59 | +3. Reads the historical schema using the path declared in that file's |
| 60 | + `schema:` field. |
| 61 | +4. Compares the historical schema to the current `config.json` on `main`, |
| 62 | + classifies the cumulative diff, and applies the recommended bump kind |
| 63 | + (major/minor/patch). |
| 64 | +5. Opens a PR titled `chore: bump agent config schema version` for team |
| 65 | + review. |
| 66 | + |
| 67 | +If the latest release tag predates the schema (the `.fleetControl/` |
| 68 | +directory or the `schema:` field in `configurationDefinitions.yml`), |
| 69 | +`bump-schema-version.py` exits 0 with a bootstrap message and no PR |
| 70 | +is opened. The first release that includes the schema ships at whatever |
| 71 | +version is currently in `configurationDefinitions.yml`. |
| 72 | + |
| 73 | +### Release ordering -- run the bump workflow before cutting the tag |
| 74 | + |
| 75 | +The bump PR is a separate review/merge step from the agent's `vX.Y.Z` |
| 76 | +release tag. Run the workflows in this order: |
| 77 | + |
| 78 | +1. Trigger `Fleet Control Config Schema Bump` (manual `workflow_dispatch`). |
| 79 | +2. Wait for the PR to open (or the workflow to report that no bump is needed). |
| 80 | +3. Review and merge the bump PR if one was opened. |
| 81 | +4. Cut the GitHub Release from the post-merge `main`. |
| 82 | + |
| 83 | +If the release tag is cut before the bump PR merges, the tag's |
| 84 | +`configurationDefinitions.yml` will still say the pre-bump version, |
| 85 | +even though the schema itself (`config.json`) at that tag reflects the |
| 86 | +new keys. Consumers see a mismatch. The next release will compute its |
| 87 | +bump correctly from this tag's metadata, but the tag itself ships |
| 88 | +mismatched. |
| 89 | + |
| 90 | +## Quick start |
| 91 | + |
| 92 | +Regenerate the schema, run tests, and surface excluded settings in one |
| 93 | +command: |
| 94 | + |
| 95 | +```bash |
| 96 | +tox -e fleet-schema |
| 97 | +``` |
| 98 | + |
| 99 | +This runs the unit tests, regenerates `.fleetControl/schemas/config.json` |
| 100 | +(deep-merged into the existing schema), prints the classified diff, and |
| 101 | +dumps any settings that didn't make it into the schema. Exit code matches |
| 102 | +`generate-schema.py`: `0` if no schema changes, `1` if the schema changed |
| 103 | +(commit before pushing), `2` on a hard failure. |
| 104 | + |
| 105 | +> **Run with a clean shell.** `import newrelic.core.config` reads |
| 106 | +> `NEW_RELIC_*` env vars at import time and bakes them into the live |
| 107 | +> defaults. The tox env unsets them defensively; if you invoke the |
| 108 | +> generator directly, do the same (`env -i PATH="$PATH" HOME="$HOME" |
| 109 | +> python3 ...`). |
| 110 | +
|
| 111 | +### Lower-level commands |
| 112 | + |
| 113 | +If you want to invoke a single step directly without going through tox: |
| 114 | + |
| 115 | +```bash |
| 116 | +# Regenerate schema only (from repo root) |
| 117 | +python3 .fleetControl/schemaGeneration/generate-schema.py |
| 118 | + |
| 119 | +# Force-regenerate without comparing to existing on-disk schema |
| 120 | +python3 .fleetControl/schemaGeneration/generate-schema.py --force |
| 121 | + |
| 122 | +# Dry-run a release-time bump against a tag |
| 123 | +python3 .fleetControl/schemaGeneration/bump-schema-version.py --since=v10.21.0 |
| 124 | + |
| 125 | +# Apply a release-time bump (writes configurationDefinitions.yml) |
| 126 | +python3 .fleetControl/schemaGeneration/bump-schema-version.py --since=v10.21.0 --ci |
| 127 | + |
| 128 | +# Dump every live setting alongside how it appears in the schema |
| 129 | +python3 .fleetControl/schemaGeneration/dump-settings.py |
| 130 | + |
| 131 | +# Filter the dump to settings missing from the schema |
| 132 | +python3 .fleetControl/schemaGeneration/dump-settings.py --missing |
| 133 | +``` |
| 134 | + |
| 135 | +## Adding new configuration keys |
| 136 | + |
| 137 | +When new settings land in `newrelic/core/config.py`, the generator |
| 138 | +picks them up automatically on the next push -- no manual schema edit |
| 139 | +needed for the common case. |
| 140 | + |
| 141 | +**Special handling is required for certain key types**, configured via |
| 142 | +override maps in `generate-schema.py`. |
| 143 | + |
| 144 | +### Array-or-string keys (`_environ_as_set`-backed) |
| 145 | + |
| 146 | +Many agent config keys accept either a structured array OR a delimited |
| 147 | +string (the INI form): |
| 148 | + |
| 149 | +```ini |
| 150 | +attributes.include = request.parameters.* response.headers.content-type |
| 151 | +``` |
| 152 | + |
| 153 | +These keys are parsed via `_environ_as_set` / |
| 154 | +`_environ_as_comma_separated_set` in `newrelic/core/config.py`. For the |
| 155 | +JSON Schema to correctly represent both forms, set-typed live values |
| 156 | +auto-detect into the right shape -- you don't need a per-key entry |
| 157 | +unless the live default is empty (in which case `set` vs. `list` cannot |
| 158 | +be distinguished from the value alone). Empty defaults need an explicit |
| 159 | +override: |
| 160 | + |
| 161 | +```python |
| 162 | +'new_feature.include': string_array_or_delimited(default=[]), |
| 163 | +'new_feature.exclude': string_array_or_delimited(default=[]), |
| 164 | +``` |
| 165 | + |
| 166 | +The auto-detection covers the long tail (the seven `*.attributes.*` |
| 167 | +subtrees, `opentelemetry.traces.*`, etc.) because their live values |
| 168 | +arrive as Python `set` objects. |
| 169 | + |
| 170 | +### Status code keys |
| 171 | + |
| 172 | +Keys that accept integers, arrays of integers, or range strings (e.g., |
| 173 | +`"100-102 200-208 226 300-308 404"`) should use: |
| 174 | + |
| 175 | +```python |
| 176 | +'error_collector.new_status_codes': status_code_array_or_range(), |
| 177 | +'error_collector.new_status_codes_with_default': status_code_array_or_range(default=[404]), |
| 178 | +``` |
| 179 | + |
| 180 | +### Enum keys |
| 181 | + |
| 182 | +Keys with a fixed set of allowed values should be added to `ENUM_OVERRIDES`: |
| 183 | + |
| 184 | +```python |
| 185 | +ENUM_OVERRIDES = { |
| 186 | + 'new_feature.mode': ['option1', 'option2', 'option3'], |
| 187 | +} |
| 188 | +``` |
| 189 | + |
| 190 | +### None-defaulted leaves |
| 191 | + |
| 192 | +Settings whose live default is `None` cannot have their type inferred, |
| 193 | +so they're skipped from the schema with a warning. To surface them, add |
| 194 | +an explicit type in `TYPE_OVERRIDES`: |
| 195 | + |
| 196 | +```python |
| 197 | +'proxy_user': {'type': 'string'}, |
| 198 | +``` |
| 199 | + |
| 200 | +## Excluding keys |
| 201 | + |
| 202 | +Add keys to `EXCLUDE_KEYS` to drop them from the schema: |
| 203 | + |
| 204 | +```python |
| 205 | +EXCLUDE_KEYS = { |
| 206 | + 'agent_run_id', # exact match |
| 207 | + 'cross_application_tracer.*', # subtree exclusion |
| 208 | +} |
| 209 | +``` |
| 210 | + |
| 211 | +The `.*` suffix matches both the prefix itself and any descendant. |
| 212 | + |
| 213 | +## Checklist for new config keys |
| 214 | + |
| 215 | +1. Add the setting to `newrelic/core/config.py` as you would any other. |
| 216 | +2. **Run the generator locally** (`python3 .fleetControl/schemaGeneration/generate-schema.py`) to pick up the new key. |
| 217 | +3. **Check the inferred type** in the generated schema. |
| 218 | +4. **If the live default is `None`** → add an entry to `TYPE_OVERRIDES`. |
| 219 | +5. **If the key uses `_environ_as_set` and the default is empty** → add to `TYPE_OVERRIDES` with `string_array_or_delimited()`. |
| 220 | +6. **If the key has enum values** → add to `ENUM_OVERRIDES`. |
| 221 | +7. **If the key should be hidden** → add to `EXCLUDE_KEYS`. |
| 222 | +8. **Run the generator again**; verify the schema entry looks correct. |
| 223 | +9. **Run the tests** (`python3 -m unittest discover .fleetControl/schemaGeneration/tests`). |
| 224 | +10. The next release will pick up the bump when the maintainer runs the |
| 225 | + bump workflow as part of release prep. |
| 226 | + |
| 227 | +## CLI options |
| 228 | + |
| 229 | +### Generator CLI (`generate-schema.py`) |
| 230 | + |
| 231 | +| Option | Description | |
| 232 | +|--------|-------------| |
| 233 | +| `--force` | Overwrite the schema without comparing to the existing one. Always exits 0. | |
| 234 | + |
| 235 | +### Bumper CLI (`bump-schema-version.py`) |
| 236 | + |
| 237 | +| Option | Description | |
| 238 | +|--------|-------------| |
| 239 | +| `--since=<ref>` | Required. Compare the current schema to the schema at `<ref>` and recommend a bump. | |
| 240 | +| `--ci` | Write the bumped version to `configurationDefinitions.yml`. Without this, the script just prints the recommendation. | |
| 241 | + |
| 242 | +## Exit codes |
| 243 | + |
| 244 | +### Generator exit codes (`generate-schema.py`) |
| 245 | + |
| 246 | +| Code | Meaning | |
| 247 | +|------|---------| |
| 248 | +| 0 | No schema changes (or first run, or `--force` mode). | |
| 249 | +| 1 | Schema regenerated and on-disk differed (CI should commit). | |
| 250 | +| 2 | Generator failure (invalid schema, malformed inputs). | |
| 251 | + |
| 252 | +### Bumper exit codes (`bump-schema-version.py`) |
| 253 | + |
| 254 | +| Code | Meaning | |
| 255 | +|------|---------| |
| 256 | +| 0 | No bump needed (no schema diff, or bootstrap case where `<ref>` predates the schema). | |
| 257 | +| 1 | Bump applied (`--ci`) or recommended (without `--ci`). | |
| 258 | +| 2 | Bump failure (uncaught exception, missing args, malformed historical inputs). | |
| 259 | + |
| 260 | +## Version bumping rules |
| 261 | + |
| 262 | +`bump-schema-version.py` classifies each schema change and the bump |
| 263 | +kind is the highest severity across all changes: |
| 264 | + |
| 265 | +| Change type | Severity | Bump | |
| 266 | +|-------------|----------|------| |
| 267 | +| Property removed | Breaking | Major | |
| 268 | +| Type changed | Breaking | Major | |
| 269 | +| Enum value removed | Breaking | Major | |
| 270 | +| Enum newly introduced | Breaking | Major | |
| 271 | +| Required field added | Breaking | Major | |
| 272 | +| `additionalProperties` tightened (true → false) | Breaking | Major | |
| 273 | +| Property added | Additive | Minor | |
| 274 | +| Enum value added | Additive | Minor | |
| 275 | +| Enum removed entirely | Additive | Minor | |
| 276 | +| Required field removed | Additive | Minor | |
| 277 | +| Default changed | Additive | Minor | |
| 278 | +| `additionalProperties` loosened (false → true) | Additive | Minor | |
| 279 | +| Description changed | Cosmetic | Patch | |
| 280 | + |
| 281 | +## Running the tests |
| 282 | + |
| 283 | +```bash |
| 284 | +# All schema-generation tests in one shot |
| 285 | +python3 -m unittest discover .fleetControl/schemaGeneration/tests |
| 286 | + |
| 287 | +# Individual files |
| 288 | +python3 -m unittest .fleetControl.schemaGeneration.tests.test_generate_schema |
| 289 | +python3 -m unittest .fleetControl.schemaGeneration.tests.test_schema_diff |
| 290 | +python3 -m unittest .fleetControl.schemaGeneration.tests.test_bump_schema_version |
| 291 | +``` |
0 commit comments