Skip to content

Commit

Permalink
Add an optimized Attributes implementation for instrumenter (open-tel…
Browse files Browse the repository at this point in the history
  • Loading branch information
Anuraag Agrawal authored and robododge committed Jun 17, 2021
1 parent 0adc7b1 commit b3773fc
Show file tree
Hide file tree
Showing 5 changed files with 315 additions and 8 deletions.
2 changes: 2 additions & 0 deletions benchmark/benchmark.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
Original file line number Diff line number Diff line change
@@ -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<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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -121,19 +119,19 @@ public Context start(Context parentContext, REQUEST request) {
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;

for (RequestListener requestListener : requestListeners) {
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) {
Expand All @@ -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<? 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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<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;
}
}
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.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"));
}
}

0 comments on commit b3773fc

Please sign in to comment.