From 86d1d17e51916cef6f2bd6ad7fcfa09ce78ece81 Mon Sep 17 00:00:00 2001 From: jedarden Date: Sun, 7 Jun 2026 09:46:06 -0400 Subject: [PATCH] fix(normalizer): add underscore OTLP attribute variants NEEDLE emits OTLP attributes with underscore naming: - needle.worker_id (not needle.worker.id) - needle.session_id (not needle.session.id) The normalizer only handled dot-separated forms, causing events to be dropped when OTLP sink is enabled. Changes: - Add needle.worker_id and needle.session_id to OTLP_ATTR_ALIASES - Underscore forms take priority (checked first in iteration) - Add test coverage for underscore attribute variants - Add test verifying underscore forms win over dot forms Resolves #bead-bf-4hzq --- src/normalizer.test.ts | 33 +++++++++++++++++++++++++++------ src/normalizer.ts | 15 ++++++++++++--- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/src/normalizer.test.ts b/src/normalizer.test.ts index 7eabfde..21b4d82 100644 --- a/src/normalizer.test.ts +++ b/src/normalizer.test.ts @@ -407,23 +407,44 @@ describe('normalize – otlp-log source', () => { expect(result!.bead_id).toBe('bd-ns'); }); - it('prefers namespaced keys over non-namespaced when both present', () => { + it('resolves underscore attribute names (needle.worker_id etc.)', () => { + const record = { + timeUnixNano: '1772641054008000000', + attributes: [ + { key: 'event_type', value: { stringValue: 'bead.claimed' } }, + { key: 'needle.worker_id', value: { stringValue: 'tcb-alpha' } }, + { key: 'needle.session_id', value: { stringValue: 'sess-ns' } }, + { key: 'needle.sequence', value: { intValue: '42' } }, + { key: 'needle.bead.id', value: { stringValue: 'bd-ns' } }, + ], + }; + const result = normalize(record, 'otlp-log'); + expect(result).not.toBeNull(); + expect(result!.worker_id).toBe('tcb-alpha'); + expect(result!.session_id).toBe('sess-ns'); + expect(result!.sequence).toBe(42); + expect(result!.bead_id).toBe('bd-ns'); + }); + + it('prefers underscore namespaced keys over dot-separated when both present', () => { const record = { timeUnixNano: '1772641054008000000', attributes: [ { key: 'event_type', value: { stringValue: 'test' } }, { key: 'worker_id', value: { stringValue: 'plain-worker' } }, - { key: 'needle.worker.id', value: { stringValue: 'ns-worker' } }, + { key: 'needle.worker.id', value: { stringValue: 'dot-worker' } }, + { key: 'needle.worker_id', value: { stringValue: 'underscore-worker' } }, { key: 'session_id', value: { stringValue: 'plain-sess' } }, - { key: 'needle.session.id', value: { stringValue: 'ns-sess' } }, + { key: 'needle.session.id', value: { stringValue: 'dot-sess' } }, + { key: 'needle.session_id', value: { stringValue: 'underscore-sess' } }, ], }; const result = normalize(record, 'otlp-log'); - expect(result!.worker_id).toBe('ns-worker'); - expect(result!.session_id).toBe('ns-sess'); + expect(result!.worker_id).toBe('underscore-worker'); + expect(result!.session_id).toBe('underscore-sess'); }); - it('falls back to non-namespaced keys when namespaced absent', () => { + it('prefers namespaced keys over non-namespaced when both present', () => { const record = { timeUnixNano: '1772641054008000000', attributes: [ diff --git a/src/normalizer.ts b/src/normalizer.ts index 078ef9e..e2a78f0 100644 --- a/src/normalizer.ts +++ b/src/normalizer.ts @@ -334,12 +334,21 @@ function normalizeLegacyLogEntry(parsed: unknown): NeedleEvent | null { * } */ -/** Namespaced → canonical field mapping */ +/** Namespaced → canonical field mapping + * + * NEEDLE emits a mix of dot-separated and underscore attribute names. + * Both forms are supported here, with underscore forms taking priority + * (checked first in resolveAttr) since they match NEEDLE's actual telemetry. + */ const OTLP_ATTR_ALIASES: ReadonlyMap = new Map([ - ['needle.worker.id', 'worker_id'], - ['needle.session.id', 'session_id'], + // Underscore forms (actual NEEDLE telemetry output) + ['needle.worker_id', 'worker_id'], + ['needle.session_id', 'session_id'], ['needle.sequence', 'sequence'], ['needle.bead.id', 'bead_id'], + // Dot-separated forms (for compatibility with some exporters) + ['needle.worker.id', 'worker_id'], + ['needle.session.id', 'session_id'], ]); /** All attribute keys that map to structural NeedleEvent fields */