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

Converge sync and async resources #5350

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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 @@ -47,7 +47,7 @@ describeBrowser('browserDetector()', () => {
},
});

const resource: IResource = await browserDetector.detect();
const resource = browserDetector.detect();
assertResource(resource, {
platform: 'platform',
brands: ['Chromium 106', 'Google Chrome 106', 'Not;A=Brand 99'],
Expand All @@ -63,7 +63,7 @@ describeBrowser('browserDetector()', () => {
userAgentData: undefined,
});

const resource: IResource = await browserDetector.detect();
const resource = browserDetector.detect();
assertResource(resource, {
language: 'en-US',
user_agent: 'dddd',
Expand All @@ -74,7 +74,7 @@ describeBrowser('browserDetector()', () => {
sinon.stub(globalThis, 'navigator').value({
userAgent: '',
});
const resource: IResource = await browserDetector.detect();
const resource = browserDetector.detect();
assertEmptyResource(resource);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { Suite } from 'mocha';
import * as assert from 'assert';
import { BROWSER_ATTRIBUTES } from '../src/types';
import { IResource } from '@opentelemetry/resources';
import { DetectedResource } from '@opentelemetry/resources/src/types';
dyladan marked this conversation as resolved.
Show resolved Hide resolved

export function describeBrowser(title: string, fn: (this: Suite) => void) {
title = `Browser: ${title}`;
Expand All @@ -27,7 +28,7 @@ export function describeBrowser(title: string, fn: (this: Suite) => void) {
}

export const assertResource = (
resource: IResource,
resource: DetectedResource,
validations: {
platform?: string;
brands?: string[];
Expand All @@ -38,32 +39,32 @@ export const assertResource = (
) => {
if (validations.platform) {
assert.strictEqual(
resource.attributes[BROWSER_ATTRIBUTES.PLATFORM],
resource.attributes?.[BROWSER_ATTRIBUTES.PLATFORM],
validations.platform
);
}
if (validations.brands) {
assert.ok(Array.isArray(resource.attributes[BROWSER_ATTRIBUTES.BRANDS]));
assert.ok(Array.isArray(resource.attributes?.[BROWSER_ATTRIBUTES.BRANDS]));
assert.deepStrictEqual(
resource.attributes[BROWSER_ATTRIBUTES.BRANDS] as string[],
resource.attributes?.[BROWSER_ATTRIBUTES.BRANDS] as string[],
validations.brands
);
}
if (validations.mobile) {
assert.strictEqual(
resource.attributes[BROWSER_ATTRIBUTES.MOBILE],
resource.attributes?.[BROWSER_ATTRIBUTES.MOBILE],
validations.mobile
);
}
if (validations.language) {
assert.strictEqual(
resource.attributes[BROWSER_ATTRIBUTES.LANGUAGE],
resource.attributes?.[BROWSER_ATTRIBUTES.LANGUAGE],
validations.language
);
}
if (validations.user_agent) {
assert.strictEqual(
resource.attributes[BROWSER_ATTRIBUTES.USER_AGENT],
resource.attributes?.[BROWSER_ATTRIBUTES.USER_AGENT],
validations.user_agent
);
}
Expand Down
182 changes: 83 additions & 99 deletions packages/opentelemetry-resources/src/Resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,54 +14,41 @@
* limitations under the License.
*/

import { Attributes, diag } from '@opentelemetry/api';
import { Attributes, AttributeValue, diag } from '@opentelemetry/api';
import { SDK_INFO } from '@opentelemetry/core';
import {
SEMRESATTRS_SERVICE_NAME,
SEMRESATTRS_TELEMETRY_SDK_LANGUAGE,
SEMRESATTRS_TELEMETRY_SDK_NAME,
SEMRESATTRS_TELEMETRY_SDK_VERSION,
ATTR_SERVICE_NAME,
ATTR_TELEMETRY_SDK_LANGUAGE,
ATTR_TELEMETRY_SDK_NAME,
ATTR_TELEMETRY_SDK_VERSION,
} from '@opentelemetry/semantic-conventions';
import { SDK_INFO } from '@opentelemetry/core';
import { defaultServiceName } from './platform';
import { IResource } from './IResource';
import { defaultServiceName } from './platform';
import {
DetectedResource,
DetectedResourceAttributes,
MaybePromise,
} from './types';
import { isPromiseLike } from './utils';

/**
* A Resource describes the entity for which a signals (metrics or trace) are
* collected.
*/
export class Resource implements IResource {
static readonly EMPTY = new Resource({});
private _syncAttributes?: Attributes;
private _asyncAttributesPromise?: Promise<Attributes>;
private _attributes?: Attributes;
private _rawAttributes: [string, MaybePromise<AttributeValue | undefined>][];
private _asyncAttributesPending = false;

/**
* Check if async attributes have resolved. This is useful to avoid awaiting
* waitForAsyncAttributes (which will introduce asynchronous behavior) when not necessary.
*
* @returns true if the resource "attributes" property is not yet settled to its final value
*/
public asyncAttributesPending?: boolean;

/**
* Returns an empty Resource
*/
static empty(): IResource {
return Resource.EMPTY;
}
private _memoizedAttributes?: Attributes;

public static EMPTY = new Resource({});
dyladan marked this conversation as resolved.
Show resolved Hide resolved
/**
* Returns a Resource that identifies the SDK in use.
*/
static default(): IResource {
return new Resource({
[SEMRESATTRS_SERVICE_NAME]: defaultServiceName(),
[SEMRESATTRS_TELEMETRY_SDK_LANGUAGE]:
SDK_INFO[SEMRESATTRS_TELEMETRY_SDK_LANGUAGE],
[SEMRESATTRS_TELEMETRY_SDK_NAME]:
SDK_INFO[SEMRESATTRS_TELEMETRY_SDK_NAME],
[SEMRESATTRS_TELEMETRY_SDK_VERSION]:
SDK_INFO[SEMRESATTRS_TELEMETRY_SDK_VERSION],
attributes: {
[ATTR_SERVICE_NAME]: defaultServiceName(),
[ATTR_TELEMETRY_SDK_LANGUAGE]: SDK_INFO[ATTR_TELEMETRY_SDK_LANGUAGE],
[ATTR_TELEMETRY_SDK_NAME]: SDK_INFO[ATTR_TELEMETRY_SDK_NAME],
[ATTR_TELEMETRY_SDK_VERSION]: SDK_INFO[ATTR_TELEMETRY_SDK_VERSION],
},
});
}

Expand All @@ -71,85 +58,82 @@ export class Resource implements IResource {
* information about the entity as numbers, strings or booleans
* TODO: Consider to add check/validation on attributes.
*/
attributes: Attributes,
asyncAttributesPromise?: Promise<Attributes>
resource: DetectedResource
) {
this._attributes = attributes;
this.asyncAttributesPending = asyncAttributesPromise != null;
this._syncAttributes = this._attributes ?? {};
this._asyncAttributesPromise = asyncAttributesPromise?.then(
asyncAttributes => {
this._attributes = Object.assign({}, this._attributes, asyncAttributes);
this.asyncAttributesPending = false;
return asyncAttributes;
},
err => {
const attributes = resource.attributes ?? {};
this._rawAttributes = Object.entries(attributes).map(([k, v]) => {
if (isPromiseLike(v)) {
// side-effect
this._asyncAttributesPending = true;
}

return [k, v];
});
}

public get asyncAttributesPending() {
dyladan marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
public get asyncAttributesPending() {
public get asyncAttributesPending(): boolean {

return this._asyncAttributesPending;
}

public async waitForAsyncAttributes(): Promise<void> {
if (!this.asyncAttributesPending) {
return;
}

for (let i = 0; i < this._rawAttributes.length; i++) {
const [k,v] = this._rawAttributes[i];
try {
this._rawAttributes[i] = [k, await v];
} catch (err) {
diag.debug("a resource's async attributes promise rejected: %s", err);
this.asyncAttributesPending = false;
return {};
this._rawAttributes[i] = [k, undefined];
}
);
}

this._asyncAttributesPending = false;
}

get attributes(): Attributes {
public get attributes(): Attributes {
if (this.asyncAttributesPending) {
diag.error(
'Accessing resource attributes before async attributes settled'
);
}

return this._attributes ?? {};
}
if (this._memoizedAttributes) {
return this._memoizedAttributes;
}

/**
* 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
* has finished.
*/
async waitForAsyncAttributes?(): Promise<void> {
if (this.asyncAttributesPending) {
await this._asyncAttributesPromise;
const attrs: Attributes = {};
for (const [k, v] of this._rawAttributes) {
if (isPromiseLike(v)) {
diag.debug(`Unsettled resource attribute ${k} skipped`);
continue;
}
if (v != null) {
attrs[k] ??= v;
}
}
}

/**
* Returns a new, merged {@link Resource} by merging the current Resource
* with the other Resource. In case of a collision, other Resource takes
* precedence.
*
* @param other the Resource that will be merged with this.
* @returns the newly merged Resource.
*/
merge(other: IResource | null): IResource {
if (!other) return this;

// Attributes from other resource overwrite attributes from this resource.
const mergedSyncAttributes = {
...this._syncAttributes,
//Support for old resource implementation where _syncAttributes is not defined
...((other as Resource)._syncAttributes ?? other.attributes),
};

if (
!this._asyncAttributesPromise &&
!(other as Resource)._asyncAttributesPromise
) {
return new Resource(mergedSyncAttributes);
// only memoize output if all attributes are settled
if (!this._asyncAttributesPending) {
this._memoizedAttributes = attrs;
}

const mergedAttributesPromise = Promise.all([
this._asyncAttributesPromise,
(other as Resource)._asyncAttributesPromise,
]).then(([thisAsyncAttributes, otherAsyncAttributes]) => {
return {
...this._syncAttributes,
...thisAsyncAttributes,
//Support for old resource implementation where _syncAttributes is not defined
...((other as Resource)._syncAttributes ?? other.attributes),
...otherAsyncAttributes,
};
});
return attrs;
}

public merge(resource: Resource | null) {
dyladan marked this conversation as resolved.
Show resolved Hide resolved
dyladan marked this conversation as resolved.
Show resolved Hide resolved
if (resource == null) return this;

// incoming attributes have a lower priority
const attributes: DetectedResourceAttributes = {};
for (const [k, v] of [...this._rawAttributes, ...resource._rawAttributes]) {
if (v != null) {
attributes[k] ??= v;
}
}

return new Resource(mergedSyncAttributes, mergedAttributesPromise);
return new Resource({ attributes });
}
}
4 changes: 2 additions & 2 deletions packages/opentelemetry-resources/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
* limitations under the License.
*/

import type { Detector, DetectorSync } from './types';
import type { ResourceDetector } from './types';

/**
* ResourceDetectionConfig provides an interface for configuring resource auto-detection.
*/
export interface ResourceDetectionConfig {
detectors?: Array<Detector | DetectorSync>;
detectors?: Array<ResourceDetector>;
}
Loading
Loading