diff --git a/api/src/common/Entity.ts b/api/src/common/Entity.ts new file mode 100644 index 00000000000..e9e9573838e --- /dev/null +++ b/api/src/common/Entity.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 { Attributes } from './Attributes'; + +export interface BindableProvider { + forEntity(entity: Entity): T; +} + +export type Entity = { + type: string; + identifier: Attributes; + attributes: Attributes; + schemaUrl?: string; + asyncAttributesPending: boolean; + waitForAsyncAttributes(): Promise; +}; diff --git a/api/src/index.ts b/api/src/index.ts index acc9f4212a3..164b01d5d43 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -27,6 +27,7 @@ export { baggageEntryMetadataFromString } from './baggage/utils'; export type { Exception } from './common/Exception'; export type { HrTime, TimeInput } from './common/Time'; export type { Attributes, AttributeValue } from './common/Attributes'; +export type { Entity } from './common/Entity'; // Context APIs export { createContextKey, ROOT_CONTEXT } from './context/context'; diff --git a/api/src/metrics/MeterProvider.ts b/api/src/metrics/MeterProvider.ts index 1266cf91d34..a40458c2ffb 100644 --- a/api/src/metrics/MeterProvider.ts +++ b/api/src/metrics/MeterProvider.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { BindableProvider } from '../common/Entity'; import { Meter, MeterOptions } from './Meter'; /** @@ -21,7 +22,7 @@ import { Meter, MeterOptions } from './Meter'; * * @since 1.3.0 */ -export interface MeterProvider { +export interface MeterProvider extends BindableProvider { /** * Returns a Meter, creating one if one with the given name, version, and * schemaUrl pair is not already created. diff --git a/api/src/metrics/NoopMeterProvider.ts b/api/src/metrics/NoopMeterProvider.ts index 7b6550ebac8..2981d99cd6e 100644 --- a/api/src/metrics/NoopMeterProvider.ts +++ b/api/src/metrics/NoopMeterProvider.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { Entity } from '../common/Entity'; import { Meter, MeterOptions } from './Meter'; import { MeterProvider } from './MeterProvider'; import { NOOP_METER } from './NoopMeter'; @@ -23,6 +24,10 @@ import { NOOP_METER } from './NoopMeter'; * for all calls to `getMeter` */ export class NoopMeterProvider implements MeterProvider { + forEntity(_entity: Entity): this { + return this; + } + getMeter(_name: string, _version?: string, _options?: MeterOptions): Meter { return NOOP_METER; } diff --git a/api/src/trace/NoopTracerProvider.ts b/api/src/trace/NoopTracerProvider.ts index f4b31b9ac5d..950b274d0cc 100644 --- a/api/src/trace/NoopTracerProvider.ts +++ b/api/src/trace/NoopTracerProvider.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { Entity } from '../common/Entity'; import { NoopTracer } from './NoopTracer'; import { Tracer } from './tracer'; import { TracerOptions } from './tracer_options'; @@ -33,4 +34,8 @@ export class NoopTracerProvider implements TracerProvider { ): Tracer { return new NoopTracer(); } + + forEntity(_entity: Entity): TracerProvider { + return this; + } } diff --git a/api/src/trace/ProxyTracerProvider.ts b/api/src/trace/ProxyTracerProvider.ts index dfd38687ac6..c0eac32bd40 100644 --- a/api/src/trace/ProxyTracerProvider.ts +++ b/api/src/trace/ProxyTracerProvider.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { Entity } from '../common/Entity'; import { Tracer } from './tracer'; import { TracerProvider } from './tracer_provider'; import { ProxyTracer } from './ProxyTracer'; @@ -35,6 +36,7 @@ const NOOP_TRACER_PROVIDER = new NoopTracerProvider(); */ export class ProxyTracerProvider implements TracerProvider { private _delegate?: TracerProvider; + private _entity?: Entity; /** * Get a {@link ProxyTracer} @@ -47,7 +49,11 @@ export class ProxyTracerProvider implements TracerProvider { } getDelegate(): TracerProvider { - return this._delegate ?? NOOP_TRACER_PROVIDER; + const delegate = this._delegate ?? NOOP_TRACER_PROVIDER; + if (this._entity && delegate !== NOOP_TRACER_PROVIDER) { + return delegate.forEntity(this._entity); + } + return delegate; } /** @@ -64,4 +70,14 @@ export class ProxyTracerProvider implements TracerProvider { ): Tracer | undefined { return this._delegate?.getTracer(name, version, options); } + + forEntity(entity: Entity): TracerProvider { + if (this._delegate) { + return this._delegate.forEntity(entity); + } + // Return a new proxy that will apply the entity when a delegate is set + const boundProxy = new ProxyTracerProvider(); + boundProxy._entity = entity; + return boundProxy; + } } diff --git a/api/src/trace/tracer_provider.ts b/api/src/trace/tracer_provider.ts index d72551ed91e..8f6517a1aa7 100644 --- a/api/src/trace/tracer_provider.ts +++ b/api/src/trace/tracer_provider.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { Entity } from '../common/Entity'; import { Tracer } from './tracer'; import { TracerOptions } from './tracer_options'; @@ -36,4 +37,13 @@ export interface TracerProvider { * @returns Tracer A Tracer with the given name and version */ getTracer(name: string, version?: string, options?: TracerOptions): Tracer; + + /** + * Creates a new TracerProvider with the same configuration but with the + * provided entity merged into the resource. + * + * @param entity The entity to merge into the resource + * @returns A new TracerProvider with the merged entity + */ + forEntity(entity: Entity): TracerProvider; } diff --git a/api/test/common/proxy-implementations/proxy-tracer.test.ts b/api/test/common/proxy-implementations/proxy-tracer.test.ts index f4f9f9c1545..ac437f174db 100644 --- a/api/test/common/proxy-implementations/proxy-tracer.test.ts +++ b/api/test/common/proxy-implementations/proxy-tracer.test.ts @@ -72,6 +72,7 @@ describe('ProxyTracer', function () { getTracerStub = sandbox.stub().returns(new NoopTracer()); delegate = { getTracer: getTracerStub, + forEntity: sandbox.stub().returnsThis(), }; provider.setDelegate(delegate); }); @@ -127,6 +128,7 @@ describe('ProxyTracer', function () { getTracer() { return delegateTracer; }, + forEntity: sandbox.stub().returnsThis(), }; provider.setDelegate(delegate); }); diff --git a/e2e-tests/test.mjs b/e2e-tests/test.mjs index 20476c580c6..1ee598cae99 100644 --- a/e2e-tests/test.mjs +++ b/e2e-tests/test.mjs @@ -28,6 +28,7 @@ import { metrics, } from '@opentelemetry/api'; import { logs } from '@opentelemetry/api-logs'; +import { resourceFromDetectedResource } from '@opentelemetry/resources'; // Enable diagnostic logging (optional) diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO); @@ -56,11 +57,24 @@ const logExporter = new OTLPLogExporter({ }); const logRecordProcessors = [new SimpleLogRecordProcessor(logExporter)]; +const resource = resourceFromDetectedResource({ + entities: [ + { + type: 'service', + identifier: { 'service.name': 'example-service' }, + attributes: { 'service.version': '1.0.0' }, + schemaUrl: 'https://opentelemetry.io/schemas/1.0.0/service', + }, + ], +}); + // Set up OpenTelemetry SDK const sdk = new NodeSDK({ spanProcessors, metricReader, logRecordProcessors, + resource, + autoDetectResources: false, // Disable automatic resource detection }); async function main() { diff --git a/e2e-tests/verify.mjs b/e2e-tests/verify.mjs index 16bafb7d913..9b2f42c5d95 100755 --- a/e2e-tests/verify.mjs +++ b/e2e-tests/verify.mjs @@ -29,16 +29,19 @@ for (const line of lines) { if (parsed.resourceSpans) { console.log('found span'); verifySpan(parsed.resourceSpans[0].scopeSpans[0].spans[0]); + verifyResource(parsed.resourceSpans[0].resource); verifiedSpan = true; } if (parsed.resourceMetrics) { console.log('found metric'); verifyMetric(parsed.resourceMetrics[0].scopeMetrics[0].metrics[0]); + verifyResource(parsed.resourceMetrics[0].resource); verifiedMetric = true; } if (parsed.resourceLogs) { console.log('found log'); verifyLog(parsed.resourceLogs[0].scopeLogs[0].logRecords[0]); + verifyResource(parsed.resourceLogs[0].resource); verifiedLog = true; } } @@ -56,6 +59,29 @@ if (!verifiedLog) { process.exit(1); } +function verifyResource(resource) { + if (!resource || !resource.attributes) { + console.error('Resource attributes are missing'); + process.exit(1); + } + const name = resource.attributes.find(attr => attr.key === 'service.name'); + if (!name || name.value.stringValue !== 'example-service') { + console.error( + `Expected service.name to be 'example-service', but got '${name?.value.stringValue}'` + ); + process.exit(1); + } + const version = resource.attributes.find( + attr => attr.key === 'service.version' + ); + if (!version || version.value.stringValue !== '1.0.0') { + console.error( + `Expected service.version to be '1.0.0', but got '${version?.value.stringValue}'` + ); + process.exit(1); + } +} + function verifySpan(span) { const expectedName = 'example-span'; if (span.name !== expectedName) { diff --git a/experimental/packages/api-logs/src/NoopLoggerProvider.ts b/experimental/packages/api-logs/src/NoopLoggerProvider.ts index aea947ed809..39465f44d32 100644 --- a/experimental/packages/api-logs/src/NoopLoggerProvider.ts +++ b/experimental/packages/api-logs/src/NoopLoggerProvider.ts @@ -14,10 +14,11 @@ * limitations under the License. */ -import { LoggerProvider } from './types/LoggerProvider'; +import { Entity } from '@opentelemetry/api'; +import { NoopLogger } from './NoopLogger'; import { Logger } from './types/Logger'; import { LoggerOptions } from './types/LoggerOptions'; -import { NoopLogger } from './NoopLogger'; +import { LoggerProvider } from './types/LoggerProvider'; export class NoopLoggerProvider implements LoggerProvider { getLogger( @@ -27,6 +28,10 @@ export class NoopLoggerProvider implements LoggerProvider { ): Logger { return new NoopLogger(); } + + forEntity(_entity: Entity): LoggerProvider { + return this; + } } export const NOOP_LOGGER_PROVIDER = new NoopLoggerProvider(); diff --git a/experimental/packages/api-logs/src/ProxyLoggerProvider.ts b/experimental/packages/api-logs/src/ProxyLoggerProvider.ts index 5c1b9ef4e7f..45064a2c762 100644 --- a/experimental/packages/api-logs/src/ProxyLoggerProvider.ts +++ b/experimental/packages/api-logs/src/ProxyLoggerProvider.ts @@ -19,9 +19,11 @@ import { Logger } from './types/Logger'; import { LoggerOptions } from './types/LoggerOptions'; import { NOOP_LOGGER_PROVIDER } from './NoopLoggerProvider'; import { ProxyLogger } from './ProxyLogger'; +import { Entity } from '@opentelemetry/api'; export class ProxyLoggerProvider implements LoggerProvider { private _delegate?: LoggerProvider; + private _boundEntity?: Entity; getLogger( name: string, @@ -34,6 +36,16 @@ export class ProxyLoggerProvider implements LoggerProvider { ); } + forEntity(entity: Entity): LoggerProvider { + const boundProvider = this._delegate?.forEntity(entity); + if (boundProvider) { + return boundProvider; + } + const proxyLoggerProvider = new ProxyLoggerProvider(); + proxyLoggerProvider._boundEntity = entity; + return proxyLoggerProvider; + } + /** * Get the delegate logger provider. * Used by tests only. @@ -59,6 +71,10 @@ export class ProxyLoggerProvider implements LoggerProvider { version?: string | undefined, options?: LoggerOptions | undefined ): Logger | undefined { + if (this._boundEntity) { + const boundProvider = this._delegate?.forEntity(this._boundEntity); + return boundProvider?.getLogger(name, version, options); + } return this._delegate?.getLogger(name, version, options); } } diff --git a/experimental/packages/api-logs/src/types/LoggerProvider.ts b/experimental/packages/api-logs/src/types/LoggerProvider.ts index 10ed6debf2f..6bb0a477c24 100644 --- a/experimental/packages/api-logs/src/types/LoggerProvider.ts +++ b/experimental/packages/api-logs/src/types/LoggerProvider.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { Entity } from '@opentelemetry/api'; import { Logger } from './Logger'; import { LoggerOptions } from './LoggerOptions'; @@ -31,4 +32,6 @@ export interface LoggerProvider { * @returns Logger A Logger with the given name and version */ getLogger(name: string, version?: string, options?: LoggerOptions): Logger; + + forEntity(entity: Entity): LoggerProvider; } diff --git a/experimental/packages/api-logs/test/proxy-implementations/proxy-logger.test.ts b/experimental/packages/api-logs/test/proxy-implementations/proxy-logger.test.ts index 43ce5691265..c64661bfba8 100644 --- a/experimental/packages/api-logs/test/proxy-implementations/proxy-logger.test.ts +++ b/experimental/packages/api-logs/test/proxy-implementations/proxy-logger.test.ts @@ -48,6 +48,9 @@ describe('ProxyLogger', () => { getLoggerStub = sandbox.stub().returns(new NoopLogger()); delegate = { getLogger: getLoggerStub, + forEntity(entity) { + return this; + }, }; provider._setDelegate(delegate); }); @@ -100,6 +103,9 @@ describe('ProxyLogger', () => { getLogger() { return delegateLogger; }, + forEntity() { + return this; + }, }; provider._setDelegate(delegateProvider); }); diff --git a/experimental/packages/opentelemetry-instrumentation-http/test/functionals/http-disable.test.ts b/experimental/packages/opentelemetry-instrumentation-http/test/functionals/http-disable.test.ts index c445acb4a74..6b75e4a159f 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/test/functionals/http-disable.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-http/test/functionals/http-disable.test.ts @@ -48,6 +48,7 @@ describe('HttpInstrumentation', () => { .returns(trace.wrapSpanContext(INVALID_SPAN_CONTEXT)); return { startSpan: startSpanStub } as any; }, + forEntity: () => provider, }; nock.cleanAll(); nock.enableNetConnect(); diff --git a/experimental/packages/opentelemetry-instrumentation-http/test/functionals/https-disable.test.ts b/experimental/packages/opentelemetry-instrumentation-http/test/functionals/https-disable.test.ts index b04259366b1..a35c2581e93 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/test/functionals/https-disable.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-http/test/functionals/https-disable.test.ts @@ -50,6 +50,7 @@ describe('HttpsInstrumentation', () => { .returns(trace.wrapSpanContext(INVALID_SPAN_CONTEXT)); return { startSpan: startSpanStub } as any; }, + forEntity: () => provider, }; nock.cleanAll(); nock.enableNetConnect(); diff --git a/experimental/packages/opentelemetry-instrumentation/test/common/autoLoader.test.ts b/experimental/packages/opentelemetry-instrumentation/test/common/autoLoader.test.ts index d110564dbe5..bfc4a19fa6d 100644 --- a/experimental/packages/opentelemetry-instrumentation/test/common/autoLoader.test.ts +++ b/experimental/packages/opentelemetry-instrumentation/test/common/autoLoader.test.ts @@ -20,6 +20,7 @@ import { Meter, MeterOptions, MeterProvider, + Entity, } from '@opentelemetry/api'; import * as assert from 'assert'; import * as sinon from 'sinon'; @@ -30,15 +31,24 @@ class DummyTracerProvider implements TracerProvider { getTracer(name: string, version?: string): Tracer { throw new Error('not implemented'); } + forEntity(entity: Entity): TracerProvider { + throw new Error('Method not implemented.'); + } } class DummyMeterProvider implements MeterProvider { + forEntity(entity: Entity): MeterProvider { + throw new Error('Method not implemented.'); + } getMeter(name: string, version?: string, options?: MeterOptions): Meter { throw new Error('not implemented'); } } class DummyLoggerProvider implements LoggerProvider { + forEntity(entity: Entity): LoggerProvider { + throw new Error('Method not implemented.'); + } getLogger(name: string, version?: string, options?: LoggerOptions): Logger { throw new Error('not implemented'); } diff --git a/experimental/packages/otlp-transformer/src/common/internal-types.ts b/experimental/packages/otlp-transformer/src/common/internal-types.ts index 3f707142b99..dd354b2d366 100644 --- a/experimental/packages/otlp-transformer/src/common/internal-types.ts +++ b/experimental/packages/otlp-transformer/src/common/internal-types.ts @@ -24,6 +24,16 @@ export interface Resource { /** Resource schemaUrl */ schemaUrl?: string; + + /** Resource entity references */ + entityRefs?: EntityRef[]; +} + +export interface EntityRef { + schemaUrl?: string; + type: string; + idKeys: string[]; + descriptionKeys: string[]; } /** Properties of an InstrumentationScope. */ diff --git a/experimental/packages/otlp-transformer/src/common/internal.ts b/experimental/packages/otlp-transformer/src/common/internal.ts index e7ea8c6a91c..77a52ba4b9c 100644 --- a/experimental/packages/otlp-transformer/src/common/internal.ts +++ b/experimental/packages/otlp-transformer/src/common/internal.ts @@ -14,6 +14,7 @@ * limitations under the License. */ import type { + EntityRef, IAnyValue, IInstrumentationScope, IKeyValue, @@ -21,7 +22,10 @@ import type { } from './internal-types'; import { Attributes } from '@opentelemetry/api'; import { InstrumentationScope } from '@opentelemetry/core'; -import { Resource as ISdkResource } from '@opentelemetry/resources'; +import { + Resource as ISdkResource, + Entity as ISdkEntity, +} from '@opentelemetry/resources'; import type { Encoder } from './utils'; export function createResource( @@ -31,6 +35,7 @@ export function createResource( const result: Resource = { attributes: toAttributes(resource.attributes, encoder), droppedAttributesCount: 0, + entityRefs: toEntityRefs(resource.entities), }; const schemaUrl = resource.schemaUrl; @@ -39,6 +44,15 @@ export function createResource( return result; } +export function toEntityRefs(entityRefs: ISdkEntity[]): EntityRef[] { + return entityRefs.map(ref => ({ + schemaUrl: ref.schemaUrl, + type: ref.type, + idKeys: Object.keys(ref.identifier), + descriptionKeys: Object.keys(ref.attributes), + })); +} + export function createInstrumentationScope( scope: InstrumentationScope ): IInstrumentationScope { diff --git a/experimental/packages/otlp-transformer/test/logs.test.ts b/experimental/packages/otlp-transformer/test/logs.test.ts index db7ea3aa190..abecfa44ccc 100644 --- a/experimental/packages/otlp-transformer/test/logs.test.ts +++ b/experimental/packages/otlp-transformer/test/logs.test.ts @@ -15,7 +15,11 @@ */ import { HrTime, TraceFlags } from '@opentelemetry/api'; import { InstrumentationScope } from '@opentelemetry/core'; -import { Resource, resourceFromAttributes } from '@opentelemetry/resources'; +import { + Resource, + resourceFromAttributes, + resourceFromDetectedResource, +} from '@opentelemetry/resources'; import * as assert from 'assert'; import { ReadableLogRecord } from '@opentelemetry/sdk-logs'; import { SeverityNumber } from '@opentelemetry/api-logs'; @@ -52,6 +56,7 @@ function createExpectedLogJson(encoder: Encoder): IExportLogsServiceRequest { }, ], droppedAttributesCount: 0, + entityRefs: [], }, schemaUrl: undefined, scopeLogs: [ @@ -109,11 +114,23 @@ function createExpectedLogProtobuf(): IExportLogsServiceRequest { resource: { attributes: [ { - key: 'resource-attribute', - value: { stringValue: 'some attribute value' }, + key: 'id_key_1', + value: { stringValue: 'id_value_1' }, + }, + { + key: 'attr_key_1', + value: { stringValue: 'attr_value_1' }, }, ], droppedAttributesCount: 0, + entityRefs: [ + { + descriptionKeys: ['attr_key_1'], + idKeys: ['id_key_1'], + schemaUrl: 'http://url.to.schema', + type: 'entity_type_1', + }, + ], }, scopeLogs: [ { @@ -175,6 +192,7 @@ const DEFAULT_LOG_FRAGMENT: Omit< describe('Logs', () => { let resource_1: Resource; let resource_2: Resource; + let resource_3: Resource; let scope_1: InstrumentationScope; let scope_2: InstrumentationScope; @@ -193,6 +211,8 @@ describe('Logs', () => { let log_1_2_1: ReadableLogRecord; // using `resource_2`, `scope_1`, `log_fragment_1` let log_2_1_1: ReadableLogRecord; + // using `resource_3`, `scope_1`, `log_fragment_1` + let log_3_1_1: ReadableLogRecord; function createReadableLogRecord( resource: Resource, @@ -213,6 +233,16 @@ describe('Logs', () => { resource_2 = resourceFromAttributes({ 'resource-attribute': 'another attribute value', }); + resource_3 = resourceFromDetectedResource({ + entities: [ + { + type: 'entity_type_1', + identifier: { id_key_1: 'id_value_1' }, + attributes: { attr_key_1: 'attr_value_1' }, + schemaUrl: 'http://url.to.schema', + }, + ], + }); scope_1 = { name: 'scope_name_1', version: '0.1.0', @@ -236,6 +266,7 @@ describe('Logs', () => { 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); + log_3_1_1 = createReadableLogRecord(resource_3, scope_1, log_fragment_1); }); describe('createExportLogsServiceRequest', () => { @@ -346,7 +377,7 @@ describe('Logs', () => { describe('ProtobufLogsSerializer', function () { it('serializes an export request', () => { - const serialized = ProtobufLogsSerializer.serializeRequest([log_1_1_1]); + const serialized = ProtobufLogsSerializer.serializeRequest([log_3_1_1]); assert.ok(serialized, 'serialized response is undefined'); const decoded = root.opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest.decode( diff --git a/experimental/packages/otlp-transformer/test/metrics.test.ts b/experimental/packages/otlp-transformer/test/metrics.test.ts index 80a9317f9e1..f6ef3ee34d4 100644 --- a/experimental/packages/otlp-transformer/test/metrics.test.ts +++ b/experimental/packages/otlp-transformer/test/metrics.test.ts @@ -14,7 +14,12 @@ * limitations under the License. */ import { ValueType } from '@opentelemetry/api'; -import { Resource, resourceFromAttributes } from '@opentelemetry/resources'; +import { hrTime, hrTimeToNanoseconds } from '@opentelemetry/core'; +import { + Resource, + resourceFromAttributes, + resourceFromDetectedResource, +} from '@opentelemetry/resources'; import { AggregationTemporality, DataPointType, @@ -22,17 +27,17 @@ import { ResourceMetrics, } from '@opentelemetry/sdk-metrics'; import * as assert from 'assert'; -import { createExportMetricsServiceRequest } from '../src/metrics/internal'; -import { EAggregationTemporality } from '../src/metrics/internal-types'; +import { TextDecoder, TextEncoder } from 'util'; import { PROTOBUF_ENCODER, encodeAsLongBits, encodeAsString, } from '../src/common/utils'; -import { hrTime, hrTimeToNanoseconds } from '@opentelemetry/core'; import * as root from '../src/generated/root'; -import { ProtobufMetricsSerializer } from '../src/metrics/protobuf'; +import { createExportMetricsServiceRequest } from '../src/metrics/internal'; +import { EAggregationTemporality } from '../src/metrics/internal-types'; import { JsonMetricsSerializer } from '../src/metrics/json'; +import { ProtobufMetricsSerializer } from '../src/metrics/protobuf'; const START_TIME = hrTime(); const END_TIME = hrTime(); @@ -47,6 +52,18 @@ const ATTRIBUTES = { describe('Metrics', () => { const expectedResource = { attributes: [ + { + key: 'resource-entity-id', + value: { + stringValue: 'resource entity value', + }, + }, + { + key: 'resource-entity-attribute', + value: { + stringValue: 'resource entity attribute value', + }, + }, { key: 'resource-attribute', value: { @@ -54,6 +71,14 @@ describe('Metrics', () => { }, }, ], + entityRefs: [ + { + descriptionKeys: ['resource-entity-attribute'], + idKeys: ['resource-entity-id'], + schemaUrl: 'http://url.to.resource.schema', + type: 'resource-entity-type', + }, + ], droppedAttributesCount: 0, }; @@ -311,8 +336,22 @@ describe('Metrics', () => { ): ResourceMetrics { const resource = customResource || - resourceFromAttributes({ - 'resource-attribute': 'resource attribute value', + resourceFromDetectedResource({ + attributes: { + 'resource-attribute': 'resource attribute value', + }, + entities: [ + { + identifier: { + 'resource-entity-id': 'resource entity value', + }, + type: 'resource-entity-type', + attributes: { + 'resource-entity-attribute': 'resource entity attribute value', + }, + schemaUrl: 'http://url.to.resource.schema', + }, + ], }); return { diff --git a/experimental/packages/otlp-transformer/test/trace.test.ts b/experimental/packages/otlp-transformer/test/trace.test.ts index 0895873cec9..51df871c3f3 100644 --- a/experimental/packages/otlp-transformer/test/trace.test.ts +++ b/experimental/packages/otlp-transformer/test/trace.test.ts @@ -13,21 +13,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import * as root from '../src/generated/root'; import { SpanKind, SpanStatusCode, TraceFlags } from '@opentelemetry/api'; import { TraceState } from '@opentelemetry/core'; -import { Resource, resourceFromAttributes } from '@opentelemetry/resources'; +import { + Resource, + resourceFromAttributes, + resourceFromDetectedResource, +} from '@opentelemetry/resources'; import { ReadableSpan } from '@opentelemetry/sdk-trace-base'; import * as assert from 'assert'; -import { toBase64 } from './utils'; +import { TextDecoder, TextEncoder } from 'util'; +import { hexToBinary } from '../src/common/hex-to-binary'; import { OtlpEncodingOptions } from '../src/common/internal-types'; -import { ESpanKind, EStatusCode } from '../src/trace/internal-types'; +import { JSON_ENCODER, PROTOBUF_ENCODER } from '../src/common/utils'; +import * as root from '../src/generated/root'; import { createExportTraceServiceRequest } from '../src/trace/internal'; -import { ProtobufTraceSerializer } from '../src/trace/protobuf'; +import { ESpanKind, EStatusCode, ISpan } from '../src/trace/internal-types'; import { JsonTraceSerializer } from '../src/trace/json'; -import { hexToBinary } from '../src/common/hex-to-binary'; -import { ISpan } from '../src/trace/internal-types'; -import { JSON_ENCODER, PROTOBUF_ENCODER } from '../src/common/utils'; +import { ProtobufTraceSerializer } from '../src/trace/protobuf'; +import { toBase64 } from './utils'; function createExpectedSpanJson(options: OtlpEncodingOptions) { const useHex = options.useHex ?? false; @@ -62,9 +66,31 @@ function createExpectedSpanJson(options: OtlpEncodingOptions) { { resource: { attributes: [ + { + key: 'resource-entity-id', + value: { + stringValue: 'resource entity value', + }, + }, + { + key: 'resource-entity-attribute', + value: { + stringValue: 'resource entity attribute value', + }, + }, { key: 'resource-attribute', - value: { stringValue: 'resource attribute value' }, + value: { + stringValue: 'resource attribute value', + }, + }, + ], + entityRefs: [ + { + descriptionKeys: ['resource-entity-attribute'], + idKeys: ['resource-entity-id'], + schemaUrl: 'http://url.to.resource.schema', + type: 'resource-entity-type', }, ], droppedAttributesCount: 0, @@ -158,9 +184,31 @@ function createExpectedSpanProtobuf() { { resource: { attributes: [ + { + key: 'resource-entity-id', + value: { + stringValue: 'resource entity value', + }, + }, + { + key: 'resource-entity-attribute', + value: { + stringValue: 'resource entity attribute value', + }, + }, { key: 'resource-attribute', - value: { stringValue: 'resource attribute value' }, + value: { + stringValue: 'resource attribute value', + }, + }, + ], + entityRefs: [ + { + descriptionKeys: ['resource-entity-attribute'], + idKeys: ['resource-entity-id'], + schemaUrl: 'http://url.to.resource.schema', + type: 'resource-entity-type', }, ], droppedAttributesCount: 0, @@ -297,8 +345,22 @@ describe('Trace', () => { } beforeEach(() => { - resource = resourceFromAttributes({ - 'resource-attribute': 'resource attribute value', + resource = resourceFromDetectedResource({ + attributes: { + 'resource-attribute': 'resource attribute value', + }, + entities: [ + { + identifier: { + 'resource-entity-id': 'resource entity value', + }, + type: 'resource-entity-type', + attributes: { + 'resource-entity-attribute': 'resource entity attribute value', + }, + schemaUrl: 'http://url.to.resource.schema', + }, + ], }); span = createSpanWithResource(resource); }); diff --git a/experimental/packages/sdk-logs/src/LoggerProvider.ts b/experimental/packages/sdk-logs/src/LoggerProvider.ts index 0da61e8d967..fd8957f1764 100644 --- a/experimental/packages/sdk-logs/src/LoggerProvider.ts +++ b/experimental/packages/sdk-logs/src/LoggerProvider.ts @@ -16,21 +16,23 @@ import { diag } from '@opentelemetry/api'; import type * as logsAPI from '@opentelemetry/api-logs'; import { NOOP_LOGGER } from '@opentelemetry/api-logs'; -import { defaultResource } from '@opentelemetry/resources'; import { BindOnceFuture } from '@opentelemetry/core'; +import { defaultResource } from '@opentelemetry/resources'; -import type { LoggerProviderConfig } from './types'; -import { Logger } from './Logger'; +import { Entity } from '@opentelemetry/api'; import { DEFAULT_LOGGER_CONFIGURATOR, LoggerProviderSharedState, } from './internal/LoggerProviderSharedState'; +import { Logger } from './Logger'; +import type { LoggerProviderConfig } from './types'; export const DEFAULT_LOGGER_NAME = 'unknown'; export class LoggerProvider implements logsAPI.LoggerProvider { private _shutdownOnce: BindOnceFuture; private readonly _sharedState: LoggerProviderSharedState; + private readonly _config: LoggerProviderConfig; constructor(config: LoggerProviderConfig = {}) { const mergedConfig = { @@ -45,6 +47,7 @@ export class LoggerProvider implements logsAPI.LoggerProvider { config.loggerConfigurator ?? DEFAULT_LOGGER_CONFIGURATOR, processors: config.processors ?? [], }; + this._config = config; this._sharedState = new LoggerProviderSharedState( mergedConfig.resource, mergedConfig.forceFlushTimeoutMillis, @@ -114,6 +117,21 @@ export class LoggerProvider implements logsAPI.LoggerProvider { return this._shutdownOnce.call(); } + /** + * Creates a new LoggerProvider with the same export pipeline but a new resource + * that includes the provided entity merged into it. + * + * @param entity - The entity to merge into the resource + * @returns A new LoggerProvider with the merged entity + */ + public forEntity(entity: Entity): LoggerProvider { + const newResource = this._sharedState.resource.addEntity(entity); + return new LoggerProvider({ + ...this._config, + resource: newResource, + }); + } + private _shutdown(): Promise { return this._sharedState.activeProcessor.shutdown(); } diff --git a/packages/opentelemetry-resources/src/Resource.ts b/packages/opentelemetry-resources/src/Resource.ts index c8e25477490..dcbca856d5e 100644 --- a/packages/opentelemetry-resources/src/Resource.ts +++ b/packages/opentelemetry-resources/src/Resource.ts @@ -16,6 +16,7 @@ import { Attributes } from '@opentelemetry/api'; import { RawResourceAttribute } from './types'; +import { Entity } from '@opentelemetry/api'; /** * An interface that represents a resource. A Resource describes the entity for which signals (metrics or trace) are @@ -46,6 +47,11 @@ export interface Resource { */ readonly schemaUrl?: string; + /** + * @returns a list of Entities associated with the resource + */ + readonly entities: Entity[]; + /** * 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 @@ -64,4 +70,6 @@ export interface Resource { merge(other: Resource | null): Resource; getRawAttributes(): RawResourceAttribute[]; + + addEntity(entity: Entity): Resource; } diff --git a/packages/opentelemetry-resources/src/detect-resources.ts b/packages/opentelemetry-resources/src/detect-resources.ts index c1bdba317d6..b7255935c2e 100644 --- a/packages/opentelemetry-resources/src/detect-resources.ts +++ b/packages/opentelemetry-resources/src/detect-resources.ts @@ -16,7 +16,7 @@ import { diag } from '@opentelemetry/api'; import { Resource } from './Resource'; -import { emptyResource, resourceFromDetectedResource } from './ResourceImpl'; +import { emptyResource, resourceFromDetectedResource } from './resource-impl'; import { ResourceDetectionConfig } from './config'; /** diff --git a/packages/opentelemetry-resources/src/entity-impl.ts b/packages/opentelemetry-resources/src/entity-impl.ts new file mode 100644 index 00000000000..79e595b7029 --- /dev/null +++ b/packages/opentelemetry-resources/src/entity-impl.ts @@ -0,0 +1,182 @@ +/* + * 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 { DetectedEntity, DetectedResourceAttributeValue } from './types'; +import { identity, isPromiseLike } from './utils'; +import { Entity } from '@opentelemetry/api'; + +export class EntityImpl implements Entity { + private _type: string; + private _schemaUrl?: string; + private _identifier: Attributes; + private _asyncAttributesPending = false; + private _rawAttributes: [string, DetectedResourceAttributeValue][]; + private _memoizedAttributes?: Attributes; + + constructor(entity: DetectedEntity) { + this._type = entity.type; + this._schemaUrl = entity.schemaUrl; + 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 schemaUrl() { + return this._schemaUrl; + } + + 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 schemaUrl is different: drop the new entity d' Note: We could offer configuration in this case + if (entity.schemaUrl !== prevEntity.schemaUrl) { + continue; + } + + // If the entity identiy and schemaUrl 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/index.ts b/packages/opentelemetry-resources/src/index.ts index 9dba58f5e85..c08ae7939a4 100644 --- a/packages/opentelemetry-resources/src/index.ts +++ b/packages/opentelemetry-resources/src/index.ts @@ -23,17 +23,19 @@ export { processDetector, serviceInstanceIdDetector, } from './detectors'; +export type { Entity } from '@opentelemetry/api'; export type { Resource } from './Resource'; export { - resourceFromAttributes, defaultResource, emptyResource, -} from './ResourceImpl'; + resourceFromAttributes, + resourceFromDetectedResource, +} from './resource-impl'; export { defaultServiceName } from './default-service-name'; export type { - ResourceDetector, DetectedResource, DetectedResourceAttributes, - RawResourceAttribute, MaybePromise, + RawResourceAttribute, + ResourceDetector, } from './types'; diff --git a/packages/opentelemetry-resources/src/ResourceImpl.ts b/packages/opentelemetry-resources/src/resource-impl.ts similarity index 72% rename from packages/opentelemetry-resources/src/ResourceImpl.ts rename to packages/opentelemetry-resources/src/resource-impl.ts index e40735e7ed6..f36787bec87 100644 --- a/packages/opentelemetry-resources/src/ResourceImpl.ts +++ b/packages/opentelemetry-resources/src/resource-impl.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Attributes, AttributeValue, diag } from '@opentelemetry/api'; +import { Attributes, diag } from '@opentelemetry/api'; import { SDK_INFO } from '@opentelemetry/core'; import { ATTR_SERVICE_NAME, @@ -27,11 +27,13 @@ import { defaultServiceName } from './default-service-name'; import { DetectedResource, DetectedResourceAttributes, - MaybePromise, + EntityRef, RawResourceAttribute, ResourceOptions, } from './types'; import { isPromiseLike } from './utils'; +import { EntityImpl, mergeEntities } from './entity-impl'; +import { Entity } from '@opentelemetry/api'; class ResourceImpl implements Resource { private _rawAttributes: RawResourceAttribute[]; @@ -40,11 +42,14 @@ class ResourceImpl implements Resource { private _memoizedAttributes?: Attributes; + private _entities: Entity[]; + private _entityRefs: EntityRef[]; + static FromAttributeList( - attributes: [string, MaybePromise][], + attributes: RawResourceAttribute[], options?: ResourceOptions ): Resource { - const res = new ResourceImpl({}, options); + const res = new ResourceImpl([], [], options); res._rawAttributes = guardedRawAttributes(attributes); res._asyncAttributesPending = attributes.filter(([_, val]) => isPromiseLike(val)).length > 0; @@ -57,25 +62,52 @@ class ResourceImpl implements Resource { * information about the entity as numbers, strings or booleans * TODO: Consider to add check/validation on attributes. */ - resource: DetectedResource, + attributes: RawResourceAttribute[], + entities: Entity[], options?: ResourceOptions ) { - const attributes = resource.attributes ?? {}; - this._rawAttributes = Object.entries(attributes).map(([k, v]) => { + this._rawAttributes = attributes.map(([k, v]) => { if (isPromiseLike(v)) { // side-effect this._asyncAttributesPending = true; } - return [k, v]; }); + this._entities = entities; + this._entityRefs = this._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) + : [], + }; + }); + this._rawAttributes = guardedRawAttributes(this._rawAttributes); this._schemaUrl = validateSchemaUrl(options?.schemaUrl); } + addEntity(entity: Entity): Resource { + const newEntities = [...this._entities, entity]; + return new ResourceImpl(this._rawAttributes, newEntities, { + schemaUrl: this._schemaUrl, + }); + } + public get asyncAttributesPending(): boolean { - return this._asyncAttributesPending; + return ( + this._asyncAttributesPending || + this._entities.reduce( + (p, c) => p || c.asyncAttributesPending, + false + ) + ); } public async waitForAsyncAttributes(): Promise { @@ -88,6 +120,10 @@ class ResourceImpl implements Resource { this._rawAttributes[i] = [k, isPromiseLike(v) ? await v : v]; } + for (const e of this._entities) { + await e.waitForAsyncAttributes(); + } + this._asyncAttributesPending = false; } @@ -103,6 +139,20 @@ class ResourceImpl implements Resource { } const attrs: Attributes = {}; + + for (const e of this._entities) { + for (const [k, v] of Object.entries(e.identifier)) { + if (v != null) { + 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`); @@ -121,6 +171,14 @@ class ResourceImpl implements Resource { return attrs; } + public get entityRefs(): EntityRef[] { + return this._entityRefs; + } + + public get entities(): Entity[] { + return this._entities; + } + public getRawAttributes(): RawResourceAttribute[] { return this._rawAttributes; } @@ -134,15 +192,17 @@ class ResourceImpl implements Resource { // Order is important // Spec states incoming attributes override existing attributes + const attrs = [...resource.getRawAttributes(), ...this.getRawAttributes()]; + + // TODO order opposite? + const entities = mergeEntities(...this._entities, ...resource.entities); + const mergedSchemaUrl = mergeSchemaUrl(this, resource); const mergedOptions: ResourceOptions | undefined = mergedSchemaUrl ? { schemaUrl: mergedSchemaUrl } : undefined; - return ResourceImpl.FromAttributeList( - [...resource.getRawAttributes(), ...this.getRawAttributes()], - mergedOptions - ); + return new ResourceImpl(attrs, entities, mergedOptions); } } @@ -157,7 +217,11 @@ export function resourceFromDetectedResource( detectedResource: DetectedResource, options?: ResourceOptions ): Resource { - return new ResourceImpl(detectedResource, options); + const entities = (detectedResource.entities ?? []).map( + e => new EntityImpl(e) + ); + const rawAttributes = Object.entries(detectedResource.attributes ?? {}); + return new ResourceImpl(rawAttributes, entities, options); } export function emptyResource(): Resource { diff --git a/packages/opentelemetry-resources/src/resource-initializer.ts b/packages/opentelemetry-resources/src/resource-initializer.ts new file mode 100644 index 00000000000..8d900bb2b88 --- /dev/null +++ b/packages/opentelemetry-resources/src/resource-initializer.ts @@ -0,0 +1,69 @@ +/* + * 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'; +import { detectResources } from './detect-resources'; +import { Resource } from './Resource'; +import { ResourceDetector } from './types'; +import { EventEmitter } from 'events'; + +export enum ResourceStatus { + DETECTING, + INITIALIZED, +} + +export type ResourceInitializeCallback = ( + resource: Resource, + status: ResourceStatus +) => void; + +export class ResourceInitializer extends EventEmitter { + private _status = ResourceStatus.DETECTING; + private _resource: Resource; + + constructor(detectors: ResourceDetector[]) { + super(); + this._resource = detectResources({ detectors }); + if (this._resource.asyncAttributesPending) { + this._resource.waitForAsyncAttributes?.().then( + () => { + this._status = ResourceStatus.INITIALIZED; + this._notify(); + }, + (err: unknown) => { + diag.error('Error initializing resource', err); + } + ); + } + } + + public get status(): ResourceStatus { + return this._status; + } + + // TODO should cb be called immediately if already initialized? + public onResourceInitialize(callback: ResourceInitializeCallback): void { + if (this._status === ResourceStatus.INITIALIZED) { + callback(this._resource, this._status); + return; + } + this.on('resourceInitialize', callback); + } + + private _notify(): void { + this.emit('resourceInitialize', this._resource, this._status); + } +} diff --git a/packages/opentelemetry-resources/src/resource-provider.ts b/packages/opentelemetry-resources/src/resource-provider.ts new file mode 100644 index 00000000000..43f5430c248 --- /dev/null +++ b/packages/opentelemetry-resources/src/resource-provider.ts @@ -0,0 +1,81 @@ +/* + * 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 { EventEmitter } from 'stream'; +import { Resource } from './Resource'; +import { + resourceFromAttributes, + resourceFromDetectedResource, +} from './resource-impl'; +import { ResourceDetector } from './types'; +import { hostname } from 'os'; + +/** + * Interface for resource providers. + */ +export interface ResourceProvider extends EventEmitter { + /** + * Returns a Resource describing the entity for which telemetry is collected. + */ + getResource(): Resource; + + on(event: 'changed', listener: (resource: Resource) => void): this; + on(event: string | symbol, listener: (...args: unknown[]) => void): this; + + addListener(event: 'changed', listener: (resource: Resource) => void): this; + addListener( + event: string | symbol, + listener: (...args: unknown[]) => void + ): this; +} + +/** + * Example implementation of a ResourceProvider with entity and resource detectors. + */ +export class DefaultResourceProvider + extends EventEmitter + implements ResourceProvider +{ + private detectors: ResourceDetector[]; + + constructor(detectors: ResourceDetector[] = []) { + super(); + this.detectors = detectors; + } + + getResource(): Resource { + let resource = resourceFromAttributes({ + 'service.name': 'default-service', + 'host.name': hostname(), + }); + + for (const detector of this.detectors) { + try { + const detected = detector.detect(); + resource = resource.merge(resourceFromDetectedResource(detected)); + } catch { + // ignore detector errors + } + } + + return resource; + } +} + +const res = new DefaultResourceProvider() as ResourceProvider; +res.on('changed', resource => { + console.log('Resource changed:', resource); +}); diff --git a/packages/opentelemetry-resources/src/types.ts b/packages/opentelemetry-resources/src/types.ts index 988b8c93f87..4188145f32d 100644 --- a/packages/opentelemetry-resources/src/types.ts +++ b/packages/opentelemetry-resources/src/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { AttributeValue } from '@opentelemetry/api'; +import { Attributes, AttributeValue } from '@opentelemetry/api'; import { ResourceDetectionConfig } from './config'; /** @@ -36,13 +36,33 @@ export type DetectedResource = { * Detected resource attributes. */ attributes?: DetectedResourceAttributes; + + /** + * Detected entities + */ + entities?: DetectedEntity[]; +}; + +export type EntityRef = { + type: string; + identifyingAttributeKeys: string[]; + descriptiveAttributeKeys: string[]; +}; + +export type DetectedEntity = { + type: string; + schemaUrl?: string; + identifier: Attributes; + attributes?: DetectedResourceAttributes; }; /** * An object representing detected resource attributes. * Value may be {@link AttributeValue}s, a promise to an {@link AttributeValue}, or undefined. */ -type DetectedResourceAttributeValue = MaybePromise; +export type DetectedResourceAttributeValue = MaybePromise< + AttributeValue | undefined +>; /** * An object representing detected resource attributes. diff --git a/packages/opentelemetry-resources/test/detectors/browser/ProcessDetector.test.ts b/packages/opentelemetry-resources/test/detectors/browser/ProcessDetector.test.ts index 38e4b3ec3e2..f12ea8fdd15 100644 --- a/packages/opentelemetry-resources/test/detectors/browser/ProcessDetector.test.ts +++ b/packages/opentelemetry-resources/test/detectors/browser/ProcessDetector.test.ts @@ -17,7 +17,7 @@ import * as sinon from 'sinon'; import { processDetector } from '../../../src'; import { describeBrowser } from '../../util'; import { assertEmptyResource } from '../../util/resource-assertions'; -import { resourceFromDetectedResource } from '../../../src/ResourceImpl'; +import { resourceFromDetectedResource } from '../../../src/resource-impl'; describeBrowser('processDetector() on web browser', () => { afterEach(() => { diff --git a/packages/opentelemetry-resources/test/detectors/node/EnvDetector.test.ts b/packages/opentelemetry-resources/test/detectors/node/EnvDetector.test.ts index 229579a6943..e2140316490 100644 --- a/packages/opentelemetry-resources/test/detectors/node/EnvDetector.test.ts +++ b/packages/opentelemetry-resources/test/detectors/node/EnvDetector.test.ts @@ -15,7 +15,7 @@ */ import { envDetector } from '../../../src'; -import { resourceFromDetectedResource } from '../../../src/ResourceImpl'; +import { resourceFromDetectedResource } from '../../../src/resource-impl'; import { describeNode } from '../../util'; import { assertEmptyResource, diff --git a/packages/opentelemetry-resources/test/detectors/node/HostDetector.test.ts b/packages/opentelemetry-resources/test/detectors/node/HostDetector.test.ts index 0e820279e89..c197bbb72da 100644 --- a/packages/opentelemetry-resources/test/detectors/node/HostDetector.test.ts +++ b/packages/opentelemetry-resources/test/detectors/node/HostDetector.test.ts @@ -17,7 +17,7 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; import { hostDetector } from '../../../src'; -import { resourceFromDetectedResource } from '../../../src/ResourceImpl'; +import { resourceFromDetectedResource } from '../../../src/resource-impl'; import { describeNode } from '../../util'; import { ATTR_HOST_ARCH, diff --git a/packages/opentelemetry-sdk-trace-base/src/BasicTracerProvider.ts b/packages/opentelemetry-sdk-trace-base/src/BasicTracerProvider.ts index 50a445e389a..41c8b327569 100644 --- a/packages/opentelemetry-sdk-trace-base/src/BasicTracerProvider.ts +++ b/packages/opentelemetry-sdk-trace-base/src/BasicTracerProvider.ts @@ -135,4 +135,19 @@ export class BasicTracerProvider implements TracerProvider { shutdown(): Promise { return this._activeSpanProcessor.shutdown(); } + + /** + * Creates a new TracerProvider with the same configuration but a new resource + * that includes the provided entity merged into it. + * + * @param entity - The entity to merge into the resource + * @returns A new TracerProvider with the merged entity + */ + forEntity(entity: import('@opentelemetry/api').Entity): TracerProvider { + const newResource = this._resource.addEntity(entity); + return new BasicTracerProvider({ + ...this._config, + resource: newResource, + }); + } } diff --git a/packages/sdk-metrics/src/MeterProvider.ts b/packages/sdk-metrics/src/MeterProvider.ts index 08871fa1ba5..5e4fd612211 100644 --- a/packages/sdk-metrics/src/MeterProvider.ts +++ b/packages/sdk-metrics/src/MeterProvider.ts @@ -21,12 +21,17 @@ import { MeterOptions, createNoopMeter, } from '@opentelemetry/api'; -import { defaultResource, Resource } from '@opentelemetry/resources'; import { IMetricReader } from './export/MetricReader'; +import { + defaultResource, + Resource, + // resourceFromDetectedResource, +} from '@opentelemetry/resources'; import { MeterProviderSharedState } from './state/MeterProviderSharedState'; import { MetricCollector } from './state/MetricCollector'; import { ForceFlushOptions, ShutdownOptions } from './types'; import { View, ViewOptions } from './view/View'; +import { Entity } from '../../../api/src/common/Entity'; /** * MeterProviderOptions provides an interface for configuring a MeterProvider. @@ -44,8 +49,15 @@ export interface MeterProviderOptions { export class MeterProvider implements IMeterProvider { private _sharedState: MeterProviderSharedState; private _shutdown = false; + private _config: MeterProviderOptions; + + forEntity(entity: Entity): IMeterProvider { + const newResource = this._sharedState.resource.addEntity(entity); + return new MeterProvider({ ...this._config, resource: newResource }); + } constructor(options?: MeterProviderOptions) { + this._config = options ?? {}; this._sharedState = new MeterProviderSharedState( options?.resource ?? defaultResource() ); diff --git a/packages/sdk-metrics/test/export/PeriodicExportingMetricReader.test.ts b/packages/sdk-metrics/test/export/PeriodicExportingMetricReader.test.ts index 28136f1c006..746e9cb038b 100644 --- a/packages/sdk-metrics/test/export/PeriodicExportingMetricReader.test.ts +++ b/packages/sdk-metrics/test/export/PeriodicExportingMetricReader.test.ts @@ -168,10 +168,14 @@ describe('PeriodicExportingMetricReader', () => { const resourceMetrics: ResourceMetrics = { resource: { attributes: {}, + entities: [], merge: sinon.stub(), asyncAttributesPending: true, // ensure we try to await async attributes waitForAsyncAttributes: waitForAsyncAttributesStub, // resolve when awaited getRawAttributes: () => [], + addEntity: () => { + return resourceMetrics.resource; + }, }, scopeMetrics: scopeMetrics, }; @@ -400,10 +404,14 @@ describe('PeriodicExportingMetricReader', () => { const resourceMetrics: ResourceMetrics = { resource: { attributes: {}, + entities: [], merge: sinon.stub(), asyncAttributesPending: true, // ensure we try to await async attributes waitForAsyncAttributes: waitForAsyncAttributesStub, // resolve when awaited getRawAttributes: () => [], + addEntity: () => { + return resourceMetrics.resource; + }, }, scopeMetrics: scopeMetrics, }; @@ -446,10 +454,14 @@ describe('PeriodicExportingMetricReader', () => { const resourceMetrics: ResourceMetrics = { resource: { attributes: {}, + entities: [], merge: sinon.stub(), asyncAttributesPending: true, // ensure we try to await async attributes waitForAsyncAttributes: waitForAsyncAttributesStub, // reject when awaited getRawAttributes: () => [], + addEntity: () => { + return resourceMetrics.resource; + }, }, scopeMetrics: [], };