Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
f056298
Reboot the entity prototype
jsuereth Jun 18, 2025
5460d5e
Add base entity class, merge logic and test
jsuereth Jun 19, 2025
ce0e66d
Move Entity and EntityBuilder to be pure interfaces
jsuereth Jun 19, 2025
cb74286
Add more merge helper methods and tests
jsuereth Jun 19, 2025
3369be3
Added test for merging raw attributes
jsuereth Jun 19, 2025
2c4aaeb
Cleanups and reverting silly changes
jsuereth Jun 19, 2025
8af2cd1
fix minor style issue
jsuereth Jun 19, 2025
da525bd
Actually wire entities into resource
jsuereth Jun 20, 2025
b119561
Fix typo
jsuereth Jun 20, 2025
ddf096d
Remove all publicly accessible references to Entity from stable packages
jsuereth Jun 20, 2025
9c222dd
Add incubating EntityProvider API for specification work
jsuereth Jun 20, 2025
2871ff0
Fix refactoring that broke reflection.
jsuereth Jun 21, 2025
34b6f60
Fix schema url and test, now that we proved it works
jsuereth Jun 21, 2025
0711350
Initial cut at moving Entity SDK to have correpsonding API.
jsuereth Jul 5, 2025
ebf9557
Further simplify the API and SDK for entities
jsuereth Jul 5, 2025
73ef00a
Wire ResourceProvider in end-to-end test with new incubating Extended…
jsuereth Jul 6, 2025
8d4288e
Fix javadoc issues
jsuereth Jul 6, 2025
f22ec8d
Update exporters/otlp/common/src/main/java/io/opentelemetry/exporter/…
jsuereth Jul 10, 2025
d220005
Update sdk/logs/src/main/java/io/opentelemetry/sdk/logs/SdkLoggerProv…
jsuereth Jul 10, 2025
f9dcb52
Fixes from review.
jsuereth Jul 10, 2025
18afb60
Remove methods we don't need yet.
jsuereth Jul 10, 2025
7ace665
Remove redundant visibility.
jsuereth Jul 10, 2025
d5219d1
More cleanups from review.
jsuereth Jul 10, 2025
cba0606
Simplify prototype of SdkResource.
jsuereth Jul 10, 2025
a38a544
Fix typo.
jsuereth Jul 10, 2025
d01f5a3
More cleanups from review.
jsuereth Jul 10, 2025
2560c51
Add more tests for ResourceProvider.
jsuereth Jul 11, 2025
bf72978
Fix stylecheck.
jsuereth Jul 11, 2025
af3bbd9
More checkstyle fixes.
jsuereth Jul 11, 2025
2809916
Rename prototype to more closely match OTEP 4316.
jsuereth Jul 11, 2025
912ba24
Finish remove implementation and listeners.
jsuereth Jul 11, 2025
634c4a3
Remove assertjs helpers, update javadoc.
jsuereth Jul 11, 2025
5c126c9
Fix entity javadoc.
jsuereth Jul 11, 2025
7a7c426
Fix tests.
jsuereth Jul 11, 2025
8ad0e5f
Tweak incubator SDK to support asynchronous configuratio and use Enti…
jsuereth Aug 23, 2025
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,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();
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>Entity represents an object of interest associated with produced telemetry: traces, metrics or
* logs.
*
* <p>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.
*
* <p>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();
Copy link
Member

Choose a reason for hiding this comment

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

Maybe call this attach() since we have Resource#attacheEntity()?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is anticipation of having an EntityBuilder that just emits Entity without attaching to active resource.

I debated two things:

  • Having EntityBuilder have two methods: emit and attach where this PR would only have attach.
  • Having the Resource method you call to get the builder be named attach or emit.

I was planning to talk about this awkwardness in the next Entities SIG, but agree with only "attach" then this should be "attach".

}
Original file line number Diff line number Diff line change
@@ -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 <i>Provider</i> is for consistency
* with other languages and it is <b>NOT</b> 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}.
*
* <p>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);
}
Original file line number Diff line number Diff line change
@@ -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() {}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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.
*** 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()
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 {
@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()
Copy link
Member

Choose a reason for hiding this comment

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

Eventually we'll want to make this more efficient, avoiding stream() and implementing a stateless version of this, which refers to a version which minimizes / avoids memory allocations using a variety of techniques centered around object reuse.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed. This reminds me of the "dictonary" / "register an attribute set" API discussion from the spec meeting.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also, if it wasn't apparent for now - This marshaller is only used ONCE per resource instance. So unless resource is being updated frequently (which we do not expect), then this cost is not paid often, if more than once.

.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
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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;
}
}
}
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);
}
}
Loading