diff --git a/api/incubator/src/main/java/io/opentelemetry/api/incubator/trace/ExtendedSpanBuilder.java b/api/incubator/src/main/java/io/opentelemetry/api/incubator/trace/ExtendedSpanBuilder.java index 419172f10be..2c76cec18c0 100644 --- a/api/incubator/src/main/java/io/opentelemetry/api/incubator/trace/ExtendedSpanBuilder.java +++ b/api/incubator/src/main/java/io/opentelemetry/api/incubator/trace/ExtendedSpanBuilder.java @@ -92,6 +92,23 @@ T startAndCall( void startAndRun( SpanRunnable runnable, BiConsumer handleException) throws E; + /** + * Sets the span type, which identifies the class of spans this span belongs to, such as HTTP + * client requests or database calls, as defined by semantic conventions. The span type can't be + * changed after the span is started. + * + *

The span type is a case-sensitive ASCII string of at most 255 characters. It starts with a + * letter and contains only letters, digits, {@code _}, {@code .}, {@code -}, and {@code /}. + * Implementations may ignore span types that don't conform to this syntax. An empty span type is + * equivalent to an unset one. + * + * @param type the span type. + * @return this. + */ + default ExtendedSpanBuilder setType(String type) { + return this; + } + /** {@inheritDoc} */ @Override ExtendedSpanBuilder setParent(Context context); diff --git a/api/incubator/src/test/java/io/opentelemetry/api/incubator/trace/ExtendedDefaultTracerTest.java b/api/incubator/src/test/java/io/opentelemetry/api/incubator/trace/ExtendedDefaultTracerTest.java index 1ccec2a4ba6..c591f3b2a00 100644 --- a/api/incubator/src/test/java/io/opentelemetry/api/incubator/trace/ExtendedDefaultTracerTest.java +++ b/api/incubator/src/test/java/io/opentelemetry/api/incubator/trace/ExtendedDefaultTracerTest.java @@ -44,6 +44,8 @@ void incubatingApi() { ExtendedSpanBuilder spanBuilder = (ExtendedSpanBuilder) ExtendedDefaultTracer.getNoop().spanBuilder("test"); assertThat(spanBuilder.setParentFrom(null, null)).isSameAs(spanBuilder); + assertThat(spanBuilder.setType("http.client.request")).isSameAs(spanBuilder); + assertThat(spanBuilder.setType(null)).isSameAs(spanBuilder); SpanRunnable spanRunnable = Mockito.mock(SpanRunnable.class); diff --git a/sdk/all/src/jmh/java/io/opentelemetry/sdk/SpanRecordBenchmark.java b/sdk/all/src/jmh/java/io/opentelemetry/sdk/SpanRecordBenchmark.java index 24aae90060e..fa1afd44f89 100644 --- a/sdk/all/src/jmh/java/io/opentelemetry/sdk/SpanRecordBenchmark.java +++ b/sdk/all/src/jmh/java/io/opentelemetry/sdk/SpanRecordBenchmark.java @@ -6,7 +6,9 @@ package io.opentelemetry.sdk; import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.incubator.trace.ExtendedSpanBuilder; import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.TraceFlags; import io.opentelemetry.api.trace.TraceState; @@ -39,6 +41,7 @@ *

    *
  • {@link BenchmarkState#spanSize}: the size of the span, which is a composite of the number * of attributes, events, and links attached to the span. + *
  • {@link BenchmarkState#withType}: whether the span type is set. *
* *

Each operation consists of recording {@link SpanRecordBenchmark#RECORDS_PER_INVOCATION} spans. @@ -54,6 +57,8 @@ public class SpanRecordBenchmark { private static final int RECORDS_PER_INVOCATION = BenchmarkUtils.RECORDS_PER_INVOCATION; private static final int MAX_THREADS = 4; private static final int QUEUE_SIZE = RECORDS_PER_INVOCATION * MAX_THREADS; + // Longest span type in semantic conventions. + private static final String SPAN_TYPE = "dotnet.http.request.wait_for_connection.internal"; @State(Scope.Benchmark) public static class BenchmarkState { @@ -62,6 +67,9 @@ public static class BenchmarkState { // benchmark various shapes of spans without combinatorial explosion. @Param SpanSize spanSize; + @Param({"false", "true"}) + boolean withType; + SdkTracerProvider tracerProvider; Tracer tracer; List> attributeKeys; @@ -144,7 +152,11 @@ public void record_MultipleThreads(BenchmarkState benchmarkState) { private static void record(BenchmarkState benchmarkState) { for (int i = 0; i < RECORDS_PER_INVOCATION; i++) { - Span span = benchmarkState.tracer.spanBuilder("test span name").startSpan(); + SpanBuilder builder = benchmarkState.tracer.spanBuilder("test span name"); + if (benchmarkState.withType) { + ((ExtendedSpanBuilder) builder).setType(SPAN_TYPE); + } + Span span = builder.startSpan(); for (int j = 0; j < benchmarkState.attributeKeys.size(); j++) { span.setAttribute( benchmarkState.attributeKeys.get(j), benchmarkState.attributeValues.get(j)); diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/ExtendedSdkSpanBuilder.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/ExtendedSdkSpanBuilder.java index 8ddf5a4a957..5435beb27a3 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/ExtendedSdkSpanBuilder.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/ExtendedSdkSpanBuilder.java @@ -53,6 +53,12 @@ public ExtendedSpanBuilder setSpanKind(SpanKind spanKind) { return this; } + @Override + public ExtendedSpanBuilder setType(String type) { + super.setType(type); + return this; + } + @Override public ExtendedSpanBuilder addLink(SpanContext spanContext) { super.addLink(spanContext); diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpan.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpan.java index ef3d2254c0b..da6afc7683c 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpan.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpan.java @@ -26,6 +26,7 @@ import io.opentelemetry.sdk.trace.data.LinkData; import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.sdk.trace.internal.ExtendedReadableSpan; import io.opentelemetry.sdk.trace.internal.ExtendedSpanProcessor; import java.util.ArrayList; import java.util.Collections; @@ -38,7 +39,7 @@ /** Implementation for the {@link Span} class that records trace events. */ @ThreadSafe -final class SdkSpan implements ReadWriteSpan { +final class SdkSpan implements ReadWriteSpan, ExtendedReadableSpan { private static final Logger logger = Logger.getLogger(SdkSpan.class.getName()); @@ -54,6 +55,7 @@ final class SdkSpan implements ReadWriteSpan { private final ExceptionAttributeResolver exceptionAttributeResolver; // The kind of the span. private final SpanKind kind; + @Nullable private final String type; // The clock used to get the time. private final AnchoredClock clock; // The resource associated with this span. @@ -127,6 +129,7 @@ private SdkSpan( String name, InstrumentationScopeInfo instrumentationScopeInfo, SpanKind kind, + @Nullable String type, SpanContext parentSpanContext, SpanLimits spanLimits, SpanProcessor spanProcessor, @@ -145,6 +148,7 @@ private SdkSpan( this.totalRecordedLinks = totalRecordedLinks; this.name = name; this.kind = kind; + this.type = type; this.spanProcessor = spanProcessor; this.exceptionAttributeResolver = exceptionAttributeResolver; this.resource = resource; @@ -162,6 +166,7 @@ private SdkSpan( * @param context supplies the trace_id and span_id for the newly started span. * @param name the displayed name for the new span. * @param kind the span kind. + * @param type the validated span type, or {@code null} if not set. * @param parentSpan the parent span, or {@link Span#getInvalid()} if this span is a root span. * @param spanLimits limits applied to this span. * @param spanProcessor handler called when the span starts and ends. @@ -177,6 +182,7 @@ static SdkSpan startSpan( String name, InstrumentationScopeInfo instrumentationScopeInfo, SpanKind kind, + @Nullable String type, Span parentSpan, Context parentContext, SpanLimits spanLimits, @@ -218,6 +224,7 @@ static SdkSpan startSpan( name, instrumentationScopeInfo, kind, + type, parentSpan.getSpanContext(), spanLimits, spanProcessor, @@ -603,6 +610,12 @@ public SpanKind getKind() { return kind; } + @Override + @Nullable + public String getType() { + return type; + } + long getStartEpochNanos() { return startEpochNanos; } @@ -670,6 +683,8 @@ public String toString() { + name + ", kind=" + kind + + ", type=" + + type + ", attributes=" + attributes + ", status=" diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpanBuilder.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpanBuilder.java index 1305e5c5210..d8963f9f666 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpanBuilder.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpanBuilder.java @@ -21,10 +21,12 @@ import io.opentelemetry.api.trace.TraceFlags; import io.opentelemetry.api.trace.TraceId; import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.common.impl.ApiUsageLogger; import io.opentelemetry.context.Context; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.common.internal.AttributeUtil; import io.opentelemetry.sdk.common.internal.AttributesMap; +import io.opentelemetry.sdk.common.internal.ThrottlingLogger; import io.opentelemetry.sdk.trace.data.LinkData; import io.opentelemetry.sdk.trace.samplers.SamplingDecision; import io.opentelemetry.sdk.trace.samplers.SamplingResult; @@ -32,11 +34,18 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.annotation.Nullable; /** {@link SdkSpanBuilder} is SDK implementation of {@link SpanBuilder}. */ class SdkSpanBuilder implements SpanBuilder { + private static final ThrottlingLogger logger = + new ThrottlingLogger(Logger.getLogger(SdkSpanBuilder.class.getName())); + + private static final int MAX_TYPE_LENGTH = 255; + private static final Span RANDOM_TRACE_ID_PRIMORDIAL_SPAN = Span.wrap( SpanContext.create( @@ -56,6 +65,7 @@ class SdkSpanBuilder implements SpanBuilder { @Nullable private Context parent; // null means: Use current context. private SpanKind spanKind = SpanKind.INTERNAL; + @Nullable private String type; @Nullable private AttributesMap attributes; @Nullable private List links; private int totalNumberOfLinksAdded = 0; @@ -96,6 +106,18 @@ public SpanBuilder setSpanKind(SpanKind spanKind) { return this; } + public SpanBuilder setType(String type) { + if (type == null) { + ApiUsageLogger.logNullParam(SpanBuilder.class, "setType", "type"); + return this; + } + if (type.isEmpty()) { + return this; + } + this.type = type; + return this; + } + @Override public SpanBuilder addLink(SpanContext spanContext) { if (spanContext == null || (!spanContext.isValid() && spanContext.getTraceState().isEmpty())) { @@ -219,6 +241,20 @@ public Span startSpan() { // startSpan is called. If that happens all the links will be added in a new list. links = null; Attributes immutableAttributes = attributes == null ? Attributes.empty() : attributes; + String validatedType = type; + if (validatedType != null && !isValidType(validatedType)) { + if (logger.isLoggable(Level.WARNING)) { + logger.log( + Level.WARNING, + "Span type \"" + + validatedType + + "\" is invalid, creating span without a type. Span types must consist of " + + MAX_TYPE_LENGTH + + " or fewer characters including alphanumeric, _, ., -, /, and start with a" + + " letter."); + } + validatedType = null; + } SamplingResult samplingResult = tracerSharedState .getSampler() @@ -266,6 +302,7 @@ public Span startSpan() { spanName, instrumentationScopeInfo, spanKind, + validatedType, parentSpan, parentContext, spanLimits, @@ -291,6 +328,28 @@ private AttributesMap attributes() { return attributes; } + // Visible for testing + static boolean isValidType(String type) { + int length = type.length(); + if (length > MAX_TYPE_LENGTH) { + return false; + } + for (int i = 0; i < length; i++) { + char c = type.charAt(i); + if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { + continue; + } + if (i == 0) { + return false; + } + if ((c >= '0' && c <= '9') || c == '_' || c == '.' || c == '-' || c == '/') { + continue; + } + return false; + } + return true; + } + // Visible for testing static boolean isRecording(SamplingDecision decision) { return SamplingDecision.RECORD_ONLY.equals(decision) diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SpanWrapper.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SpanWrapper.java index 651e07d1127..56fe3abf1db 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SpanWrapper.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SpanWrapper.java @@ -17,7 +17,9 @@ import io.opentelemetry.sdk.trace.data.LinkData; import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.sdk.trace.internal.ExtendedSpanData; import java.util.List; +import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; /** @@ -31,7 +33,7 @@ */ @Immutable @AutoValue -abstract class SpanWrapper implements SpanData { +abstract class SpanWrapper implements ExtendedSpanData { abstract SdkSpan delegate(); abstract List resolvedLinks(); @@ -121,6 +123,12 @@ public SpanKind getKind() { return delegate().getKind(); } + @Override + @Nullable + public String getType() { + return delegate().getType(); + } + @Override public long getStartEpochNanos() { return delegate().getStartEpochNanos(); @@ -192,6 +200,9 @@ public final String toString() { + "kind=" + getKind() + ", " + + "type=" + + getType() + + ", " + "startEpochNanos=" + getStartEpochNanos() + ", " diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/ExtendedReadableSpan.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/ExtendedReadableSpan.java new file mode 100644 index 00000000000..ef4cae63835 --- /dev/null +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/ExtendedReadableSpan.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.internal; + +import io.opentelemetry.sdk.trace.ReadableSpan; +import javax.annotation.Nullable; + +/** + * Extended {@link ReadableSpan} with experimental APIs. + * + *

This class is internal and experimental. Its APIs are unstable and can change at any time. Its + * APIs (or a version of them) may be promoted to the public stable API in the future, but no + * guarantees are made. + */ +public interface ExtendedReadableSpan extends ReadableSpan { + + // TODO: once span type stabilizes, move getType() to ReadableSpan as a default method + + /** + * Returns the span type, or {@code null} if not set. + * + * @return the span type, or {@code null} if not set. + */ + @Nullable + String getType(); +} diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/ExtendedSpanData.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/ExtendedSpanData.java new file mode 100644 index 00000000000..2a082a5c201 --- /dev/null +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/ExtendedSpanData.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.internal; + +import io.opentelemetry.sdk.trace.data.SpanData; +import javax.annotation.Nullable; + +/** + * Extended {@link SpanData} with experimental APIs. + * + *

This class is internal and experimental. Its APIs are unstable and can change at any time. Its + * APIs (or a version of them) may be promoted to the public stable API in the future, but no + * guarantees are made. + */ +public interface ExtendedSpanData extends SpanData { + + // TODO: once span type stabilizes, move getType() to SpanData as a default method and override + // it in DelegatingSpanData + + /** + * Returns the span type, or {@code null} if not set. + * + * @return the span type, or {@code null} if not set. + */ + @Nullable + String getType(); +} diff --git a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java index 283c80d69a4..c4972859eb1 100644 --- a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java +++ b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java @@ -1270,6 +1270,7 @@ void spanDataToString() { + "name=SpanBuilderSdkTest, version=null, schemaUrl=null, attributes=\\{}}, " + "name=span_name, " + "kind=INTERNAL, " + + "type=null, " + "startEpochNanos=[0-9]+, " + "endEpochNanos=[0-9]+, " + "attributes=AttributesMap\\{data=\\{[^}]*}, capacity=128, totalAddedValues=2}, " diff --git a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanTest.java b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanTest.java index 93d11865044..361db5a0805 100644 --- a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanTest.java +++ b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanTest.java @@ -1167,6 +1167,7 @@ void addLink_FaultIn() { SPAN_NAME, instrumentationScopeInfo, SpanKind.INTERNAL, + null, Span.getInvalid(), Context.root(), SpanLimits.getDefault(), @@ -1524,6 +1525,7 @@ void onStartOnEndNotRequired() { SPAN_NAME, instrumentationScopeInfo, SpanKind.INTERNAL, + null, parentSpanId != null ? Span.wrap( SpanContext.create( @@ -1671,6 +1673,7 @@ private SdkSpan createTestSpan( SPAN_NAME, instrumentationScopeInfo, kind, + null, parentSpanId != null ? Span.wrap( SpanContext.create( @@ -1760,6 +1763,7 @@ void testAsSpanData() { name, instrumentationScopeInfo, kind, + null, parentSpanId != null ? Span.wrap( SpanContext.create( diff --git a/sdk/trace/src/testIncubating/java/io/opentelemetry/sdk/trace/SpanTypeTest.java b/sdk/trace/src/testIncubating/java/io/opentelemetry/sdk/trace/SpanTypeTest.java new file mode 100644 index 00000000000..06745812656 --- /dev/null +++ b/sdk/trace/src/testIncubating/java/io/opentelemetry/sdk/trace/SpanTypeTest.java @@ -0,0 +1,150 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.github.netmikey.logunit.api.LogCapturer; +import io.opentelemetry.api.incubator.trace.ExtendedSpanBuilder; +import io.opentelemetry.api.incubator.trace.ExtendedTracer; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import io.opentelemetry.sdk.trace.internal.ExtendedReadableSpan; +import io.opentelemetry.sdk.trace.internal.ExtendedSpanData; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.slf4j.event.Level; + +class SpanTypeTest { + + private static final String LONGEST_VALID_TYPE = "a" + repeat('b', 254); + + @RegisterExtension + LogCapturer logs = LogCapturer.create().captureForType(SdkSpanBuilder.class, Level.WARN); + + private final InMemorySpanExporter exporter = InMemorySpanExporter.create(); + private SdkTracerProvider tracerProvider; + private ExtendedTracer tracer; + + @BeforeEach + void setUp() { + tracerProvider = + SdkTracerProvider.builder().addSpanProcessor(SimpleSpanProcessor.create(exporter)).build(); + tracer = (ExtendedTracer) tracerProvider.get("test"); + } + + @AfterEach + void tearDown() { + tracerProvider.shutdown(); + } + + @Test + void typeIsExposedOnReadableSpanAndSpanData() { + Span span = tracer.spanBuilder("GET /orders").setType("http.server.request").startSpan(); + assertThat(((ExtendedReadableSpan) span).getType()).isEqualTo("http.server.request"); + span.end(); + + assertThat(exportedType()).isEqualTo("http.server.request"); + } + + @Test + void typeIsNullWhenNotSet() { + tracer.spanBuilder("span").startSpan().end(); + + assertThat(exportedType()).isNull(); + } + + @Test + void emptyTypeIsIgnored() { + tracer.spanBuilder("span").setType("").startSpan().end(); + + assertThat(exportedType()).isNull(); + logs.assertDoesNotContain("Span type"); + } + + @Test + void emptyTypeDoesNotOverridePreviousType() { + tracer.spanBuilder("span").setType("db.client.call").setType("").startSpan().end(); + + assertThat(exportedType()).isEqualTo("db.client.call"); + } + + @Test + void nullTypeIsIgnored() { + tracer.spanBuilder("span").setType("db.client.call").setType(null).startSpan().end(); + + assertThat(exportedType()).isEqualTo("db.client.call"); + } + + @Test + void lastTypeWins() { + tracer.spanBuilder("span").setType("first").setType("second").startSpan().end(); + + assertThat(exportedType()).isEqualTo("second"); + } + + @Test + void invalidTypeIsDroppedAndLogged() { + tracer.spanBuilder("span").setType("1invalid").startSpan().end(); + + assertThat(exportedType()).isNull(); + logs.assertContains("Span type \"1invalid\" is invalid, creating span without a type."); + } + + @Test + void setTypeReturnsSameBuilder() { + ExtendedSpanBuilder builder = tracer.spanBuilder("span"); + assertThat(builder.setType("http.client.request")).isSameAs(builder); + } + + @ParameterizedTest + @MethodSource("typeSyntaxCases") + void typeSyntax(String type, boolean expectedValid) { + assertThat(SdkSpanBuilder.isValidType(type)).isEqualTo(expectedValid); + } + + private static Stream typeSyntaxCases() { + return Stream.of( + Arguments.argumentSet("empty", "", true), + Arguments.argumentSet("single letter", "a", true), + Arguments.argumentSet("uppercase", "HTTP.Server", true), + Arguments.argumentSet("semconv style", "messaging.consumer.process", true), + Arguments.argumentSet("all allowed chars", "a0_.-/Z9", true), + Arguments.argumentSet("max length", LONGEST_VALID_TYPE, true), + Arguments.argumentSet("over max length", LONGEST_VALID_TYPE + "c", false), + Arguments.argumentSet("starts with digit", "1abc", false), + Arguments.argumentSet("starts with underscore", "_abc", false), + Arguments.argumentSet("starts with dot", ".abc", false), + Arguments.argumentSet("starts with dash", "-abc", false), + Arguments.argumentSet("starts with slash", "/abc", false), + Arguments.argumentSet("space", "a b", false), + Arguments.argumentSet("colon", "a:b", false), + Arguments.argumentSet("non-ascii letter", "café", false), + Arguments.argumentSet("non-ascii first char", "éa", false)); + } + + @Nullable + private String exportedType() { + assertThat(exporter.getFinishedSpanItems()).hasSize(1); + return ((ExtendedSpanData) exporter.getFinishedSpanItems().get(0)).getType(); + } + + private static String repeat(char c, int count) { + StringBuilder sb = new StringBuilder(count); + for (int i = 0; i < count; i++) { + sb.append(c); + } + return sb.toString(); + } +}