opentelemetry-js/packages/opentelemetry-resources/test/Resource.test.ts

334 lines
11 KiB
TypeScript

/*
* 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 { diag } from '@opentelemetry/api';
import { SDK_INFO } from '@opentelemetry/core';
import {
ATTR_SERVICE_NAME,
ATTR_TELEMETRY_SDK_LANGUAGE,
ATTR_TELEMETRY_SDK_NAME,
ATTR_TELEMETRY_SDK_VERSION,
} from '@opentelemetry/semantic-conventions';
import * as assert from 'assert';
import * as sinon from 'sinon';
import { describeBrowser, describeNode } from './util';
import { defaultResource, emptyResource, resourceFromAttributes } from '../src';
import * as EventEmitter from 'events';
describe('Resource', () => {
const resource1 = resourceFromAttributes({
'k8s.io/container/name': 'c1',
'k8s.io/namespace/name': 'default',
'k8s.io/pod/name': 'pod-xyz-123',
});
const resource2 = resourceFromAttributes({
'k8s.io/zone': 'zone1',
'k8s.io/location': 'location',
});
const resource3 = resourceFromAttributes({
'k8s.io/container/name': 'c2',
'k8s.io/location': 'location1',
});
it('should return merged resource', () => {
const expectedResource = resourceFromAttributes({
'k8s.io/container/name': 'c1',
'k8s.io/namespace/name': 'default',
'k8s.io/pod/name': 'pod-xyz-123',
'k8s.io/zone': 'zone1',
'k8s.io/location': 'location',
});
const actualResource = resource1.merge(resource2);
assert.strictEqual(Object.keys(actualResource.attributes).length, 5);
assert.deepStrictEqual(
actualResource.attributes,
expectedResource.attributes
);
});
it('should return merged resource when collision in attributes', () => {
const expectedResource = resourceFromAttributes({
'k8s.io/container/name': 'c2',
'k8s.io/namespace/name': 'default',
'k8s.io/pod/name': 'pod-xyz-123',
'k8s.io/location': 'location1',
});
const actualResource = resource1.merge(resource3);
assert.strictEqual(Object.keys(actualResource.attributes).length, 4);
assert.deepStrictEqual(
actualResource.attributes,
expectedResource.attributes
);
});
it('should return merged resource when first resource is empty', () => {
const actualResource = emptyResource().merge(resource2);
assert.strictEqual(Object.keys(actualResource.attributes).length, 2);
assert.deepStrictEqual(actualResource.attributes, resource2.attributes);
});
it('should return merged resource when other resource is empty', () => {
const actualResource = resource1.merge(emptyResource());
assert.strictEqual(Object.keys(actualResource.attributes).length, 3);
assert.deepStrictEqual(actualResource.attributes, resource1.attributes);
});
it('should return merged resource when other resource is null', () => {
const actualResource = resource1.merge(null);
assert.strictEqual(Object.keys(actualResource.attributes).length, 3);
assert.deepStrictEqual(actualResource.attributes, resource1.attributes);
});
it('should accept string, number, and boolean values', () => {
const resource = resourceFromAttributes({
'custom.string': 'strvalue',
'custom.number': 42,
'custom.boolean': true,
});
assert.strictEqual(resource.attributes['custom.string'], 'strvalue');
assert.strictEqual(resource.attributes['custom.number'], 42);
assert.strictEqual(resource.attributes['custom.boolean'], true);
});
it('should log when accessing attributes before async attributes promise has settled', () => {
const debugStub = sinon.spy(diag, 'error');
const resource = resourceFromAttributes({
async: new Promise(resolve => {
setTimeout(resolve, 1);
}),
});
resource.attributes;
assert.ok(
debugStub.calledWithMatch(
'Accessing resource attributes before async attributes settled'
)
);
});
describe('asynchronous attributes', () => {
afterEach(() => {
sinon.restore();
});
it('should return false for asyncAttributesPending if no promise provided', () => {
assert.ok(!resourceFromAttributes({ foo: 'bar' }).asyncAttributesPending);
assert.ok(!emptyResource().asyncAttributesPending);
assert.ok(!defaultResource().asyncAttributesPending);
});
it('should return false for asyncAttributesPending once promise settles', async () => {
const resourceResolve = resourceFromAttributes({
async: new Promise(resolve => {
setTimeout(resolve, 1);
}),
});
const resourceReject = resourceFromAttributes({
async: new Promise((_, reject) => {
setTimeout(() => {
reject(new Error('reject'));
}, 1);
}),
});
for (const resource of [resourceResolve, resourceReject]) {
assert.ok(resource.asyncAttributesPending);
await resource.waitForAsyncAttributes?.();
assert.ok(!resource.asyncAttributesPending);
}
});
it('should merge async attributes into sync attributes once resolved', async () => {
const resource = resourceFromAttributes({
sync: 'fromsync',
// async attribute resolves after 1ms
async: new Promise(resolve =>
setTimeout(() => resolve('fromasync'), 1)
),
});
await resource.waitForAsyncAttributes?.();
assert.deepStrictEqual(resource.attributes, {
sync: 'fromsync',
async: 'fromasync',
});
});
it('should merge async attributes when both resources have promises', async () => {
const resource1 = resourceFromAttributes({
promise1: Promise.resolve('promise1val'),
shared: Promise.resolve('promise1val'),
});
const resource2 = resourceFromAttributes({
promise2: Promise.resolve('promise2val'),
shared: Promise.resolve('promise2val'),
});
// this one rejects
const resource3 = resourceFromAttributes({
err: Promise.reject(new Error('reject')),
});
const resource4 = resourceFromAttributes({
promise4: Promise.resolve('promise4val'),
shared: Promise.resolve('promise4val'),
});
const merged = resource1
.merge(resource2)
.merge(resource3)
.merge(resource4);
await merged.waitForAsyncAttributes?.();
assert.deepStrictEqual(merged.attributes, {
promise1: 'promise1val',
promise2: 'promise2val',
promise4: 'promise4val',
shared: 'promise4val',
});
});
it('should merge async attributes correctly when resource1 fulfils after resource2', async () => {
const resource1 = resourceFromAttributes({
promise1: Promise.resolve('promise1val'),
shared: Promise.resolve('promise1val'),
});
const resource2 = resourceFromAttributes({
promise2: 'promise2val',
shared: 'promise2val',
});
const merged = resource1.merge(resource2);
await merged.waitForAsyncAttributes?.();
assert.deepStrictEqual(merged.attributes, {
promise1: 'promise1val',
promise2: 'promise2val',
shared: 'promise2val',
});
});
it('should merge async attributes correctly when resource2 fulfils after resource1', async () => {
const resource1 = resourceFromAttributes({
promise1: Promise.resolve('promise1val'),
shared: 'promise1val',
});
const resource2 = resourceFromAttributes({
promise2: new Promise(res => setTimeout(() => res('promise2val'), 1)),
shared: new Promise(res => setTimeout(() => res('promise2val'), 1)),
});
const merged = resource1.merge(resource2);
await merged.waitForAsyncAttributes?.();
assert.deepStrictEqual(merged.attributes, {
promise1: 'promise1val',
promise2: 'promise2val',
shared: 'promise2val',
});
});
it('should log when promise rejects', async () => {
const debugStub = sinon.spy(diag, 'debug');
const resource = resourceFromAttributes({
rejected: Promise.reject(new Error('rejected')),
});
await resource.waitForAsyncAttributes?.();
assert.ok(
debugStub.calledWithMatch('promise rejection for resource attribute')
);
});
it('should guard against asynchronous attribute rejections', async () => {
const ee = new EventEmitter();
const badAttribute = new Promise<string>((resolve, reject) => {
ee.on('fail', reason => reject(reason));
});
const goodAttribute = new Promise<string>((resolve, reject) => {
ee.on('fail', reason => resolve(reason));
});
const res = resourceFromAttributes({ badAttribute, goodAttribute });
let noUnhandledRejection = true;
function onUnhandledRejection() {
noUnhandledRejection = false;
}
process.once('unhandledRejection', onUnhandledRejection);
try {
ee.emit('fail', 'resource attribute value promise rejected');
// yield to event loop to make sure we don't miss anything
await new Promise((resolve, reject) => setTimeout(resolve, 1));
assert.ok(noUnhandledRejection);
await res.waitForAsyncAttributes?.();
assert.notDeepStrictEqual(res.attributes, { goodAttribute: 'fail' });
} finally {
process.removeListener('unhandledRejection', onUnhandledRejection);
}
});
});
describeNode('.default()', () => {
it('should return a default resource', () => {
const resource = defaultResource();
assert.strictEqual(
resource.attributes[ATTR_TELEMETRY_SDK_NAME],
SDK_INFO[ATTR_TELEMETRY_SDK_NAME]
);
assert.strictEqual(
resource.attributes[ATTR_TELEMETRY_SDK_LANGUAGE],
SDK_INFO[ATTR_TELEMETRY_SDK_LANGUAGE]
);
assert.strictEqual(
resource.attributes[ATTR_TELEMETRY_SDK_VERSION],
SDK_INFO[ATTR_TELEMETRY_SDK_VERSION]
);
assert.strictEqual(
resource.attributes[ATTR_SERVICE_NAME],
`unknown_service:${process.argv0}`
);
});
});
describeBrowser('.default()', () => {
it('should return a default resource', () => {
const resource = defaultResource();
assert.strictEqual(
resource.attributes[ATTR_TELEMETRY_SDK_NAME],
SDK_INFO[ATTR_TELEMETRY_SDK_NAME]
);
assert.strictEqual(
resource.attributes[ATTR_TELEMETRY_SDK_LANGUAGE],
SDK_INFO[ATTR_TELEMETRY_SDK_LANGUAGE]
);
assert.strictEqual(
resource.attributes[ATTR_TELEMETRY_SDK_VERSION],
SDK_INFO[ATTR_TELEMETRY_SDK_VERSION]
);
assert.strictEqual(
resource.attributes[ATTR_SERVICE_NAME],
'unknown_service'
);
});
});
});