Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions experimental/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2
* user-facing: `LogRecord` class is now not exported anymore. A newly exported interface `SdkLogRecord` is used in its place.
* refactor: Removed `api-events` and `sdk-events` [#5737](https://github.com/open-telemetry/opentelemetry-js/pull/5737) @svetlanabrennan
* chore: Regenerated certs [#5752](https://github.com/open-telemetry/opentelemetry-js/pull/5752) @svetlanabrennan
* fix(api-logs,sdk-logs): allow AnyValue attributes for logs and handle circular references [#5765](https://github.com/open-telemetry/opentelemetry-js/pull/5765) @alec2435

## 0.202.0

Expand Down
1 change: 1 addition & 0 deletions experimental/packages/api-logs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export { SeverityNumber } from './types/LogRecord';
export type { LogAttributes, LogBody, LogRecord } from './types/LogRecord';
export type { LoggerOptions } from './types/LoggerOptions';
export type { AnyValue, AnyValueMap } from './types/AnyValue';
export { isLogAttributeValue } from './utils/validation';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is implementation code and should therefore go into sdk-logs instead of here. The API is mostly intended for just type definitions and registering the global, anything else should go into sdk-logs :)

export { NOOP_LOGGER, NoopLogger } from './NoopLogger';
export { NOOP_LOGGER_PROVIDER, NoopLoggerProvider } from './NoopLoggerProvider';
export { ProxyLogger } from './ProxyLogger';
Expand Down
77 changes: 77 additions & 0 deletions experimental/packages/api-logs/src/utils/validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { AnyValue } from '../types/AnyValue';

/**
* Validates if a value is a valid AnyValue for Log Attributes according to OpenTelemetry spec.
* Log Attributes support a superset of standard Attributes and must support:
* - Scalar values: string, boolean, signed 64 bit integer, or double precision floating point
* - Byte arrays (Uint8Array)
* - Arrays of any values (heterogeneous arrays allowed)
* - Maps from string to any value (nested objects)
* - Empty values (null/undefined)
*
* @param val - The value to validate
* @returns true if the value is a valid AnyValue, false otherwise
*/
export function isLogAttributeValue(val: unknown): val is AnyValue {
return isLogAttributeValueInternal(val, new WeakSet());
}

function isLogAttributeValueInternal(val: unknown, visited: WeakSet<object>): val is AnyValue {
// null and undefined are explicitly allowed
if (val == null) {
return true;
}

// Scalar values
if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') {
return true;
}

// Byte arrays
if (val instanceof Uint8Array) {
return true;
}

// For objects and arrays, check for circular references
if (typeof val === 'object') {
if (visited.has(val as object)) {
// Circular reference detected - reject it
return false;
}
visited.add(val as object);

// Arrays (can contain any AnyValue, including heterogeneous)
if (Array.isArray(val)) {
return val.every(item => isLogAttributeValueInternal(item, visited));
}

// Only accept plain objects (not built-in objects like Date, RegExp, Error, etc.)
// Check if it's a plain object by verifying its constructor is Object or it has no constructor
const obj = val as Record<string, unknown>;
if (obj.constructor !== Object && obj.constructor !== undefined) {
return false;
}

// Objects/Maps (including empty objects)
// All object properties must be valid AnyValues
return Object.values(obj).every(item => isLogAttributeValueInternal(item, visited));
}

return false;
}
255 changes: 255 additions & 0 deletions experimental/packages/api-logs/test/common/utils/validation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import * as assert from 'assert';
import { isLogAttributeValue } from '../../../src/utils/validation';

describe('isLogAttributeValue', () => {
describe('should accept scalar values', () => {
it('should accept strings', () => {
assert.strictEqual(isLogAttributeValue('test'), true);
assert.strictEqual(isLogAttributeValue(''), true);
assert.strictEqual(isLogAttributeValue('multi\nline'), true);
assert.strictEqual(isLogAttributeValue('unicode: 🎉'), true);
});

it('should accept numbers', () => {
assert.strictEqual(isLogAttributeValue(42), true);
assert.strictEqual(isLogAttributeValue(0), true);
assert.strictEqual(isLogAttributeValue(-123), true);
assert.strictEqual(isLogAttributeValue(3.14159), true);
assert.strictEqual(isLogAttributeValue(Number.MAX_SAFE_INTEGER), true);
assert.strictEqual(isLogAttributeValue(Number.MIN_SAFE_INTEGER), true);
assert.strictEqual(isLogAttributeValue(Infinity), true);
assert.strictEqual(isLogAttributeValue(-Infinity), true);
assert.strictEqual(isLogAttributeValue(NaN), true);
});

it('should accept booleans', () => {
assert.strictEqual(isLogAttributeValue(true), true);
assert.strictEqual(isLogAttributeValue(false), true);
});
});

describe('should accept null and undefined values', () => {
it('should accept null', () => {
assert.strictEqual(isLogAttributeValue(null), true);
});

it('should accept undefined', () => {
assert.strictEqual(isLogAttributeValue(undefined), true);
});
});

describe('should accept byte arrays', () => {
it('should accept Uint8Array', () => {
assert.strictEqual(isLogAttributeValue(new Uint8Array([1, 2, 3])), true);
assert.strictEqual(isLogAttributeValue(new Uint8Array(0)), true);
assert.strictEqual(isLogAttributeValue(new Uint8Array([255, 0, 128])), true);
});
});

describe('should accept arrays', () => {
it('should accept homogeneous arrays', () => {
assert.strictEqual(isLogAttributeValue(['a', 'b', 'c']), true);
assert.strictEqual(isLogAttributeValue([1, 2, 3]), true);
assert.strictEqual(isLogAttributeValue([true, false]), true);
});

it('should accept heterogeneous arrays', () => {
assert.strictEqual(isLogAttributeValue(['string', 42, true]), true);
assert.strictEqual(isLogAttributeValue([null, undefined, 'test']), true);
assert.strictEqual(isLogAttributeValue(['test', new Uint8Array([1, 2])]), true);
});

it('should accept nested arrays', () => {
assert.strictEqual(isLogAttributeValue([['a', 'b'], [1, 2]]), true);
assert.strictEqual(isLogAttributeValue([[1, 2, 3], ['nested', 'array']]), true);
});

it('should accept arrays with null/undefined', () => {
assert.strictEqual(isLogAttributeValue([null, 'test', undefined]), true);
assert.strictEqual(isLogAttributeValue([]), true);
});

it('should accept arrays with objects', () => {
assert.strictEqual(isLogAttributeValue([{ key: 'value' }, 'string']), true);
});
});

describe('should accept objects/maps', () => {
it('should accept simple objects', () => {
assert.strictEqual(isLogAttributeValue({ key: 'value' }), true);
assert.strictEqual(isLogAttributeValue({ num: 42, bool: true }), true);
});

it('should accept empty objects', () => {
assert.strictEqual(isLogAttributeValue({}), true);
});

it('should accept nested objects', () => {
const nested = {
level1: {
level2: {
deep: 'value',
number: 123
}
}
};
assert.strictEqual(isLogAttributeValue(nested), true);
});

it('should accept objects with arrays', () => {
const obj = {
strings: ['a', 'b'],
numbers: [1, 2, 3],
mixed: ['str', 42, true]
};
assert.strictEqual(isLogAttributeValue(obj), true);
});

it('should accept objects with null/undefined values', () => {
assert.strictEqual(isLogAttributeValue({ nullVal: null, undefVal: undefined }), true);
});

it('should accept objects with byte arrays', () => {
assert.strictEqual(isLogAttributeValue({ bytes: new Uint8Array([1, 2, 3]) }), true);
});
});

describe('should accept complex combinations', () => {
it('should accept deeply nested structures', () => {
const complex = {
scalars: {
str: 'test',
num: 42,
bool: true
},
arrays: {
homogeneous: ['a', 'b', 'c'],
heterogeneous: [1, 'two', true, null],
nested: [[1, 2], ['a', 'b']]
},
bytes: new Uint8Array([255, 254, 253]),
nullish: {
nullValue: null,
undefinedValue: undefined
},
empty: {}
};
assert.strictEqual(isLogAttributeValue(complex), true);
});

it('should accept arrays of complex objects', () => {
const arrayOfObjects = [
{ name: 'obj1', value: 123 },
{ name: 'obj2', nested: { deep: 'value' } },
{ bytes: new Uint8Array([1, 2, 3]) }
];
assert.strictEqual(isLogAttributeValue(arrayOfObjects), true);
});
});

describe('should reject invalid values', () => {
it('should reject functions', () => {
assert.strictEqual(isLogAttributeValue(() => {}), false);
assert.strictEqual(isLogAttributeValue(function() {}), false);
});

it('should reject symbols', () => {
assert.strictEqual(isLogAttributeValue(Symbol('test')), false);
assert.strictEqual(isLogAttributeValue(Symbol.for('test')), false);
});

it('should reject Date objects', () => {
assert.strictEqual(isLogAttributeValue(new Date()), false);
});

it('should reject RegExp objects', () => {
assert.strictEqual(isLogAttributeValue(/test/), false);
});

it('should reject Error objects', () => {
assert.strictEqual(isLogAttributeValue(new Error('test')), false);
});

it('should reject class instances', () => {
class TestClass {
value = 'test';
}
assert.strictEqual(isLogAttributeValue(new TestClass()), false);
});

it('should reject arrays containing invalid values', () => {
assert.strictEqual(isLogAttributeValue(['valid', () => {}]), false);
assert.strictEqual(isLogAttributeValue([Symbol('test'), 'valid']), false);
assert.strictEqual(isLogAttributeValue([new Date()]), false);
});

it('should reject objects containing invalid values', () => {
assert.strictEqual(isLogAttributeValue({ valid: 'test', invalid: () => {} }), false);
assert.strictEqual(isLogAttributeValue({ symbol: Symbol('test') }), false);
assert.strictEqual(isLogAttributeValue({ date: new Date() }), false);
});

it('should reject deeply nested invalid values', () => {
const nested = {
level1: {
level2: {
valid: 'value',
invalid: Symbol('test')
}
}
};
assert.strictEqual(isLogAttributeValue(nested), false);
});

it('should reject arrays with nested invalid values', () => {
const nestedArray = [
['valid', 'array'],
['has', Symbol('invalid')]
];
assert.strictEqual(isLogAttributeValue(nestedArray), false);
});
});

describe('edge cases', () => {
it('should handle circular references gracefully', () => {
const circular: any = { a: 'test' };
circular.self = circular;

// This should not throw an error, though it might return false
// The exact behavior isn't specified in the OpenTelemetry spec
const result = isLogAttributeValue(circular);
assert.strictEqual(typeof result, 'boolean');
});

it('should handle very deep nesting', () => {
let deep: any = 'bottom';
for (let i = 0; i < 100; i++) {
deep = { level: i, nested: deep };
}

const result = isLogAttributeValue(deep);
assert.strictEqual(typeof result, 'boolean');
});

it('should handle large arrays', () => {
const largeArray = new Array(1000).fill('test');
assert.strictEqual(isLogAttributeValue(largeArray), true);
});
});
});
Loading