diff --git a/packages/opentelemetry-resources/src/IResource.ts b/packages/opentelemetry-resources/src/IResource.ts index b53a0e02442..d6dc4489f90 100644 --- a/packages/opentelemetry-resources/src/IResource.ts +++ b/packages/opentelemetry-resources/src/IResource.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +// import { EntityRef } from './entity'; import { ResourceAttributes } from './types'; /** @@ -42,6 +43,8 @@ export interface IResource { */ waitForAsyncAttributes?(): Promise; + // readonly entityRefs: EntityRef[]; + /** * Returns a new, merged {@link Resource} by merging the current Resource * with the other Resource. In case of a collision, other Resource takes diff --git a/packages/opentelemetry-resources/src/ResWithEntity.ts b/packages/opentelemetry-resources/src/ResWithEntity.ts new file mode 100644 index 00000000000..999bec2fbda --- /dev/null +++ b/packages/opentelemetry-resources/src/ResWithEntity.ts @@ -0,0 +1,171 @@ +/* + * 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 { Attributes, AttributeValue, diag } from '@opentelemetry/api'; +import { Entity, EntityRef, mergeEntities } from './entity'; +import { DetectedResourceAttributes, ResourceAttributes } from './types'; +import { identity, isPromiseLike } from './utils'; + +export class ResWithEntity { + private _rawAttributes: [string, AttributeValue | Promise][]; + private _asyncAttributesPending = false; + private _entities: Entity[]; + private _entityRefs: EntityRef[]; + private _asyncAttributesPromise?: Promise; + + private _memoizedAttributes?: Attributes; + + 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: DetectedResourceAttributes, + /** @deprecated please put all sync and async atributes in the first parameter */ + asyncAttributesPromise?: Promise, + entities: Entity[] = [] + ) { + this._entities = entities; + this._asyncAttributesPromise = asyncAttributesPromise; + this._rawAttributes = Object.entries(attributes).map(([k, v]) => { + if (isPromiseLike(v)) { + // side-effect + this._asyncAttributesPending = true; + + return [ + k, + v.then(identity, err => { + diag.debug( + "a resource's async attributes promise rejected: %s", + err + ); + return [k, undefined]; + }), + ]; + } + + return [k, v]; + }); + + this._entityRefs = entities.map(entity => { + if (entity.asyncAttributesPending) { + this._asyncAttributesPending = true; + } + + return { + type: entity.type, + identifyingAttributeKeys: Object.keys(entity.identifier), + descriptiveAttributeKeys: entity.attributes + ? Object.keys(entity.attributes) + : [], + }; + }); + } + + public get asyncAttributesPending() { + return ( + this._asyncAttributesPromise != null || + this._asyncAttributesPending || + this._entities.reduce( + (p, c) => p || c.asyncAttributesPending, + false + ) + ); + } + + public async waitForAsyncAttributes(): Promise { + if (!this.asyncAttributesPending) { + return; + } + + if (this._asyncAttributesPromise) { + for (const [k, v] of await Promise.all( + Object.entries(await this._asyncAttributesPromise) + )) { + if (v != null) { + this._rawAttributes.push([k, v]); + } + } + } + + this._rawAttributes = await Promise.all( + this._rawAttributes.map>( + async ([k, v]) => [k, await v] + ) + ); + for (const e of this._entities) { + await e.waitForAsyncAttributes(); + } + this._asyncAttributesPending = false; + } + + public get attributes(): Attributes { + if (this.asyncAttributesPending) { + diag.error( + 'Accessing resource attributes before async attributes settled' + ); + } + + if (this._memoizedAttributes) { + return this._memoizedAttributes; + } + + const attrs: Attributes = {}; + + for (const e of this._entities) { + for (const [k, v] of Object.entries(e.identifier)) { + attrs[k] = v; + } + if (e.attributes) { + for (const [k, v] of Object.entries(e.attributes)) { + attrs[k] ??= v; + } + } + } + + for (const [k, v] of this._rawAttributes) { + if (isPromiseLike(v)) { + diag.debug(`Unsettled resource attribute ${k} skipped`); + continue; + } + attrs[k] ??= v; + } + + // only memoize output if all attributes are settled + if (!this._asyncAttributesPending) { + this._memoizedAttributes = attrs; + } + + return attrs; + } + + public get entityRefs() { + return this._entityRefs; + } + + public merge(resource: ResWithEntity) { + // incoming attributes and entities have a lower priority + const rawAttributes: DetectedResourceAttributes = {}; + for (const [k, v] of [...this._rawAttributes, ...resource._rawAttributes]) { + rawAttributes[k] ??= v; + } + + const entities = mergeEntities(...this._entities, ...resource._entities); + + return new ResWithEntity(rawAttributes, undefined, entities); + } +} diff --git a/packages/opentelemetry-resources/src/Resource.ts b/packages/opentelemetry-resources/src/Resource.ts index d44dbacc4df..2a8c8b1399b 100644 --- a/packages/opentelemetry-resources/src/Resource.ts +++ b/packages/opentelemetry-resources/src/Resource.ts @@ -35,6 +35,7 @@ export class Resource implements IResource { private _syncAttributes?: ResourceAttributes; private _asyncAttributesPromise?: Promise; private _attributes?: ResourceAttributes; + // public entityRefs: EntityRef[] = []; /** * Check if async attributes have resolved. This is useful to avoid awaiting @@ -76,6 +77,7 @@ export class Resource implements IResource { asyncAttributesPromise?: Promise ) { this._attributes = attributes; + this.asyncAttributesPending = asyncAttributesPromise != null; this._syncAttributes = this._attributes ?? {}; this._asyncAttributesPromise = asyncAttributesPromise?.then( diff --git a/packages/opentelemetry-resources/src/entity-detector.ts b/packages/opentelemetry-resources/src/entity-detector.ts new file mode 100644 index 00000000000..b34b8caf972 --- /dev/null +++ b/packages/opentelemetry-resources/src/entity-detector.ts @@ -0,0 +1,30 @@ +/* + * 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 { Entity } from './entity'; + +/* + * The Entity detector in the SDK is responsible for detecting possible entities that could identify + * the SDK (called "associated entities"). For Example, if the SDK is running in a kubernetes pod, + * it may provide an Entity for that pod. + */ +export interface EntityDetector { + /** + * Discovers {@link Entity} and their current attributes. + * + * @return a list of discovered entities. + */ + detectEntities(): Entity[]; +} diff --git a/packages/opentelemetry-resources/src/entity.ts b/packages/opentelemetry-resources/src/entity.ts new file mode 100644 index 00000000000..15ef0d44ef7 --- /dev/null +++ b/packages/opentelemetry-resources/src/entity.ts @@ -0,0 +1,187 @@ +/* + * 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 { Attributes, AttributeValue, diag } from '@opentelemetry/api'; +import { IDetectedEntity } from './types'; +import { identity, isPromiseLike } from './utils'; + +export interface EntityRef { + type: string; + identifyingAttributeKeys: string[]; + descriptiveAttributeKeys: string[]; +} + +export class Entity { + private _type: string; + private _schema_url?: string; + private _identifier: Attributes; + private _asyncAttributesPending = false; + private _rawAttributes: [string, AttributeValue | Promise][]; + private _memoizedAttributes?: Attributes; + + constructor(entity: IDetectedEntity) { + this._type = entity.type; + this._schema_url = entity.schema_url; + this._identifier = entity.identifier; + + if (entity.attributes) { + this._rawAttributes = Object.entries(entity.attributes).map(([k, v]) => { + if (isPromiseLike(v)) { + // side-effect + this._asyncAttributesPending = true; + + return [ + k, + v.then(identity, err => { + diag.debug( + "a resource's async attributes promise rejected: %s", + err + ); + return [k, undefined]; + }), + ]; + } + + return [k, v]; + }); + } else { + this._rawAttributes = []; + } + } + + get type() { + return this._type; + } + + get schema_url() { + return this._schema_url; + } + + get identifier() { + return this._identifier; + } + + get asyncAttributesPending() { + return this._asyncAttributesPending; + } + + public async waitForAsyncAttributes(): Promise { + if (!this._asyncAttributesPending) { + return; + } + this._rawAttributes = await Promise.all( + this._rawAttributes.map>( + async ([k, v]) => [k, await v] + ) + ); + this._asyncAttributesPending = false; + } + + public get attributes(): Attributes { + if (this.asyncAttributesPending) { + diag.error( + 'Accessing resource attributes before async attributes settled' + ); + } + + if (this._memoizedAttributes) { + return this._memoizedAttributes; + } + + const attrs: Attributes = {}; + for (const [k, v] of this._rawAttributes) { + if (isPromiseLike(v)) { + diag.debug(`Unsettled resource attribute ${k} skipped`); + continue; + } + attrs[k] ??= v; + } + + // only memoize output if all attributes are settled + if (!this._asyncAttributesPending) { + this._memoizedAttributes = attrs; + } + + return attrs; + } +} + +/** + * Merge detected entities. Entities are assumed to be in priority order (highest first). + */ +export function mergeEntities(...entities: Entity[]): Entity[] { + // Construct a set of detected entities, E + const entityMap: Record = {}; + + // For each entity detector D, detect entities (already done) + + // For each entity detected, d' + for (const entity of entities) { + // If an entity e' exists in E with same entity type as d', do one of the following: + const prevEntity = entityMap[entity.type]; + if (prevEntity != null) { + // If the entity identity is different: drop the new entity d'. + if (!attrsEqual(prevEntity.identifier, entity.identifier)) { + continue; + } + + // If the entity identity is the same, but schema_url is different: drop the new entity d' Note: We could offer configuration in this case + if (entity.schema_url !== prevEntity.schema_url) { + continue; + } + + // If the entity identiy and schema_url are the same, merge the descriptive attributes of d' into e': + // For each descriptive attribute da' in d' + for (const [k, v] of Object.entries(entity.attributes)) { + // If da'.key does not exist in e', then add da' to ei + if (prevEntity.attributes[k] != null) { + prevEntity.attributes[k] = v; + } + + // otherwise, ignore + } + } + } + + return [...Object.values(entityMap)]; +} + +function attrsEqual(obj1: Attributes, obj2: Attributes) { + if (Object.keys(obj1).length !== Object.keys(obj2).length) { + return false; + } + + for (const [k, v] of Object.entries(obj1)) { + const v2 = obj2[k]; + + if (Array.isArray(v)) { + if (!Array.isArray(v2) || v.length !== v2.length) { + return false; + } + + // arrays can only contain primitives, so simple equality checks are sufficient + for (let i = 0; i < v.length; i++) { + if (v[i] !== v2[i]) { + return false; + } + } + } else if (v !== v2) { + return false; + } + } + + return true; +} diff --git a/packages/opentelemetry-resources/src/resource-provider.ts b/packages/opentelemetry-resources/src/resource-provider.ts new file mode 100644 index 00000000000..19cd1398ad3 --- /dev/null +++ b/packages/opentelemetry-resources/src/resource-provider.ts @@ -0,0 +1,38 @@ +/* + * 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 { Resource } from './Resource'; + +/** + * A Registry which provides the {@link Resource} to the SDK. + * + * Note: this does not do much initially, but eventually will be extended for resource mutation over time. + * */ +export class ResourceProvider { + private _resource: Resource; + + constructor(resource: Resource) { + this._resource = resource; + } + + /** + * Provides the currently discovered {@link Resource}. + * + * @return the Resource. + */ + getResource(): Resource { + return this._resource; + } +} diff --git a/packages/opentelemetry-resources/src/types.ts b/packages/opentelemetry-resources/src/types.ts index d20c09faa2f..5097e2a6306 100644 --- a/packages/opentelemetry-resources/src/types.ts +++ b/packages/opentelemetry-resources/src/types.ts @@ -15,8 +15,9 @@ */ import { ResourceDetectionConfig } from './config'; -import { SpanAttributes } from '@opentelemetry/api'; +import { AttributeValue, SpanAttributes } from '@opentelemetry/api'; import { IResource } from './IResource'; +import { Attributes } from '@opentelemetry/api'; /** * Interface for Resource attributes. @@ -35,7 +36,33 @@ export interface Detector { /** * Interface for a synchronous Resource Detector. In order to detect attributes asynchronously, a detector * can pass a Promise as the second parameter to the Resource constructor. + * + * @deprecated please use {@link ResourceEntityDetector} */ export interface DetectorSync { detect(config?: ResourceDetectionConfig): IResource; } + +export interface ResourceEntityDetector { + detect(): IDetectedResource; +} + +export type IDetectedResource = { + resourceAttributes: DetectedResourceAttributes; + entities: IDetectedEntity[]; +}; + +export type IDetectedEntity = { + type: string; + schema_url?: string; + identifier: Attributes; + attributes?: DetectedResourceAttributes; +}; + +/** + * Represents a set of detected synchronous and asynchronous resource attributes. + */ +export type DetectedResourceAttributes = Record< + string, + AttributeValue | Promise +>; diff --git a/packages/opentelemetry-resources/src/utils.ts b/packages/opentelemetry-resources/src/utils.ts index 73d81040b3f..827f24d52ba 100644 --- a/packages/opentelemetry-resources/src/utils.ts +++ b/packages/opentelemetry-resources/src/utils.ts @@ -14,8 +14,14 @@ * limitations under the License. */ -export const isPromiseLike = (val: any): val is PromiseLike => { +export const isPromiseLike = (val: unknown): val is PromiseLike => { return ( - val !== null && typeof val === 'object' && typeof val.then === 'function' + val !== null && + typeof val === 'object' && + typeof Object.getOwnPropertyDescriptor(val, 'then')?.value === 'function' ); }; + +export function identity(_: T): T { + return _; +} diff --git a/packages/opentelemetry-resources/test/regression/existing-detectors-1-9-1.test.ts b/packages/opentelemetry-resources/test/regression/existing-detectors-1-9-1.test.ts index 56c996744f2..5764837ecde 100644 --- a/packages/opentelemetry-resources/test/regression/existing-detectors-1-9-1.test.ts +++ b/packages/opentelemetry-resources/test/regression/existing-detectors-1-9-1.test.ts @@ -13,13 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Resource, Detector, ResourceDetectionConfig } from '../../src'; +import { + Resource, + Detector, + ResourceDetectionConfig, + IResource, +} from '../../src'; import * as assert from 'assert'; // DO NOT MODIFY THIS DETECTOR: Previous detectors used Resource as IResource did not yet exist. // If compilation fails at this point then the changes made are breaking. class RegressionTestResourceDetector_1_9_1 implements Detector { - async detect(_config?: ResourceDetectionConfig): Promise { + async detect(_config?: ResourceDetectionConfig): Promise { return Resource.empty(); } }