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 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
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 @@ -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
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 a stable interface, we can only add new optional properties here - this is likely why you had to adjust the tests (adding as Resource and as any)

}
44 changes: 33 additions & 11 deletions packages/opentelemetry-resources/src/Resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export class Resource implements IResource {
private _syncAttributes?: ResourceAttributes;
private _asyncAttributesPromise?: Promise<ResourceAttributes>;
private _attributes?: ResourceAttributes;
private _schemaUrl?: string; // Added schemaUrl property
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
private _schemaUrl?: string; // Added schemaUrl property
private _schemaUrl?: string;


/**
* Check if async attributes have resolved. This is useful to avoid awaiting
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: string = '' // Added schemaUrl parameter
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
schemaUrl: string = '' // Added schemaUrl parameter
schemaUrl: string = ''

) {
this._attributes = attributes;
this.asyncAttributesPending = asyncAttributesPromise != null;
Expand All @@ -90,6 +87,7 @@ export class Resource implements IResource {
return {};
}
);
this._schemaUrl = schemaUrl; // Store the schemaUrl
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
this._schemaUrl = schemaUrl; // Store the schemaUrl
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,18 @@ 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, handling conflicts
const mergedSchemaUrl = this._mergeSchemaUrls(this._schemaUrl || '', (other as Resource)._schemaUrl || '');
Copy link
Member

Choose a reason for hiding this comment

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

I think since IResource#getSchemaUrl() is defined, we can use

Suggested change
const mergedSchemaUrl = this._mergeSchemaUrls(this._schemaUrl || '', (other as Resource)._schemaUrl || '');
const mergedSchemaUrl = this._mergeSchemaUrls(this._schemaUrl || '', other.getSchemaUrl?.() || '');

and we can get rid of the back-cast. 🙂


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 +153,26 @@ 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);
}

/**
* Helper function to merge schema URLs. If both schema URLs are present and differ,
* a warning is logged and the first schema URL is prioritized.
*/
private _mergeSchemaUrls(
Copy link
Member

Choose a reason for hiding this comment

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

I think this would be better as a stand-alone utility function as it does not access anything on the instance.

schemaUrl1: string,
schemaUrl2: string
): string {
if (schemaUrl1 && schemaUrl2 && schemaUrl1 !== schemaUrl2) {
diag.warn('Schema URLs differ. Using the original schema URL.');
}
Copy link
Member

Choose a reason for hiding this comment

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

here we've already established that schema URL is a string (not undefined or null) though the type-system - why add null-checks? 🤔

return schemaUrl1 || schemaUrl2; // Return merged schema URL as string
}
}
}
Copy link
Member

Choose a reason for hiding this comment

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

missing newline 🙂

Suggested change
}
}

56 changes: 54 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,63 @@ 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 debugStub = sinon.spy(diag, 'warn');
const mergedResource = resource1.merge(resource2);

assert.strictEqual(mergedResource.getSchemaUrl(), schemaUrl1);
assert.ok(debugStub.calledWithMatch('Schema URLs differ. Using the original schema URL.'));
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
const debugStub = sinon.spy(diag, 'warn');
const mergedResource = resource1.merge(resource2);
assert.strictEqual(mergedResource.getSchemaUrl(), schemaUrl1);
assert.ok(debugStub.calledWithMatch('Schema URLs differ. Using the original schema URL.'));
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
Loading