fix(normalizer): add underscore OTLP attribute variants
Some checks are pending
CI / test (18.x) (push) Waiting to run
CI / test (20.x) (push) Waiting to run
CI / test (22.x) (push) Waiting to run

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
This commit is contained in:
jedarden 2026-06-07 09:46:06 -04:00
parent e863c8ccca
commit 86d1d17e51
2 changed files with 39 additions and 9 deletions

View file

@ -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: [

View file

@ -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<string, string> = 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 */