diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/ExtendedOpenTelemetry.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/ExtendedOpenTelemetry.java new file mode 100644 index 00000000000..3021b24a2c4 --- /dev/null +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/ExtendedOpenTelemetry.java @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.incubator; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.incubator.entities.EntityProvider; + +/** Extension to {@link OpenTelemetry} that adds {@link EntityProvider}. */ +public interface ExtendedOpenTelemetry extends OpenTelemetry { + /** Returns the {@link EntityProvider} for this {@link OpenTelemetry}. */ + default EntityProvider getEntityProvider() { + return EntityProvider.noop(); + } +} diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/entities/EntityBuilder.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/entities/EntityBuilder.java new file mode 100644 index 00000000000..9795ba2f724 --- /dev/null +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/entities/EntityBuilder.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.incubator.entities; + +import io.opentelemetry.api.common.Attributes; + +/** + * A builder of an Entity that allows to add identifying or descriptive {@link Attributes}, as well + * as type and schema_url. + * + *

Entity represents an object of interest associated with produced telemetry: traces, metrics or + * logs. + * + *

For example, telemetry produced using OpenTelemetry SDK is normally associated with a Service + * entity. Similarly, OpenTelemetry defines system metrics for a host. The Host is the entity we + * want to associate metrics with in this case. + * + *

Entities may be also associated with produced telemetry indirectly. For example a service that + * produces telemetry is also related with a process in which the service runs, so we say that the + * Service entity is related to the Process entity. The process normally also runs on a host, so we + * say that the Process entity is related to the Host entity. + */ +public interface EntityBuilder { + /** + * Assign an OpenTelemetry schema URL to the resulting Entity. + * + * @param schemaUrl The URL of the OpenTelemetry schema being used to create this Entity. + * @return this + */ + EntityBuilder setSchemaUrl(String schemaUrl); + + /** + * Modify the descriptive attributes of this Entity. + * + * @param description The {@link Attributes} which describe this Entity. + * @return this + */ + EntityBuilder withDescription(Attributes description); + + /** + * Modify the identifying attributes of this Entity. + * + * @param id The {@link Attributes} which identify this Entity. + * @return this + */ + EntityBuilder withId(Attributes id); + + /** Emits the current entity. */ + void emit(); +} diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/entities/EntityProvider.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/entities/EntityProvider.java new file mode 100644 index 00000000000..f9854724c4b --- /dev/null +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/entities/EntityProvider.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.incubator.entities; + +/** + * A registry for interacting with {@code Resource}s. The name Provider is for consistency + * with other languages and it is NOT loaded using reflection. + */ +public interface EntityProvider { + /** + * Returns a no-op {@link EntityProvider} which only creates no-op {@link EntityBuilder}s which do + * not record nor are emitted. + */ + static EntityProvider noop() { + return NoopEntityProvider.INSTANCE; + } + + /** + * Removes an entity from this resource. + * + * @param entityType the type of entity to remove. + * @return true if entity was found and removed. + */ + boolean removeEntity(String entityType); + + /** + * Attaches an entity to the current {@code Resource}. + * + *

This will only add new entities or update description of existing entities. + * + * @param entityType The type of the entity. + * @return A builder that can construct an entity. + */ + EntityBuilder attachOrUpdateEntity(String entityType); +} diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/entities/NoopEntityBuilder.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/entities/NoopEntityBuilder.java new file mode 100644 index 00000000000..59474533a98 --- /dev/null +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/entities/NoopEntityBuilder.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.incubator.entities; + +import io.opentelemetry.api.common.Attributes; + +final class NoopEntityBuilder implements EntityBuilder { + + static final EntityBuilder INSTANCE = new NoopEntityBuilder(); + + @Override + public EntityBuilder setSchemaUrl(String schemaUrl) { + return this; + } + + @Override + public EntityBuilder withDescription(Attributes description) { + return this; + } + + @Override + public EntityBuilder withId(Attributes id) { + return this; + } + + @Override + public void emit() {} +} diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/entities/NoopEntityProvider.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/entities/NoopEntityProvider.java new file mode 100644 index 00000000000..08a60093531 --- /dev/null +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/entities/NoopEntityProvider.java @@ -0,0 +1,21 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.api.incubator.entities; + +final class NoopEntityProvider implements EntityProvider { + + static final EntityProvider INSTANCE = new NoopEntityProvider(); + + @Override + public boolean removeEntity(String entityType) { + return false; + } + + @Override + public EntityBuilder attachOrUpdateEntity(String entityType) { + return NoopEntityBuilder.INSTANCE; + } +} diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-common.txt b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-common.txt index 5d251deb80c..6a88413d8c4 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-common.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-common.txt @@ -1,2 +1,4 @@ Comparing source compatibility of opentelemetry-sdk-common-1.52.0-SNAPSHOT.jar against opentelemetry-sdk-common-1.51.0.jar -No changes. \ No newline at end of file +*** MODIFIED CLASS: PUBLIC ABSTRACT io.opentelemetry.sdk.resources.Resource (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + *** MODIFIED METHOD: PUBLIC NON_ABSTRACT (<- ABSTRACT) io.opentelemetry.api.common.Attributes getAttributes() diff --git a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/EntityRefMarshaler.java b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/EntityRefMarshaler.java new file mode 100644 index 00000000000..e77ab7f8308 --- /dev/null +++ b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/EntityRefMarshaler.java @@ -0,0 +1,83 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.internal.otlp; + +import io.opentelemetry.api.internal.StringUtils; +import io.opentelemetry.exporter.internal.marshal.MarshalerUtil; +import io.opentelemetry.exporter.internal.marshal.MarshalerWithSize; +import io.opentelemetry.exporter.internal.marshal.Serializer; +import io.opentelemetry.proto.common.v1.internal.EntityRef; +import io.opentelemetry.sdk.resources.internal.Entity; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import javax.annotation.Nullable; + +/** + * A Marshaler of {@link io.opentelemetry.sdk.resources.internal.Entity}. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +final class EntityRefMarshaler extends MarshalerWithSize { + @Nullable private final byte[] schemaUrlUtf8; + private final byte[] typeUtf8; + private final byte[][] idKeysUtf8; + private final byte[][] descriptionKeysUtf8; + + @Override + protected void writeTo(Serializer output) throws IOException { + if (schemaUrlUtf8 != null) { + output.writeString(EntityRef.SCHEMA_URL, schemaUrlUtf8); + } + output.writeString(EntityRef.TYPE, typeUtf8); + output.writeRepeatedString(EntityRef.ID_KEYS, idKeysUtf8); + output.writeRepeatedString(EntityRef.DESCRIPTION_KEYS, descriptionKeysUtf8); + } + + /** Consttructs an entity reference marshaler from a full entity. */ + static EntityRefMarshaler createForEntity(Entity e) { + byte[] schemaUrlUtf8 = null; + if (!StringUtils.isNullOrEmpty(e.getSchemaUrl())) { + schemaUrlUtf8 = e.getSchemaUrl().getBytes(StandardCharsets.UTF_8); + } + return new EntityRefMarshaler( + schemaUrlUtf8, + e.getType().getBytes(StandardCharsets.UTF_8), + e.getId().asMap().keySet().stream() + .map(key -> key.getKey().getBytes(StandardCharsets.UTF_8)) + .toArray(byte[][]::new), + e.getDescription().asMap().keySet().stream() + .map(key -> key.getKey().getBytes(StandardCharsets.UTF_8)) + .toArray(byte[][]::new)); + } + + private EntityRefMarshaler( + @Nullable byte[] schemaUrlUtf8, + byte[] typeUtf8, + byte[][] idKeysUtf8, + byte[][] descriptionKeysUtf8) { + super(calculateSize(schemaUrlUtf8, typeUtf8, idKeysUtf8, descriptionKeysUtf8)); + this.schemaUrlUtf8 = schemaUrlUtf8; + this.typeUtf8 = typeUtf8; + this.idKeysUtf8 = idKeysUtf8; + this.descriptionKeysUtf8 = descriptionKeysUtf8; + } + + private static int calculateSize( + @Nullable byte[] schemaUrlUtf8, + byte[] typeUtf8, + byte[][] idKeysUtf8, + byte[][] descriptionKeysUtf8) { + int size = 0; + if (schemaUrlUtf8 != null) { + size += MarshalerUtil.sizeBytes(EntityRef.SCHEMA_URL, schemaUrlUtf8); + } + size += MarshalerUtil.sizeBytes(EntityRef.TYPE, typeUtf8); + MarshalerUtil.sizeRepeatedString(EntityRef.ID_KEYS, idKeysUtf8); + MarshalerUtil.sizeRepeatedString(EntityRef.DESCRIPTION_KEYS, descriptionKeysUtf8); + return size; + } +} diff --git a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/ResourceMarshaler.java b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/ResourceMarshaler.java index b3395448a79..8be7df10099 100644 --- a/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/ResourceMarshaler.java +++ b/exporters/otlp/common/src/main/java/io/opentelemetry/exporter/internal/otlp/ResourceMarshaler.java @@ -10,6 +10,7 @@ import io.opentelemetry.exporter.internal.marshal.MarshalerWithSize; import io.opentelemetry.exporter.internal.marshal.Serializer; import io.opentelemetry.proto.resource.v1.internal.Resource; +import io.opentelemetry.sdk.resources.internal.EntityUtil; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.UncheckedIOException; @@ -37,7 +38,10 @@ public static ResourceMarshaler create(io.opentelemetry.sdk.resources.Resource r RealResourceMarshaler realMarshaler = new RealResourceMarshaler( - KeyValueMarshaler.createForAttributes(resource.getAttributes())); + KeyValueMarshaler.createForAttributes(resource.getAttributes()), + EntityUtil.getEntities(resource).stream() + .map(EntityRefMarshaler::createForEntity) + .toArray(MarshalerWithSize[]::new)); ByteArrayOutputStream binaryBos = new ByteArrayOutputStream(realMarshaler.getBinarySerializedSize()); @@ -70,19 +74,26 @@ public void writeTo(Serializer output) throws IOException { private static final class RealResourceMarshaler extends MarshalerWithSize { private final KeyValueMarshaler[] attributes; + private final MarshalerWithSize[] entityRefs; - private RealResourceMarshaler(KeyValueMarshaler[] attributes) { - super(calculateSize(attributes)); + private RealResourceMarshaler(KeyValueMarshaler[] attributes, MarshalerWithSize[] entityRefs) { + super(calculateSize(attributes, entityRefs)); this.attributes = attributes; + this.entityRefs = entityRefs; } @Override protected void writeTo(Serializer output) throws IOException { output.serializeRepeatedMessage(Resource.ATTRIBUTES, attributes); + output.serializeRepeatedMessage(Resource.ENTITY_REFS, entityRefs); } - private static int calculateSize(KeyValueMarshaler[] attributeMarshalers) { - return MarshalerUtil.sizeRepeatedMessage(Resource.ATTRIBUTES, attributeMarshalers); + private static int calculateSize( + KeyValueMarshaler[] attributeMarshalers, MarshalerWithSize[] entityRefs) { + int size = 0; + size += MarshalerUtil.sizeRepeatedMessage(Resource.ATTRIBUTES, attributeMarshalers); + size += size += MarshalerUtil.sizeRepeatedMessage(Resource.ENTITY_REFS, entityRefs); + return size; } } } diff --git a/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/EntityRefMarshalerTest.java b/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/EntityRefMarshalerTest.java new file mode 100644 index 00000000000..b5e8cad39f5 --- /dev/null +++ b/exporters/otlp/common/src/test/java/io/opentelemetry/exporter/internal/otlp/EntityRefMarshalerTest.java @@ -0,0 +1,87 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.internal.otlp; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.Message; +import com.google.protobuf.util.JsonFormat; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.exporter.internal.marshal.Marshaler; +import io.opentelemetry.proto.common.v1.EntityRef; +import io.opentelemetry.sdk.resources.internal.Entity; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Test; + +class EntityRefMarshalerTest { + @Test + void toEntityRefs() { + Entity e = + Entity.builder("test") + .setSchemaUrl("test-url") + .withDescription(Attributes.builder().put("desc.key", "desc.value").build()) + .withId(Attributes.builder().put("id.key", "id.value").build()) + .build(); + EntityRef proto = parse(EntityRef.getDefaultInstance(), EntityRefMarshaler.createForEntity(e)); + assertThat(proto.getType()).isEqualTo("test"); + assertThat(proto.getSchemaUrl()).isEqualTo("test-url"); + assertThat(proto.getIdKeysList()).containsExactly("id.key"); + assertThat(proto.getDescriptionKeysList()).containsExactly("desc.key"); + } + + @SuppressWarnings("unchecked") + private static T parse(T prototype, Marshaler marshaler) { + byte[] serialized = toByteArray(marshaler); + T result; + try { + result = (T) prototype.newBuilderForType().mergeFrom(serialized).build(); + } catch (InvalidProtocolBufferException e) { + throw new UncheckedIOException(e); + } + // Our marshaler should produce the exact same length of serialized output (for example, field + // default values are not outputted), so we check that here. The output itself may have slightly + // different ordering, mostly due to the way we don't output oneof values in field order all the + // tieme. If the lengths are equal and the resulting protos are equal, the marshaling is + // guaranteed to be valid. + assertThat(result.getSerializedSize()).isEqualTo(serialized.length); + + // Compare JSON + String json = toJson(marshaler); + Message.Builder builder = prototype.newBuilderForType(); + try { + JsonFormat.parser().merge(json, builder); + } catch (InvalidProtocolBufferException e) { + throw new UncheckedIOException(e); + } + assertThat(builder.build()).isEqualTo(result); + + return result; + } + + private static byte[] toByteArray(Marshaler marshaler) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try { + marshaler.writeBinaryTo(bos); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return bos.toByteArray(); + } + + private static String toJson(Marshaler marshaler) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try { + marshaler.writeJsonTo(bos); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + return new String(bos.toByteArray(), StandardCharsets.UTF_8); + } +} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/ExtendedOpenTelemetrySdk.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/ExtendedOpenTelemetrySdk.java new file mode 100644 index 00000000000..d4fdb19647b --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/ExtendedOpenTelemetrySdk.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator; + +import io.opentelemetry.api.incubator.ExtendedOpenTelemetry; +import io.opentelemetry.api.incubator.entities.EntityProvider; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.logs.SdkLoggerProvider; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import java.io.Closeable; + +/** A new interface for creating OpenTelemetrySdk that supports {@link EntityProvider}. */ +public interface ExtendedOpenTelemetrySdk extends ExtendedOpenTelemetry, Closeable { + /** + * Shutdown the SDK. Calls {@link SdkTracerProvider#shutdown()}, {@link + * SdkMeterProvider#shutdown()}, and {@link SdkLoggerProvider#shutdown()}. + * + * @return a {@link CompletableResultCode} which completes when all providers are shutdown + */ + CompletableResultCode shutdown(); + + /** Returns a builder for {@link ExtendedOpenTelemetrySdk}. */ + static ExtendedOpenTelemetrySdkBuilder builder() { + return new ExtendedOpenTelemetrySdkBuilder(); + } +} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/ExtendedOpenTelemetrySdkBuilder.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/ExtendedOpenTelemetrySdkBuilder.java new file mode 100644 index 00000000000..c1c0d805264 --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/ExtendedOpenTelemetrySdkBuilder.java @@ -0,0 +1,118 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.incubator.entities.EntityProvider; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.OpenTelemetrySdkBuilder; +import io.opentelemetry.sdk.extension.incubator.entities.LatestResourceSupplier; +import io.opentelemetry.sdk.extension.incubator.entities.SdkEntityProvider; +import io.opentelemetry.sdk.extension.incubator.entities.SdkEntityProviderBuilder; +import io.opentelemetry.sdk.logs.SdkLoggerProvider; +import io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder; +import io.opentelemetry.sdk.logs.internal.SdkLoggerProviderUtil; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; +import io.opentelemetry.sdk.metrics.internal.SdkMeterProviderUtil; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; +import io.opentelemetry.sdk.trace.internal.SdkTracerProviderUtil; +import java.util.function.Consumer; + +/** A new interface for creating OpenTelemetrySdk that supports {@link EntityProvider}. */ +public final class ExtendedOpenTelemetrySdkBuilder { + private ContextPropagators propagators = ContextPropagators.noop(); + private final SdkTracerProviderBuilder tracerProviderBuilder = SdkTracerProvider.builder(); + private final SdkMeterProviderBuilder meterProviderBuilder = SdkMeterProvider.builder(); + private final SdkLoggerProviderBuilder loggerProviderBuilder = SdkLoggerProvider.builder(); + private final SdkEntityProviderBuilder resourceProviderBuilder = SdkEntityProvider.builder(); + + /** Sets the {@link ContextPropagators} to use. */ + public ExtendedOpenTelemetrySdkBuilder setPropagators(ContextPropagators propagators) { + this.propagators = propagators; + return this; + } + + /** + * Applies a consumer callback to configure the TracerProvider being built for this OpenTelemetry. + * + * @param configurator A callback fleshing out tracers. + * @return this + */ + public ExtendedOpenTelemetrySdkBuilder withTracerProvider( + Consumer configurator) { + configurator.accept(this.tracerProviderBuilder); + return this; + } + + /** + * Applies a consumer callback to configure the MeterProvider being built for this OpenTelemetry. + * + * @param configurator A callback fleshing out meters. + * @return this + */ + public ExtendedOpenTelemetrySdkBuilder withMeterProvider( + Consumer configurator) { + configurator.accept(this.meterProviderBuilder); + return this; + } + + /** + * Applies a consumer callback to configure the LoggerProvider being built for this OpenTelemetry. + * + * @param configurator A callback fleshing out meters. + * @return this + */ + public ExtendedOpenTelemetrySdkBuilder withLoggerProvider( + Consumer configurator) { + configurator.accept(this.loggerProviderBuilder); + return this; + } + + /** + * Returns a new {@link OpenTelemetrySdk} built with the configuration of this {@link + * OpenTelemetrySdkBuilder}. This SDK is not registered as the global {@link + * io.opentelemetry.api.OpenTelemetry}. It is recommended that you register one SDK using {@link + * OpenTelemetrySdkBuilder#buildAndRegisterGlobal()} for use by instrumentation that requires + * access to a global instance of {@link io.opentelemetry.api.OpenTelemetry}. + * + * @see GlobalOpenTelemetry + */ + public ExtendedOpenTelemetrySdk build() { + SdkEntityProvider resourceProvider = resourceProviderBuilder.build(); + // TODO - allow startup delay configuration + LatestResourceSupplier sdkResourceSupplier = new LatestResourceSupplier(200); + resourceProvider.onChange(sdkResourceSupplier); + SdkTracerProvider tracerProvider = + SdkTracerProviderUtil.setResourceSupplier(tracerProviderBuilder, sdkResourceSupplier) + .build(); + SdkMeterProvider meterProvider = + SdkMeterProviderUtil.setResourceSupplier(meterProviderBuilder, sdkResourceSupplier).build(); + SdkLoggerProvider loggerProvider = + SdkLoggerProviderUtil.setResourceSupplier(loggerProviderBuilder, sdkResourceSupplier) + .build(); + return new ObfuscatedExtendedOpenTelemerySdk( + resourceProvider, tracerProvider, meterProvider, loggerProvider, propagators); + } + + /** + * Returns a new {@link OpenTelemetrySdk} built with the configuration of this {@link + * OpenTelemetrySdkBuilder} and registers it as the global {@link + * io.opentelemetry.api.OpenTelemetry}. An exception will be thrown if this method is attempted to + * be called multiple times in the lifecycle of an application - ensure you have only one SDK for + * use as the global instance. If you need to configure multiple SDKs for tests, use {@link + * GlobalOpenTelemetry#resetForTest()} between them. + * + * @see GlobalOpenTelemetry + */ + public ExtendedOpenTelemetrySdk buildAndRegisterGlobal() { + ExtendedOpenTelemetrySdk sdk = build(); + GlobalOpenTelemetry.set(sdk); + return sdk; + } +} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/ObfuscatedExtendedOpenTelemerySdk.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/ObfuscatedExtendedOpenTelemerySdk.java new file mode 100644 index 00000000000..d868e0b26a5 --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/ObfuscatedExtendedOpenTelemerySdk.java @@ -0,0 +1,236 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator; + +import io.opentelemetry.api.incubator.entities.EntityBuilder; +import io.opentelemetry.api.incubator.entities.EntityProvider; +import io.opentelemetry.api.logs.LoggerBuilder; +import io.opentelemetry.api.logs.LoggerProvider; +import io.opentelemetry.api.metrics.MeterBuilder; +import io.opentelemetry.api.metrics.MeterProvider; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.TracerBuilder; +import io.opentelemetry.api.trace.TracerProvider; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.extension.incubator.entities.SdkEntityProvider; +import io.opentelemetry.sdk.logs.SdkLoggerProvider; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Logger; +import javax.annotation.concurrent.ThreadSafe; + +/** The SDK implementation of {@link ExtendedOpenTelemetrySdk}. */ +final class ObfuscatedExtendedOpenTelemerySdk implements ExtendedOpenTelemetrySdk { + + private static final Logger LOGGER = + Logger.getLogger(ObfuscatedExtendedOpenTelemerySdk.class.getName()); + private final AtomicBoolean isShutdown = new AtomicBoolean(false); + private final ObfuscatedTracerProvider tracerProvider; + private final ObfuscatedMeterProvider meterProvider; + private final ObfuscatedLoggerProvider loggerProvider; + private final ObfuscatedEntityProvider entityProvider; + private final ContextPropagators propagators; + + ObfuscatedExtendedOpenTelemerySdk( + SdkEntityProvider entityProvider, + SdkTracerProvider tracerProvider, + SdkMeterProvider meterProvider, + SdkLoggerProvider loggerProvider, + ContextPropagators propagators) { + this.entityProvider = new ObfuscatedEntityProvider(entityProvider); + this.tracerProvider = new ObfuscatedTracerProvider(tracerProvider); + this.meterProvider = new ObfuscatedMeterProvider(meterProvider); + this.loggerProvider = new ObfuscatedLoggerProvider(loggerProvider); + this.propagators = propagators; + } + + @Override + public CompletableResultCode shutdown() { + if (!isShutdown.compareAndSet(false, true)) { + LOGGER.info("Multiple shutdown calls"); + return CompletableResultCode.ofSuccess(); + } + List results = new ArrayList<>(); + results.add(tracerProvider.unobfuscate().shutdown()); + results.add(meterProvider.unobfuscate().shutdown()); + results.add(loggerProvider.unobfuscate().shutdown()); + return CompletableResultCode.ofAll(results); + } + + @Override + public void close() { + shutdown().join(10, TimeUnit.SECONDS); + } + + @Override + public TracerProvider getTracerProvider() { + return tracerProvider; + } + + @Override + public MeterProvider getMeterProvider() { + return meterProvider; + } + + @Override + public LoggerProvider getLogsBridge() { + return loggerProvider; + } + + @Override + public EntityProvider getEntityProvider() { + return entityProvider; + } + + @Override + public ContextPropagators getPropagators() { + return propagators; + } + + @Override + public String toString() { + return "ExtendedOpenTelemetrySdk{" + + "entityProvider=" + + entityProvider.unobfuscate() + + ", tracerProvider=" + + tracerProvider.unobfuscate() + + ", meterProvider=" + + meterProvider.unobfuscate() + + ", loggerProvider=" + + loggerProvider.unobfuscate() + + ", propagators=" + + propagators + + "}"; + } + + /** + * This class allows the SDK to unobfuscate an obfuscated static global provider. + * + *

Static global providers are obfuscated when they are returned from the API to prevent users + * from casting them to their SDK specific implementation. For example, we do not want users to + * use patterns like {@code (SdkTracerProvider) openTelemetry.getTracerProvider()}. + */ + @ThreadSafe + // Visible for testing + static class ObfuscatedTracerProvider implements TracerProvider { + + private final SdkTracerProvider delegate; + + ObfuscatedTracerProvider(SdkTracerProvider delegate) { + this.delegate = delegate; + } + + @Override + public Tracer get(String instrumentationScopeName) { + return delegate.get(instrumentationScopeName); + } + + @Override + public Tracer get(String instrumentationScopeName, String instrumentationScopeVersion) { + return delegate.get(instrumentationScopeName, instrumentationScopeVersion); + } + + @Override + public TracerBuilder tracerBuilder(String instrumentationScopeName) { + return delegate.tracerBuilder(instrumentationScopeName); + } + + public SdkTracerProvider unobfuscate() { + return delegate; + } + } + + /** + * This class allows the SDK to unobfuscate an obfuscated static global provider. + * + *

Static global providers are obfuscated when they are returned from the API to prevent users + * from casting them to their SDK specific implementation. For example, we do not want users to + * use patterns like {@code (SdkMeterProvider) openTelemetry.getMeterProvider()}. + */ + @ThreadSafe + // Visible for testing + static class ObfuscatedMeterProvider implements MeterProvider { + + private final SdkMeterProvider delegate; + + ObfuscatedMeterProvider(SdkMeterProvider delegate) { + this.delegate = delegate; + } + + @Override + public MeterBuilder meterBuilder(String instrumentationScopeName) { + return delegate.meterBuilder(instrumentationScopeName); + } + + public SdkMeterProvider unobfuscate() { + return delegate; + } + } + + /** + * This class allows the SDK to unobfuscate an obfuscated static global provider. + * + *

Static global providers are obfuscated when they are returned from the API to prevent users + * from casting them to their SDK specific implementation. For example, we do not want users to + * use patterns like {@code (SdkMeterProvider) openTelemetry.getMeterProvider()}. + */ + @ThreadSafe + // Visible for testing + static class ObfuscatedLoggerProvider implements LoggerProvider { + + private final SdkLoggerProvider delegate; + + ObfuscatedLoggerProvider(SdkLoggerProvider delegate) { + this.delegate = delegate; + } + + @Override + public LoggerBuilder loggerBuilder(String instrumentationScopeName) { + return delegate.loggerBuilder(instrumentationScopeName); + } + + public SdkLoggerProvider unobfuscate() { + return delegate; + } + } + + /** + * This class allows the SDK to unobfuscate an obfuscated static global provider. + * + *

Static global providers are obfuscated when they are returned from the API to prevent users + * from casting them to their SDK specific implementation. For example, we do not want users to + * use patterns like {@code (SdkResourceProvider) openTelemetry.getResourceProvider()}. + */ + @ThreadSafe + // Visible for testing + static class ObfuscatedEntityProvider implements EntityProvider { + + private final SdkEntityProvider delegate; + + ObfuscatedEntityProvider(SdkEntityProvider delegate) { + this.delegate = delegate; + } + + public SdkEntityProvider unobfuscate() { + return delegate; + } + + @Override + public boolean removeEntity(String entityType) { + return delegate.removeEntity(entityType); + } + + @Override + public EntityBuilder attachOrUpdateEntity(String entityType) { + return delegate.attachOrUpdateEntity(entityType); + } + } +} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/CurrentThreadExecutorService.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/CurrentThreadExecutorService.java new file mode 100644 index 00000000000..8718ecf1804 --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/CurrentThreadExecutorService.java @@ -0,0 +1,137 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.entities; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * An executor service that runs all jobs immediately on the current thread. + * + *

We use this so SDK users can determine how to isolate {@link EntityListener}s with the default + * being no isolation of events. + */ +final class CurrentThreadExecutorService implements ExecutorService { + private volatile boolean shutdown = false; + + @Override + public void execute(Runnable command) { + if (shutdown) { + throw new RejectedExecutionException("ExecutorService is shut down"); + } + command.run(); + } + + @Override + public List> invokeAll(Collection> tasks) { + if (shutdown) { + throw new RejectedExecutionException("ExecutorService is shut down"); + } + // Execute all tasks synchronously and collect their Futures + return tasks.stream().map(task -> submit(task)).collect(Collectors.toList()); + } + + @Override + public List> invokeAll( + Collection> tasks, long timeout, TimeUnit unit) { + return invokeAll(tasks); + } + + @Override + public T invokeAny(Collection> tasks) + throws InterruptedException, ExecutionException { + if (shutdown) { + throw new RejectedExecutionException("ExecutorService is shut down"); + } + // Execute all tasks synchronously and return first success. + for (Callable task : tasks) { + try { + // We wrap the task in a `submit` call to get ExecutionExceptions. + return submit(task).get(); + } catch (ExecutionException e) { + // Ignore this error, and try the next one. + } + } + throw new ExecutionException("No tasks completed successfully", null); + } + + @Override + public T invokeAny(Collection> tasks, long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException { + if (shutdown) { + throw new RejectedExecutionException("ExecutorService is shut down"); + } + return invokeAny(tasks); + } + + @Override + public Future submit(Callable task) { + if (shutdown) { + throw new RejectedExecutionException("ExecutorService is shut down"); + } + FutureTask future = new FutureTask<>(task); + // Run in this thread. + future.run(); + return future; + } + + @Override + public Future submit(Runnable task) { + if (shutdown) { + throw new RejectedExecutionException("ExecutorService is shut down"); + } + FutureTask future = new FutureTask<>(task, null); + // Run in this thread. + future.run(); + return future; + } + + @Override + public Future submit(Runnable task, T result) { + if (shutdown) { + throw new RejectedExecutionException("ExecutorService is shut down"); + } + FutureTask future = new FutureTask<>(task, result); + // Run in this thread. + future.run(); + return future; + } + + @Override + public boolean isShutdown() { + return shutdown; + } + + @Override + public boolean isTerminated() { + return shutdown; + } + + @Override + public void shutdown() { + shutdown = true; + } + + @Override + public List shutdownNow() { + shutdown = true; + return Collections.emptyList(); + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) { + return isTerminated(); + } +} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/EntityListener.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/EntityListener.java new file mode 100644 index 00000000000..55aece757b3 --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/EntityListener.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.entities; + +import io.opentelemetry.sdk.resources.Resource; + +/** A listener for changes in the EntityState of this SDK. */ +public interface EntityListener { + + /** + * Called when the EntityProvider is initialized with full resource state. + * + * @param resource The initialized state of the Resource. + */ + public void onResourceInit(Resource resource); + + /** + * Called when an entity has been added or its state has changed. + * + * @param state The current state of the entity. + * @param resource The current state of the Resource. + */ + public void onEntityState(EntityState state, Resource resource); + + /** + * Called when an entity has been removed. + * + * @param state The current state of the removed entity. + * @param resource The current state of the Resource. + */ + public void onEntityDelete(EntityState state, Resource resource); +} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/EntityState.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/EntityState.java new file mode 100644 index 00000000000..07ef93bf359 --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/EntityState.java @@ -0,0 +1,25 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.entities; + +import io.opentelemetry.api.common.Attributes; +import javax.annotation.Nullable; + +/** The current state of an Entity. */ +public interface EntityState { + /** Returns the type of the Entity. */ + String getType(); + + /** Returns the schema_url of the Entity, or null. */ + @Nullable + String getSchemaUrl(); + + /** Returns the identity of the Entity. */ + Attributes getId(); + + /** Returns the description of the Entity. */ + Attributes getDescription(); +} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/LatestResourceSupplier.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/LatestResourceSupplier.java new file mode 100644 index 00000000000..0458517c7cc --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/LatestResourceSupplier.java @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.entities; + +import io.opentelemetry.sdk.resources.Resource; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +/** + * A supplier of resource which listenes to Entity events. + * + *

This class will wait for availability via ResourceInitialized event before returning from any + * `getResource` call. + */ +public class LatestResourceSupplier implements EntityListener, Supplier { + + private final AtomicReference current = new AtomicReference<>(null); + private final Object initializationLock = new Object(); + private final long maxStartupDelayMs; + + public LatestResourceSupplier(long maxStartupDelayMs) { + this.maxStartupDelayMs = maxStartupDelayMs; + } + + @Override + public void onResourceInit(Resource resource) { + current.lazySet(resource); + // Here we can notify anyone waiting on initialization. + synchronized (initializationLock) { + initializationLock.notifyAll(); + } + } + + @Override + public void onEntityState(EntityState state, Resource resource) { + current.lazySet(resource); + } + + @Override + public void onEntityDelete(EntityState state, Resource resource) { + current.lazySet(resource); + } + + @Override + public Resource get() { + Resource result = this.current.get(); + if (result == null) { + synchronized (initializationLock) { + result = this.current.get(); + long startTime = System.currentTimeMillis(); + boolean stillWaiting = true; + while (result == null || stillWaiting) { + long elapsedTime = System.currentTimeMillis() - startTime; + long remainingTime = maxStartupDelayMs - elapsedTime; + if (remainingTime <= 0) { + stillWaiting = false; + break; + } + try { + initializationLock.wait(remainingTime); + } catch (InterruptedException e) { + break; + } + } + if (result == null) { + result = Resource.getDefault(); + } + } + } + return result; + } +} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/ResourceDetector.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/ResourceDetector.java new file mode 100644 index 00000000000..a80bf0ddbcd --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/ResourceDetector.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.entities; + +import io.opentelemetry.api.incubator.entities.EntityProvider; +import io.opentelemetry.sdk.common.CompletableResultCode; + +/** + * The Resource 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. + */ +public interface ResourceDetector { + /** + * Reports detected entities. + * + * @param provider The provider where entities are reported. + */ + public CompletableResultCode report(EntityProvider provider); +} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/SdkEntityBuilder.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/SdkEntityBuilder.java new file mode 100644 index 00000000000..3cf68e2f56c --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/SdkEntityBuilder.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.entities; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.incubator.entities.EntityBuilder; +import io.opentelemetry.sdk.resources.internal.Entity; +import java.util.function.Consumer; + +final class SdkEntityBuilder implements EntityBuilder { + private final io.opentelemetry.sdk.resources.internal.EntityBuilder builder; + private final Consumer emitter; + + SdkEntityBuilder(String entityType, Consumer emitter) { + this.builder = Entity.builder(entityType); + this.emitter = emitter; + } + + @Override + public EntityBuilder setSchemaUrl(String schemaUrl) { + builder.setSchemaUrl(schemaUrl); + return this; + } + + @Override + public EntityBuilder withDescription(Attributes description) { + builder.withDescription(description); + return this; + } + + @Override + public EntityBuilder withId(Attributes id) { + builder.withId(id); + return this; + } + + @Override + public void emit() { + emitter.accept(builder.build()); + } +} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/SdkEntityProvider.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/SdkEntityProvider.java new file mode 100644 index 00000000000..4197c7e12f2 --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/SdkEntityProvider.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.entities; + +import io.opentelemetry.api.incubator.entities.EntityBuilder; +import io.opentelemetry.api.incubator.entities.EntityProvider; +import io.opentelemetry.sdk.common.CompletableResultCode; +import java.util.Collection; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; + +/** The SDK implementation of {@link EntityProvider}. */ +public final class SdkEntityProvider implements EntityProvider { + private final SdkResourceSharedState state; + + SdkEntityProvider(ExecutorService executorService, Collection detectors) { + this.state = new SdkResourceSharedState(executorService); + state.beginInitialization(detectors, this); + } + + /** + * Creates a builder for SdkEntityProvider. + * + * @return The new builder. + */ + public static SdkEntityProviderBuilder builder() { + return new SdkEntityProviderBuilder(); + } + + @Override + public String toString() { + return "SdkResourceProvider{}"; + } + + @Override + public boolean removeEntity(String entityType) { + return state.removeEntity(entityType); + } + + @Override + public EntityBuilder attachOrUpdateEntity(String entityType) { + return new SdkEntityBuilder(entityType, state::addOrUpdateEntity); + } + + public void onChange(EntityListener listener) { + state.addListener(listener); + } + + /** + * Shutdown the provider. The resulting {@link CompletableResultCode} completes when all complete. + */ + public CompletableResultCode shutdown() { + return state.shutdown(); + } + + /** Close the provider. Calls {@link #shutdown()} and blocks waiting for it to complete. */ + public void close() { + shutdown().join(10, TimeUnit.SECONDS); + } +} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/SdkEntityProviderBuilder.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/SdkEntityProviderBuilder.java new file mode 100644 index 00000000000..48454d2bbe8 --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/SdkEntityProviderBuilder.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.entities; + +import io.opentelemetry.sdk.extension.incubator.entities.detectors.ServiceDetector; +import io.opentelemetry.sdk.extension.incubator.entities.detectors.TelemetrySdkDetector; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutorService; + +/** A builder for {@link SdkEntityProvider}. */ +public final class SdkEntityProviderBuilder { + private final List detectors = new ArrayList<>(); + private boolean includeDefaults = true; + // TODO - add configuraiton settings for this. + private ExecutorService executorService = new CurrentThreadExecutorService(); + + /** + * Adds a {@link ResourceDetector} that will be run when constructing this provider. + * + * @param detector The resource detector. + * @return this + */ + public SdkEntityProviderBuilder addDetector(ResourceDetector detector) { + this.detectors.add(detector); + return this; + } + + /** + * Sets the excutor service which isolates entity listeners and resource detectors. + * + * @param executorService The executor service to use for async tasks. + * @return this + */ + SdkEntityProviderBuilder setListenerExecutorService(ExecutorService executorService) { + this.executorService = executorService; + return this; + } + + /** + * Configure whether to include SDK default resoruce detection. + * + * @param include true if defaults should be used. + * @return this + */ + public SdkEntityProviderBuilder includeDefaults(boolean include) { + this.includeDefaults = include; + return this; + } + + public SdkEntityProvider build() { + // TODO - have defaults in the front? + if (includeDefaults) { + detectors.add(new ServiceDetector()); + detectors.add(new TelemetrySdkDetector()); + } + return new SdkEntityProvider(executorService, Collections.unmodifiableCollection(detectors)); + } +} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/SdkEntityState.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/SdkEntityState.java new file mode 100644 index 00000000000..de049edd46e --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/SdkEntityState.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.entities; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.resources.internal.Entity; +import javax.annotation.Nullable; + +final class SdkEntityState implements EntityState { + private final Entity delegate; + + SdkEntityState(Entity delegate) { + this.delegate = delegate; + } + + @Override + public String getType() { + return delegate.getType(); + } + + @Override + @Nullable + public String getSchemaUrl() { + return delegate.getSchemaUrl(); + } + + @Override + public Attributes getId() { + return delegate.getId(); + } + + @Override + public Attributes getDescription() { + return delegate.getDescription(); + } +} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/SdkResourceSharedState.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/SdkResourceSharedState.java new file mode 100644 index 00000000000..951bd50f8ef --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/SdkResourceSharedState.java @@ -0,0 +1,223 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.entities; + +import io.opentelemetry.api.incubator.entities.EntityProvider; +import io.opentelemetry.api.internal.GuardedBy; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.internal.ThrottlingLogger; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.resources.internal.Entity; +import io.opentelemetry.sdk.resources.internal.EntityUtil; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +/** + * This class does all state and listener management for a {@link Resource} constructed of {@link + * Entity}s. + */ +final class SdkResourceSharedState { + + // The currently advertised Resource to other SDK providers. + private final AtomicReference resource = new AtomicReference<>(Resource.empty()); + private final AtomicBoolean initialized = new AtomicBoolean(false); + private final Object writeLock = new Object(); + private final List listeners = new CopyOnWriteArrayList<>(); + private final ExecutorService listenerExecutor; + + // Our internal storage of registered entities. + @GuardedBy("writeLock") + private final ArrayList entities = new ArrayList<>(); + + private static final ThrottlingLogger logger = + new ThrottlingLogger(Logger.getLogger(SdkResourceSharedState.class.getName())); + + SdkResourceSharedState(ExecutorService listenerExecutor) { + this.listenerExecutor = listenerExecutor; + } + + /** + * Begins initializing state with the given resource detectors. + * + *

This will initialize after all resource detectors have completed, or 200 ms. + * + * @param detectors The set of resource detectors that are considered initializing. + * @param provider The entity provider for the resource detectors. + */ + @SuppressWarnings("FutureReturnValueIgnored") + void beginInitialization(Collection detectors, EntityProvider provider) { + // TODO - Should we create a different instance of entity provider for initial resource + // detection? + Collection results = new ArrayList<>(detectors.size()); + for (ResourceDetector d : detectors) { + results.add(d.report(provider)); + } + CompletableResultCode allDone = CompletableResultCode.ofAll(results); + // Ensure our blocking is done using the async model provided to use via + // ExecutorService. + listenerExecutor.submit( + () -> { + allDone.join(200, TimeUnit.MILLISECONDS); + this.initialize(); + }); + } + + /** + * Shutdown the provider. The resulting {@link CompletableResultCode} completes when all complete. + */ + CompletableResultCode shutdown() { + // TODO - Actually figure out how to wait for shutdown and deal with pending tasks. + listenerExecutor.shutdown(); + return CompletableResultCode.ofSuccess(); + } + + /** Returns the currently active resource. */ + public Resource getResource() { + + Resource result = resource.get(); + // We do this to make NullAway happy. + if (result == null) { + throw new IllegalStateException("SdkResource should never have null resource"); + } + return result; + } + + private static boolean hasSameSchemaUrl(Entity lhs, Entity rhs) { + if (lhs.getSchemaUrl() != null) { + return lhs.getSchemaUrl().equals(rhs.getSchemaUrl()); + } + return rhs.getSchemaUrl() == null; + } + + /** + * Removes an entity by type and notifies listeners. + * + * @param entityType The entity type to remove. + */ + boolean removeEntity(String entityType) { + synchronized (writeLock) { + @Nullable Entity removed = null; + for (Entity existing : entities) { + if (existing.getType().equals(entityType)) { + removed = existing; + } + } + if (removed == null) { + return false; + } + entities.remove(removed); + Resource result = EntityUtil.createResource(entities); + resource.lazySet(result); + publishEntityDelete(new SdkEntityState(removed), result); + return true; + } + } + + /** + * Adds an entity and notifies listeners. + * + *

Note: This will not add an entity on conflict. This will update the description if the + * entity already exists. + * + * @param e The entity type to add. + */ + void addOrUpdateEntity(Entity e) { + synchronized (writeLock) { + @Nullable Entity conflict = null; + for (Entity existing : entities) { + if (existing.getType().equals(e.getType())) { + conflict = existing; + } + } + Entity newState = e; + if (conflict != null) { + if (hasSameSchemaUrl(conflict, e) && conflict.getId().equals(e.getId())) { + // We can merge descriptive attributes. + entities.remove(conflict); + io.opentelemetry.sdk.resources.internal.EntityBuilder newEntity = + Entity.builder(conflict.getType()) + .withId(conflict.getId()) + .withDescription( + conflict.getDescription().toBuilder().putAll(e.getDescription()).build()); + if (conflict.getSchemaUrl() != null) { + newEntity.setSchemaUrl(conflict.getSchemaUrl()); + } + newState = newEntity.build(); + entities.add(newState); + } else { + logger.log(Level.INFO, "Ignoring new entity, conflicts with existing: " + e); + return; + } + } else { + entities.add(e); + } + Resource result = EntityUtil.createResource(entities); + resource.lazySet(result); + publishEntityStateChange(new SdkEntityState(newState), result); + } + } + + /** Mark the resource as initialized and notify listeners. */ + @SuppressWarnings("FutureReturnValueIgnored") + private void initialize() { + // Prevent writes so initialize events happen before updates, + // in the event of other issues. + synchronized (writeLock) { + Resource resource = this.resource.get(); + if (resource == null) { + // Catastrophic failure, TODO - some kind of logic error message + return; + } + // We only do this once. + if (initialized.compareAndSet(false, true)) { + for (EntityListener listener : listeners) { + listenerExecutor.submit(() -> listener.onResourceInit(resource)); + } + } + } + } + + @SuppressWarnings("FutureReturnValueIgnored") + private void publishEntityStateChange(EntityState state, Resource resource) { + if (initialized.get()) { + for (EntityListener listener : listeners) { + // We isolate listener execution via executor, if configured. + // We ignore failures on futures to avoid having one listener block others. + listenerExecutor.submit(() -> listener.onEntityState(state, resource)); + } + } + } + + @SuppressWarnings("FutureReturnValueIgnored") + private void publishEntityDelete(EntityState deleted, Resource resource) { + if (initialized.get()) { + for (EntityListener listener : listeners) { + // We isolate listener execution via executor, if configured. + // We ignore failures on futures to avoid having one listener block others. + listenerExecutor.submit(() -> listener.onEntityDelete(deleted, resource)); + } + } + } + + @SuppressWarnings("FutureReturnValueIgnored") + public void addListener(EntityListener listener) { + listeners.add(listener); + if (initialized.get()) { + // We isolate listener execution via executor, if configured. + // We ignore failures on futures to avoid having one listener block others. + listenerExecutor.submit(() -> listener.onResourceInit(getResource())); + } + } +} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/detectors/ServiceDetector.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/detectors/ServiceDetector.java new file mode 100644 index 00000000000..29e4536bbb4 --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/detectors/ServiceDetector.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.entities.detectors; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.incubator.entities.EntityProvider; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.extension.incubator.entities.ResourceDetector; +import java.util.UUID; + +/** + * Detection for {@code service} entity. + * + *

See: service + * entity + */ +public final class ServiceDetector implements ResourceDetector { + private static final String SCHEMA_URL = "https://opentelemetry.io/schemas/1.34.0"; + private static final String ENTITY_TYPE = "service"; + private static final AttributeKey SERVICE_NAME = AttributeKey.stringKey("service.name"); + private static final AttributeKey SERVICE_INSTANCE_ID = + AttributeKey.stringKey("service.instance.id"); + private static final UUID FALLBACK_INSTANCE_ID = UUID.randomUUID(); + + private static String getServiceName() { + return System.getenv().getOrDefault("OTEL_SERVICE_NAME", "unknown_service:java"); + } + + private static String getServiceInstanceId() { + // TODO - no way for users to specify a non-default. + return FALLBACK_INSTANCE_ID.toString(); + } + + @Override + public CompletableResultCode report(EntityProvider provider) { + // We only run on startup. + provider + .attachOrUpdateEntity(ENTITY_TYPE) + .setSchemaUrl(SCHEMA_URL) + .withId( + // Note: Identifying attributes MUST be provided together. + Attributes.builder() + .put(SERVICE_NAME, getServiceName()) + .put(SERVICE_INSTANCE_ID, getServiceInstanceId()) + .build()) + // TODO - Need to figure out version + .emit(); + return CompletableResultCode.ofSuccess(); + } +} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/detectors/TelemetrySdkDetector.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/detectors/TelemetrySdkDetector.java new file mode 100644 index 00000000000..458a1afddb9 --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/entities/detectors/TelemetrySdkDetector.java @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.entities.detectors; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.incubator.entities.EntityProvider; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.common.internal.OtelVersion; +import io.opentelemetry.sdk.extension.incubator.entities.ResourceDetector; + +/** + * Detection for {@code telemetry.sdk} entity. + * + *

See: teleemtry.sdk + * entity + */ +public final class TelemetrySdkDetector implements ResourceDetector { + private static final String SCHEMA_URL = "https://opentelemetry.io/schemas/1.34.0"; + private static final String ENTITY_TYPE = "telemetry.sdk"; + private static final AttributeKey TELEMETRY_SDK_LANGUAGE = + AttributeKey.stringKey("telemetry.sdk.language"); + private static final AttributeKey TELEMETRY_SDK_NAME = + AttributeKey.stringKey("telemetry.sdk.name"); + private static final AttributeKey TELEMETRY_SDK_VERSION = + AttributeKey.stringKey("telemetry.sdk.version"); + + @Override + public CompletableResultCode report(EntityProvider provider) { + provider + .attachOrUpdateEntity(ENTITY_TYPE) + .setSchemaUrl(SCHEMA_URL) + .withId( + Attributes.builder() + .put(TELEMETRY_SDK_NAME, "opentelemetry") + .put(TELEMETRY_SDK_LANGUAGE, "java") + .build()) + .withDescription( + Attributes.builder().put(TELEMETRY_SDK_VERSION, OtelVersion.VERSION).build()) + .emit(); + return CompletableResultCode.ofSuccess(); + } +} diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/TestExtendedOpenTelemetrySdk.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/TestExtendedOpenTelemetrySdk.java new file mode 100644 index 00000000000..80c8321ab2a --- /dev/null +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/TestExtendedOpenTelemetrySdk.java @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; +import org.junit.jupiter.api.Test; + +class TestExtendedOpenTelemetrySdk { + private final InMemoryMetricReader sdkMeterReader = InMemoryMetricReader.create(); + + @Test + void endToEnd() { + ExtendedOpenTelemetrySdk otel = + ExtendedOpenTelemetrySdk.builder() + .withMeterProvider(builder -> builder.registerMetricReader(sdkMeterReader)) + .build(); + // Generate our first entity. + otel.getEntityProvider() + .attachOrUpdateEntity("test") + .withId(Attributes.builder().put("test.id", 1).build()) + .emit(); + // Write a metric. + Meter meter = otel.getMeterProvider().get("test.scope"); + LongCounter counter = meter.counterBuilder("testCounter").build(); + counter.add(1, Attributes.empty()); + + // Verify we see the entity and the metric. + assertThat(sdkMeterReader.collectAllMetrics()) + .anySatisfy( + metric -> + assertThat(metric) + .hasName("testCounter") + .hasResourceSatisfying( + resource -> + resource.hasAttributesSatisfying( + attributes -> assertThat(attributes).containsEntry("test.id", 1)))); + + // Now update the resource and check the point. + otel.getEntityProvider() + .attachOrUpdateEntity("test2") + .withId(Attributes.builder().put("test2.id", 1).build()) + .emit(); + // Verify we see the new entity and the metric. + assertThat(sdkMeterReader.collectAllMetrics()) + .anySatisfy( + metric -> + assertThat(metric) + .hasName("testCounter") + .hasResourceSatisfying( + resource -> + resource.hasAttributesSatisfying( + attributes -> + assertThat(attributes) + .containsEntry("test.id", 1) + .containsEntry("test2.id", 1)))); + } +} diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/entities/TestEntityProvider.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/entities/TestEntityProvider.java new file mode 100644 index 00000000000..6df28a8ff16 --- /dev/null +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/entities/TestEntityProvider.java @@ -0,0 +1,224 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.entities; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.incubator.entities.EntityProvider; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.resources.internal.EntityUtil; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +class TestEntityProvider { + @Test + void defaults_includeServiceAndSdk() { + LatestResourceSupplier resource = new LatestResourceSupplier(200); + SdkEntityProvider provider = SdkEntityProvider.builder().includeDefaults(true).build(); + provider.onChange(resource); + + assertThat(resource.get().getAttributes()) + .containsKey("service.name") + .containsKey("service.instance.id") + .containsKey("telemetry.sdk.language") + .containsKey("telemetry.sdk.name") + .containsKey("telemetry.sdk.version"); + assertThat(resource.get().getSchemaUrl()).isEqualTo("https://opentelemetry.io/schemas/1.34.0"); + + assertThat(EntityUtil.getEntities(resource.get())) + .satisfiesExactlyInAnyOrder( + e -> assertThat(e.getType()).isEqualTo("service"), + e -> assertThat(e.getType()).isEqualTo("telemetry.sdk")); + } + + @Test + void resource_updatesDescription() { + LatestResourceSupplier resource = new LatestResourceSupplier(200); + SdkEntityProvider provider = SdkEntityProvider.builder().includeDefaults(false).build(); + provider.onChange(resource); + + provider + .attachOrUpdateEntity("one") + .setSchemaUrl("one") + .withId(Attributes.builder().put("one.id", 1).build()) + .emit(); + + provider + .attachOrUpdateEntity("one") + .setSchemaUrl("one") + .withId(Attributes.builder().put("one.id", 1).build()) + .withDescription(Attributes.builder().put("one.desc", "desc").build()) + .emit(); + + assertThat(resource.get().getAttributes()) + .hasSize(2) + .containsKey("one.id") + .containsKey("one.desc"); + } + + @Test + void resource_ignoresNewIds() { + LatestResourceSupplier resource = new LatestResourceSupplier(200); + SdkEntityProvider provider = SdkEntityProvider.builder().includeDefaults(false).build(); + provider.onChange(resource); + + provider + .attachOrUpdateEntity("one") + .setSchemaUrl("one") + .withId(Attributes.builder().put("one.id", 1).build()) + .emit(); + + provider + .attachOrUpdateEntity("one") + .setSchemaUrl("one") + .withId(Attributes.builder().put("one.id", 2).build()) + .withDescription(Attributes.builder().put("one.desc", "desc").build()) + .emit(); + + assertThat(resource.get().getAttributes()).hasSize(1).containsKey("one.id"); + } + + @Test + void resource_ignoresNewSchemaUrl() { + SdkEntityProvider provider = SdkEntityProvider.builder().includeDefaults(false).build(); + LatestResourceSupplier resource = new LatestResourceSupplier(200); + provider.onChange(resource); + + provider + .attachOrUpdateEntity("one") + .setSchemaUrl("one") + .withId(Attributes.builder().put("one.id", 1).build()) + .emit(); + + provider + .attachOrUpdateEntity("one") + .setSchemaUrl("two") + .withId(Attributes.builder().put("one.id", 1).build()) + .withDescription(Attributes.builder().put("one.desc", "desc").build()) + .emit(); + + assertThat(resource.get().getAttributes()).hasSize(1).containsKey("one.id"); + } + + @Test + void resource_addsNewEntity() { + SdkEntityProvider provider = SdkEntityProvider.builder().includeDefaults(false).build(); + LatestResourceSupplier resource = new LatestResourceSupplier(200); + provider.onChange(resource); + + provider + .attachOrUpdateEntity("one") + .setSchemaUrl("one") + .withId(Attributes.builder().put("one.id", 1).build()) + .emit(); + + provider + .attachOrUpdateEntity("two") + .setSchemaUrl("two") + .withId(Attributes.builder().put("two.id", 2).build()) + .emit(); + + assertThat(resource.get().getAttributes()) + .hasSize(2) + .containsKey("one.id") + .containsKey("two.id"); + } + + @Test + void resource_removesEntity() { + SdkEntityProvider provider = SdkEntityProvider.builder().includeDefaults(false).build(); + LatestResourceSupplier resource = new LatestResourceSupplier(200); + provider.onChange(resource); + + provider + .attachOrUpdateEntity("one") + .setSchemaUrl("one") + .withId(Attributes.builder().put("one.id", 1).build()) + .emit(); + + assertThat(resource.get().getAttributes()).hasSize(1).containsKey("one.id"); + + assertThat(provider.removeEntity("one")).isTrue(); + assertThat(resource.get().getAttributes()).isEmpty(); + } + + @Test + void entityListener_notifiesOnAdd() { + SdkEntityProvider provider = SdkEntityProvider.builder().includeDefaults(false).build(); + + EntityListener listener = mock(EntityListener.class); + provider.onChange(listener); + + provider + .attachOrUpdateEntity("one") + .setSchemaUrl("one") + .withId(Attributes.builder().put("one.id", 1).build()) + .emit(); + ArgumentCaptor entityCapture = ArgumentCaptor.forClass(EntityState.class); + ArgumentCaptor resourceCapture = ArgumentCaptor.forClass(Resource.class); + verify(listener, times(1)).onEntityState(entityCapture.capture(), resourceCapture.capture()); + assertThat(entityCapture.getValue().getType()).isEqualTo("one"); + assertThat(resourceCapture.getValue().getAttributes()).hasSize(1).containsKey("one.id"); + } + + @Test + void entityListener_notifiesOnRemove() { + SdkEntityProvider provider = SdkEntityProvider.builder().includeDefaults(false).build(); + provider + .attachOrUpdateEntity("one") + .setSchemaUrl("one") + .withId(Attributes.builder().put("one.id", 1).build()) + .emit(); + EntityListener listener = mock(EntityListener.class); + provider.onChange(listener); + + provider.removeEntity("one"); + ArgumentCaptor entityCapture = ArgumentCaptor.forClass(EntityState.class); + ArgumentCaptor resourceCapture = ArgumentCaptor.forClass(Resource.class); + verify(listener, times(1)).onEntityDelete(entityCapture.capture(), resourceCapture.capture()); + assertThat(entityCapture.getValue().getType()).isEqualTo("one"); + assertThat(resourceCapture.getValue().getAttributes()).isEmpty(); + } + + @Test + void entityListener_initializesAfterTimeout() throws InterruptedException { + // Because we're using same-thread-executor, we know entity provider blocked + // until everything started up. + // Instead we fork the resource detection. + ExecutorService service = Executors.newSingleThreadExecutor(); + ResourceDetector forever = + (EntityProvider provider) -> { + // This will never complete. + return new CompletableResultCode(); + }; + SdkEntityProvider provider = + SdkEntityProvider.builder() + .setListenerExecutorService(service) + .includeDefaults(false) + .addDetector(forever) + .build(); + EntityListener listener = mock(EntityListener.class); + provider.onChange(listener); + // Ensure we haven't seen initialization yet (If this is flaky, remove this) + verify(listener, never()).onResourceInit(any()); + + // Wait long enough that initialization has happened. + Thread.sleep(500); + ArgumentCaptor resourceCapture = ArgumentCaptor.forClass(Resource.class); + verify(listener, times(1)).onResourceInit(resourceCapture.capture()); + assertThat(resourceCapture.getValue().getAttributes()).isEmpty(); + } +} diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/entities/TestLatestResourceSupplier.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/entities/TestLatestResourceSupplier.java new file mode 100644 index 00000000000..d2d5afee418 --- /dev/null +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/entities/TestLatestResourceSupplier.java @@ -0,0 +1,24 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.entities; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; + +import io.opentelemetry.sdk.resources.Resource; +import org.junit.jupiter.api.Test; + +class TestLatestResourceSupplier { + + @Test + void getResource_defaultsAfterTimeout() { + LatestResourceSupplier supplier = new LatestResourceSupplier(0); + // This will block. We haven't registered our listener, so + // we never get an initialize event. We should still get + // a default resource. + Resource resource = supplier.get(); + assertThat(resource.getAttributes()).containsKey("service.name"); + } +} diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationCreateTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationCreateTest.java index 2429ae150a2..7057d17c099 100644 --- a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationCreateTest.java +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationCreateTest.java @@ -165,7 +165,7 @@ void create_ModelCustomizer() { DeclarativeConfigurationCreateTest.class.getClassLoader())); assertThat(sdk.toString()) .contains( - "resource=Resource{schemaUrl=null, attributes={" + "resource=Resource{schemaUrl=null, rawAttributes={" + "color=\"blue\", " + "foo=\"bar\", " + "service.name=\"unknown_service:java\", " diff --git a/sdk/all/src/main/java/io/opentelemetry/sdk/IncubatingUtil.java b/sdk/all/src/main/java/io/opentelemetry/sdk/IncubatingUtil.java new file mode 100644 index 00000000000..0ada6e9f55d --- /dev/null +++ b/sdk/all/src/main/java/io/opentelemetry/sdk/IncubatingUtil.java @@ -0,0 +1,22 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk; + +import javax.annotation.Nullable; + +/** + * Utilities for interacting with {@code io.opentelemetry:opentelemetry-api-incubator}, which is not + * guaranteed to be present on the classpath. For all methods, callers MUST first separately + * reflectively confirm that the incubator is available on the classpath. + */ +final class IncubatingUtil { + private IncubatingUtil() {} + + @Nullable + static OpenTelemetrySdk createExtendedOpenTelemetrySdk() { + return null; + } +} diff --git a/sdk/all/src/test/java/io/opentelemetry/sdk/OpenTelemetrySdkTest.java b/sdk/all/src/test/java/io/opentelemetry/sdk/OpenTelemetrySdkTest.java index 8fb5eafdf47..c6a48a0dbad 100644 --- a/sdk/all/src/test/java/io/opentelemetry/sdk/OpenTelemetrySdkTest.java +++ b/sdk/all/src/test/java/io/opentelemetry/sdk/OpenTelemetrySdkTest.java @@ -406,34 +406,13 @@ void stringRepresentation() { .setPropagators(ContextPropagators.create(propagator)) .build(); + // Test that toString delegates to underlying classes, and make sure their toString is also + // nice. assertThat(sdk.toString()) - .isEqualTo( - "OpenTelemetrySdk{" - + "tracerProvider=SdkTracerProvider{" - + "clock=SystemClock{}, " - + "idGenerator=RandomIdGenerator{}, " - + "resource=Resource{schemaUrl=null, attributes={service.name=\"otel-test\"}}, " - + "spanLimitsSupplier=SpanLimitsValue{maxNumberOfAttributes=128, maxNumberOfEvents=128, maxNumberOfLinks=128, maxNumberOfAttributesPerEvent=128, maxNumberOfAttributesPerLink=128, maxAttributeValueLength=2147483647}, " - + "sampler=ParentBased{root:AlwaysOnSampler,remoteParentSampled:AlwaysOnSampler,remoteParentNotSampled:AlwaysOffSampler,localParentSampled:AlwaysOnSampler,localParentNotSampled:AlwaysOffSampler}, " - + "spanProcessor=SimpleSpanProcessor{spanExporter=MultiSpanExporter{spanExporters=[MockSpanExporter{}, MockSpanExporter{}]}, exportUnsampledSpans=false}, " - + "tracerConfigurator=ScopeConfiguratorImpl{conditions=[]}" - + "}, " - + "meterProvider=SdkMeterProvider{" - + "clock=SystemClock{}, " - + "resource=Resource{schemaUrl=null, attributes={service.name=\"otel-test\"}}, " - + "metricReaders=[PeriodicMetricReader{exporter=MockMetricExporter{}, intervalNanos=60000000000}], " - + "metricProducers=[], " - + "views=[RegisteredView{instrumentSelector=InstrumentSelector{instrumentName=instrument}, view=View{name=new-instrument, aggregation=DefaultAggregation, attributesProcessor=NoopAttributesProcessor{}, cardinalityLimit=2000}}], " - + "meterConfigurator=ScopeConfiguratorImpl{conditions=[]}" - + "}, " - + "loggerProvider=SdkLoggerProvider{" - + "clock=SystemClock{}, " - + "resource=Resource{schemaUrl=null, attributes={service.name=\"otel-test\"}}, " - + "logLimits=LogLimits{maxNumberOfAttributes=128, maxAttributeValueLength=2147483647}, " - + "logRecordProcessor=SimpleLogRecordProcessor{logRecordExporter=MultiLogRecordExporter{logRecordExporters=[MockLogRecordExporter{}, MockLogRecordExporter{}]}}, " - + "loggerConfigurator=ScopeConfiguratorImpl{conditions=[]}" - + "}, " - + "propagators=DefaultContextPropagators{textMapPropagator=MockTextMapPropagator{}}" - + "}"); + .matches("OpenTelemetrySdk\\{.*}") + .matches("OpenTelemetrySdk\\{tracerProvider=SdkTracerProvider\\{.*}.*}") + .matches("OpenTelemetrySdk\\{.*, meterProvider=SdkMeterProvider\\{.*}.*}") + .matches("OpenTelemetrySdk\\{.*, loggerProvider=SdkLoggerProvider\\{.*}.*}") + .matches("OpenTelemetrySdk\\{.*, propagators=DefaultContextPropagators\\{.*}}"); } } diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/Resource.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/Resource.java index c406ef87a5e..e72e5db90a4 100644 --- a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/Resource.java +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/Resource.java @@ -9,11 +9,13 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.api.internal.StringUtils; -import io.opentelemetry.api.internal.Utils; import io.opentelemetry.sdk.common.internal.OtelVersion; +import io.opentelemetry.sdk.resources.internal.AttributeCheckUtil; +import io.opentelemetry.sdk.resources.internal.Entity; +import io.opentelemetry.sdk.resources.internal.EntityUtil; +import java.util.Collection; +import java.util.Collections; import java.util.Objects; -import java.util.logging.Logger; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; @@ -24,8 +26,6 @@ @Immutable @AutoValue public abstract class Resource { - private static final Logger logger = Logger.getLogger(Resource.class.getName()); - private static final AttributeKey SERVICE_NAME = AttributeKey.stringKey("service.name"); private static final AttributeKey TELEMETRY_SDK_LANGUAGE = AttributeKey.stringKey("telemetry.sdk.language"); @@ -34,13 +34,6 @@ public abstract class Resource { private static final AttributeKey TELEMETRY_SDK_VERSION = AttributeKey.stringKey("telemetry.sdk.version"); - private static final int MAX_LENGTH = 255; - private static final String ERROR_MESSAGE_INVALID_CHARS = - " should be a ASCII string with a length greater than 0 and not exceed " - + MAX_LENGTH - + " characters."; - private static final String ERROR_MESSAGE_INVALID_VALUE = - " should be a ASCII string with a length not exceed " + MAX_LENGTH + " characters."; private static final Resource EMPTY = create(Attributes.empty()); private static final Resource TELEMETRY_SDK; @@ -91,7 +84,7 @@ public static Resource empty() { * @return a {@code Resource}. * @throws NullPointerException if {@code attributes} is null. * @throws IllegalArgumentException if attribute key or attribute value is not a valid printable - * ASCII string or exceed {@link #MAX_LENGTH} characters. + * ASCII string or exceed {@link AttributeCheckUtil#MAX_LENGTH} characters. */ public static Resource create(Attributes attributes) { return create(attributes, null); @@ -105,11 +98,27 @@ public static Resource create(Attributes attributes) { * @return a {@code Resource}. * @throws NullPointerException if {@code attributes} is null. * @throws IllegalArgumentException if attribute key or attribute value is not a valid printable - * ASCII string or exceed {@link #MAX_LENGTH} characters. + * ASCII string or exceed {@link AttributeCheckUtil#MAX_LENGTH} characters. */ public static Resource create(Attributes attributes, @Nullable String schemaUrl) { - checkAttributes(Objects.requireNonNull(attributes, "attributes")); - return new AutoValue_Resource(schemaUrl, attributes); + return create(attributes, schemaUrl, Collections.emptyList()); + } + + /** + * Returns a {@link Resource}. + * + * @param attributes a map of {@link Attributes} that describe the resource. + * @param schemaUrl The URL of the OpenTelemetry schema used to create this Resource. + * @param entities The set of detected {@link Entity}s that participate in this resource. + * @return a {@code Resource}. + * @throws NullPointerException if {@code attributes} is null. + * @throws IllegalArgumentException if attribute key or attribute value is not a valid printable + * ASCII string or exceed {@link AttributeCheckUtil#MAX_LENGTH} characters. + */ + static Resource create( + Attributes attributes, @Nullable String schemaUrl, Collection entities) { + AttributeCheckUtil.checkAttributes(Objects.requireNonNull(attributes, "attributes")); + return new AutoValue_Resource(schemaUrl, attributes, entities); } /** @@ -121,12 +130,38 @@ public static Resource create(Attributes attributes, @Nullable String schemaUrl) @Nullable public abstract String getSchemaUrl(); + /** + * Returns a map of attributes that describe the resource, not associated with entites. + * + * @return a map of attributes. + */ + abstract Attributes getRawAttributes(); + + /** + * Returns a collectoion of associated entities. + * + * @return a collection of entities. + */ + abstract Collection getEntities(); + /** * Returns a map of attributes that describe the resource. * * @return a map of attributes. */ - public abstract Attributes getAttributes(); + // @Memoized - This breaks nullaway. + public Attributes getAttributes() { + AttributesBuilder result = Attributes.builder(); + getEntities() + .forEach( + e -> { + result.putAll(e.getId()); + result.putAll(e.getDescription()); + }); + // In merge rules, raw comes last, so we return these last. + result.putAll(getRawAttributes()); + return result.build(); + } /** * Returns the value for a given resource attribute key. @@ -146,63 +181,7 @@ public T getAttribute(AttributeKey key) { * @return the newly merged {@code Resource}. */ public Resource merge(@Nullable Resource other) { - if (other == null || other == EMPTY) { - return this; - } - - AttributesBuilder attrBuilder = Attributes.builder(); - attrBuilder.putAll(this.getAttributes()); - attrBuilder.putAll(other.getAttributes()); - - if (other.getSchemaUrl() == null) { - return create(attrBuilder.build(), getSchemaUrl()); - } - if (getSchemaUrl() == null) { - return create(attrBuilder.build(), other.getSchemaUrl()); - } - if (!other.getSchemaUrl().equals(getSchemaUrl())) { - logger.info( - "Attempting to merge Resources with different schemaUrls. " - + "The resulting Resource will have no schemaUrl assigned. Schema 1: " - + getSchemaUrl() - + " Schema 2: " - + other.getSchemaUrl()); - // currently, behavior is undefined if schema URLs don't match. In the future, we may - // apply schema transformations if possible. - return create(attrBuilder.build(), null); - } - return create(attrBuilder.build(), getSchemaUrl()); - } - - private static void checkAttributes(Attributes attributes) { - attributes.forEach( - (key, value) -> { - Utils.checkArgument( - isValidAndNotEmpty(key), "Attribute key" + ERROR_MESSAGE_INVALID_CHARS); - Objects.requireNonNull(value, "Attribute value" + ERROR_MESSAGE_INVALID_VALUE); - }); - } - - /** - * Determines whether the given {@code String} is a valid printable ASCII string with a length not - * exceed {@link #MAX_LENGTH} characters. - * - * @param name the name to be validated. - * @return whether the name is valid. - */ - private static boolean isValid(String name) { - return name.length() <= MAX_LENGTH && StringUtils.isPrintableString(name); - } - - /** - * Determines whether the given {@code String} is a valid printable ASCII string with a length - * greater than 0 and not exceed {@link #MAX_LENGTH} characters. - * - * @param name the name to be validated. - * @return whether the name is valid. - */ - private static boolean isValidAndNotEmpty(AttributeKey name) { - return !name.getKey().isEmpty() && isValid(name.getKey()); + return EntityUtil.merge(this, other); } /** diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/ResourceBuilder.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/ResourceBuilder.java index 9963eeaf541..f2abb8d8b20 100644 --- a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/ResourceBuilder.java +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/ResourceBuilder.java @@ -8,7 +8,14 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.sdk.resources.internal.Entity; +import io.opentelemetry.sdk.resources.internal.EntityUtil; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; import java.util.function.Predicate; +import java.util.stream.Collectors; import javax.annotation.Nullable; /** @@ -20,6 +27,7 @@ public class ResourceBuilder { private final AttributesBuilder attributesBuilder = Attributes.builder(); + private final List entities = new ArrayList<>(); @Nullable private String schemaUrl; /** @@ -194,6 +202,32 @@ public ResourceBuilder setSchemaUrl(String schemaUrl) { /** Create the {@link Resource} from this. */ public Resource build() { - return Resource.create(attributesBuilder.build(), schemaUrl); + // What checks should we do on "real" resource here? + // Derive schemaUrl from entitiy, if able. + if (schemaUrl == null) { + Set entitySchemas = + entities.stream().map(Entity::getSchemaUrl).collect(Collectors.toSet()); + if (entitySchemas.size() == 1) { + // Updated Entities use same schema, we can preserve it. + schemaUrl = entitySchemas.iterator().next(); + } + } + + // TODO - here we deal with conflicts between entities and raw attributes. + // When adding an entity, we remove any raw attributes it may conflict with. + this.attributesBuilder.removeIf(key -> EntityUtil.hasAttributeKey(this.entities, key)); + return Resource.create(attributesBuilder.build(), schemaUrl, entities); + } + + /** Appends a new entity on to the end of the list of entities. */ + ResourceBuilder add(Entity e) { + this.entities.add(e); + return this; + } + + /** Appends a new collection of entities on to the end of the list of entities. */ + ResourceBuilder addAll(Collection entities) { + this.entities.addAll(entities); + return this; } } diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/AttributeCheckUtil.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/AttributeCheckUtil.java new file mode 100644 index 00000000000..966c5b58277 --- /dev/null +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/AttributeCheckUtil.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.resources.internal; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.internal.StringUtils; +import io.opentelemetry.api.internal.Utils; +import java.util.Objects; + +/** + * Helpers to check resource attributes. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class AttributeCheckUtil { + private AttributeCheckUtil() {} + + // Note: Max length is actually configurable by specification. + private static final int MAX_LENGTH = 255; + private static final String ERROR_MESSAGE_INVALID_CHARS = + " should be a ASCII string with a length greater than 0 and not exceed " + + MAX_LENGTH + + " characters."; + private static final String ERROR_MESSAGE_INVALID_VALUE = + " should be a ASCII string with a length not exceed " + MAX_LENGTH + " characters."; + + /** Determine if the set of attributes if valid for Resource / Entity. */ + public static void checkAttributes(Attributes attributes) { + attributes.forEach( + (key, value) -> { + Utils.checkArgument( + isValidAndNotEmpty(key), "Attribute key" + ERROR_MESSAGE_INVALID_CHARS); + Objects.requireNonNull(value, "Attribute value" + ERROR_MESSAGE_INVALID_VALUE); + }); + } + + /** + * Determines whether the given {@code String} is a valid printable ASCII string with a length + * greater than 0 and not exceed {@link #MAX_LENGTH} characters. + * + * @param name the name to be validated. + * @return whether the name is valid. + */ + public static boolean isValidAndNotEmpty(AttributeKey name) { + return !name.getKey().isEmpty() && isValid(name.getKey()); + } + + /** + * Determines whether the given {@code String} is a valid printable ASCII string with a length not + * exceed {@link #MAX_LENGTH} characters. + * + * @param name the name to be validated. + * @return whether the name is valid. + */ + public static boolean isValid(String name) { + return name.length() <= MAX_LENGTH && StringUtils.isPrintableString(name); + } +} diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/Entity.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/Entity.java new file mode 100644 index 00000000000..40cf63e126b --- /dev/null +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/Entity.java @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.resources.internal; + +import io.opentelemetry.api.common.Attributes; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * Entity represents an object of interest associated with produced telemetry: traces, metrics or + * logs. + * + *

For example, telemetry produced using OpenTelemetry SDK is normally associated with a Service + * entity. Similarly, OpenTelemetry defines system metrics for a host. The Host is the entity we + * want to associate metrics with in this case. + * + *

Entities may be also associated with produced telemetry indirectly. For example a service that + * produces telemetry is also related with a process in which the service runs, so we say that the + * Service entity is related to the Process entity. The process normally also runs on a host, so we + * say that the Process entity is related to the Host entity. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +@Immutable +public interface Entity { + /** + * Returns the entity type string of this entity. Must not be null. + * + * @return the entity type. + */ + String getType(); + + /** + * Returns a map of attributes that identify the entity. + * + * @return the entity identity. + */ + Attributes getId(); + + /** + * Returns a map of attributes that describe the entity. + * + * @return the entity description. + */ + Attributes getDescription(); + + /** + * Returns the URL of the OpenTelemetry schema used by this resource. May be null if this entity + * does not abide by schema conventions (i.e. is custom). + * + * @return An OpenTelemetry schema URL. + */ + @Nullable + String getSchemaUrl(); + + /** + * Returns a new {@link EntityBuilder} instance populated with the data of this {@link Entity}. + */ + EntityBuilder toBuilder(); + + /** + * Returns a new {@link EntityBuilder} instance for creating arbitrary {@link Entity}. + * + * @param entityType the entity type string of this entity. + */ + public static EntityBuilder builder(String entityType) { + return SdkEntity.builder(entityType); + } +} diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/EntityBuilder.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/EntityBuilder.java new file mode 100644 index 00000000000..bb2b21c998b --- /dev/null +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/EntityBuilder.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.resources.internal; + +import io.opentelemetry.api.common.Attributes; + +/** + * A builder of {@link Entity} that allows to add identifying or descriptive {@link Attributes}, as + * well as type and schema_url. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public interface EntityBuilder { + /** + * Assign an OpenTelemetry schema URL to the resulting Entity. + * + * @param schemaUrl The URL of the OpenTelemetry schema being used to create this Entity. + * @return this + */ + EntityBuilder setSchemaUrl(String schemaUrl); + + /** + * Modify the descriptive attributes of this Entity. + * + * @param description The attributes that describe the Entity. + * @return this + */ + EntityBuilder withDescription(Attributes description); + + /** + * Modify the identifying attributes of this Entity. + * + * @param id The identifying attributes. + * @return this + */ + EntityBuilder withId(Attributes id); + + /** Create the {@link Entity} from this. */ + Entity build(); +} diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/EntityUtil.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/EntityUtil.java new file mode 100644 index 00000000000..d2379f1a4e0 --- /dev/null +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/EntityUtil.java @@ -0,0 +1,311 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.resources.internal; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.resources.ResourceBuilder; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import javax.annotation.Nullable; + +/** + * Helper class for dealing with Entities. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class EntityUtil { + private static final Logger logger = Logger.getLogger(EntityUtil.class.getName()); + + private EntityUtil() {} + + /** + * Constructs a new {@link Resource} with Entity support. + * + * @param entities The set of entities the resource needs. + * @return A constructed resource. + */ + public static Resource createResource(Collection entities) { + return createResourceRaw( + Attributes.empty(), EntityUtil.mergeResourceSchemaUrl(entities, null, null), entities); + } + + /** + * Constructs a new {@link Resource} with Entity support. + * + * @param attributes The raw attributes for the resource. + * @param schemaUrl The schema url for the resource. + * @param entities The set of entities the resource needs. + * @return A constructed resource. + */ + static Resource createResourceRaw( + Attributes attributes, @Nullable String schemaUrl, Collection entities) { + try { + Method method = + Resource.class.getDeclaredMethod( + "create", Attributes.class, String.class, Collection.class); + if (method != null) { + method.setAccessible(true); + Object result = method.invoke(null, attributes, schemaUrl, entities); + if (result instanceof Resource) { + return (Resource) result; + } + } + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + logger.log(Level.WARNING, "Attempting to use entities with unsupported resource", e); + } + // Fall back to non-entity behavior? + logger.log(Level.WARNING, "Attempting to use entities with unsupported resource"); + return Resource.empty(); + } + + /** Appends a new entity on to the end of the list of entities. */ + public static ResourceBuilder addEntity(ResourceBuilder rb, Entity e) { + try { + Method method = ResourceBuilder.class.getDeclaredMethod("add", Entity.class); + if (method != null) { + method.setAccessible(true); + method.invoke(rb, e); + } + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) { + logger.log(Level.WARNING, "Attempting to use entities with unsupported resource", ex); + } + return rb; + } + + /** Appends a new collection of entities on to the end of the list of entities. */ + public static ResourceBuilder addAllEntity(ResourceBuilder rb, Collection e) { + try { + Method method = ResourceBuilder.class.getDeclaredMethod("addAll", Collection.class); + if (method != null) { + method.setAccessible(true); + method.invoke(rb, e); + } + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) { + logger.log(Level.WARNING, "Attempting to use entities with unsupported resource", ex); + } + return rb; + } + + /** + * Returns a collectoion of associated entities. + * + * @return a collection of entities. + */ + @SuppressWarnings("unchecked") + public static Collection getEntities(Resource r) { + try { + Method method = Resource.class.getDeclaredMethod("getEntities"); + if (method != null) { + method.setAccessible(true); + return (Collection) method.invoke(r); + } + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + logger.log(Level.WARNING, "Attempting to use entities with unsupported resource", e); + } + return Collections.emptyList(); + } + + /** + * Returns a map of attributes that describe the resource, not associated with entites. + * + * @return a map of attributes. + */ + public static Attributes getRawAttributes(Resource r) { + try { + Method method = Resource.class.getDeclaredMethod("getRawAttributes"); + if (method != null) { + method.setAccessible(true); + return (Attributes) method.invoke(r); + } + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + logger.log(Level.WARNING, "Attempting to use entities with unsupported resource", e); + } + return Attributes.empty(); + } + + /** Returns true if any entity in the collection has the attribute key, in id or description. */ + public static boolean hasAttributeKey(Collection entities, AttributeKey key) { + return entities.stream() + .anyMatch( + e -> e.getId().asMap().containsKey(key) || e.getDescription().asMap().containsKey(key)); + } + + /** Decides on a final SchemaURL for OTLP Resource based on entities chosen. */ + @Nullable + static String mergeResourceSchemaUrl( + Collection entities, @Nullable String baseUrl, @Nullable String nextUrl) { + // Check if entities all share the same URL. + Set entitySchemas = + entities.stream().map(Entity::getSchemaUrl).collect(Collectors.toSet()); + // If we have no entities, we preserve previous schema url behavior. + String result = baseUrl; + if (entitySchemas.size() == 1) { + // Updated Entities use same schema, we can preserve it. + result = entitySchemas.iterator().next(); + } else if (entitySchemas.size() > 1) { + // Entities use different schemas, resource must treat this as no schema_url. + result = null; + } + + // If schema url of merging resource is null, we use our current result. + if (nextUrl == null) { + return result; + } + // When there are no entities, we use old schema url merge behavior + if (result == null && entities.isEmpty()) { + return nextUrl; + } + if (!nextUrl.equals(result)) { + logger.info( + "Attempting to merge Resources with different schemaUrls. " + + "The resulting Resource will have no schemaUrl assigned. Schema 1: " + + baseUrl + + " Schema 2: " + + nextUrl); + return null; + } + return result; + } + + /** + * Merges "loose" attributes on resource, removing those which conflict with the set of entities. + * + * @param base loose attributes from base resource + * @param additional additional attributes to add to the resource. + * @param entities the set of entites on the resource. + * @return the new set of raw attributes for Resource and the set of conflicting entities that + * MUST NOT be reported on OTLP resource. + */ + @SuppressWarnings("unchecked") + static final RawAttributeMergeResult mergeRawAttributes( + Attributes base, Attributes additional, Collection entities) { + AttributesBuilder result = base.toBuilder(); + // We know attribute conflicts were handled perviously on the resource, so + // This needs to account for entity merge of new entities, and remove raw + // attributes that would have been removed with new entities. + result.removeIf(key -> hasAttributeKey(entities, key)); + // For every "raw" attribute on the other resource, we merge into the + // resource, but check for entity conflicts from previous entities. + ArrayList conflicts = new ArrayList<>(); + if (!additional.isEmpty()) { + additional.forEach( + (key, value) -> { + for (Entity e : entities) { + if (e.getId().get(key) != null || e.getDescription().get(key) != null) { + // Remove the entity and push all attributes as raw, + // we have an override. + conflicts.add(e); + result.putAll(e.getId()).putAll(e.getDescription()); + } + } + result.put((AttributeKey) key, value); + }); + } + return RawAttributeMergeResult.create(result.build(), conflicts); + } + + /** + * Merges entities according to specification rules. + * + * @param base the initial set of entities. + * @param additional Additional entities to merge with base set. + * @return A new set of entities with no duplicate types. + */ + static Collection mergeEntities(Collection base, Collection additional) { + if (base.isEmpty()) { + return additional; + } + if (additional.isEmpty()) { + return base; + } + Map entities = new HashMap<>(); + base.forEach(e -> entities.put(e.getType(), e)); + for (Entity e : additional) { + if (!entities.containsKey(e.getType())) { + entities.put(e.getType(), e); + } else { + Entity old = entities.get(e.getType()); + // 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 (old.getSchemaUrl() == null || !old.getSchemaUrl().equals(e.getSchemaUrl())) { + logger.info( + "Discovered conflicting entities. Entity [" + + old.getType() + + "] has different schema url [" + + old.getSchemaUrl() + + "], new entity with schema url[" + + e.getSchemaUrl() + + "] is dropped."); + } else if (!old.getId().equals(e.getId())) { + // If the entity identity is different: drop the new entity d'. + logger.info( + "Discovered conflicting entities. Entity [" + + old.getType() + + "] has identity [" + + old.getId() + + "], new entity [" + + e.getId() + + "] is dropped."); + } else { + // If the entity identity and schema_url are the same, merge the descriptive attributes + // of d' into e': + // For each descriptive attribute da' in d' + // If da'.key does not exist in e', then add da' to ei + // otherwise, ignore. + Entity next = + old.toBuilder() + .withDescription( + Attributes.builder() + .putAll(e.getDescription()) + .putAll(old.getDescription()) + .build()) + .build(); + entities.put(next.getType(), next); + } + } + } + return entities.values(); + } + + /** + * Returns a new, merged {@link Resource} by merging the {@code base} {@code Resource} with the + * {@code next} {@code Resource}. In case of a collision, the "next" {@code Resource} takes + * precedence. + * + * @param base the {@code Resource} into which we merge new values. + * @param next the {@code Resource} that will be merged with {@code base}. + * @return the newly merged {@code Resource}. + */ + public static Resource merge(Resource base, @Nullable Resource next) { + if (next == null || next == Resource.empty()) { + return base; + } + // Merge Algorithm from + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/oteps/entities/0264-resource-and-entities.md#entity-merging-and-resource + Collection entities = EntityUtil.mergeEntities(getEntities(base), getEntities(next)); + RawAttributeMergeResult attributeResult = + EntityUtil.mergeRawAttributes(getRawAttributes(base), getRawAttributes(next), entities); + // Remove entities that are conflicting with raw attributes, and therefore in an unknown state. + entities.removeAll(attributeResult.getConflicts()); + // Now figure out schema url for overall resource. + String schemaUrl = + EntityUtil.mergeResourceSchemaUrl(entities, base.getSchemaUrl(), next.getSchemaUrl()); + return createResourceRaw(attributeResult.getAttributes(), schemaUrl, entities); + } +} diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/RawAttributeMergeResult.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/RawAttributeMergeResult.java new file mode 100644 index 00000000000..f1cc081b035 --- /dev/null +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/RawAttributeMergeResult.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.resources.internal; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.api.common.Attributes; +import java.util.Collection; +import javax.annotation.concurrent.Immutable; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +@Immutable +@AutoValue +abstract class RawAttributeMergeResult { + /** Merged raw attributes. */ + abstract Attributes getAttributes(); + + /** + * Entities in conflict that should be removed from resource to avoid reporting invalid attribute + * sets in OTLP resource. + */ + abstract Collection getConflicts(); + + static final RawAttributeMergeResult create(Attributes attributes, Collection conflicts) { + return new AutoValue_RawAttributeMergeResult(attributes, conflicts); + } +} diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkEntity.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkEntity.java new file mode 100644 index 00000000000..2ddf17dde33 --- /dev/null +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkEntity.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.resources.internal; + +import com.google.auto.value.AutoValue; +import io.opentelemetry.api.common.Attributes; +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +/** + * SDK implementation of Entity. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +@Immutable +@AutoValue +abstract class SdkEntity implements Entity { + /** + * Returns a {@link Entity}. + * + * @param entityType the entity type string of this entity. + * @param id a map of attributes that identify the entity. + * @param description a map of attributes that describe the entity. + * @return a {@code Entity}. + */ + static final Entity create( + String entityType, Attributes id, Attributes description, @Nullable String schemaUrl) { + return new AutoValue_SdkEntity(entityType, id, description, schemaUrl); + } + + @Override + public final EntityBuilder toBuilder() { + return new SdkEntityBuilder(this); + } + + /** + * Returns a new {@link EntityBuilder} instance for creating arbitrary {@link Entity}. + * + * @param entityType the entity type string of this entity. + */ + public static final EntityBuilder builder(String entityType) { + return new SdkEntityBuilder(entityType); + } +} diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkEntityBuilder.java b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkEntityBuilder.java new file mode 100644 index 00000000000..2a425f5a0d3 --- /dev/null +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/resources/internal/SdkEntityBuilder.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.resources.internal; + +import io.opentelemetry.api.common.Attributes; +import javax.annotation.Nullable; + +/** + * A builder of {@link Entity} that allows to add identifying or descriptive {@link Attributes}, as + * well as type and schema_url. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +final class SdkEntityBuilder implements EntityBuilder { + private final String entityType; + private Attributes description; + private Attributes id; + @Nullable private String schemaUrl; + + SdkEntityBuilder(String entityType) { + AttributeCheckUtil.isValid(entityType); + this.entityType = entityType; + this.description = Attributes.empty(); + this.id = Attributes.empty(); + } + + SdkEntityBuilder(Entity seed) { + this.entityType = seed.getType(); + this.schemaUrl = seed.getSchemaUrl(); + this.id = seed.getId(); + this.description = seed.getDescription(); + } + + @Override + public EntityBuilder setSchemaUrl(String schemaUrl) { + this.schemaUrl = schemaUrl; + return this; + } + + @Override + public EntityBuilder withDescription(Attributes description) { + AttributeCheckUtil.checkAttributes(description); + this.description = description; + return this; + } + + @Override + public EntityBuilder withId(Attributes id) { + AttributeCheckUtil.checkAttributes(id); + this.id = id; + return this; + } + + @Override + public Entity build() { + return SdkEntity.create(entityType, id, description, schemaUrl); + } +} diff --git a/sdk/common/src/test/java/io/opentelemetry/sdk/resources/ResourceTest.java b/sdk/common/src/test/java/io/opentelemetry/sdk/resources/ResourceTest.java index 1654a001a96..6e170226ed5 100644 --- a/sdk/common/src/test/java/io/opentelemetry/sdk/resources/ResourceTest.java +++ b/sdk/common/src/test/java/io/opentelemetry/sdk/resources/ResourceTest.java @@ -13,6 +13,7 @@ import static io.opentelemetry.api.common.AttributeKey.longKey; import static io.opentelemetry.api.common.AttributeKey.stringArrayKey; import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; @@ -21,6 +22,7 @@ import io.opentelemetry.api.common.AttributeType; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.sdk.resources.internal.Entity; import java.util.Arrays; import java.util.Collections; import org.junit.jupiter.api.BeforeEach; @@ -234,6 +236,31 @@ void testMergeResources_Resource2_Null() { assertThat(resource.getAttributes()).isEqualTo(expectedAttributes); } + @Test + void testMergeResources_entities_separate_types_and_schema() { + Resource resource1 = + Resource.builder() + .add( + Entity.builder("a") + .setSchemaUrl("one") + .withId(Attributes.builder().put("a.id", "a").build()) + .build()) + .build(); + Resource resource2 = + Resource.builder() + .add( + Entity.builder("b") + .setSchemaUrl("two") + .withId(Attributes.builder().put("b.id", "b").build()) + .build()) + .build(); + Resource merged = resource1.merge(resource2); + assertThat(merged.getSchemaUrl()).isNull(); + assertThat(merged.getEntities()).hasSize(2); + assertThat(merged.getAttributes()).containsEntry("a.id", "a"); + assertThat(merged.getAttributes()).containsEntry("b.id", "b"); + } + @Test void testDefaultResources() { Resource resource = Resource.getDefault(); diff --git a/sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/EntityUtilTest.java b/sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/EntityUtilTest.java new file mode 100644 index 00000000000..23aece73575 --- /dev/null +++ b/sdk/common/src/test/java/io/opentelemetry/sdk/resources/internal/EntityUtilTest.java @@ -0,0 +1,253 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.resources.internal; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.resources.Resource; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +/** Unit tests for {@link EntityUtil}. */ +class EntityUtilTest { + @Test + void testMerge_entities_same_types_and_id() { + Collection base = + Arrays.asList( + Entity.builder("a") + .setSchemaUrl("one") + .withId(Attributes.builder().put("a.id", "a").build()) + .withDescription(Attributes.builder().put("a.desc1", "a").build()) + .build()); + Collection added = + Arrays.asList( + Entity.builder("a") + .setSchemaUrl("one") + .withId(Attributes.builder().put("a.id", "a").build()) + .withDescription(Attributes.builder().put("a.desc2", "b").build()) + .build()); + Collection merged = EntityUtil.mergeEntities(base, added); + assertThat(merged).hasSize(1); + assertThat(merged) + .anySatisfy( + entity -> { + assertThat(entity.getType()).isEqualTo("a"); + assertThat(entity.getSchemaUrl()).isEqualTo("one"); + assertThat(entity.getId()).containsEntry("a.id", "a"); + assertThat(entity.getDescription()) + .containsEntry("a.desc1", "a") + .containsEntry("a.desc2", "b"); + }); + } + + @Test + void testMerge_entities_same_types_and_id_different_schema() { + Collection base = + Arrays.asList( + Entity.builder("a") + .setSchemaUrl("one") + .withId(Attributes.builder().put("a.id", "a").build()) + .withDescription(Attributes.builder().put("a.desc1", "a").build()) + .build()); + Collection added = + Arrays.asList( + Entity.builder("a") + .setSchemaUrl("two") + .withId(Attributes.builder().put("a.id", "a").build()) + .withDescription(Attributes.builder().put("a.desc2", "b").build()) + .build()); + Collection merged = EntityUtil.mergeEntities(base, added); + assertThat(merged).hasSize(1); + assertThat(merged) + .anySatisfy( + entity -> { + assertThat(entity.getType()).isEqualTo("a"); + assertThat(entity.getSchemaUrl()).isEqualTo("one"); + assertThat(entity.getId()).containsEntry("a.id", "a"); + assertThat(entity.getDescription()) + .containsEntry("a.desc1", "a") + // Don't merge between versions. + .doesNotContainKey("a.desc2"); + }); + } + + @Test + void testMerge_entities_same_types_different_id() { + Collection base = + Arrays.asList( + Entity.builder("a") + .setSchemaUrl("one") + .withId(Attributes.builder().put("a.id", "a").build()) + .withDescription(Attributes.builder().put("a.desc1", "a").build()) + .build()); + Collection added = + Arrays.asList( + Entity.builder("a") + .setSchemaUrl("one") + .withId(Attributes.builder().put("a.id", "b").build()) + .withDescription(Attributes.builder().put("a.desc2", "b").build()) + .build()); + Collection merged = EntityUtil.mergeEntities(base, added); + assertThat(merged).hasSize(1); + assertThat(merged) + .satisfiesExactly( + entity -> { + assertThat(entity.getType()).isEqualTo("a"); + assertThat(entity.getSchemaUrl()).isEqualTo("one"); + assertThat(entity.getId()).containsEntry("a.id", "a"); + assertThat(entity.getDescription()) + .containsEntry("a.desc1", "a") + // Don't merge between different ids. + .doesNotContainKey("a.desc2"); + }); + } + + @Test + void testMerge_entities_separate_types_and_schema() { + Collection base = + Arrays.asList( + Entity.builder("a") + .setSchemaUrl("one") + .withId(Attributes.builder().put("a.id", "a").build()) + .build()); + Collection added = + Arrays.asList( + Entity.builder("b") + .setSchemaUrl("two") + .withId(Attributes.builder().put("b.id", "b").build()) + .build()); + Collection merged = EntityUtil.mergeEntities(base, added); + // Make sure we keep both entities when no conflict. + assertThat(merged) + .satisfiesExactlyInAnyOrder( + a -> assertThat(a.getType()).isEqualTo("a"), + b -> assertThat(b.getType()).isEqualTo("b")); + } + + @Test + void testSchemaUrlMerge_no_entities_differentUrls() { + // If the we find conflicting schema URLs in resource we must drop schema url (set to null). + String result = EntityUtil.mergeResourceSchemaUrl(Collections.emptyList(), "one", "two"); + assertThat(result).isNull(); + } + + @Test + void testSchemaUrlMerge_no_entities_base_null() { + // If the our resource had no schema url it abides by, we use the incoming schema url. + String result = EntityUtil.mergeResourceSchemaUrl(Collections.emptyList(), null, "two"); + assertThat(result).isEqualTo("two"); + } + + @Test + void testSchemaUrlMerge_no_entities_next_null() { + // If the new resource had no schema url it abides by, we preserve ours. + // NOTE: this is by specification, but seems problematic if conflicts in merge + // cause violation of SchemaURL. + String result = EntityUtil.mergeResourceSchemaUrl(Collections.emptyList(), "one", null); + assertThat(result).isEqualTo("one"); + } + + @Test + void testSchemaUrlMerge_entities_same_url() { + // If the new resource had no schema url it abides by, we preserve ours. + // NOTE: this is by specification, but seems problematic if conflicts in merge + // cause violation of SchemaURL. + String result = + EntityUtil.mergeResourceSchemaUrl( + Arrays.asList( + Entity.builder("t") + .setSchemaUrl("one") + .withId(Attributes.builder().put("id", 1).build()) + .build()), + "one", + null); + assertThat(result).isEqualTo("one"); + } + + @Test + void testSchemaUrlMerge_entities_different_url() { + // When entities have conflicting schema urls, we cannot fill out resource schema url, + // no matter what. + String result = + EntityUtil.mergeResourceSchemaUrl( + Arrays.asList( + Entity.builder("t") + .setSchemaUrl("one") + .withId(Attributes.builder().put("id", 1).build()) + .build(), + Entity.builder("t2") + .setSchemaUrl("two") + .withId(Attributes.builder().put("id2", 1).build()) + .build()), + "one", + "one"); + assertThat(result).isEqualTo(null); + } + + @Test + void testRawAttributeMerge_no_entities() { + // When no entities are present all attributes are merged. + RawAttributeMergeResult result = + EntityUtil.mergeRawAttributes( + Attributes.builder().put("a", 1).put("b", 1).build(), + Attributes.builder().put("b", 2).put("c", 2).build(), + Collections.emptyList()); + assertThat(result.getConflicts()).isEmpty(); + assertThat(result.getAttributes()) + .hasSize(3) + .containsEntry("a", 1) + .containsEntry("b", 2) + .containsEntry("c", 2); + } + + @Test + void testRawAttributeMerge_entity_with_conflict() { + // When an entity conflicts with incoming raw attributes, we need to call out that conflict + // so resource merge logic can remove the entity from resource. + RawAttributeMergeResult result = + EntityUtil.mergeRawAttributes( + Attributes.builder().put("a", 1).put("b", 1).build(), + Attributes.builder().put("b", 2).put("c", 2).build(), + Arrays.asList( + Entity.builder("c").withId(Attributes.builder().put("c", 1).build()).build())); + assertThat(result.getConflicts()).satisfiesExactly(e -> assertThat(e.getType()).isEqualTo("c")); + assertThat(result.getAttributes()) + .hasSize(3) + .containsEntry("a", 1) + .containsEntry("b", 2) + .containsEntry("c", 2); + } + + @Test + void testAddEntity_reflection() { + Resource result = + EntityUtil.addEntity( + Resource.builder(), + Entity.builder("a").withId(Attributes.builder().put("a", 1).build()).build()) + .build(); + assertThat(EntityUtil.getEntities(result)) + .satisfiesExactlyInAnyOrder(e -> assertThat(e.getType()).isEqualTo("a")); + } + + @Test + void testAddAllEntity_reflection() { + Resource result = + EntityUtil.addAllEntity( + Resource.builder(), + Arrays.asList( + Entity.builder("a").withId(Attributes.builder().put("a", 1).build()).build(), + Entity.builder("b").withId(Attributes.builder().put("b", 1).build()).build())) + .build(); + assertThat(EntityUtil.getEntities(result)) + .satisfiesExactlyInAnyOrder( + e -> assertThat(e.getType()).isEqualTo("a"), + e -> assertThat(e.getType()).isEqualTo("b")); + } +} diff --git a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/LoggerSharedState.java b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/LoggerSharedState.java index 5b40f897a32..232822a5f20 100644 --- a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/LoggerSharedState.java +++ b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/LoggerSharedState.java @@ -18,7 +18,7 @@ */ final class LoggerSharedState { private final Object lock = new Object(); - private final Resource resource; + private final Supplier resourceSupplier; private final Supplier logLimitsSupplier; private final LogRecordProcessor logRecordProcessor; private final Clock clock; @@ -26,12 +26,12 @@ final class LoggerSharedState { @Nullable private volatile CompletableResultCode shutdownResult = null; LoggerSharedState( - Resource resource, + Supplier resourceSupplier, Supplier logLimitsSupplier, LogRecordProcessor logRecordProcessor, Clock clock, ExceptionAttributeResolver exceptionAttributeResolver) { - this.resource = resource; + this.resourceSupplier = resourceSupplier; this.logLimitsSupplier = logLimitsSupplier; this.logRecordProcessor = logRecordProcessor; this.clock = clock; @@ -39,7 +39,7 @@ final class LoggerSharedState { } Resource getResource() { - return resource; + return resourceSupplier.get(); } LogLimits getLogLimits() { diff --git a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProvider.java b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProvider.java index 54825caf21e..aa4f4d63b79 100644 --- a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProvider.java +++ b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProvider.java @@ -53,7 +53,7 @@ public static SdkLoggerProviderBuilder builder() { } SdkLoggerProvider( - Resource resource, + Supplier resource, Supplier logLimitsSupplier, List processors, Clock clock, diff --git a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProviderBuilder.java b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProviderBuilder.java index 25fdeaa4454..d42245386f4 100644 --- a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProviderBuilder.java +++ b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProviderBuilder.java @@ -25,6 +25,7 @@ import java.util.Objects; import java.util.function.Predicate; import java.util.function.Supplier; +import javax.annotation.Nullable; /** * Builder class for {@link SdkLoggerProvider} instances. @@ -34,6 +35,7 @@ public final class SdkLoggerProviderBuilder { private final List logRecordProcessors = new ArrayList<>(); + @Nullable private Supplier resourceSupplier = null; private Resource resource = Resource.getDefault(); private Supplier logLimitsSupplier = LogLimits::getDefault; private Clock clock = Clock.getDefault(); @@ -69,6 +71,22 @@ public SdkLoggerProviderBuilder addResource(Resource resource) { return this; } + /** + * Registers a supplier of {@link Resource}. + * + *

This will override any {@link #addResource(Resource)} or {@link #setResource(Resource)} + * calls with the current supplier. + * + *

This method is experimental so not public. You may reflectively call it using {@link + * SdkLoggerProviderUtil#setResourceSupplier(SdkLoggerProviderBuilder, Supplier)}. + * + * @param supplier The supplier of {@link Resource}. + */ + SdkLoggerProviderBuilder setResourceSupplier(Supplier supplier) { + this.resourceSupplier = supplier; + return this; + } + /** * Assign a {@link Supplier} of {@link LogLimits}. {@link LogLimits} will be retrieved each time a * {@link Logger#logRecordBuilder()} is called. @@ -193,8 +211,12 @@ SdkLoggerProviderBuilder setExceptionAttributeResolver( * @return an instance configured with the provided options */ public SdkLoggerProvider build() { + Supplier resolvedSupplier = () -> this.resource; + if (resourceSupplier != null) { + resolvedSupplier = this.resourceSupplier; + } return new SdkLoggerProvider( - resource, + resolvedSupplier, logLimitsSupplier, logRecordProcessors, clock, diff --git a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/internal/SdkLoggerProviderUtil.java b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/internal/SdkLoggerProviderUtil.java index 417601bbad4..1b1848ffa55 100644 --- a/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/internal/SdkLoggerProviderUtil.java +++ b/sdk/logs/src/main/java/io/opentelemetry/sdk/logs/internal/SdkLoggerProviderUtil.java @@ -10,9 +10,11 @@ import io.opentelemetry.sdk.internal.ScopeConfigurator; import io.opentelemetry.sdk.logs.SdkLoggerProvider; import io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder; +import io.opentelemetry.sdk.resources.Resource; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.function.Predicate; +import java.util.function.Supplier; /** * A collection of methods that allow use of experimental features prior to availability in public @@ -90,4 +92,24 @@ public static void setExceptionAttributeResolver( "Error calling setExceptionAttributeResolver on SdkLoggerProviderBuilder", e); } } + + /** + * Reflectively assign the {@link Supplier} of {@link Resource} to the {@link + * SdkLoggerProviderBuilder}. + * + * @param sdkLoggerProvider the builder + */ + public static SdkLoggerProviderBuilder setResourceSupplier( + SdkLoggerProviderBuilder sdkLoggerProvider, Supplier resourceSupplier) { + try { + Method method = + SdkLoggerProviderBuilder.class.getDeclaredMethod("setResourceSupplier", Supplier.class); + method.setAccessible(true); + method.invoke(sdkLoggerProvider, resourceSupplier); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException( + "Error calling setLoggerConfigurator on SdkLoggerProvider", e); + } + return sdkLoggerProvider; + } } diff --git a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/LoggerSharedStateTest.java b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/LoggerSharedStateTest.java index 31b46c65206..d00bb73a50f 100644 --- a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/LoggerSharedStateTest.java +++ b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/LoggerSharedStateTest.java @@ -25,7 +25,7 @@ void shutdown() { when(logRecordProcessor.shutdown()).thenReturn(code); LoggerSharedState state = new LoggerSharedState( - Resource.empty(), + () -> Resource.empty(), LogLimits::getDefault, logRecordProcessor, Clock.getDefault(), diff --git a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerProviderBuilderTest.java b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerProviderBuilderTest.java index 5c49d35e5ca..4a4a4c17525 100644 --- a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerProviderBuilderTest.java +++ b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerProviderBuilderTest.java @@ -5,11 +5,13 @@ package io.opentelemetry.sdk.logs; +import static org.assertj.core.api.Assertions.as; import static org.assertj.core.api.Assertions.assertThat; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.sdk.resources.Resource; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; public class SdkLoggerProviderBuilderTest { @@ -25,7 +27,25 @@ void addResource() { SdkLoggerProvider.builder().addResource(customResource).build(); assertThat(sdkLoggerProvider) - .extracting("sharedState") - .hasFieldOrPropertyWithValue("resource", Resource.getDefault().merge(customResource)); + .extracting("sharedState", as(InstanceOfAssertFactories.type(LoggerSharedState.class))) + .extracting(LoggerSharedState::getResource) + .isEqualTo(Resource.getDefault().merge(customResource)); + } + + @Test + void setResourceSupplier() { + Resource customResource = + Resource.create( + Attributes.of( + AttributeKey.stringKey("custom_attribute_key"), "custom_attribute_value")); + + SdkLoggerProvider sdkLoggerProvider = + SdkLoggerProvider.builder().setResourceSupplier(() -> customResource).build(); + + assertThat(sdkLoggerProvider) + .extracting("sharedState", as(InstanceOfAssertFactories.type(LoggerSharedState.class))) + .extracting(LoggerSharedState::getResource) + // Validate the default resource values are NO Longer here when a supplier takes over. + .isEqualTo(customResource); } } diff --git a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerProviderTest.java b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerProviderTest.java index cb0f08a86b5..685c23b9e4f 100644 --- a/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerProviderTest.java +++ b/sdk/logs/src/test/java/io/opentelemetry/sdk/logs/SdkLoggerProviderTest.java @@ -343,15 +343,15 @@ void close() { @Test void toString_Valid() { when(logRecordProcessor.toString()).thenReturn("MockLogRecordProcessor"); + // Check that components of Logger are to-stringed. assertThat(sdkLoggerProvider.toString()) - .isEqualTo( - "SdkLoggerProvider{" - + "clock=SystemClock{}, " - + "resource=Resource{schemaUrl=null, attributes={key=\"value\"}}, " - + "logLimits=LogLimits{maxNumberOfAttributes=128, maxAttributeValueLength=2147483647}, " - + "logRecordProcessor=MockLogRecordProcessor, " - + "loggerConfigurator=ScopeConfiguratorImpl{conditions=[]}" - + "}"); + .matches("SdkLoggerProvider\\{.*}") + .matches(".*clock=SystemClock\\{}.*") + .matches(".*resource=Resource\\{.*}.*") + .matches( + ".*logLimits=LogLimits\\{maxNumberOfAttributes=128, maxAttributeValueLength=2147483647},.*") + .matches(".*logRecordProcessor=MockLogRecordProcessor, .*") + .matches(".*loggerConfigurator=ScopeConfiguratorImpl\\{conditions=\\[]}.*"); } private static ScopeConfigurator flipConfigurator(boolean enabled) { diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProvider.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProvider.java index 30ae0b1da5a..15199027b3b 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProvider.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProvider.java @@ -35,6 +35,7 @@ import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; import java.util.logging.Logger; /** @@ -65,7 +66,7 @@ public static SdkMeterProviderBuilder builder() { IdentityHashMap metricReaders, List metricProducers, Clock clock, - Resource resource, + Supplier resource, ExemplarFilter exemplarFilter, ScopeConfigurator meterConfigurator) { long startEpochNanos = clock.now(); diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilder.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilder.java index 90d0c7a8fef..77a4bce64a7 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilder.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/SdkMeterProviderBuilder.java @@ -23,6 +23,8 @@ import java.util.List; import java.util.Objects; import java.util.function.Predicate; +import java.util.function.Supplier; +import javax.annotation.Nullable; /** * Builder class for the {@link SdkMeterProvider}. @@ -39,6 +41,7 @@ public final class SdkMeterProviderBuilder { private static final ExemplarFilter DEFAULT_EXEMPLAR_FILTER = ExemplarFilter.traceBased(); private Clock clock = Clock.getDefault(); + @Nullable private Supplier resourceSupplier = null; private Resource resource = Resource.getDefault(); private final IdentityHashMap metricReaders = new IdentityHashMap<>(); @@ -80,6 +83,23 @@ public SdkMeterProviderBuilder addResource(Resource resource) { return this; } + /** + * Registers a supplier of {@link Resource}. + * + *

This will override any {@link #addResource(Resource)} or {@link #setResource(Resource)} + * calls with the current supplier. + * + *

This method is experimental so not public. You may reflectively call it using {@link + * SdkMeterProviderUtil#setResourceSupplier(SdkMeterProviderBuilder, Supplier)}. + * + * @param supplier The supplier of {@link Resource}. + * @since 1.X.0 + */ + SdkMeterProviderBuilder setResourceSupplier(Supplier supplier) { + this.resourceSupplier = supplier; + return this; + } + /** * Assign an {@link ExemplarFilter} for all metrics created by Meters. * @@ -200,12 +220,16 @@ SdkMeterProviderBuilder addMeterConfiguratorCondition( /** Returns an {@link SdkMeterProvider} built with the configuration of this builder. */ public SdkMeterProvider build() { + Supplier resolvedSupplier = () -> this.resource; + if (resourceSupplier != null) { + resolvedSupplier = this.resourceSupplier; + } return new SdkMeterProvider( registeredViews, metricReaders, metricProducers, clock, - resource, + resolvedSupplier, exemplarFilter, meterConfiguratorBuilder.build()); } diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/SdkMeterProviderUtil.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/SdkMeterProviderUtil.java index 9fc690366ed..0af2c8f63b5 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/SdkMeterProviderUtil.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/SdkMeterProviderUtil.java @@ -13,9 +13,11 @@ import io.opentelemetry.sdk.metrics.internal.exemplar.ExemplarFilter; import io.opentelemetry.sdk.metrics.internal.view.AttributesProcessor; import io.opentelemetry.sdk.metrics.internal.view.StringPredicates; +import io.opentelemetry.sdk.resources.Resource; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.function.Predicate; +import java.util.function.Supplier; /** * A collection of methods that allow use of experimental features prior to availability in public @@ -49,6 +51,26 @@ public static SdkMeterProviderBuilder setExemplarFilter( return sdkMeterProviderBuilder; } + /** + * Reflectively assign the {@link Supplier} of {@link Resource} to the {@link + * SdkMeterProviderBuilder}. + * + * @param sdkMeterProviderBuilder the builder + */ + public static SdkMeterProviderBuilder setResourceSupplier( + SdkMeterProviderBuilder sdkMeterProviderBuilder, Supplier supplier) { + try { + Method method = + SdkMeterProviderBuilder.class.getDeclaredMethod("setResourceSupplier", Supplier.class); + method.setAccessible(true); + method.invoke(sdkMeterProviderBuilder, supplier); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException( + "Error calling setResourceSupplier on SdkMeterProviderBuilder", e); + } + return sdkMeterProviderBuilder; + } + /** Reflectively set the {@link ScopeConfigurator} to the {@link SdkMeterProviderBuilder}. */ public static SdkMeterProviderBuilder setMeterConfigurator( SdkMeterProviderBuilder sdkMeterProviderBuilder, diff --git a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/MeterProviderSharedState.java b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/MeterProviderSharedState.java index aaf932d9202..8265bf415bd 100644 --- a/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/MeterProviderSharedState.java +++ b/sdk/metrics/src/main/java/io/opentelemetry/sdk/metrics/internal/state/MeterProviderSharedState.java @@ -10,6 +10,7 @@ import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.metrics.internal.exemplar.ExemplarFilter; import io.opentelemetry.sdk.resources.Resource; +import java.util.function.Supplier; import javax.annotation.concurrent.Immutable; /** @@ -23,9 +24,13 @@ public abstract class MeterProviderSharedState { public static MeterProviderSharedState create( - Clock clock, Resource resource, ExemplarFilter exemplarFilter, long startEpochNanos) { + Clock clock, + Supplier resourceSupplier, + ExemplarFilter exemplarFilter, + long startEpochNanos) { MeterProviderSharedState sharedState = - new AutoValue_MeterProviderSharedState(clock, resource, startEpochNanos, exemplarFilter); + new AutoValue_MeterProviderSharedState( + clock, resourceSupplier, startEpochNanos, exemplarFilter); return sharedState; } @@ -35,7 +40,11 @@ public static MeterProviderSharedState create( public abstract Clock getClock(); /** Returns the {@link Resource} to attach telemetry to. */ - public abstract Resource getResource(); + public Resource getResource() { + return getResourceSupplier().get(); + } + + abstract Supplier getResourceSupplier(); /** Returns the timestamp when the {@link SdkMeterProvider} was started, in epoch nanos. */ public abstract long getStartEpochNanos(); diff --git a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/InstrumentBuilderTest.java b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/InstrumentBuilderTest.java index de1a351e619..3f4c8cdf048 100644 --- a/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/InstrumentBuilderTest.java +++ b/sdk/metrics/src/test/java/io/opentelemetry/sdk/metrics/InstrumentBuilderTest.java @@ -21,7 +21,7 @@ class InstrumentBuilderTest { public static final MeterProviderSharedState PROVIDER_SHARED_STATE = MeterProviderSharedState.create( - TestClock.create(), Resource.getDefault(), ExemplarFilter.alwaysOff(), 0); + TestClock.create(), () -> Resource.getDefault(), ExemplarFilter.alwaysOff(), 0); static final InstrumentationScopeInfo SCOPE = InstrumentationScopeInfo.create("scope-name"); public static final SdkMeter SDK_METER = new SdkMeter( diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProvider.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProvider.java index f39ce565731..9b74070c191 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProvider.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProvider.java @@ -49,7 +49,7 @@ public static SdkTracerProviderBuilder builder() { SdkTracerProvider( Clock clock, IdGenerator idsGenerator, - Resource resource, + Supplier resourceSupplier, Supplier spanLimitsSupplier, Sampler sampler, List spanProcessors, @@ -59,7 +59,7 @@ public static SdkTracerProviderBuilder builder() { new TracerSharedState( clock, idsGenerator, - resource, + resourceSupplier, spanLimitsSupplier, sampler, spanProcessors, diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilder.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilder.java index 194aa67bc17..df9a5e21e8f 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilder.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilder.java @@ -23,6 +23,7 @@ import java.util.Objects; import java.util.function.Predicate; import java.util.function.Supplier; +import javax.annotation.Nullable; /** Builder of {@link SdkTracerProvider}. */ public final class SdkTracerProviderBuilder { @@ -33,6 +34,7 @@ public final class SdkTracerProviderBuilder { private Clock clock = Clock.getDefault(); private IdGenerator idsGenerator = IdGenerator.random(); private Resource resource = Resource.getDefault(); + @Nullable private Supplier resourceSupplier = null; private Supplier spanLimitsSupplier = SpanLimits::getDefault; private Sampler sampler = DEFAULT_SAMPLER; private ScopeConfiguratorBuilder tracerConfiguratorBuilder = @@ -96,6 +98,23 @@ public SdkTracerProviderBuilder addResource(Resource resource) { return this; } + /** + * Registers a supplier of {@link Resource}. + * + *

This will override any {@link #addResource(Resource)} or {@link #setResource(Resource)} + * calls with the current supplier. + * + *

This method is experimental so not public. You may reflectively call it using {@link + * SdkTracerProviderUtil#setResourceSupplier(SdkTracerProviderBuilder, Supplier)}. + * + * @param supplier The supplier of {@link Resource}. + * @since 1.X.0 + */ + SdkTracerProviderBuilder setResourceSupplier(Supplier supplier) { + this.resourceSupplier = supplier; + return this; + } + /** * Assign an initial {@link SpanLimits} that should be used with this SDK. * @@ -239,10 +258,14 @@ SdkTracerProviderBuilder setExceptionAttributeResolver( * @return The instance. */ public SdkTracerProvider build() { + Supplier resolvedSupplier = () -> resource; + if (resourceSupplier != null) { + resolvedSupplier = resourceSupplier; + } return new SdkTracerProvider( clock, idsGenerator, - resource, + resolvedSupplier, spanLimitsSupplier, sampler, spanProcessors, diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/TracerSharedState.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/TracerSharedState.java index 74d43076b97..f2bd64b946c 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/TracerSharedState.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/TracerSharedState.java @@ -22,7 +22,7 @@ final class TracerSharedState { private final IdGenerator idGenerator; // tracks whether it is safe to skip id validation on ids from the above generator private final boolean idGeneratorSafeToSkipIdValidation; - private final Resource resource; + private final Supplier resourceSupplier; private final Supplier spanLimitsSupplier; private final Sampler sampler; @@ -34,7 +34,7 @@ final class TracerSharedState { TracerSharedState( Clock clock, IdGenerator idGenerator, - Resource resource, + Supplier resourceSupplier, Supplier spanLimitsSupplier, Sampler sampler, List spanProcessors, @@ -42,7 +42,7 @@ final class TracerSharedState { this.clock = clock; this.idGenerator = idGenerator; this.idGeneratorSafeToSkipIdValidation = idGenerator instanceof RandomIdGenerator; - this.resource = resource; + this.resourceSupplier = resourceSupplier; this.spanLimitsSupplier = spanLimitsSupplier; this.sampler = sampler; this.activeSpanProcessor = SpanProcessor.composite(spanProcessors); @@ -61,8 +61,9 @@ boolean isIdGeneratorSafeToSkipIdValidation() { return idGeneratorSafeToSkipIdValidation; } - Resource getResource() { - return resource; + // Needed for tests. + public Resource getResource() { + return resourceSupplier.get(); } /** Returns the current {@link SpanLimits}. */ diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/SdkTracerProviderUtil.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/SdkTracerProviderUtil.java index f9ca8fafc56..1fc4ccb2eb1 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/SdkTracerProviderUtil.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/SdkTracerProviderUtil.java @@ -8,11 +8,13 @@ import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.internal.ExceptionAttributeResolver; import io.opentelemetry.sdk.internal.ScopeConfigurator; +import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.function.Predicate; +import java.util.function.Supplier; /** * A collection of methods that allow use of experimental features prior to availability in public @@ -89,4 +91,22 @@ public static void setExceptionAttributeResolver( "Error calling setExceptionAttributeResolver on SdkTracerProviderBuilder", e); } } + + /** + * Reflectively set the {@link Supplier} of {@link Resource} to the {@link + * SdkTracerProviderBuilder}. + */ + public static SdkTracerProviderBuilder setResourceSupplier( + SdkTracerProviderBuilder sdkTracerProviderBuilder, Supplier supplier) { + try { + Method method = + SdkTracerProviderBuilder.class.getDeclaredMethod("setResourceSupplier", Supplier.class); + method.setAccessible(true); + method.invoke(sdkTracerProviderBuilder, supplier); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException( + "Error calling setTracerConfigurator on SdkTracerProvider", e); + } + return sdkTracerProviderBuilder; + } } diff --git a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java index 9c9ecdc7e9c..aa81ce1e426 100644 --- a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java +++ b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java @@ -977,8 +977,12 @@ void spanDataToString() { span.setAttribute("http.url", "https://opentelemetry.io"); span.setStatus(StatusCode.ERROR, "error"); span.end(); - assertThat(span.toSpanData().toString()) + .matches("SpanData\\{.*}") + .matches(".*traceId=[0-9a-f]{32}, .*") + .matches(".*spanId=[0-9a-f]{16}, .*") + .matches(".*traceFlags=01, .*") + .matches(".*traceFlags=01, .*") .matches( "SpanData\\{spanContext=ImmutableSpanContext\\{" + "traceId=[0-9a-f]{32}, " @@ -990,10 +994,7 @@ void spanDataToString() { + "spanId=0000000000000000, " + "traceFlags=00, " + "traceState=ArrayBasedTraceState\\{entries=\\[]}, remote=false, valid=false}, " - + "resource=Resource\\{schemaUrl=null, " - + "attributes=\\{service.name=\"unknown_service:java\", " - + "telemetry.sdk.language=\"java\", telemetry.sdk.name=\"opentelemetry\", " - + "telemetry.sdk.version=\"\\d+.\\d+.\\d+(-rc.\\d+)?(-SNAPSHOT)?\"}}, " + + "resource=Resource\\{.*}, " + "instrumentationScopeInfo=InstrumentationScopeInfo\\{" + "name=SpanBuilderSdkTest, version=null, schemaUrl=null, attributes=\\{}}, " + "name=span_name, "