feat(opentelemetry-resources): add schema url (#5753)
Co-authored-by: Marc Pichler <marc.pichler@dynatrace.com>
This commit is contained in:
parent
60b701dc9d
commit
23677fd0da
|
|
@ -16,6 +16,8 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2
|
|||
|
||||
### :rocket: Features
|
||||
|
||||
* feat(opentelemetry-resources): add schema url [#5070](https://github.com/open-telemetry/opentelemetry-js/pull/5753) @c-ehrlich
|
||||
|
||||
### :bug: Bug Fixes
|
||||
|
||||
* fix(sdk-metrics): Remove invalid default value for `startTime` param to ExponentialHistogramAccumulation. This only impacted the closurescript compiler. [#5763](https://github.com/open-telemetry/opentelemetry-js/pull/5763) @trentm
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ export interface Resource {
|
|||
|
||||
/** Resource droppedAttributesCount */
|
||||
droppedAttributesCount: number;
|
||||
|
||||
/** Resource schemaUrl */
|
||||
schemaUrl?: string;
|
||||
}
|
||||
|
||||
/** Properties of an InstrumentationScope. */
|
||||
|
|
|
|||
|
|
@ -24,10 +24,15 @@ import { InstrumentationScope } from '@opentelemetry/core';
|
|||
import { Resource as ISdkResource } from '@opentelemetry/resources';
|
||||
|
||||
export function createResource(resource: ISdkResource): Resource {
|
||||
return {
|
||||
const result: Resource = {
|
||||
attributes: toAttributes(resource.attributes),
|
||||
droppedAttributesCount: 0,
|
||||
};
|
||||
|
||||
const schemaUrl = resource.schemaUrl;
|
||||
if (schemaUrl && schemaUrl !== '') result.schemaUrl = schemaUrl;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function createInstrumentationScope(
|
||||
|
|
|
|||
|
|
@ -79,8 +79,10 @@ function logRecordsToResourceLogs(
|
|||
encoder: Encoder
|
||||
): IResourceLogs[] {
|
||||
const resourceMap = createResourceMap(logRecords);
|
||||
return Array.from(resourceMap, ([resource, ismMap]) => ({
|
||||
resource: createResource(resource),
|
||||
return Array.from(resourceMap, ([resource, ismMap]) => {
|
||||
const processedResource = createResource(resource);
|
||||
return {
|
||||
resource: processedResource,
|
||||
scopeLogs: Array.from(ismMap, ([, scopeLogs]) => {
|
||||
return {
|
||||
scope: createInstrumentationScope(scopeLogs[0].instrumentationScope),
|
||||
|
|
@ -88,8 +90,9 @@ function logRecordsToResourceLogs(
|
|||
schemaUrl: scopeLogs[0].instrumentationScope.schemaUrl,
|
||||
};
|
||||
}),
|
||||
schemaUrl: undefined,
|
||||
}));
|
||||
schemaUrl: processedResource.schemaUrl,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function toLogRecord(log: ReadableLogRecord, encoder: Encoder): ILogRecord {
|
||||
|
|
|
|||
|
|
@ -47,9 +47,10 @@ export function toResourceMetrics(
|
|||
options?: OtlpEncodingOptions
|
||||
): IResourceMetrics {
|
||||
const encoder = getOtlpEncoder(options);
|
||||
const processedResource = createResource(resourceMetrics.resource);
|
||||
return {
|
||||
resource: createResource(resourceMetrics.resource),
|
||||
schemaUrl: undefined,
|
||||
resource: processedResource,
|
||||
schemaUrl: processedResource.schemaUrl,
|
||||
scopeMetrics: toScopeMetrics(resourceMetrics.scopeMetrics, encoder),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -170,11 +170,11 @@ function spanRecordsToResourceSpans(
|
|||
}
|
||||
ilmEntry = ilmIterator.next();
|
||||
}
|
||||
// TODO SDK types don't provide resource schema URL at this time
|
||||
const processedResource = createResource(resource);
|
||||
const transformedSpans: IResourceSpans = {
|
||||
resource: createResource(resource),
|
||||
resource: processedResource,
|
||||
scopeSpans: scopeResourceSpans,
|
||||
schemaUrl: undefined,
|
||||
schemaUrl: processedResource.schemaUrl,
|
||||
};
|
||||
|
||||
out.push(transformedSpans);
|
||||
|
|
|
|||
|
|
@ -144,6 +144,27 @@ function createExpectedLogProtobuf(): IExportLogsServiceRequest {
|
|||
};
|
||||
}
|
||||
|
||||
const DEFAULT_LOG_FRAGMENT: Omit<
|
||||
ReadableLogRecord,
|
||||
'resource' | 'instrumentationScope'
|
||||
> = {
|
||||
hrTime: [1680253513, 123241635] as HrTime,
|
||||
hrTimeObserved: [1683526948, 965142784] as HrTime,
|
||||
attributes: {
|
||||
'some-attribute': 'some attribute value',
|
||||
},
|
||||
droppedAttributesCount: 0,
|
||||
severityNumber: SeverityNumber.ERROR,
|
||||
severityText: 'error',
|
||||
body: 'some_log_body',
|
||||
eventName: 'some.event.name',
|
||||
spanContext: {
|
||||
spanId: '0000000000000002',
|
||||
traceFlags: TraceFlags.SAMPLED,
|
||||
traceId: '00000000000000000000000000000001',
|
||||
},
|
||||
} as const;
|
||||
|
||||
describe('Logs', () => {
|
||||
let resource_1: Resource;
|
||||
let resource_2: Resource;
|
||||
|
|
@ -166,6 +187,18 @@ describe('Logs', () => {
|
|||
// using `resource_2`, `scope_1`, `log_fragment_1`
|
||||
let log_2_1_1: ReadableLogRecord;
|
||||
|
||||
function createReadableLogRecord(
|
||||
resource: Resource,
|
||||
scope: InstrumentationScope,
|
||||
logFragment: Omit<ReadableLogRecord, 'resource' | 'instrumentationScope'>
|
||||
): ReadableLogRecord {
|
||||
return {
|
||||
...logFragment,
|
||||
resource: resource,
|
||||
instrumentationScope: scope,
|
||||
} as ReadableLogRecord;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
resource_1 = resourceFromAttributes({
|
||||
'resource-attribute': 'some attribute value',
|
||||
|
|
@ -181,23 +214,8 @@ describe('Logs', () => {
|
|||
scope_2 = {
|
||||
name: 'scope_name_2',
|
||||
};
|
||||
const log_fragment_1 = {
|
||||
hrTime: [1680253513, 123241635] as HrTime,
|
||||
hrTimeObserved: [1683526948, 965142784] as HrTime,
|
||||
attributes: {
|
||||
'some-attribute': 'some attribute value',
|
||||
},
|
||||
droppedAttributesCount: 0,
|
||||
severityNumber: SeverityNumber.ERROR,
|
||||
severityText: 'error',
|
||||
body: 'some_log_body',
|
||||
eventName: 'some.event.name',
|
||||
spanContext: {
|
||||
spanId: '0000000000000002',
|
||||
traceFlags: TraceFlags.SAMPLED,
|
||||
traceId: '00000000000000000000000000000001',
|
||||
},
|
||||
};
|
||||
|
||||
const log_fragment_1 = DEFAULT_LOG_FRAGMENT;
|
||||
const log_fragment_2 = {
|
||||
hrTime: [1680253797, 687038506] as HrTime,
|
||||
hrTimeObserved: [1680253797, 687038506] as HrTime,
|
||||
|
|
@ -206,26 +224,11 @@ describe('Logs', () => {
|
|||
},
|
||||
droppedAttributesCount: 0,
|
||||
};
|
||||
log_1_1_1 = {
|
||||
...log_fragment_1,
|
||||
resource: resource_1,
|
||||
instrumentationScope: scope_1,
|
||||
};
|
||||
log_1_1_2 = {
|
||||
...log_fragment_2,
|
||||
resource: resource_1,
|
||||
instrumentationScope: scope_1,
|
||||
};
|
||||
log_1_2_1 = {
|
||||
...log_fragment_1,
|
||||
resource: resource_1,
|
||||
instrumentationScope: scope_2,
|
||||
};
|
||||
log_2_1_1 = {
|
||||
...log_fragment_1,
|
||||
resource: resource_2,
|
||||
instrumentationScope: scope_1,
|
||||
};
|
||||
|
||||
log_1_1_1 = createReadableLogRecord(resource_1, scope_1, log_fragment_1);
|
||||
log_1_1_2 = createReadableLogRecord(resource_1, scope_1, log_fragment_2);
|
||||
log_1_2_1 = createReadableLogRecord(resource_1, scope_2, log_fragment_1);
|
||||
log_2_1_1 = createReadableLogRecord(resource_2, scope_1, log_fragment_1);
|
||||
});
|
||||
|
||||
describe('createExportLogsServiceRequest', () => {
|
||||
|
|
@ -292,6 +295,30 @@ describe('Logs', () => {
|
|||
assert.ok(exportRequest);
|
||||
assert.strictEqual(exportRequest.resourceLogs?.length, 2);
|
||||
});
|
||||
|
||||
it('supports schema URL on resource', () => {
|
||||
const resourceWithSchema = resourceFromAttributes(
|
||||
{},
|
||||
{ schemaUrl: 'https://opentelemetry.test/schemas/1.2.3' }
|
||||
);
|
||||
|
||||
const logWithSchema = createReadableLogRecord(
|
||||
resourceWithSchema,
|
||||
scope_1,
|
||||
DEFAULT_LOG_FRAGMENT
|
||||
);
|
||||
|
||||
const exportRequest = createExportLogsServiceRequest([logWithSchema], {
|
||||
useHex: true,
|
||||
});
|
||||
|
||||
assert.ok(exportRequest);
|
||||
assert.strictEqual(exportRequest.resourceLogs?.length, 1);
|
||||
assert.strictEqual(
|
||||
exportRequest.resourceLogs?.[0].schemaUrl,
|
||||
'https://opentelemetry.test/schemas/1.2.3'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ProtobufLogsSerializer', function () {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
import { ValueType } from '@opentelemetry/api';
|
||||
import { resourceFromAttributes } from '@opentelemetry/resources';
|
||||
import { Resource, resourceFromAttributes } from '@opentelemetry/resources';
|
||||
import {
|
||||
AggregationTemporality,
|
||||
DataPointType,
|
||||
|
|
@ -301,10 +301,16 @@ describe('Metrics', () => {
|
|||
};
|
||||
}
|
||||
|
||||
function createResourceMetrics(metricData: MetricData[]): ResourceMetrics {
|
||||
const resource = resourceFromAttributes({
|
||||
function createResourceMetrics(
|
||||
metricData: MetricData[],
|
||||
customResource?: Resource
|
||||
): ResourceMetrics {
|
||||
const resource =
|
||||
customResource ||
|
||||
resourceFromAttributes({
|
||||
'resource-attribute': 'resource attribute value',
|
||||
});
|
||||
|
||||
return {
|
||||
resource: resource,
|
||||
scopeMetrics: [
|
||||
|
|
@ -773,6 +779,29 @@ describe('Metrics', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('supports schema URL on resource', () => {
|
||||
const resourceWithSchema = resourceFromAttributes(
|
||||
{},
|
||||
{ schemaUrl: 'https://opentelemetry.test/schemas/1.2.3' }
|
||||
);
|
||||
|
||||
const resourceMetrics = createResourceMetrics(
|
||||
[createCounterData(10, AggregationTemporality.DELTA)],
|
||||
resourceWithSchema
|
||||
);
|
||||
|
||||
const exportRequest = createExportMetricsServiceRequest([
|
||||
resourceMetrics,
|
||||
]);
|
||||
|
||||
assert.ok(exportRequest);
|
||||
assert.strictEqual(exportRequest.resourceMetrics?.length, 1);
|
||||
assert.strictEqual(
|
||||
exportRequest.resourceMetrics?.[0].schemaUrl,
|
||||
'https://opentelemetry.test/schemas/1.2.3'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ProtobufMetricsSerializer', function () {
|
||||
|
|
|
|||
|
|
@ -232,11 +232,8 @@ describe('Trace', () => {
|
|||
let resource: Resource;
|
||||
let span: ReadableSpan;
|
||||
|
||||
beforeEach(() => {
|
||||
resource = resourceFromAttributes({
|
||||
'resource-attribute': 'resource attribute value',
|
||||
});
|
||||
span = {
|
||||
function createSpanWithResource(spanResource: Resource): ReadableSpan {
|
||||
return {
|
||||
spanContext: () => ({
|
||||
spanId: '0000000000000002',
|
||||
traceFlags: TraceFlags.SAMPLED,
|
||||
|
|
@ -283,7 +280,7 @@ describe('Trace', () => {
|
|||
},
|
||||
],
|
||||
name: 'span-name',
|
||||
resource,
|
||||
resource: spanResource,
|
||||
startTime: [1640715557, 342725388],
|
||||
status: {
|
||||
code: SpanStatusCode.OK,
|
||||
|
|
@ -292,6 +289,13 @@ describe('Trace', () => {
|
|||
droppedEventsCount: 0,
|
||||
droppedLinksCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
resource = resourceFromAttributes({
|
||||
'resource-attribute': 'resource attribute value',
|
||||
});
|
||||
span = createSpanWithResource(resource);
|
||||
});
|
||||
|
||||
describe('createExportTraceServiceRequest', () => {
|
||||
|
|
@ -443,6 +447,26 @@ describe('Trace', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('supports schema URL on resource', () => {
|
||||
const resourceWithSchema = resourceFromAttributes(
|
||||
{ 'resource-attribute': 'resource attribute value' },
|
||||
{ schemaUrl: 'https://opentelemetry.test/schemas/1.2.3' }
|
||||
);
|
||||
|
||||
const spanFromSDK = createSpanWithResource(resourceWithSchema);
|
||||
|
||||
const exportRequest = createExportTraceServiceRequest([spanFromSDK], {
|
||||
useHex: true,
|
||||
});
|
||||
|
||||
assert.ok(exportRequest);
|
||||
assert.strictEqual(exportRequest.resourceSpans?.length, 1);
|
||||
assert.strictEqual(
|
||||
exportRequest.resourceSpans?.[0].schemaUrl,
|
||||
'https://opentelemetry.test/schemas/1.2.3'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ProtobufTracesSerializer', function () {
|
||||
|
|
|
|||
|
|
@ -41,6 +41,11 @@ export interface Resource {
|
|||
*/
|
||||
readonly attributes: Attributes;
|
||||
|
||||
/**
|
||||
* @returns the Resource's schema URL or undefined if not set.
|
||||
*/
|
||||
readonly schemaUrl?: string;
|
||||
|
||||
/**
|
||||
* Returns a promise that will never be rejected. Resolves when all async attributes have finished being added to
|
||||
* this Resource's attributes. This is useful in exporters to block until resource detection
|
||||
|
|
|
|||
|
|
@ -29,19 +29,22 @@ import {
|
|||
DetectedResourceAttributes,
|
||||
MaybePromise,
|
||||
RawResourceAttribute,
|
||||
ResourceOptions,
|
||||
} from './types';
|
||||
import { isPromiseLike } from './utils';
|
||||
|
||||
class ResourceImpl implements Resource {
|
||||
private _rawAttributes: RawResourceAttribute[];
|
||||
private _asyncAttributesPending = false;
|
||||
private _schemaUrl?: string;
|
||||
|
||||
private _memoizedAttributes?: Attributes;
|
||||
|
||||
static FromAttributeList(
|
||||
attributes: [string, MaybePromise<AttributeValue | undefined>][]
|
||||
attributes: [string, MaybePromise<AttributeValue | undefined>][],
|
||||
options?: ResourceOptions
|
||||
): Resource {
|
||||
const res = new ResourceImpl({});
|
||||
const res = new ResourceImpl({}, options);
|
||||
res._rawAttributes = guardedRawAttributes(attributes);
|
||||
res._asyncAttributesPending =
|
||||
attributes.filter(([_, val]) => isPromiseLike(val)).length > 0;
|
||||
|
|
@ -54,7 +57,8 @@ class ResourceImpl implements Resource {
|
|||
* information about the entity as numbers, strings or booleans
|
||||
* TODO: Consider to add check/validation on attributes.
|
||||
*/
|
||||
resource: DetectedResource
|
||||
resource: DetectedResource,
|
||||
options?: ResourceOptions
|
||||
) {
|
||||
const attributes = resource.attributes ?? {};
|
||||
this._rawAttributes = Object.entries(attributes).map(([k, v]) => {
|
||||
|
|
@ -67,6 +71,7 @@ class ResourceImpl implements Resource {
|
|||
});
|
||||
|
||||
this._rawAttributes = guardedRawAttributes(this._rawAttributes);
|
||||
this._schemaUrl = validateSchemaUrl(options?.schemaUrl);
|
||||
}
|
||||
|
||||
public get asyncAttributesPending(): boolean {
|
||||
|
|
@ -120,28 +125,39 @@ class ResourceImpl implements Resource {
|
|||
return this._rawAttributes;
|
||||
}
|
||||
|
||||
public get schemaUrl(): string | undefined {
|
||||
return this._schemaUrl;
|
||||
}
|
||||
|
||||
public merge(resource: Resource | null): Resource {
|
||||
if (resource == null) return this;
|
||||
|
||||
// Order is important
|
||||
// Spec states incoming attributes override existing attributes
|
||||
return ResourceImpl.FromAttributeList([
|
||||
...resource.getRawAttributes(),
|
||||
...this.getRawAttributes(),
|
||||
]);
|
||||
const mergedSchemaUrl = mergeSchemaUrl(this, resource);
|
||||
const mergedOptions: ResourceOptions | undefined = mergedSchemaUrl
|
||||
? { schemaUrl: mergedSchemaUrl }
|
||||
: undefined;
|
||||
|
||||
return ResourceImpl.FromAttributeList(
|
||||
[...resource.getRawAttributes(), ...this.getRawAttributes()],
|
||||
mergedOptions
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function resourceFromAttributes(
|
||||
attributes: DetectedResourceAttributes
|
||||
attributes: DetectedResourceAttributes,
|
||||
options?: ResourceOptions
|
||||
): Resource {
|
||||
return ResourceImpl.FromAttributeList(Object.entries(attributes));
|
||||
return ResourceImpl.FromAttributeList(Object.entries(attributes), options);
|
||||
}
|
||||
|
||||
export function resourceFromDetectedResource(
|
||||
detectedResource: DetectedResource
|
||||
detectedResource: DetectedResource,
|
||||
options?: ResourceOptions
|
||||
): Resource {
|
||||
return new ResourceImpl(detectedResource);
|
||||
return new ResourceImpl(detectedResource, options);
|
||||
}
|
||||
|
||||
export function emptyResource(): Resource {
|
||||
|
|
@ -177,3 +193,48 @@ function guardedRawAttributes(
|
|||
return [k, v];
|
||||
});
|
||||
}
|
||||
|
||||
function validateSchemaUrl(schemaUrl?: string): string | undefined {
|
||||
if (typeof schemaUrl === 'string' || schemaUrl === undefined) {
|
||||
return schemaUrl;
|
||||
}
|
||||
|
||||
diag.warn(
|
||||
'Schema URL must be string or undefined, got %s. Schema URL will be ignored.',
|
||||
schemaUrl
|
||||
);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function mergeSchemaUrl(
|
||||
old: Resource,
|
||||
updating: Resource | null
|
||||
): string | undefined {
|
||||
const oldSchemaUrl = old?.schemaUrl;
|
||||
const updatingSchemaUrl = updating?.schemaUrl;
|
||||
|
||||
const isOldEmpty = oldSchemaUrl === undefined || oldSchemaUrl === '';
|
||||
const isUpdatingEmpty =
|
||||
updatingSchemaUrl === undefined || updatingSchemaUrl === '';
|
||||
|
||||
if (isOldEmpty) {
|
||||
return updatingSchemaUrl;
|
||||
}
|
||||
|
||||
if (isUpdatingEmpty) {
|
||||
return oldSchemaUrl;
|
||||
}
|
||||
|
||||
if (oldSchemaUrl === updatingSchemaUrl) {
|
||||
return oldSchemaUrl;
|
||||
}
|
||||
|
||||
diag.warn(
|
||||
'Schema URL merge conflict: old resource has "%s", updating resource has "%s". Resulting resource will have undefined Schema URL.',
|
||||
oldSchemaUrl,
|
||||
updatingSchemaUrl
|
||||
);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,3 +59,10 @@ export type RawResourceAttribute = [
|
|||
string,
|
||||
MaybePromise<AttributeValue | undefined>,
|
||||
];
|
||||
|
||||
/**
|
||||
* Options for creating a {@link Resource}.
|
||||
*/
|
||||
export type ResourceOptions = {
|
||||
schemaUrl?: string;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -287,6 +287,192 @@ describe('Resource', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('schema URL support', () => {
|
||||
it('should create resource with schema URL', () => {
|
||||
const schemaUrl = 'https://example.test/schemas/1.2.3';
|
||||
const resource = resourceFromAttributes({ attr: 'value' }, { schemaUrl });
|
||||
|
||||
assert.strictEqual(resource.schemaUrl, schemaUrl);
|
||||
});
|
||||
|
||||
it('should create resource without schema URL', () => {
|
||||
const resource = resourceFromAttributes({ attr: 'value' });
|
||||
|
||||
assert.strictEqual(resource.schemaUrl, undefined);
|
||||
});
|
||||
|
||||
it('should retain schema URL from base resource when other has no schema URL', () => {
|
||||
const schemaUrl = 'https://opentelemetry.test/schemas/1.2.3';
|
||||
const resource1 = resourceFromAttributes(
|
||||
{ attr1: 'value1' },
|
||||
{ schemaUrl }
|
||||
);
|
||||
const resource2 = resourceFromAttributes({ attr2: 'value2' });
|
||||
|
||||
const mergedResource = resource1.merge(resource2);
|
||||
|
||||
assert.strictEqual(mergedResource.schemaUrl, schemaUrl);
|
||||
});
|
||||
|
||||
it('should retain schema URL from other resource when base has no schema URL', () => {
|
||||
const resource1 = resourceFromAttributes({ attr1: 'value1' });
|
||||
const resource2 = resourceFromAttributes(
|
||||
{ attr2: 'value2' },
|
||||
{ schemaUrl: 'https://opentelemetry.test/schemas/1.2.3' }
|
||||
);
|
||||
|
||||
const mergedResource = resource1.merge(resource2);
|
||||
|
||||
assert.strictEqual(
|
||||
mergedResource.schemaUrl,
|
||||
'https://opentelemetry.test/schemas/1.2.3'
|
||||
);
|
||||
});
|
||||
|
||||
it('should have empty schema URL when merging resources with no schema URL', () => {
|
||||
const resource1 = resourceFromAttributes(
|
||||
{ attr1: 'value1' },
|
||||
{ schemaUrl: '' }
|
||||
);
|
||||
const resource2 = resourceFromAttributes(
|
||||
{ attr2: 'value2' },
|
||||
{ schemaUrl: '' }
|
||||
);
|
||||
|
||||
const mergedResource = resource1.merge(resource2);
|
||||
|
||||
assert.strictEqual(mergedResource.schemaUrl, undefined);
|
||||
});
|
||||
|
||||
it('should maintain backward compatibility - schemaUrl is optional', () => {
|
||||
const resource = emptyResource();
|
||||
|
||||
const schemaUrl = resource.schemaUrl;
|
||||
assert.strictEqual(schemaUrl, undefined);
|
||||
});
|
||||
|
||||
it('should work with async attributes and schema URLs', async () => {
|
||||
const resource = resourceFromAttributes(
|
||||
{
|
||||
sync: 'fromsync',
|
||||
async: new Promise(resolve =>
|
||||
setTimeout(() => resolve('fromasync'), 1)
|
||||
),
|
||||
},
|
||||
{ schemaUrl: 'https://opentelemetry.test/schemas/1.2.3' }
|
||||
);
|
||||
|
||||
await resource.waitForAsyncAttributes?.();
|
||||
|
||||
assert.deepStrictEqual(resource.attributes, {
|
||||
sync: 'fromsync',
|
||||
async: 'fromasync',
|
||||
});
|
||||
assert.strictEqual(
|
||||
resource.schemaUrl,
|
||||
'https://opentelemetry.test/schemas/1.2.3'
|
||||
);
|
||||
});
|
||||
|
||||
it('should merge schema URLs according to OpenTelemetry spec - same URLs', () => {
|
||||
const resource1 = resourceFromAttributes(
|
||||
{ attr1: 'value1' },
|
||||
{ schemaUrl: 'https://opentelemetry.test/schemas/1.2.3' }
|
||||
);
|
||||
const resource2 = resourceFromAttributes(
|
||||
{ attr2: 'value2' },
|
||||
{ schemaUrl: 'https://opentelemetry.test/schemas/1.2.3' }
|
||||
);
|
||||
|
||||
const mergedResource = resource1.merge(resource2);
|
||||
|
||||
assert.strictEqual(
|
||||
mergedResource.schemaUrl,
|
||||
'https://opentelemetry.test/schemas/1.2.3'
|
||||
);
|
||||
});
|
||||
|
||||
it('should merge schema URLs according to OpenTelemetry spec - conflict case (undefined behavior)', () => {
|
||||
const warnStub = sinon.spy(diag, 'warn');
|
||||
|
||||
const resource1 = resourceFromAttributes(
|
||||
{ attr1: 'value1' },
|
||||
{ schemaUrl: 'https://opentelemetry.test/schemas/1.2.3' }
|
||||
);
|
||||
const resource2 = resourceFromAttributes(
|
||||
{ attr2: 'value2' },
|
||||
{ schemaUrl: 'https://opentelemetry.test/schemas/1.2.4' }
|
||||
);
|
||||
|
||||
const mergedResource = resource1.merge(resource2);
|
||||
|
||||
// Implementation-specific: we return undefined to indicate error state
|
||||
// This aligns with Go, Java, and PHP SDKs which return null/empty for conflicts
|
||||
assert.strictEqual(mergedResource.schemaUrl, undefined);
|
||||
|
||||
assert.ok(warnStub.calledWithMatch('Schema URL merge conflict'));
|
||||
|
||||
warnStub.restore();
|
||||
});
|
||||
|
||||
it('should accept valid schema URL formats', () => {
|
||||
const validSchemaUrls = [
|
||||
'https://opentelemetry.test/schemas/1.2.3',
|
||||
'http://example.test/schema',
|
||||
'https://schemas.opentelemetry.test/path/to/schema/1.21.0',
|
||||
'https://example.test:8080/path/to/schema',
|
||||
];
|
||||
|
||||
validSchemaUrls.forEach(validUrl => {
|
||||
const resource = resourceFromAttributes(
|
||||
{ attr: 'value' },
|
||||
{ schemaUrl: validUrl }
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
resource.schemaUrl,
|
||||
validUrl,
|
||||
`Expected valid schema URL to be preserved: ${validUrl}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle invalid schema URL formats gracefully', () => {
|
||||
const warnStub = sinon.spy(diag, 'warn');
|
||||
|
||||
const invalidSchemaUrls = [
|
||||
null,
|
||||
123,
|
||||
12345678901234567890n,
|
||||
{ schemaUrl: 'http://example.test/schema' },
|
||||
['http://example.test/schema'],
|
||||
];
|
||||
|
||||
invalidSchemaUrls.forEach(invalidUrl => {
|
||||
const resource = resourceFromAttributes(
|
||||
{ attr: 'value' },
|
||||
// @ts-expect-error the function signature doesn't allow these, but can still happen at runtime
|
||||
{ schemaUrl: invalidUrl }
|
||||
);
|
||||
|
||||
// Invalid schema URLs should be ignored (set to undefined)
|
||||
assert.strictEqual(
|
||||
resource.schemaUrl,
|
||||
undefined,
|
||||
`Expected undefined for invalid schema URL: ${invalidUrl}`
|
||||
);
|
||||
});
|
||||
|
||||
// Should have logged warnings for each invalid URL
|
||||
assert.strictEqual(warnStub.callCount, invalidSchemaUrls.length);
|
||||
assert.ok(
|
||||
warnStub.alwaysCalledWithMatch('Schema URL must be string or undefined')
|
||||
);
|
||||
|
||||
warnStub.restore();
|
||||
});
|
||||
});
|
||||
|
||||
describeNode('.default()', () => {
|
||||
it('should return a default resource', () => {
|
||||
const resource = defaultResource();
|
||||
|
|
|
|||
Loading…
Reference in New Issue