Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(opentelemetry-resources): add schema url #5070

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ For semantic convention package changes, see the [semconv CHANGELOG](packages/se

## Unreleased

### :feature-request:sparkles: (Feature)

* feat(opentelemetry-resources): add schema url [#5070](https://github.com/open-telemetry/opentelemetry-js/pull/5070) @taylorhelene

### :boom: Breaking Change

### :rocket: (Enhancement)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -827,7 +827,7 @@ describe('Node SDK', () => {
const resource = sdk['_resource'];
await resource.waitForAsyncAttributes?.();

assertServiceInstanceIdIsUUID(resource);
assertServiceInstanceIdIsUUID(resource as Resource);
delete process.env.OTEL_NODE_RESOURCE_DETECTORS;
await sdk.shutdown();
});
Expand All @@ -847,7 +847,7 @@ describe('Node SDK', () => {
const resource = sdk['_resource'];
await resource.waitForAsyncAttributes?.();

assertServiceInstanceIdIsUUID(resource);
assertServiceInstanceIdIsUUID(resource as Resource);
await sdk.shutdown();
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ export function createResource(resource: ISdkResource): IResource {
return {
attributes: toAttributes(resource.attributes),
droppedAttributesCount: 0,
schemaUrl: resource.getSchemaUrl ? resource.getSchemaUrl() : '',
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,7 @@ export interface IResource {

/** Resource droppedAttributesCount */
droppedAttributesCount: number;

/** Schema URL */
schemaUrl?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ describe('SimpleLogRecordProcessor', () => {
const exporter = new InMemoryLogRecordExporter();
const { processor, logRecord } = setup(
exporter,
new Resource190({ fromold: 'fromold' })
new Resource190({ fromold: 'fromold' }) as any
);
assert.strictEqual(exporter.getFinishedLogRecords().length, 0);
processor.onEmit(logRecord);
Expand Down
5 changes: 5 additions & 0 deletions packages/opentelemetry-resources/src/IResource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,9 @@ export interface IResource {
* @returns the newly merged Resource.
*/
merge(other: IResource | null): IResource;

/**
* Returns the schema URL of the resource.
*/
getSchemaUrl(): string; // Add this line
}
37 changes: 26 additions & 11 deletions packages/opentelemetry-resources/src/Resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { SDK_INFO } from '@opentelemetry/core';
import { ResourceAttributes } from './types';
import { defaultServiceName } from './platform';
import { IResource } from './IResource';
import { mergeSchemaUrls } from './schemaUrlUtils'; // Import the utility function

/**
* A Resource describes the entity for which a signals (metrics or trace) are
Expand All @@ -35,7 +36,7 @@ export class Resource implements IResource {
private _syncAttributes?: ResourceAttributes;
private _asyncAttributesPromise?: Promise<ResourceAttributes>;
private _attributes?: ResourceAttributes;

private _schemaUrl?: string;
/**
* Check if async attributes have resolved. This is useful to avoid awaiting
* waitForAsyncAttributes (which will introduce asynchronous behavior) when not necessary.
Expand Down Expand Up @@ -67,13 +68,9 @@ export class Resource implements IResource {
}

constructor(
/**
* A dictionary of attributes with string keys and values that provide
* information about the entity as numbers, strings or booleans
* TODO: Consider to add check/validation on attributes.
*/
attributes: ResourceAttributes,
asyncAttributesPromise?: Promise<ResourceAttributes>
asyncAttributesPromise?: Promise<ResourceAttributes>,
schemaUrl = ''
) {
this._attributes = attributes;
this.asyncAttributesPending = asyncAttributesPromise != null;
Expand All @@ -90,6 +87,7 @@ export class Resource implements IResource {
return {};
}
);
this._schemaUrl = schemaUrl;
}

get attributes(): ResourceAttributes {
Expand All @@ -102,6 +100,13 @@ export class Resource implements IResource {
return this._attributes ?? {};
}

/**
* Returns the schema URL of the resource.
*/
public getSchemaUrl(): string {
return this._schemaUrl || ''; // Return schema URL as 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
Expand All @@ -127,15 +132,21 @@ export class Resource implements IResource {
// SpanAttributes from other resource overwrite attributes from this resource.
const mergedSyncAttributes = {
...this._syncAttributes,
//Support for old resource implementation where _syncAttributes is not defined
// Support for old resource implementation where _syncAttributes is not defined
...((other as Resource)._syncAttributes ?? other.attributes),
};

// Merge schema URLs using the utility function
const mergedSchemaUrl = mergeSchemaUrls(
this._schemaUrl || '',
other.getSchemaUrl?.() || ''
);

if (
!this._asyncAttributesPromise &&
!(other as Resource)._asyncAttributesPromise
) {
return new Resource(mergedSyncAttributes);
return new Resource(mergedSyncAttributes, undefined, mergedSchemaUrl);
}

const mergedAttributesPromise = Promise.all([
Expand All @@ -145,12 +156,16 @@ export class Resource implements IResource {
return {
...this._syncAttributes,
...thisAsyncAttributes,
//Support for old resource implementation where _syncAttributes is not defined
// Support for old resource implementation where _syncAttributes is not defined
...((other as Resource)._syncAttributes ?? other.attributes),
...otherAsyncAttributes,
};
});

return new Resource(mergedSyncAttributes, mergedAttributesPromise);
return new Resource(
mergedSyncAttributes,
mergedAttributesPromise,
mergedSchemaUrl
);
}
}
36 changes: 36 additions & 0 deletions packages/opentelemetry-resources/src/schemaUrlUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* 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';

/**
* Merges two schema URLs. If both schema URLs are present and differ,
* a warning is logged and the first schema URL is prioritized.
*
* @param schemaUrl1 - The first schema URL
* @param schemaUrl2 - The second schema URL
* @returns The prioritized schema URL
*/

export function mergeSchemaUrls(
schemaUrl1: string,
schemaUrl2: string
): string {
if (schemaUrl1 !== schemaUrl2) {
diag.warn('Schema URLs differ. Using the original schema URL.');
}
return schemaUrl1 || schemaUrl2;
}
66 changes: 64 additions & 2 deletions packages/opentelemetry-resources/test/Resource.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ describe('Resource', () => {
const resource = Resource.EMPTY;
const oldResource = new Resource190({ fromold: 'fromold' });

const mergedResource = resource.merge(oldResource);
const mergedResource = resource.merge(oldResource as any);

assert.strictEqual(mergedResource.attributes['fromold'], 'fromold');
});
Expand All @@ -342,11 +342,73 @@ describe('Resource', () => {
);
const oldResource = new Resource190({ fromold: 'fromold' });

const mergedResource = resource.merge(oldResource);
const mergedResource = resource.merge(oldResource as any);
assert.strictEqual(mergedResource.attributes['fromold'], 'fromold');

await mergedResource.waitForAsyncAttributes?.();
assert.strictEqual(mergedResource.attributes['fromnew'], 'fromnew');
});
});

it('should initialize resource with schema URL', () => {
const schemaUrl = 'https://example.com/schema';
const resource = new Resource(
{ 'k8s.io/container/name': 'c1' },
undefined,
schemaUrl
);
assert.strictEqual(resource.getSchemaUrl(), schemaUrl);
});

it('should merge resources with different schema URLs', () => {
const schemaUrl1 = 'https://example.com/schema1';
const schemaUrl2 = 'https://example.com/schema2';
const resource1 = new Resource({ attr1: 'value1' }, undefined, schemaUrl1);
const resource2 = new Resource({ attr2: 'value2' }, undefined, schemaUrl2);

const warnStub = sinon.spy(diag, 'warn');
const mergedResource = resource1.merge(resource2);

assert.strictEqual(mergedResource.getSchemaUrl(), schemaUrl1);
assert.ok(
warnStub.calledWithMatch(
'Schema URLs differ. Using the original schema URL.'
)
);
});

it('should retain the same schema URL when merging resources with identical URLs', () => {
const schemaUrl = 'https://example.com/schema';
const resource1 = new Resource({ attr1: 'value1' }, undefined, schemaUrl);
const resource2 = new Resource({ attr2: 'value2' }, undefined, schemaUrl);

const mergedResource = resource1.merge(resource2);

assert.strictEqual(mergedResource.getSchemaUrl(), schemaUrl);
});

it('should retain schema URL from the resource that has it when merging', () => {
const resource1 = new Resource({ attr1: 'value1' }, undefined, '');
const resource2 = new Resource(
{ attr2: 'value2' },
undefined,
'https://example.com/schema'
);

const mergedResource = resource1.merge(resource2);

assert.strictEqual(
mergedResource.getSchemaUrl(),
'https://example.com/schema'
);
});

it('should have empty schema URL when merging resources with no schema URL', () => {
const resource1 = new Resource({ attr1: 'value1' }, undefined, '');
const resource2 = new Resource({ attr2: 'value2' }, undefined, '');

const mergedResource = resource1.merge(resource2);

assert.strictEqual(mergedResource.getSchemaUrl(), '');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import * as assert from 'assert';
// If compilation fails at this point then the changes made are breaking.
class RegressionTestResourceDetector_1_9_1 implements Detector {
async detect(_config?: ResourceDetectionConfig): Promise<Resource> {
return Resource.empty();
return Resource.empty() as any;
Copy link
Member

Choose a reason for hiding this comment

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

The comment above this detector still holds - if this needs to be modified, the changes in this PR are breaking.

}
}

Expand Down