Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 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
54e31d8
Sketch out central entity erasure
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,47 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.gradle.js2p;

import com.sun.codemodel.JDocComment;
import com.sun.codemodel.JDocCommentable;
import com.sun.codemodel.JFieldVar;
import com.sun.codemodel.JMethod;
import org.jsonschema2pojo.Schema;
import org.jsonschema2pojo.rules.DescriptionRule;

import com.fasterxml.jackson.databind.JsonNode;

/**
* A {@link DescriptionRule} that suppresses javadoc placement on fields and getters.
*
* <p>jsonschema2pojo's {@code PropertyRule.apply()} calls {@code resolveRefs()} before applying
* annotations. When a property node is {@code {"$ref": "..."}}, the resolved node is the
* referenced {@code $def}'s content. If that {@code $def} has a {@code description}, jsonschema2pojo
* applies it to both the field and the getter via this rule.
*
* <p>This rule skips {@link JFieldVar} targets entirely (fields carry no javadoc) and skips
* {@link JMethod} targets (descriptions on getters are applied explicitly by
* {@link OtelPropertyRule}, using the property's own {@code description}, not the {@code $def}'s).
* Class-level targets ({@link com.sun.codemodel.JDefinedClass}) are passed through unchanged.
*/
class OtelDescriptionRule extends DescriptionRule {

@Override
public JDocComment apply(
String nodeName,
JsonNode node,
JsonNode parent,
JDocCommentable generatableType,
Schema schema) {
if (generatableType instanceof JFieldVar || generatableType instanceof JMethod) {
// Do not call generatableType.javadoc() here: invoking javadoc() on a target that has
// no existing JDocComment auto-creates an empty one, which causes JFieldVar.declare() to
// emit an empty /** */ block in the generated source.
return null;
}
return super.apply(nodeName, node, parent, generatableType, schema);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import com.sun.codemodel.JFieldVar;
import com.sun.codemodel.JMethod;
import org.jsonschema2pojo.Schema;
import org.jsonschema2pojo.exception.GenerationException;
import org.jsonschema2pojo.rules.PropertyRule;
import org.jsonschema2pojo.rules.RuleFactory;

Expand All @@ -32,9 +31,14 @@
* valid; re-applying from the original node restores those descriptions on the getter too.
*
* <p>Mechanism: strip {@code description} from the node before delegating to {@link
* PropertyRule#apply} so the superclass adds it nowhere (not the field javadoc, the getter javadoc,
* or a {@code @JsonPropertyDescription}), then apply it to the getter only — leaving fields without
* any javadoc.
* PropertyRule#apply} so the superclass does not add the property description to the field or
* getter. The superclass calls {@code resolveRefs()} before applying annotations, so if the
* property node is a {@code $ref} whose target {@code $def} has its own description, that
* description would otherwise land on the field and getter via {@link
* org.jsonschema2pojo.rules.DescriptionRule}. {@link OtelDescriptionRule} suppresses all
* description placements on fields and getters from that path. The property's own description
* (stripped from the node before delegation) is then applied to the getter only by
* {@link #applyGetterDescription}, which writes directly to the getter's javadoc.
*
* <p>{@link OtelJacksonAnnotator} puts {@code @JsonProperty} on the getter, but has no hook for the
* {@code withX} builder methods, so this rule annotates them.
Expand Down Expand Up @@ -69,19 +73,6 @@ public JDefinedClass apply(
return result;
}

// A $ref target with its own top-level description would land on the field via DescriptionRule
// and conflict with the sibling description we re-apply to the getter. The schema puts
// descriptions on properties, not $defs, so this doesn't happen today; fail loud if it does.
if (referencedTypeHasDescription(node, schema)) {
throw new GenerationException(
"Property '"
+ nodeName
+ "' resolves to a type that defines its own top-level description, which is not"
+ " handled (it would land on the field). See "
+ OtelPropertyRule.class.getName()
+ ".");
}

annotateBuilderMethod(result, propertyName, node, nodeName);

if (description != null) {
Expand All @@ -102,29 +93,6 @@ private void annotateBuilderMethod(
}
}

/**
* Returns whether a (possibly chained) {@code $ref} resolves to a schema with its own
* description. Uses {@link org.jsonschema2pojo.SchemaStore} rather than inspecting {@code
* field.javadoc()} (which would instantiate an empty comment).
*/
private boolean referencedTypeHasDescription(JsonNode node, Schema schema) {
JsonNode current = node;
Schema currentSchema = schema;
boolean resolved = false;
while (current.has("$ref")) {
currentSchema =
ruleFactory
.getSchemaStore()
.create(
currentSchema,
current.get("$ref").asText(),
ruleFactory.getGenerationConfig().getRefFragmentPathDelimiters());
current = currentSchema.getContent();
resolved = true;
}
return resolved && current.has("description");
}

private void applyGetterDescription(
String nodeName,
JsonNode node,
Expand All @@ -134,11 +102,13 @@ private void applyGetterDescription(
JFieldVar field,
JsonNode description) {
String getterName = ruleFactory.getNameHelper().getGetterName(propertyName, field.type(), node);
String text = preserveLineBreaks(description).asText();
for (JMethod method : jclass.methods()) {
if (method.name().equals(getterName)) {
ruleFactory
.getDescriptionRule()
.apply(nodeName, preserveLineBreaks(description), node, method, schema);
// OtelDescriptionRule suppresses DescriptionRule for JMethod targets, so write the
// description directly. JCommentPart.format() handles embedded '\n' characters by
// emitting line breaks, so append the whole text as one string to preserve newlines.
method.javadoc().append(text);
return;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import com.sun.codemodel.JClassContainer;
import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JDocComment;
import com.sun.codemodel.JDocCommentable;
import com.sun.codemodel.JPackage;
import com.sun.codemodel.JType;
Expand Down Expand Up @@ -51,6 +52,11 @@ public Rule<JDefinedClass, JDefinedClass> getPropertyRule() {
return new OtelPropertyRule(this);
}

@Override
public Rule<JDocCommentable, JDocComment> getDescriptionRule() {
return new OtelDescriptionRule();
}

@Override
public Rule<JDefinedClass, JDefinedClass> getAdditionalPropertiesRule() {
return new OtelAdditionalPropertiesRule(this);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
Comparing source compatibility of opentelemetry-sdk-extension-autoconfigure-1.65.0-SNAPSHOT.jar against opentelemetry-sdk-extension-autoconfigure-1.64.0.jar
No changes.
+++ NEW CLASS: PUBLIC(+) FINAL(+) io.opentelemetry.sdk.autoconfigure.EnvironmentEntityResourceProvider (not serializable)
+++ CLASS FILE FORMAT VERSION: 52.0 <- n.a.
+++ NEW INTERFACE: io.opentelemetry.sdk.autoconfigure.spi.Ordered
+++ NEW INTERFACE: io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider
+++ NEW SUPERCLASS: java.lang.Object
+++ NEW CONSTRUCTOR: PUBLIC(+) EnvironmentEntityResourceProvider()
+++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.resources.Resource createResource(io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties)
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);
}

/** Constructs 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);
size += MarshalerUtil.sizeRepeatedString(EntityRef.ID_KEYS, idKeysUtf8);
size += 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 += MarshalerUtil.sizeRepeatedMessage(Resource.ENTITY_REFS, entityRefs);
return size;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* 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", Attributes.builder().put("id.key", "id.value").build())
.setSchemaUrl("test-url")
.setDescription(Attributes.builder().put("desc.key", "desc.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
// time. 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
Loading