Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7f67a89
Start the reboot of entities SDK for Java, this time something mergable.
jsuereth Apr 24, 2026
f611175
Add missing tests, spotlessApply
jsuereth Apr 24, 2026
2ccb300
Add more tests and entity detectors.
jsuereth Apr 24, 2026
509a7b9
First attempt to unify Entities with SDK Configuration.
jsuereth Jun 9, 2026
b4953f4
Merge main
jsuereth Jun 25, 2026
bdcc5d7
Merge Jack's change to not expose two Entity APIs.
jsuereth Jun 25, 2026
279b1ea
Fix some PR feedback.
jsuereth Jun 25, 2026
da0b3fb
Fixes from review - Move to using existing resource detectors and a f…
jsuereth Jun 25, 2026
25e35e6
Move experiment flag to common location.
jsuereth Jun 25, 2026
6d78602
Spotless fixes.
jsuereth Jun 25, 2026
d200481
minor fixes from review, tests to follow
jsuereth Jun 26, 2026
c6fc930
Add tests.
jsuereth Jun 26, 2026
09f0c57
Simplify Resource, fix tests.
jsuereth Jun 29, 2026
e2aec2a
Fix nit about builder method.
jsuereth Jun 29, 2026
c83a242
Remove entities from EnvironmentResource. Create new SHELL=/bin/bash
jsuereth Jun 29, 2026
a92a7ba
Flesh out tests.
jsuereth Jun 29, 2026
169d186
Fix build time dependency issue my IDE missed.
jsuereth Jun 29, 2026
632cf30
Fix tests and move constants.
jsuereth Jun 29, 2026
0dcab3e
Feedback for entities impl
jack-berg Jul 13, 2026
99f2a42
Merge pull request #1 from jack-berg/wip-entity-sdk-for-reals
jsuereth Jul 13, 2026
f78fc90
Merge remote-tracking branch 'origin/main' into wip-entity-sdk-for-reals
jsuereth Jul 14, 2026
de5ea47
Update method name to match spec PR.
jsuereth Jul 14, 2026
82b4d12
Spotless Apply
jsuereth Jul 14, 2026
74fd7ff
declarative config model package change
jack-berg Jul 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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}.
*
* <p>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 {
Comment thread
jsuereth marked this conversation as resolved.
@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;
}
}
Original file line number Diff line number Diff line change
@@ -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 extends Message> 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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.sdk.autoconfigure;

import io.opentelemetry.sdk.autoconfigure.internal.SpiHelper;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import io.opentelemetry.sdk.extension.incubator.resources.EntityDetector;
import io.opentelemetry.sdk.extension.incubator.resources.internal.ExtendedEntityUtil;
import io.opentelemetry.sdk.resources.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;

final class IncubatingEntityUtil {

private IncubatingEntityUtil() {}

@Nullable
static Resource configureEntityResource(
ConfigProperties config,
SpiHelper spiHelper,
Set<String> enabledProviders,
Set<String> disabledProviders) {

List<EntityDetector> detectors = new ArrayList<>();
for (EntityDetector detector : spiHelper.loadOrdered(EntityDetector.class)) {
String fqcn = detector.getClass().getName();
String shortName = detector.getName();
if (!enabledProviders.isEmpty()
&& !enabledProviders.contains(fqcn)
&& !enabledProviders.contains(shortName)) {
continue;
}
if (disabledProviders.contains(fqcn) || disabledProviders.contains(shortName)) {
continue;
}
detectors.add(detector);
}

if (detectors.isEmpty()) {
return null;
}

return ExtendedEntityUtil.runDetection(detectors, config);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,22 @@
*/
public final class ResourceConfiguration {

private static final boolean ENTITY_INCUBATOR_AVAILABLE;

static {
boolean incubatorAvailable = false;
try {
Class.forName(
"io.opentelemetry.sdk.extension.incubator.resources.EntityDetector",
false,
ResourceConfiguration.class.getClassLoader());
incubatorAvailable = true;
} catch (ClassNotFoundException e) {
// Not available
}
ENTITY_INCUBATOR_AVAILABLE = incubatorAvailable;
}

// Visible for testing
static final String DISABLED_ATTRIBUTE_KEYS = "otel.resource.disabled.keys";
static final String ENABLED_RESOURCE_PROVIDERS = "otel.java.enabled.resource.providers";
Expand Down Expand Up @@ -66,23 +82,35 @@ static Resource configureResource(
Set<String> enabledProviders = new HashSet<>(config.getList(ENABLED_RESOURCE_PROVIDERS));
Set<String> disabledProviders = new HashSet<>(config.getList(DISABLED_RESOURCE_PROVIDERS));

for (ResourceProvider resourceProvider : spiHelper.loadOrdered(ResourceProvider.class)) {
if (!enabledProviders.isEmpty()
&& !enabledProviders.contains(resourceProvider.getClass().getName())) {
continue;
}
if (disabledProviders.contains(resourceProvider.getClass().getName())) {
continue;
// If Entity experiment is enabled, we use a new flow to instantiate resources.
boolean entitiesEnabled = config.getBoolean("otel.experimental.entities.enabled", false);
if (entitiesEnabled && ENTITY_INCUBATOR_AVAILABLE) {
Resource entityResource =
IncubatingEntityUtil.configureEntityResource(
config, spiHelper, enabledProviders, disabledProviders);
if (entityResource != null) {
result = entityResource;
}
if (resourceProvider instanceof ConditionalResourceProvider
&& !((ConditionalResourceProvider) resourceProvider).shouldApply(config, result)) {
continue;
} else {

for (ResourceProvider resourceProvider : spiHelper.loadOrdered(ResourceProvider.class)) {
if (!enabledProviders.isEmpty()
&& !enabledProviders.contains(resourceProvider.getClass().getName())) {
continue;
}
if (disabledProviders.contains(resourceProvider.getClass().getName())) {
continue;
}
if (resourceProvider instanceof ConditionalResourceProvider
&& !((ConditionalResourceProvider) resourceProvider).shouldApply(config, result)) {
continue;
}
result = result.merge(resourceProvider.createResource(config));
}
result = result.merge(resourceProvider.createResource(config));
}

result = filterAttributes(result, config);

// TODO(jsuereth): Should filter attributes be used with entities?
result = filterAttributes(result, config);
}
return resourceCustomizer.apply(result, config);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import io.github.netmikey.logunit.api.LogCapturer;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.incubator.ExtendedOpenTelemetry;
import io.opentelemetry.api.incubator.config.ConfigProvider;
import io.opentelemetry.api.incubator.config.InstrumentationConfigUtil;
Expand All @@ -34,14 +35,19 @@
import io.opentelemetry.sdk.logs.SdkLoggerProvider;
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.resources.internal.Entity;
import io.opentelemetry.sdk.resources.internal.EntityUtil;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
Expand Down Expand Up @@ -243,4 +249,53 @@ void configFile_ConfigProvider() {
.getScalarList("request_captured_headers", String.class))
.isEqualTo(Arrays.asList("Content-Type", "Accept"));
}

@Test
void entitiesEnabled() {
ConfigProperties config =
DefaultConfigProperties.createFromMap(
Collections.singletonMap("otel.experimental.entities.enabled", "true"));

AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk =
AutoConfiguredOpenTelemetrySdk.builder().setConfig(config).build();
OpenTelemetrySdk openTelemetrySdk = autoConfiguredOpenTelemetrySdk.getOpenTelemetrySdk();
cleanup.addCloseable(openTelemetrySdk);

Resource resource = autoConfiguredOpenTelemetrySdk.getResource();

Collection<Entity> entities = EntityUtil.getEntities(resource);
assertThat(entities)
.anyMatch(
e ->
e.getType().equals("telemetry.sdk")
&& "opentelemetry"
.equals(e.getId().get(AttributeKey.stringKey("telemetry.sdk.name")))
&& "java"
.equals(e.getId().get(AttributeKey.stringKey("telemetry.sdk.language"))));
}

@Test
void entitiesEnabled_WithEnabledProviders() {
Map<String, String> props = new HashMap<>();
props.put("otel.experimental.entities.enabled", "true");
props.put("otel.service.name", "my-filtered-service");
props.put("otel.java.enabled.resource.providers", "service");
ConfigProperties config = DefaultConfigProperties.createFromMap(props);

AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk =
AutoConfiguredOpenTelemetrySdk.builder().setConfig(config).build();
OpenTelemetrySdk openTelemetrySdk = autoConfiguredOpenTelemetrySdk.getOpenTelemetrySdk();
cleanup.addCloseable(openTelemetrySdk);

Resource resource = autoConfiguredOpenTelemetrySdk.getResource();

Collection<Entity> entities = EntityUtil.getEntities(resource);
assertThat(entities)
.anyMatch(
e ->
e.getType().equals("service")
&& "my-filtered-service"
.equals(e.getId().get(AttributeKey.stringKey("service.name"))));
assertThat(entities).noneMatch(e -> e.getType().equals("telemetry.sdk"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,18 @@ private InternalTelemetryVersion getInternalTelemetryVersion() {
}
}

boolean isEntitiesEnabled() {
if (configProvider == null) {
return false;
}
// TODO - Check with Jack if this is the right thing to do for config flags,
// of if we want to put this in the experimental resource config space.
return Boolean.TRUE.equals(
configProvider
.getInstrumentationConfig("otel_sdk")
.getBoolean("experimental_entities_enabled"));
}

/**
* Find a registered {@link ComponentProvider} with {@link ComponentProvider#getType()} matching
* {@code type}, {@link ComponentProvider#getName()} matching {@code name}, and call {@link
Expand Down
Loading
Loading