Generate field-name constants from the schema instead of hand-typing them
Builds a generator that reads specification/schemas/log-entry-schema.json (the single source of truth for field names) and emits a per-language constants module, so a new language implementation gets correct, typo-proof field names by construction — the exact step where the historical dots-vs-underscores-style bugs originate — rather than an LLM retyping service_name, trace_id, etc. by hand from prose. Scope is narrower than the original one-line backlog description once verified against the actual code: see "Problem" for why this isn't a blind refactor of both existing implementations.
IMPLEMENTATION RULES: Before implementing this plan, read and follow:
- WORKFLOW.md - The implementation process
- PLANS.md - Plan structure and best practices
Status: Completed
Completed: 2026-07-08
Investigation: INVESTIGATE-multi-language-conformance.md — Option E, accepted 2026-07-08 as a fast-follow once PLAN-001 exists
Prerequisite: PLAN-001-master-comparison-mode.md — completed; compare-with-master.sh is what lets any adoption of generated constants be verified rather than trusted blindly
Goal: A generator exists that turns specification/schemas/log-entry-schema.json into a per-language field-name constants module. It's validated against both current implementations (schema↔code parity, not a forced refactor). Python — where hand-typed string dict keys are a real bug vector — adopts the generated constants, verified with zero regression via compare-with-master.sh. TypeScript does not, for a documented reason, not a silent omission. specification/implementation-guide.md is updated so any future language (Go, C#, Rust, PHP) runs the generator as its first implementation step.
Last Updated: 2026-07-08
Problem
Confirmed by reading the actual code before scoping this plan, not assumed from the investigation's one-line description:
The investigation's own framing already flags this as narrower than it sounds
Option E's own "Cons" section says it plainly: "precisely one of the three documented bugs is a naming bug" — it doesn't cover the enum-to-string conversion bug (a value bug) or the missing-field bug (a completeness bug). Both stay squarely compare-with-master.sh's job. This plan only targets the naming-bug class.
log-entry-schema.json has 17 field properties — that's the actual generation surface
specification/schemas/log-entry-schema.json defines exactly 17 properties (timestamp, level, message, service_name, service_version, peer_service, session_id, function_name, log_type, trace_id, span_id, event_id, input_json, response_json, exception_type, exception_message, exception_stacktrace). This is the only schema with field names to generate from — prometheus-response-schema.json/loki-response-schema.json/tempo-response-schema.json validate response structure, not implementation-side field names.
Metric names are explicitly out of scope: sovdev_operations_total etc. don't appear in any schema today — generating them would mean writing a new schema first, which is new spec-writing, not "reading data that's already correct" (Option E's own stated approach). Separately, PLAN-003 already found the "dots in metric names" concern doesn't hold in practice — the OTel Collector's Prometheus exporter sanitizes them regardless of what the SDK emits — so there's no live bug here to prevent.
TypeScript's field names are unquoted object-literal keys — the target bug class can't occur there the way it can in Python
Checked directly: typescript/src/logger.ts writes field names as bare object keys (service_name: value, 81 occurrences via grep -cE '\b(service_name|...)\s*[:=]') — valid JS identifier syntax, which means a stray dot (service.name) is a syntax error, not a silently-wrong string. python/src/logger.py, by contrast, writes them as string literals (log_entry["service_name"], 75 occurrences via grep) — exactly the kind of value where a typo or a dot silently produces a wrong-but-valid string, which is the actual failure mode this plan exists to prevent.
This means generating constants and forcing TypeScript to use them (e.g., via computed property syntax, [FIELDS.SERVICE_NAME]: value) would trade a currently-safe, idiomatic pattern for a less-readable one, for a bug class the language's own grammar already rules out. Refactoring Python's field-name string literals into generated constants has real value; doing the same to TypeScript's bare keys does not, once checked against the actual code rather than assumed to apply symmetrically.
Revised scope, before implementation starts, based on this: build the generator once, targeting any language's syntax; validate it (schema↔code parity) against both current implementations without forcing a refactor of either; adopt it for real in Python only, where the risk it addresses is real; document, not silently skip, the TypeScript decision; make the generator a required first step for genuinely new languages (Go, C#, Rust, PHP), which is where the investigation's own stated benefit — "future languages get correct field names by construction" — actually lives.
Phase 1: Build the generator
Tasks
- 1.1 Wrote
specification/tools/generate-field-constants.py: reads the schema'spropertieskeys viajson.load(dict order = declaration order, confirmed), renders either a Pythonclass FieldNames:(python/src/field_names.py) or a TypeScriptexport const FieldNames = {...} as const;(typescript/src/fieldNames.ts). - 1.2 Naming convention is
field_name.upper()— one line, applied uniformly, no per-field table. - 1.3
--checkmode implemented: reads the existing output file, renders what it would currently produce, compares as strings, exits 1 with aSTALE:/MISSING:message on mismatch, 0 with anOK:message (including the field count) otherwise. Writes nothing in--checkmode.
Validation
python3 specification/tools/generate-field-constants.py --lang python --check
python3 specification/tools/generate-field-constants.py --lang typescript --check
Phase 1: DONE. Both generate exactly 17 constants (timestamp through exception_stacktrace, matching the schema's properties count exactly — verified with a standalone json.load count first, since an earlier draft of this plan said 18 before that was checked and corrected here). Confirmed the generator runs with no devcontainer/OTLP dependency. Confirmed --check actually detects drift, not just always passing: appended a stray line to the generated TypeScript file, --check correctly reported STALE (exit 1), then passed again (exit 0) after regenerating.
Phase 2: Validate the generator against both current implementations (no refactor yet)
Tasks
- 2.1 Schema↔code parity for Python, checked at both write sites (
create_log_entry(), which builds the dict passed viaextra=, andJSONFormatter.format(), which independently reconstructs the same 17 fields from theLogRecordto serializedev.log): all 17 accounted for — 10 unconditional (timestamp,level,message,service_name,service_version,peer_service,function_name,log_type,trace_id,event_id), 5 conditional-but-present (span_id,input_json,response_json,exception_type,exception_message,exception_stacktrace— viahasattr/_NOT_PROVIDEDchecks), andsession_idlegitimately absent from every file-log entry by design (only ever appears in the OTelResourceconstruction, matching the schema's own"comment"field verbatim). No extra field name used in either write site that isn't one of the 17. - 2.2 Same check for TypeScript's
create_log_entry()(line 396) — the same 17-field pattern:levelis deliberately omitted from this object (Winston adds it at.log(level, entry)),span_idadded later inwrite_log(),input_json/response_jsondropped whenundefinedviaremove_undefined_fields()(matches PLAN-002's finding of TS's ownundefined-vs-nullsemantics), exception fields spread conditionally,session_idresource-only (identical pattern to Python, same schema comment). Full parity confirmed. - 2.3 No parity gap found in either direction. This is also independently corroborated:
validate-log-format.py's schema check (additionalProperties: false) already passes for both languages (confirmed in PLAN-001/002), which rules out either implementation silently writing a field name absent from the schema — Phase 2 additionally confirms the reverse direction (no schema field silently unused by either implementation).
Validation
Cross-checked both write sites per language against the schema's required list (10 fields) vs. optional fields (session_id, span_id, input_json, response_json, exception_*) — every "optional" field's absence on some code paths is legitimate (e.g. exception_type only on ERROR/FATAL paths, session_id never in file logs by design), not a bug. Phase 2: DONE, zero gaps found — both implementations already have correct field-name coverage; this phase validates the generator would have nothing to fix, not that it found something broken.
Phase 3: Adopt generated constants in Python (the language where it matters)
Tasks
- 3.1 Refactored all 72 field-name string-literal occurrences in
python/src/logger.pytoFieldNames.CONSTANT(imported from the new generatedfield_names.py), one field at a time via exact-stringreplace_allper constant (17 passes) rather than a blanket regex — each pass grep-verified beforehand to confirm every occurrence of that exact quoted token was a genuine field-name usage (dict key orgetattr/hasattr/incheck), not a false positive. Two docstring lines describing the JSON output (not code) were reworded first so the mechanical replace wouldn't turn readable prose intoFieldNames.INPUT_JSON: null-style text. Correctly left untouched:self.service_name/self.service_version(object attributes, not string literals) and"log_level"(a Prometheus metric label name — not one of the 17 schema fields, confirmed distinct from"level"). - 3.2 Ran the Python E2E test before and after the refactor (real devcontainer), diffed all 17 entries field-by-field excluding the fields that legitimately vary per run (
timestamp,trace_id,span_id,event_id) — 0 mismatches. The refactor is byte-for-byte behaviorally invisible. - 3.3 Ran
compare-with-master.sh pythonagainst a fresh TypeScript run — 0 errors, 0 warnings, all 17 entries match.
Validation
python3 -m py_compile python/src/logger.py # confirmed clean
# before/after E2E diff (excluding timestamp/trace_id/span_id/event_id): 0 mismatches across 17 entries
cd specification/tools && ./compare-with-master.sh python
Phase 3: DONE. compare-with-master.sh python — ✅ MATCH — output is identical to TypeScript's, Errors: 0, Warnings: 0. The refactor is invisible from the outside; only the source changed.
Phase 4: Wire the generator into the implementation process for future languages
Tasks
- 4.1 Added the generator as new step 2 in
specification/implementation-guide.md(renumbering the rest 3-8), before "study TypeScript" — reads the 17 field names straight from the schema, with guidance to add a new renderer to the generator itself (not hand-type) if the target language isn't supported yet. - 4.2 Documented the TypeScript non-adoption decision inline in step 2 itself (not a separate, easy-to-miss note): bare object-literal keys already make a stray dot a syntax error, so generated constants add less value there, with a link to this plan for the full reasoning. Also updated step 7 (the comparison-mode step) to note it's complementary to, not redundant with, this step — the two catch different bug classes.
- 4.3 Added
compare-with-master.sh(missing from the Quick Reference table since PLAN-001 shipped it — fixed as an adjacent stale-doc finding, not new scope) andgenerate-field-constants.pytospecification/tools/README.md's Quick Reference table. Also fixed a stale link found in the same section: Step 9's "see PLAN-001" pointed atplans/active/, which no longer exists — PLAN-001 has been inplans/completed/since it merged. - 4.4 Mid-phase finding: Phase 1's generated
typescript/src/fieldNames.tswas still sitting in the actual master implementation's source tree, unimported, since TypeScript doesn't adopt it (Phase 3 scope). Shipping unused generated code intotypescript/src/— as opposed to a doc-only demonstration — would be dead code in the one implementation everything else is verified against. Deleted it; the generator's ability to render TypeScript syntax was already proven by Phase 1's--checkcycle (deliberately-corrupted-then-regenerated test) and doesn't require a persisted, unused artifact to remain true. Re-rancompare-with-master.sh python(still 0 errors/warnings, all 17 match) and the Docusaurus build (clean) after all doc edits.
Validation
cd specification/tools && ./compare-with-master.sh python
cd website && npm run build
Phase 4: DONE. compare-with-master.sh python — ✅ MATCH, 0 errors, 0 warnings. npm run build — [SUCCESS] Generated static files in "build".
Acceptance Criteria
-
specification/tools/generate-field-constants.pyexists, generates all 17 field-name constants (Python proven and shipped; TypeScript proven capable via--checkbut not persisted, since it's unused — see Phase 4.4), and has a--checkmode - Schema↔code parity confirmed for both current implementations — zero gaps found in either direction
-
python/src/logger.pyuses generated constants instead of hand-typed field-name string literals, verified byte-identical output before/after and zerocompare-with-master.shregressions - The TypeScript non-adoption decision is documented, not silently skipped (
implementation-guide.mdstep 2) -
specification/implementation-guide.mdrequires running the generator as an early step for any new language -
website/docs/builds cleanly after all doc edits
Files to Modify
specification/tools/generate-field-constants.py(new)python/src/logger.py(refactored to use generated constants, 72 occurrences)python/src/field_names.py(new, generated)specification/implementation-guide.md(generator step added as step 2, TypeScript decision documented, step 7 cross-referenced)specification/tools/README.md(tool reference entries forcompare-with-master.shandgenerate-field-constants.py, stale PLAN-001 path fixed)