Skip to content

Commit cb0461a

Browse files
committed
fix(api-logs,sdk-logs): allow AnyValue attributes for logs and handle circular json
1 parent 8505a61 commit cb0461a

6 files changed

Lines changed: 552 additions & 29 deletions

File tree

experimental/packages/api-logs/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export { SeverityNumber } from './types/LogRecord';
2020
export type { LogAttributes, LogBody, LogRecord } from './types/LogRecord';
2121
export type { LoggerOptions } from './types/LoggerOptions';
2222
export type { AnyValue, AnyValueMap } from './types/AnyValue';
23+
export { isLogAttributeValue } from './utils/validation';
2324
export { NOOP_LOGGER, NoopLogger } from './NoopLogger';
2425
export { NOOP_LOGGER_PROVIDER, NoopLoggerProvider } from './NoopLoggerProvider';
2526
export { ProxyLogger } from './ProxyLogger';
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { AnyValue } from '../types/AnyValue';
18+
19+
/**
20+
* Validates if a value is a valid AnyValue for Log Attributes according to OpenTelemetry spec.
21+
* Log Attributes support a superset of standard Attributes and must support:
22+
* - Scalar values: string, boolean, signed 64 bit integer, or double precision floating point
23+
* - Byte arrays (Uint8Array)
24+
* - Arrays of any values (heterogeneous arrays allowed)
25+
* - Maps from string to any value (nested objects)
26+
* - Empty values (null/undefined)
27+
*
28+
* @param val - The value to validate
29+
* @returns true if the value is a valid AnyValue, false otherwise
30+
*/
31+
export function isLogAttributeValue(val: unknown): val is AnyValue {
32+
return isLogAttributeValueInternal(val, new WeakSet());
33+
}
34+
35+
function isLogAttributeValueInternal(val: unknown, visited: WeakSet<object>): val is AnyValue {
36+
// null and undefined are explicitly allowed
37+
if (val == null) {
38+
return true;
39+
}
40+
41+
// Scalar values
42+
if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') {
43+
return true;
44+
}
45+
46+
// Byte arrays
47+
if (val instanceof Uint8Array) {
48+
return true;
49+
}
50+
51+
// For objects and arrays, check for circular references
52+
if (typeof val === 'object') {
53+
if (visited.has(val as object)) {
54+
// Circular reference detected - reject it
55+
return false;
56+
}
57+
visited.add(val as object);
58+
59+
// Arrays (can contain any AnyValue, including heterogeneous)
60+
if (Array.isArray(val)) {
61+
return val.every(item => isLogAttributeValueInternal(item, visited));
62+
}
63+
64+
// Only accept plain objects (not built-in objects like Date, RegExp, Error, etc.)
65+
// Check if it's a plain object by verifying its constructor is Object or it has no constructor
66+
const obj = val as Record<string, unknown>;
67+
if (obj.constructor !== Object && obj.constructor !== undefined) {
68+
return false;
69+
}
70+
71+
// Objects/Maps (including empty objects)
72+
// All object properties must be valid AnyValues
73+
return Object.values(obj).every(item => isLogAttributeValueInternal(item, visited));
74+
}
75+
76+
return false;
77+
}
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as assert from 'assert';
18+
import { isLogAttributeValue } from '../../../src/utils/validation';
19+
20+
describe('isLogAttributeValue', () => {
21+
describe('should accept scalar values', () => {
22+
it('should accept strings', () => {
23+
assert.strictEqual(isLogAttributeValue('test'), true);
24+
assert.strictEqual(isLogAttributeValue(''), true);
25+
assert.strictEqual(isLogAttributeValue('multi\nline'), true);
26+
assert.strictEqual(isLogAttributeValue('unicode: 🎉'), true);
27+
});
28+
29+
it('should accept numbers', () => {
30+
assert.strictEqual(isLogAttributeValue(42), true);
31+
assert.strictEqual(isLogAttributeValue(0), true);
32+
assert.strictEqual(isLogAttributeValue(-123), true);
33+
assert.strictEqual(isLogAttributeValue(3.14159), true);
34+
assert.strictEqual(isLogAttributeValue(Number.MAX_SAFE_INTEGER), true);
35+
assert.strictEqual(isLogAttributeValue(Number.MIN_SAFE_INTEGER), true);
36+
assert.strictEqual(isLogAttributeValue(Infinity), true);
37+
assert.strictEqual(isLogAttributeValue(-Infinity), true);
38+
assert.strictEqual(isLogAttributeValue(NaN), true);
39+
});
40+
41+
it('should accept booleans', () => {
42+
assert.strictEqual(isLogAttributeValue(true), true);
43+
assert.strictEqual(isLogAttributeValue(false), true);
44+
});
45+
});
46+
47+
describe('should accept null and undefined values', () => {
48+
it('should accept null', () => {
49+
assert.strictEqual(isLogAttributeValue(null), true);
50+
});
51+
52+
it('should accept undefined', () => {
53+
assert.strictEqual(isLogAttributeValue(undefined), true);
54+
});
55+
});
56+
57+
describe('should accept byte arrays', () => {
58+
it('should accept Uint8Array', () => {
59+
assert.strictEqual(isLogAttributeValue(new Uint8Array([1, 2, 3])), true);
60+
assert.strictEqual(isLogAttributeValue(new Uint8Array(0)), true);
61+
assert.strictEqual(isLogAttributeValue(new Uint8Array([255, 0, 128])), true);
62+
});
63+
});
64+
65+
describe('should accept arrays', () => {
66+
it('should accept homogeneous arrays', () => {
67+
assert.strictEqual(isLogAttributeValue(['a', 'b', 'c']), true);
68+
assert.strictEqual(isLogAttributeValue([1, 2, 3]), true);
69+
assert.strictEqual(isLogAttributeValue([true, false]), true);
70+
});
71+
72+
it('should accept heterogeneous arrays', () => {
73+
assert.strictEqual(isLogAttributeValue(['string', 42, true]), true);
74+
assert.strictEqual(isLogAttributeValue([null, undefined, 'test']), true);
75+
assert.strictEqual(isLogAttributeValue(['test', new Uint8Array([1, 2])]), true);
76+
});
77+
78+
it('should accept nested arrays', () => {
79+
assert.strictEqual(isLogAttributeValue([['a', 'b'], [1, 2]]), true);
80+
assert.strictEqual(isLogAttributeValue([[1, 2, 3], ['nested', 'array']]), true);
81+
});
82+
83+
it('should accept arrays with null/undefined', () => {
84+
assert.strictEqual(isLogAttributeValue([null, 'test', undefined]), true);
85+
assert.strictEqual(isLogAttributeValue([]), true);
86+
});
87+
88+
it('should accept arrays with objects', () => {
89+
assert.strictEqual(isLogAttributeValue([{ key: 'value' }, 'string']), true);
90+
});
91+
});
92+
93+
describe('should accept objects/maps', () => {
94+
it('should accept simple objects', () => {
95+
assert.strictEqual(isLogAttributeValue({ key: 'value' }), true);
96+
assert.strictEqual(isLogAttributeValue({ num: 42, bool: true }), true);
97+
});
98+
99+
it('should accept empty objects', () => {
100+
assert.strictEqual(isLogAttributeValue({}), true);
101+
});
102+
103+
it('should accept nested objects', () => {
104+
const nested = {
105+
level1: {
106+
level2: {
107+
deep: 'value',
108+
number: 123
109+
}
110+
}
111+
};
112+
assert.strictEqual(isLogAttributeValue(nested), true);
113+
});
114+
115+
it('should accept objects with arrays', () => {
116+
const obj = {
117+
strings: ['a', 'b'],
118+
numbers: [1, 2, 3],
119+
mixed: ['str', 42, true]
120+
};
121+
assert.strictEqual(isLogAttributeValue(obj), true);
122+
});
123+
124+
it('should accept objects with null/undefined values', () => {
125+
assert.strictEqual(isLogAttributeValue({ nullVal: null, undefVal: undefined }), true);
126+
});
127+
128+
it('should accept objects with byte arrays', () => {
129+
assert.strictEqual(isLogAttributeValue({ bytes: new Uint8Array([1, 2, 3]) }), true);
130+
});
131+
});
132+
133+
describe('should accept complex combinations', () => {
134+
it('should accept deeply nested structures', () => {
135+
const complex = {
136+
scalars: {
137+
str: 'test',
138+
num: 42,
139+
bool: true
140+
},
141+
arrays: {
142+
homogeneous: ['a', 'b', 'c'],
143+
heterogeneous: [1, 'two', true, null],
144+
nested: [[1, 2], ['a', 'b']]
145+
},
146+
bytes: new Uint8Array([255, 254, 253]),
147+
nullish: {
148+
nullValue: null,
149+
undefinedValue: undefined
150+
},
151+
empty: {}
152+
};
153+
assert.strictEqual(isLogAttributeValue(complex), true);
154+
});
155+
156+
it('should accept arrays of complex objects', () => {
157+
const arrayOfObjects = [
158+
{ name: 'obj1', value: 123 },
159+
{ name: 'obj2', nested: { deep: 'value' } },
160+
{ bytes: new Uint8Array([1, 2, 3]) }
161+
];
162+
assert.strictEqual(isLogAttributeValue(arrayOfObjects), true);
163+
});
164+
});
165+
166+
describe('should reject invalid values', () => {
167+
it('should reject functions', () => {
168+
assert.strictEqual(isLogAttributeValue(() => {}), false);
169+
assert.strictEqual(isLogAttributeValue(function() {}), false);
170+
});
171+
172+
it('should reject symbols', () => {
173+
assert.strictEqual(isLogAttributeValue(Symbol('test')), false);
174+
assert.strictEqual(isLogAttributeValue(Symbol.for('test')), false);
175+
});
176+
177+
it('should reject Date objects', () => {
178+
assert.strictEqual(isLogAttributeValue(new Date()), false);
179+
});
180+
181+
it('should reject RegExp objects', () => {
182+
assert.strictEqual(isLogAttributeValue(/test/), false);
183+
});
184+
185+
it('should reject Error objects', () => {
186+
assert.strictEqual(isLogAttributeValue(new Error('test')), false);
187+
});
188+
189+
it('should reject class instances', () => {
190+
class TestClass {
191+
value = 'test';
192+
}
193+
assert.strictEqual(isLogAttributeValue(new TestClass()), false);
194+
});
195+
196+
it('should reject arrays containing invalid values', () => {
197+
assert.strictEqual(isLogAttributeValue(['valid', () => {}]), false);
198+
assert.strictEqual(isLogAttributeValue([Symbol('test'), 'valid']), false);
199+
assert.strictEqual(isLogAttributeValue([new Date()]), false);
200+
});
201+
202+
it('should reject objects containing invalid values', () => {
203+
assert.strictEqual(isLogAttributeValue({ valid: 'test', invalid: () => {} }), false);
204+
assert.strictEqual(isLogAttributeValue({ symbol: Symbol('test') }), false);
205+
assert.strictEqual(isLogAttributeValue({ date: new Date() }), false);
206+
});
207+
208+
it('should reject deeply nested invalid values', () => {
209+
const nested = {
210+
level1: {
211+
level2: {
212+
valid: 'value',
213+
invalid: Symbol('test')
214+
}
215+
}
216+
};
217+
assert.strictEqual(isLogAttributeValue(nested), false);
218+
});
219+
220+
it('should reject arrays with nested invalid values', () => {
221+
const nestedArray = [
222+
['valid', 'array'],
223+
['has', Symbol('invalid')]
224+
];
225+
assert.strictEqual(isLogAttributeValue(nestedArray), false);
226+
});
227+
});
228+
229+
describe('edge cases', () => {
230+
it('should handle circular references gracefully', () => {
231+
const circular: any = { a: 'test' };
232+
circular.self = circular;
233+
234+
// This should not throw an error, though it might return false
235+
// The exact behavior isn't specified in the OpenTelemetry spec
236+
const result = isLogAttributeValue(circular);
237+
assert.strictEqual(typeof result, 'boolean');
238+
});
239+
240+
it('should handle very deep nesting', () => {
241+
let deep: any = 'bottom';
242+
for (let i = 0; i < 100; i++) {
243+
deep = { level: i, nested: deep };
244+
}
245+
246+
const result = isLogAttributeValue(deep);
247+
assert.strictEqual(typeof result, 'boolean');
248+
});
249+
250+
it('should handle large arrays', () => {
251+
const largeArray = new Array(1000).fill('test');
252+
assert.strictEqual(isLogAttributeValue(largeArray), true);
253+
});
254+
});
255+
});

0 commit comments

Comments
 (0)