Add an optimized Attributes implementation for instrumenter
MODIFY
benchmark/benchmark.gradle
benchmark/benchmark.gradle
@@ -6,6 +6,8 @@ plugins {
apply from: "$rootDir/gradle/java.gradle"
dependencies {
+ jmh platform(project(":dependencyManagement"))
+
jmh "io.opentelemetry:opentelemetry-api"
jmh "net.bytebuddy:byte-buddy-agent"

ADD
benchmark/src/jmh/java/io/opentelemetry/benchmark/InstrumenterBenchmark.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.benchmark;
+
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.context.Context;
+import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
+import io.opentelemetry.instrumentation.api.instrumenter.http.HttpAttributesExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.http.HttpSpanNameExtractor;
+import io.opentelemetry.instrumentation.api.instrumenter.net.InetSocketAddressNetAttributesExtractor;
+import io.opentelemetry.semconv.trace.attributes.SemanticAttributes;
+import java.net.InetSocketAddress;
+import java.util.concurrent.TimeUnit;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+@Fork(3)
+@Warmup(iterations = 10, time = 1)
+@Measurement(iterations = 5, time = 1)
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+@BenchmarkMode(Mode.AverageTime)
+@State(Scope.Thread)
+public class InstrumenterBenchmark {
+
+ private static final Instrumenter<Void, Void> INSTRUMENTER =
+ Instrumenter.<Void, Void>newBuilder(
+ OpenTelemetry.noop(),
+ "benchmark",
+ HttpSpanNameExtractor.create(ConstantHttpAttributesExtractor.INSTANCE))
+ .addAttributesExtractor(ConstantHttpAttributesExtractor.INSTANCE)
+ .addAttributesExtractor(new ConstantNetAttributesExtractor())
+ .newInstrumenter();
+
+ @Benchmark
+ public Context start() {
+ return INSTRUMENTER.start(Context.root(), null);
+ }
+
+ @Benchmark
+ public Context startEnd() {
+ Context context = INSTRUMENTER.start(Context.root(), null);
+ INSTRUMENTER.end(context, null, null, null);
+ return context;
+ }
+
+ static class ConstantHttpAttributesExtractor extends HttpAttributesExtractor<Void, Void> {
+ static HttpAttributesExtractor<Void, Void> INSTANCE = new ConstantHttpAttributesExtractor();
+
+ @Override
+ protected @Nullable String method(Void unused) {
+ return "GET";
+ }
+
+ @Override
+ protected @Nullable String url(Void unused) {
+ return "https://opentelemetry.io/benchmark";
+ }
+
+ @Override
+ protected @Nullable String target(Void unused) {
+ return "/benchmark";
+ }
+
+ @Override
+ protected @Nullable String host(Void unused) {
+ return "opentelemetry.io";
+ }
+
+ @Override
+ protected @Nullable String route(Void unused) {
+ return "/benchmark";
+ }
+
+ @Override
+ protected @Nullable String scheme(Void unused) {
+ return "https";
+ }
+
+ @Override
+ protected @Nullable String userAgent(Void unused) {
+ return "OpenTelemetryBot";
+ }
+
+ @Override
+ protected @Nullable Long requestContentLength(Void unused, @Nullable Void unused2) {
+ return 100L;
+ }
+
+ @Override
+ protected @Nullable Long requestContentLengthUncompressed(Void unused, @Nullable Void unused2) {
+ return null;
+ }
+
+ @Override
+ protected @Nullable String flavor(Void unused, @Nullable Void unused2) {
+ return SemanticAttributes.HttpFlavorValues.HTTP_2_0;
+ }
+
+ @Override
+ protected @Nullable String serverName(Void unused, @Nullable Void unused2) {
+ return null;
+ }
+
+ @Override
+ protected @Nullable String clientIp(Void unused, @Nullable Void unused2) {
+ return null;
+ }
+
+ @Override
+ protected @Nullable Integer statusCode(Void unused, Void unused2) {
+ return 200;
+ }
+
+ @Override
+ protected @Nullable Long responseContentLength(Void unused, Void unused2) {
+ return 100L;
+ }
+
+ @Override
+ protected @Nullable Long responseContentLengthUncompressed(Void unused, Void unused2) {
+ return null;
+ }
+ }
+
+ static class ConstantNetAttributesExtractor
+ extends InetSocketAddressNetAttributesExtractor<Void, Void> {
+
+ private static final InetSocketAddress ADDRESS =
+ InetSocketAddress.createUnresolved("localhost", 8080);
+
+ @Override
+ public @Nullable InetSocketAddress getAddress(Void unused, @Nullable Void unused2) {
+ return ADDRESS;
+ }
+
+ @Override
+ public @Nullable String transport(Void unused) {
+ return SemanticAttributes.NetTransportValues.IP_TCP;
+ }
+ }
+}

MODIFY
instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/Instrumenter.java
instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/Instrumenter.java
package io.opentelemetry.instrumentation.api.instrumenter;
import io.opentelemetry.api.OpenTelemetry;
-import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
-import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanBuilder;
import io.opentelemetry.api.trace.SpanKind;
@@ -121,11 +119,11 @@ public class Instrumenter<REQUEST, RESPONSE> {
spanBuilder.setStartTimestamp(startTimeExtractor.extract(request));
}
- AttributesBuilder attributesBuilder = Attributes.builder();
+ UnsafeAttributes attributesBuilder = new UnsafeAttributes();
for (AttributesExtractor<? super REQUEST, ? super RESPONSE> extractor : extractors) {
extractor.onStart(attributesBuilder, request);
}
- Attributes attributes = attributesBuilder.build();
+ Attributes attributes = attributesBuilder;
Context context = parentContext;
@@ -133,7 +131,7 @@ public class Instrumenter<REQUEST, RESPONSE> {
context = requestListener.start(context, attributes);
}
- attributes.forEach((key, value) -> spanBuilder.setAttribute((AttributeKey) key, value));
+ spanBuilder.setAllAttributes(attributes);
Span span = spanBuilder.startSpan();
context = context.with(span);
switch (spanKind) {
@@ -155,17 +153,17 @@ public class Instrumenter<REQUEST, RESPONSE> {
public void end(Context context, REQUEST request, RESPONSE response, @Nullable Throwable error) {
Span span = Span.fromContext(context);
- AttributesBuilder attributesBuilder = Attributes.builder();
+ UnsafeAttributes attributesBuilder = new UnsafeAttributes();
for (AttributesExtractor<? super REQUEST, ? super RESPONSE> extractor : extractors) {
extractor.onEnd(attributesBuilder, request, response);
}
- Attributes attributes = attributesBuilder.build();
+ Attributes attributes = attributesBuilder;
for (RequestListener requestListener : requestListeners) {
requestListener.end(context, attributes);
}
- attributes.forEach((key, value) -> span.setAttribute((AttributeKey) key, value));
+ span.setAllAttributes(attributes);
if (error != null) {
error = errorCauseExtractor.extractCause(error);

ADD
instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/UnsafeAttributes.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.instrumenter;
+
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.common.AttributesBuilder;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * The {@link AttributesBuilder} and {@link Attributes} used by the instrumentation API. We are able
+ * to take advantage of the fact that we know our attributes builder cannot be reused to create
+ * multiple Attributes instances. So we use just one storage for both the builder and attributes. A
+ * couple of methods still require copying to satisfy the interface contracts, but in practice
+ * should never be called by user code even though they can.
+ */
+final class UnsafeAttributes extends HashMap<AttributeKey<?>, Object>
+ implements Attributes, AttributesBuilder {
+
+ // Attributes
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public <T> T get(AttributeKey<T> key) {
+ return (T) super.get(key);
+ }
+
+ @Override
+ public Map<AttributeKey<?>, Object> asMap() {
+ return this;
+ }
+
+ // This can be called by user code in a RequestListener so copy. In practice, it should not be
+ // called as there is no real use case.
+ @Override
+ public AttributesBuilder toBuilder() {
+ return Attributes.builder().putAll(this);
+ }
+
+ // AttributesBuilder
+
+ // This can be called by user code in an AttributesExtractor so copy. In practice, it should not
+ // be called as there is no real use case.
+ @Override
+ public Attributes build() {
+ return toBuilder().build();
+ }
+
+ @Override
+ public <T> AttributesBuilder put(AttributeKey<Long> key, int value) {
+ return put(key, (long) value);
+ }
+
+ @Override
+ public <T> AttributesBuilder put(AttributeKey<T> key, T value) {
+ super.put(key, value);
+ return this;
+ }
+
+ @Override
+ public AttributesBuilder putAll(Attributes attributes) {
+ attributes.forEach(this::put);
+ return this;
+ }
+}

ADD
instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/UnsafeAttributesTest.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.api.instrumenter;
+
+import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
+import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.attributeEntry;
+
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import org.junit.jupiter.api.Test;
+
+class UnsafeAttributesTest {
+
+ @Test
+ void buildAndUse() {
+ Attributes previous =
+ new UnsafeAttributes().put("world", "earth").put("country", "japan").build();
+
+ UnsafeAttributes attributes = new UnsafeAttributes();
+ attributes.put(AttributeKey.stringKey("animal"), "cat");
+ attributes.put("needs_catnip", false);
+ // Overwrites
+ attributes.put("needs_catnip", true);
+ attributes.put(AttributeKey.longKey("lives"), 9);
+ attributes.putAll(previous);
+
+ assertThat((Attributes) attributes)
+ .containsOnly(
+ attributeEntry("world", "earth"),
+ attributeEntry("country", "japan"),
+ attributeEntry("animal", "cat"),
+ attributeEntry("needs_catnip", true),
+ attributeEntry("lives", 9L));
+
+ Attributes built = attributes.build();
+ assertThat(built)
+ .containsOnly(
+ attributeEntry("world", "earth"),
+ attributeEntry("country", "japan"),
+ attributeEntry("animal", "cat"),
+ attributeEntry("needs_catnip", true),
+ attributeEntry("lives", 9L));
+
+ attributes.put("clothes", "fur");
+ assertThat((Attributes) attributes)
+ .containsOnly(
+ attributeEntry("world", "earth"),
+ attributeEntry("country", "japan"),
+ attributeEntry("animal", "cat"),
+ attributeEntry("needs_catnip", true),
+ attributeEntry("lives", 9L),
+ attributeEntry("clothes", "fur"));
+
+ // Unmodified
+ assertThat(built)
+ .containsOnly(
+ attributeEntry("world", "earth"),
+ attributeEntry("country", "japan"),
+ attributeEntry("animal", "cat"),
+ attributeEntry("needs_catnip", true),
+ attributeEntry("lives", 9L));
+
+ Attributes modified = attributes.toBuilder().put("country", "us").build();
+ assertThat(modified)
+ .containsOnly(
+ attributeEntry("world", "earth"),
+ attributeEntry("country", "us"),
+ attributeEntry("animal", "cat"),
+ attributeEntry("needs_catnip", true),
+ attributeEntry("lives", 9L),
+ attributeEntry("clothes", "fur"));
+
+ // Unmodified
+ assertThat((Attributes) attributes)
+ .containsOnly(
+ attributeEntry("world", "earth"),
+ attributeEntry("country", "japan"),
+ attributeEntry("animal", "cat"),
+ attributeEntry("needs_catnip", true),
+ attributeEntry("lives", 9L),
+ attributeEntry("clothes", "fur"));
+ }
+}

Add an optimized Attributes implementation for instrumenter (#3136)