diff --git a/benchmark/benchmark.gradle b/benchmark/benchmark.gradle index af3f643a7e54..beef2f25638f 100644 --- a/benchmark/benchmark.gradle +++ b/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" diff --git a/benchmark/src/jmh/java/io/opentelemetry/benchmark/InstrumenterBenchmark.java b/benchmark/src/jmh/java/io/opentelemetry/benchmark/InstrumenterBenchmark.java new file mode 100644 index 000000000000..5dae8dac3f29 --- /dev/null +++ b/benchmark/src/jmh/java/io/opentelemetry/benchmark/InstrumenterBenchmark.java @@ -0,0 +1,152 @@ +/* + * 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 INSTRUMENTER = + Instrumenter.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 { + static HttpAttributesExtractor 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 { + + 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; + } + } +} diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/Instrumenter.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/Instrumenter.java index 1b76b392f5bd..0b99aff16796 100644 --- a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/Instrumenter.java +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/Instrumenter.java @@ -6,9 +6,7 @@ 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 Context start(Context parentContext, REQUEST request) { spanBuilder.setStartTimestamp(startTimeExtractor.extract(request)); } - AttributesBuilder attributesBuilder = Attributes.builder(); + UnsafeAttributes attributesBuilder = new UnsafeAttributes(); for (AttributesExtractor extractor : extractors) { extractor.onStart(attributesBuilder, request); } - Attributes attributes = attributesBuilder.build(); + Attributes attributes = attributesBuilder; Context context = parentContext; @@ -133,7 +131,7 @@ public Context start(Context parentContext, REQUEST request) { 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 Context start(Context parentContext, REQUEST request) { 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 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); diff --git a/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/UnsafeAttributes.java b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/UnsafeAttributes.java new file mode 100644 index 000000000000..07995752aea1 --- /dev/null +++ b/instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/instrumenter/UnsafeAttributes.java @@ -0,0 +1,69 @@ +/* + * 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, Object> + implements Attributes, AttributesBuilder { + + // Attributes + + @SuppressWarnings("unchecked") + @Override + public T get(AttributeKey key) { + return (T) super.get(key); + } + + @Override + public Map, 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 AttributesBuilder put(AttributeKey key, int value) { + return put(key, (long) value); + } + + @Override + public AttributesBuilder put(AttributeKey key, T value) { + super.put(key, value); + return this; + } + + @Override + public AttributesBuilder putAll(Attributes attributes) { + attributes.forEach(this::put); + return this; + } +} diff --git a/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/UnsafeAttributesTest.java b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/UnsafeAttributesTest.java new file mode 100644 index 000000000000..8e66b746a16b --- /dev/null +++ b/instrumentation-api/src/test/java/io/opentelemetry/instrumentation/api/instrumenter/UnsafeAttributesTest.java @@ -0,0 +1,86 @@ +/* + * 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")); + } +}