diff --git a/sdk_extensions/zpages/README.md b/sdk_extensions/zpages/README.md new file mode 100644 index 0000000000..fcff15b1bf --- /dev/null +++ b/sdk_extensions/zpages/README.md @@ -0,0 +1,9 @@ +# OpenTelemetry SDK Contrib - zPages + +[![Javadocs][javadoc-image]][javadoc-url] + +This module contains code for OpenTelemetry's Java zPages. + + +[javadoc-image]: https://www.javadoc.io/badge/io.opentelemetry/opentelemetry-sdk-contrib-auto-config.svg +[javadoc-url]: https://www.javadoc.io/doc/io.opentelemetry/opentelemetry-sdk-contrib-auto-config diff --git a/sdk_extensions/zpages/build.gradle b/sdk_extensions/zpages/build.gradle new file mode 100644 index 0000000000..1a8e83455d --- /dev/null +++ b/sdk_extensions/zpages/build.gradle @@ -0,0 +1,19 @@ +plugins { + id "java" + id "maven-publish" + + id "ru.vyarus.animalsniffer" +} + +description = 'OpenTelemetry - zPages' +ext.moduleName = "io.opentelemetry.sdk.extension.zpages" + +dependencies { + implementation project(':opentelemetry-api'), + project(':opentelemetry-sdk') + + implementation libraries.guava + compileOnly 'com.sun.net.httpserver:http:20070405' + + signature "org.codehaus.mojo.signature:java17:1.0@signature" +} diff --git a/sdk_extensions/zpages/src/main/java/io/opentelemetry/sdk/extensions/zpages/LatencyBoundary.java b/sdk_extensions/zpages/src/main/java/io/opentelemetry/sdk/extensions/zpages/LatencyBoundary.java new file mode 100644 index 0000000000..0b701a1828 --- /dev/null +++ b/sdk_extensions/zpages/src/main/java/io/opentelemetry/sdk/extensions/zpages/LatencyBoundary.java @@ -0,0 +1,100 @@ +/* + * Copyright 2020, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.sdk.extensions.zpages; + +import java.util.concurrent.TimeUnit; + +/** + * A class of boundaries for the latency buckets. The completed spans with a status of {@link + * io.opentelemetry.trace.Status#OK} are categorized into one of these buckets om the traceZ zPage. + */ +enum LatencyBoundary { + /** Stores finished successful requests of duration within the interval [0, 10us). */ + ZERO_MICROSx10(0, TimeUnit.MICROSECONDS.toNanos(10)), + + /** Stores finished successful requests of duration within the interval [10us, 100us). */ + MICROSx10_MICROSx100(TimeUnit.MICROSECONDS.toNanos(10), TimeUnit.MICROSECONDS.toNanos(100)), + + /** Stores finished successful requests of duration within the interval [100us, 1ms). */ + MICROSx100_MILLIx1(TimeUnit.MICROSECONDS.toNanos(100), TimeUnit.MILLISECONDS.toNanos(1)), + + /** Stores finished successful requests of duration within the interval [1ms, 10ms). */ + MILLIx1_MILLIx10(TimeUnit.MILLISECONDS.toNanos(1), TimeUnit.MILLISECONDS.toNanos(10)), + + /** Stores finished successful requests of duration within the interval [10ms, 100ms). */ + MILLIx10_MILLIx100(TimeUnit.MILLISECONDS.toNanos(10), TimeUnit.MILLISECONDS.toNanos(100)), + + /** Stores finished successful requests of duration within the interval [100ms, 1sec). */ + MILLIx100_SECONDx1(TimeUnit.MILLISECONDS.toNanos(100), TimeUnit.SECONDS.toNanos(1)), + + /** Stores finished successful requests of duration within the interval [1sec, 10sec). */ + SECONDx1_SECONDx10(TimeUnit.SECONDS.toNanos(1), TimeUnit.SECONDS.toNanos(10)), + + /** Stores finished successful requests of duration within the interval [10sec, 100sec). */ + SECONDx10_SECONDx100(TimeUnit.SECONDS.toNanos(10), TimeUnit.SECONDS.toNanos(100)), + + /** Stores finished successful requests of duration greater than or equal to 100sec. */ + SECONDx100_MAX(TimeUnit.SECONDS.toNanos(100), Long.MAX_VALUE); + + private final long latencyLowerBound; + private final long latencyUpperBound; + + /** + * Constructs a {@code LatencyBoundaries} with the given boundaries and label. + * + * @param latencyLowerBound the latency lower bound of the bucket. + * @param latencyUpperBound the latency upper bound of the bucket. + */ + LatencyBoundary(long latencyLowerBound, long latencyUpperBound) { + this.latencyLowerBound = latencyLowerBound; + this.latencyUpperBound = latencyUpperBound; + } + + /** + * Returns the latency lower bound of the bucket. + * + * @return the latency lower bound of the bucket. + */ + long getLatencyLowerBound() { + return latencyLowerBound; + } + + /** + * Returns the latency upper bound of the bucket. + * + * @return the latency upper bound of the bucket. + */ + long getLatencyUpperBound() { + return latencyUpperBound; + } + + /** + * Returns the LatencyBoundary that the argument falls into. + * + * @param latencyNanos latency in nanoseconds. + * @return the LatencyBoundary that latencyNanos falls into. + */ + static LatencyBoundary getBoundary(long latencyNanos) { + for (LatencyBoundary bucket : LatencyBoundary.values()) { + if (latencyNanos >= bucket.getLatencyLowerBound() + && latencyNanos < bucket.getLatencyUpperBound()) { + return bucket; + } + } + return ZERO_MICROSx10; + } +} diff --git a/sdk_extensions/zpages/src/main/java/io/opentelemetry/sdk/extensions/zpages/SpanBucket.java b/sdk_extensions/zpages/src/main/java/io/opentelemetry/sdk/extensions/zpages/SpanBucket.java new file mode 100644 index 0000000000..e611211d0e --- /dev/null +++ b/sdk_extensions/zpages/src/main/java/io/opentelemetry/sdk/extensions/zpages/SpanBucket.java @@ -0,0 +1,64 @@ +/* + * Copyright 2020, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.sdk.extensions.zpages; + +import com.google.common.primitives.UnsignedInts; +import io.opentelemetry.sdk.trace.ReadableSpan; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReferenceArray; + +final class SpanBucket { + // A power of 2 means Integer.MAX_VALUE % bucketSize = bucketSize - 1, so the index will always + // loop back to 0. + private static final int LATENCY_BUCKET_SIZE = 16; + private static final int ERROR_BUCKET_SIZE = 8; + + private final AtomicReferenceArray spans; + private final AtomicInteger index; + private final int bucketSize; + + SpanBucket(boolean isLatencyBucket) { + bucketSize = isLatencyBucket ? LATENCY_BUCKET_SIZE : ERROR_BUCKET_SIZE; + spans = new AtomicReferenceArray<>(bucketSize); + index = new AtomicInteger(); + } + + void add(ReadableSpan span) { + spans.set(UnsignedInts.remainder(index.getAndIncrement(), bucketSize), span); + } + + int size() { + for (int i = bucketSize - 1; i >= 0; i--) { + if (spans.get(i) != null) { + return i + 1; + } + } + return 0; + } + + void addTo(List result) { + for (int i = 0; i < bucketSize; i++) { + ReadableSpan span = spans.get(i); + if (span != null) { + result.add(span); + } else { + break; + } + } + } +} diff --git a/sdk_extensions/zpages/src/main/java/io/opentelemetry/sdk/extensions/zpages/TracezDataAggregator.java b/sdk_extensions/zpages/src/main/java/io/opentelemetry/sdk/extensions/zpages/TracezDataAggregator.java new file mode 100644 index 0000000000..00ebb850a6 --- /dev/null +++ b/sdk_extensions/zpages/src/main/java/io/opentelemetry/sdk/extensions/zpages/TracezDataAggregator.java @@ -0,0 +1,174 @@ +/* + * Copyright 2020, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.sdk.extensions.zpages; + +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.trace.Status; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeSet; +import javax.annotation.concurrent.ThreadSafe; + +/** + * A data aggregator for the traceZ zPage. + * + *

The traceZ data aggregator compiles information about the running spans, span latencies, and + * error spans for the frontend of the zPage. + */ +@ThreadSafe +final class TracezDataAggregator { + private final TracezSpanProcessor spanProcessor; + + /** + * Constructor for {@link TracezDataAggregator}. + * + * @param spanProcessor collects span data. + */ + TracezDataAggregator(TracezSpanProcessor spanProcessor) { + this.spanProcessor = spanProcessor; + } + + /** + * Returns a Set of running and completed span names for {@link TracezDataAggregator}. + * + * @return a Set of {@link String}. + */ + Set getSpanNames() { + Set spanNames = new TreeSet<>(); + Collection allRunningSpans = spanProcessor.getRunningSpans(); + for (ReadableSpan span : allRunningSpans) { + spanNames.add(span.getName()); + } + spanNames.addAll(spanProcessor.getCompletedSpanCache().keySet()); + return spanNames; + } + + /** + * Returns a Map of the running span counts for {@link TracezDataAggregator}. + * + * @return a Map of span counts for each span name. + */ + Map getRunningSpanCounts() { + Collection allRunningSpans = spanProcessor.getRunningSpans(); + Map numSpansPerName = new HashMap<>(); + for (ReadableSpan span : allRunningSpans) { + Integer prevValue = numSpansPerName.get(span.getName()); + numSpansPerName.put(span.getName(), prevValue != null ? prevValue + 1 : 1); + } + return numSpansPerName; + } + + /** + * Returns a List of all running spans with a given span name for {@link TracezDataAggregator}. + * + * @param spanName name to filter returned spans. + * @return a List of {@link SpanData}. + */ + List getRunningSpans(String spanName) { + Collection allRunningSpans = spanProcessor.getRunningSpans(); + List filteredSpans = new ArrayList<>(); + for (ReadableSpan span : allRunningSpans) { + if (span.getName().equals(spanName)) { + filteredSpans.add(span.toSpanData()); + } + } + return filteredSpans; + } + + /** + * Returns a Map of span names to counts for all {@link Status#OK} spans in {@link + * TracezDataAggregator}. + * + * @return a Map of span names to counts, where the counts are further indexed by the latency + * boundaries. + */ + Map> getSpanLatencyCounts() { + Map completedSpanCache = spanProcessor.getCompletedSpanCache(); + Map> numSpansPerName = new HashMap<>(); + for (Entry cacheEntry : completedSpanCache.entrySet()) { + numSpansPerName.put( + cacheEntry.getKey(), cacheEntry.getValue().getLatencyBoundaryToCountMap()); + } + return numSpansPerName; + } + + /** + * Returns a List of all {@link Status#OK} spans with a given span name between [lowerBound, + * upperBound) for {@link TracezDataAggregator}. + * + * @param spanName name to filter returned spans. + * @param lowerBound latency lower bound (inclusive) + * @param upperBound latency upper bound (exclusive) + * @return a List of {@link SpanData}. + */ + List getOkSpans(String spanName, long lowerBound, long upperBound) { + Map completedSpanCache = spanProcessor.getCompletedSpanCache(); + TracezSpanBuckets buckets = completedSpanCache.get(spanName); + if (buckets == null) { + return Collections.emptyList(); + } + Collection allOkSpans = buckets.getOkSpans(); + List filteredSpans = new ArrayList<>(); + for (ReadableSpan span : allOkSpans) { + if (span.getLatencyNanos() >= lowerBound && span.getLatencyNanos() < upperBound) { + filteredSpans.add(span.toSpanData()); + } + } + return Collections.unmodifiableList(filteredSpans); + } + + /** + * Returns a Map of error span counts for {@link TracezDataAggregator}. + * + * @return a Map of error span counts for each span name. + */ + Map getErrorSpanCounts() { + Map completedSpanCache = spanProcessor.getCompletedSpanCache(); + Map numErrorsPerName = new HashMap<>(); + for (Entry cacheEntry : completedSpanCache.entrySet()) { + numErrorsPerName.put(cacheEntry.getKey(), cacheEntry.getValue().getErrorSpans().size()); + } + return numErrorsPerName; + } + + /** + * Returns a List of error spans with a given span name for {@link TracezDataAggregator}. + * + * @param spanName name to filter returned spans. + * @return a List of {@link SpanData}. + */ + List getErrorSpans(String spanName) { + Map completedSpanCache = spanProcessor.getCompletedSpanCache(); + TracezSpanBuckets buckets = completedSpanCache.get(spanName); + if (buckets == null) { + return Collections.emptyList(); + } + Collection allErrorSpans = buckets.getErrorSpans(); + List errorSpans = new ArrayList<>(); + for (ReadableSpan span : allErrorSpans) { + errorSpans.add(span.toSpanData()); + } + return Collections.unmodifiableList(errorSpans); + } +} diff --git a/sdk_extensions/zpages/src/main/java/io/opentelemetry/sdk/extensions/zpages/TracezSpanBuckets.java b/sdk_extensions/zpages/src/main/java/io/opentelemetry/sdk/extensions/zpages/TracezSpanBuckets.java new file mode 100644 index 0000000000..fd201ad94d --- /dev/null +++ b/sdk_extensions/zpages/src/main/java/io/opentelemetry/sdk/extensions/zpages/TracezSpanBuckets.java @@ -0,0 +1,87 @@ +/* + * Copyright 2020, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.sdk.extensions.zpages; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.trace.Status; +import io.opentelemetry.trace.Status.CanonicalCode; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +final class TracezSpanBuckets { + private final ImmutableMap latencyBuckets; + private final ImmutableMap errorBuckets; + + TracezSpanBuckets() { + ImmutableMap.Builder latencyBucketsBuilder = + ImmutableMap.builder(); + for (LatencyBoundary bucket : LatencyBoundary.values()) { + latencyBucketsBuilder.put(bucket, new SpanBucket(/* isLatencyBucket= */ true)); + } + latencyBuckets = latencyBucketsBuilder.build(); + ImmutableMap.Builder errorBucketsBuilder = ImmutableMap.builder(); + for (CanonicalCode code : CanonicalCode.values()) { + if (!code.toStatus().isOk()) { + errorBucketsBuilder.put(code, new SpanBucket(/* isLatencyBucket= */ false)); + } + } + errorBuckets = errorBucketsBuilder.build(); + } + + void addToBucket(ReadableSpan span) { + Status status = span.toSpanData().getStatus(); + if (status.isOk()) { + latencyBuckets.get(LatencyBoundary.getBoundary(span.getLatencyNanos())).add(span); + return; + } + errorBuckets.get(status.getCanonicalCode()).add(span); + } + + Map getLatencyBoundaryToCountMap() { + Map latencyCounts = new EnumMap<>(LatencyBoundary.class); + for (LatencyBoundary bucket : LatencyBoundary.values()) { + latencyCounts.put(bucket, latencyBuckets.get(bucket).size()); + } + return latencyCounts; + } + + List getOkSpans() { + List okSpans = new ArrayList<>(); + for (SpanBucket latencyBucket : latencyBuckets.values()) { + latencyBucket.addTo(okSpans); + } + return okSpans; + } + + List getErrorSpans() { + List errorSpans = new ArrayList<>(); + for (SpanBucket errorBucket : errorBuckets.values()) { + errorBucket.addTo(errorSpans); + } + return errorSpans; + } + + List getSpans() { + List spans = new ArrayList<>(); + spans.addAll(getOkSpans()); + spans.addAll(getErrorSpans()); + return spans; + } +} diff --git a/sdk_extensions/zpages/src/main/java/io/opentelemetry/sdk/extensions/zpages/TracezSpanProcessor.java b/sdk_extensions/zpages/src/main/java/io/opentelemetry/sdk/extensions/zpages/TracezSpanProcessor.java new file mode 100644 index 0000000000..83325ef91b --- /dev/null +++ b/sdk_extensions/zpages/src/main/java/io/opentelemetry/sdk/extensions/zpages/TracezSpanProcessor.java @@ -0,0 +1,195 @@ +/* + * Copyright 2020, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.sdk.extensions.zpages; + +import io.opentelemetry.sdk.common.export.ConfigBuilder; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.SpanProcessor; +import io.opentelemetry.trace.SpanId; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import javax.annotation.concurrent.ThreadSafe; + +/** + * A {@link SpanProcessor} implementation for the traceZ zPage. + * + *

Configuration options for {@link TracezSpanProcessor} can be read from system properties, + * environment variables, or {@link java.util.Properties} objects. + * + *

For system properties and {@link java.util.Properties} objects, {@link TracezSpanProcessor} + * will look for the following names: + * + *

+ * + *

For environment variables, {@link TracezSpanProcessor} will look for the following names: + * + *

+ */ +@ThreadSafe +final class TracezSpanProcessor implements SpanProcessor { + private final ConcurrentMap runningSpanCache; + private final ConcurrentMap completedSpanCache; + private final boolean sampled; + + /** + * Constructor for {@link TracezSpanProcessor}. + * + * @param sampled report only sampled spans. + */ + TracezSpanProcessor(boolean sampled) { + runningSpanCache = new ConcurrentHashMap<>(); + completedSpanCache = new ConcurrentHashMap<>(); + this.sampled = sampled; + } + + @Override + public void onStart(ReadableSpan span) { + runningSpanCache.put(span.getSpanContext().getSpanId(), span); + } + + @Override + public boolean isStartRequired() { + return true; + } + + @Override + public void onEnd(ReadableSpan span) { + runningSpanCache.remove(span.getSpanContext().getSpanId()); + if (!sampled || span.getSpanContext().getTraceFlags().isSampled()) { + completedSpanCache.putIfAbsent(span.getName(), new TracezSpanBuckets()); + completedSpanCache.get(span.getName()).addToBucket(span); + } + } + + @Override + public boolean isEndRequired() { + return true; + } + + @Override + public void shutdown() { + // Do nothing. + } + + @Override + public void forceFlush() { + // Do nothing. + } + + /** + * Returns a Collection of all running spans for {@link TracezSpanProcessor}. + * + * @return a Collection of {@link ReadableSpan}. + */ + Collection getRunningSpans() { + return runningSpanCache.values(); + } + + /** + * Returns a Collection of all completed spans for {@link TracezSpanProcessor}. + * + * @return a Collection of {@link ReadableSpan}. + */ + Collection getCompletedSpans() { + Collection completedSpans = new ArrayList<>(); + for (TracezSpanBuckets buckets : completedSpanCache.values()) { + completedSpans.addAll(buckets.getSpans()); + } + return completedSpans; + } + + /** + * Returns the completed span cache for {@link TracezSpanProcessor}. + * + * @return a Map of String to {@link TracezSpanBuckets}. + */ + Map getCompletedSpanCache() { + return completedSpanCache; + } + + /** + * Returns a new Builder for {@link TracezSpanProcessor}. + * + * @return a new {@link TracezSpanProcessor}. + */ + public static Builder newBuilder() { + return new Builder(); + } + + /** Builder class for {@link TracezSpanProcessor}. */ + public static final class Builder extends ConfigBuilder { + + private static final String KEY_SAMPLED = "otel.zpages.export.sampled"; + private static final boolean DEFAULT_EXPORT_ONLY_SAMPLED = true; + private boolean sampled = DEFAULT_EXPORT_ONLY_SAMPLED; + + private Builder() {} + + /** + * Sets the configuration values from the given configuration map for only the available keys. + * This method looks for the following keys: + * + *
    + *
  • {@code otel.zpages.export.sampled}: to set whether only sampled spans should be + * exported. + *
+ * + * @param configMap {@link Map} holding the configuration values. + * @return this. + */ + @Override + protected Builder fromConfigMap( + Map configMap, NamingConvention namingConvention) { + configMap = namingConvention.normalize(configMap); + Boolean boolValue = getBooleanProperty(KEY_SAMPLED, configMap); + if (boolValue != null) { + return this.setExportOnlySampled(boolValue); + } + return this; + } + + /** + * Sets whether only sampled spans should be exported. + * + *

Default value is {@code true}. + * + * @see Builder#DEFAULT_EXPORT_ONLY_SAMPLED + * @param sampled report only sampled spans. + * @return this. + */ + public Builder setExportOnlySampled(boolean sampled) { + this.sampled = sampled; + return this; + } + + /** + * Returns a new {@link TracezSpanProcessor}. + * + * @return a new {@link TracezSpanProcessor}. + */ + public TracezSpanProcessor build() { + return new TracezSpanProcessor(sampled); + } + } +} diff --git a/sdk_extensions/zpages/src/main/java/io/opentelemetry/sdk/extensions/zpages/TracezZPageHandler.java b/sdk_extensions/zpages/src/main/java/io/opentelemetry/sdk/extensions/zpages/TracezZPageHandler.java new file mode 100644 index 0000000000..8653ad6c71 --- /dev/null +++ b/sdk_extensions/zpages/src/main/java/io/opentelemetry/sdk/extensions/zpages/TracezZPageHandler.java @@ -0,0 +1,611 @@ +/* + * Copyright 2020, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.sdk.extensions.zpages; + +import static com.google.common.html.HtmlEscapers.htmlEscaper; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.common.AttributeValue; +import io.opentelemetry.common.ReadableAttributes; +import io.opentelemetry.common.ReadableKeyValuePairs.KeyValueConsumer; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.SpanData.Event; +import io.opentelemetry.trace.SpanId; +import io.opentelemetry.trace.Status; +import io.opentelemetry.trace.Status.CanonicalCode; +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Formatter; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +final class TracezZPageHandler extends ZPageHandler { + private enum SampleType { + RUNNING(0), + LATENCY(1), + ERROR(2), + UNKNOWN(-1); + + private final int value; + + SampleType(int value) { + this.value = value; + } + + static SampleType fromString(String str) { + int value = Integer.parseInt(str); + switch (value) { + case 0: + return RUNNING; + case 1: + return LATENCY; + case 2: + return ERROR; + default: + return UNKNOWN; + } + } + + int getValue() { + return value; + } + } + + private static final String TRACEZ_URL = "/tracez"; + // Background color used for zebra striping rows of summary table + private static final String ZEBRA_STRIPE_COLOR = "#e6e6e6"; + // Color for sampled traceIds + private static final String SAMPLED_TRACE_ID_COLOR = "#c1272d"; + // Color for not sampled traceIds + private static final String NOT_SAMPLED_TRACE_ID_COLOR = "black"; + // Query string parameter name for span name + private static final String PARAM_SPAN_NAME = "zspanname"; + // Query string parameter name for type to display + // * 0 = running, 1 = latency, 2 = error + private static final String PARAM_SAMPLE_TYPE = "ztype"; + // Query string parameter name for sub-type: + // * for latency based sampled spans [0, 8] corresponds to each latency boundaries + // where 0 corresponds to the first boundary + // * for error based sampled spans [0, 15], 0 means all, otherwise the error code + private static final String PARAM_SAMPLE_SUB_TYPE = "zsubtype"; + // Map from LatencyBoundary to human readable string on the UI + private static final ImmutableMap LATENCY_BOUNDARIES_STRING_MAP = + buildLatencyBoundaryStringMap(); + private static final Logger logger = Logger.getLogger(TracezZPageHandler.class.getName()); + @Nullable private final TracezDataAggregator dataAggregator; + + /** Constructs a new {@code TracezZPageHandler}. */ + TracezZPageHandler(@Nullable TracezDataAggregator dataAggregator) { + this.dataAggregator = dataAggregator; + } + + @Override + public String getUrlPath() { + return TRACEZ_URL; + } + + /** + * Emits CSS Styles to the {@link PrintStream} {@code out}. Content emitted by this function + * should be enclosed by tag. + * + * @param out the {@link PrintStream} {@code out}. + */ + private static void emitHtmlStyle(PrintStream out) { + out.print(""); + } + + /** + * Emits the header of the summary table to the {@link PrintStream} {@code out}. + * + * @param out the {@link PrintStream} {@code out}. + */ + private static void emitSummaryTableHeader(PrintStream out) { + // First row + out.print(""); + out.print("Span Name"); + out.print("Running"); + out.print("Latency Samples"); + out.print("Error Samples"); + out.print(""); + + // Second row + out.print(""); + out.print(""); + out.print(""); + for (LatencyBoundary latencyBoundary : LatencyBoundary.values()) { + out.print( + "[" + + LATENCY_BOUNDARIES_STRING_MAP.get(latencyBoundary) + + "]"); + } + out.print(""); + out.print(""); + } + + /** + * Emits a single cell of the summary table depends on the paramters passed in, to the {@link + * PrintStream} {@code out}. + * + * @param out the {@link PrintStream} {@code out}. + * @param spanName the name of the corresponding span. + * @param numOfSamples the number of samples of the corresponding span. + * @param type the type of the corresponding span (running, latency, error). + * @param subtype the sub-type of the corresponding span (latency [0, 8], error [0, 15]). + */ + private static void emitSummaryTableCell( + PrintStream out, String spanName, int numOfSamples, SampleType type, int subtype) + throws UnsupportedEncodingException { + // If numOfSamples is greater than 0, emit a link to see detailed span information + // If numOfSamples is smaller than 0, print the text "N/A", otherwise print the text "0" + if (numOfSamples > 0) { + out.print("" + numOfSamples + ""); + } else if (numOfSamples < 0) { + out.print("N/A"); + } else { + out.print("0"); + } + } + + /** + * Emits the summary table of running spans and sampled spans to the {@link PrintStream} {@code + * out}. + * + * @param out the {@link PrintStream} {@code out}. + */ + private void emitSummaryTable(PrintStream out) throws UnsupportedEncodingException { + if (dataAggregator == null) { + return; + } + out.print(""); + emitSummaryTableHeader(out); + + Set spanNames = dataAggregator.getSpanNames(); + boolean zebraStripe = false; + + Map runningSpanCounts = dataAggregator.getRunningSpanCounts(); + Map> latencySpanCounts = + dataAggregator.getSpanLatencyCounts(); + Map errorSpanCounts = dataAggregator.getErrorSpanCounts(); + for (String spanName : spanNames) { + if (zebraStripe) { + out.print(""); + } else { + out.print(""); + } + zebraStripe = !zebraStripe; + out.print(""); + + // Running spans column + int numOfRunningSpans = + runningSpanCounts.containsKey(spanName) ? runningSpanCounts.get(spanName) : 0; + // subtype is ignored for running spans + emitSummaryTableCell(out, spanName, numOfRunningSpans, SampleType.RUNNING, 0); + + // Latency based sampled spans column + int subtype = 0; + for (LatencyBoundary latencyBoundary : LatencyBoundary.values()) { + int numOfLatencySamples = + latencySpanCounts.containsKey(spanName) + && latencySpanCounts.get(spanName).containsKey(latencyBoundary) + ? latencySpanCounts.get(spanName).get(latencyBoundary) + : 0; + emitSummaryTableCell(out, spanName, numOfLatencySamples, SampleType.LATENCY, subtype); + subtype += 1; + } + + // Error based sampled spans column + int numOfErrorSamples = + errorSpanCounts.containsKey(spanName) ? errorSpanCounts.get(spanName) : 0; + // subtype 0 means all errors + emitSummaryTableCell(out, spanName, numOfErrorSamples, SampleType.ERROR, 0); + } + out.print("
" + htmlEscaper().escape(spanName) + "
"); + } + + private static void emitSpanNameAndCount( + PrintStream out, String spanName, int count, SampleType type) { + out.print( + "

Span Name: " + htmlEscaper().escape(spanName) + "

"); + String typeString = + type == SampleType.RUNNING + ? "running" + : type == SampleType.LATENCY ? "latency samples" : "error samples"; + out.print("

Number of " + typeString + ": " + count + "

"); + } + + private static void emitSpanDetails( + PrintStream out, Formatter formatter, Collection spans) { + out.print(""); + out.print(""); + out.print( + ""); + out.print( + ""); + out.print(""); + out.print(""); + boolean zebraStripe = false; + for (SpanData span : spans) { + zebraStripe = emitSingleSpan(out, formatter, span, zebraStripe); + } + out.print("
When
" + + "
Elapsed(s)
"); + } + + private static boolean emitSingleSpan( + PrintStream out, Formatter formatter, SpanData span, boolean zebraStripe) { + Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(TimeUnit.NANOSECONDS.toMillis(span.getStartEpochNanos())); + long microsField = TimeUnit.NANOSECONDS.toMicros(span.getStartEpochNanos()); + String elapsedSecondsStr = + span.getHasEnded() + ? String.format("%.6f", (span.getEndEpochNanos() - span.getStartEpochNanos()) * 1.0e-9) + : ""; + formatter.format( + "", zebraStripe ? ZEBRA_STRIPE_COLOR : "#fff"); + formatter.format( + "
"
+            + "%04d/%02d/%02d-%02d:%02d:%02d.%06d
", + calendar.get(Calendar.YEAR), + calendar.get(Calendar.MONTH) + 1, + calendar.get(Calendar.DAY_OF_MONTH), + calendar.get(Calendar.HOUR_OF_DAY), + calendar.get(Calendar.MINUTE), + calendar.get(Calendar.SECOND), + microsField); + formatter.format( + "
%s
", + elapsedSecondsStr); + formatter.format( + "
"
+            + "TraceId: %s "
+            + " | SpanId: %s | ParentSpanId: %s
", + span.getTraceFlags().isSampled() ? SAMPLED_TRACE_ID_COLOR : NOT_SAMPLED_TRACE_ID_COLOR, + span.getTraceId().toLowerBase16(), + span.getSpanId().toLowerBase16(), + (span.getParentSpanId() == null + ? SpanId.getInvalid().toLowerBase16() + : span.getParentSpanId().toLowerBase16())); + out.print(""); + zebraStripe = !zebraStripe; + + int lastEntryDayOfYear = calendar.get(Calendar.DAY_OF_YEAR); + + long lastEpochNanos = span.getStartEpochNanos(); + List timedEvents = new ArrayList<>(span.getEvents()); + Collections.sort(timedEvents, new EventComparator()); + for (Event event : timedEvents) { + calendar.setTimeInMillis(TimeUnit.NANOSECONDS.toMillis(event.getEpochNanos())); + formatter.format( + "", zebraStripe ? ZEBRA_STRIPE_COLOR : "#fff"); + emitSingleEvent(out, formatter, event, calendar, lastEntryDayOfYear, lastEpochNanos); + out.print(""); + if (calendar.get(Calendar.DAY_OF_YEAR) != lastEntryDayOfYear) { + lastEntryDayOfYear = calendar.get(Calendar.DAY_OF_YEAR); + } + lastEpochNanos = event.getEpochNanos(); + zebraStripe = !zebraStripe; + } + formatter.format( + "" + + "
",
+        zebraStripe ? ZEBRA_STRIPE_COLOR : "#fff");
+    Status status = span.getStatus();
+    if (status != null) {
+      formatter.format("%s | ", htmlEscaper().escape(status.toString()));
+    }
+    formatter.format("%s
", htmlEscaper().escape(renderAttributes(span.getAttributes()))); + zebraStripe = !zebraStripe; + return zebraStripe; + } + + private static void emitSingleEvent( + PrintStream out, + Formatter formatter, + Event event, + Calendar calendar, + int lastEntryDayOfYear, + long lastEpochNanos) { + if (calendar.get(Calendar.DAY_OF_YEAR) == lastEntryDayOfYear) { + out.print("
");
+    } else {
+      formatter.format(
+          "
%04d/%02d/%02d-",
+          calendar.get(Calendar.YEAR),
+          calendar.get(Calendar.MONTH) + 1,
+          calendar.get(Calendar.DAY_OF_MONTH));
+    }
+
+    // Special printing so that durations smaller than one second
+    // are left padded with blanks instead of '0' characters.
+    // E.g.,
+    //        Number                  Printout
+    //        ---------------------------------
+    //        0.000534                  .   534
+    //        1.000534                 1.000534
+    long deltaMicros = TimeUnit.NANOSECONDS.toMicros(event.getEpochNanos() - lastEpochNanos);
+    String deltaString;
+    if (deltaMicros >= 1000000) {
+      deltaString = String.format("%.6f", (deltaMicros / 1000000.0));
+    } else {
+      deltaString = String.format("%1s.%6d", "", deltaMicros);
+    }
+
+    long microsField = TimeUnit.NANOSECONDS.toMicros(event.getEpochNanos());
+    formatter.format(
+        "%02d:%02d:%02d.%06d
" + + "
%s
" + + "
%s
", + calendar.get(Calendar.HOUR_OF_DAY), + calendar.get(Calendar.MINUTE), + calendar.get(Calendar.SECOND), + microsField, + deltaString, + htmlEscaper().escape(renderEvent(event))); + } + + private static String renderAttributes(ReadableAttributes attributes) { + final StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("Attributes:{"); + attributes.forEach( + new KeyValueConsumer() { + private boolean first = true; + + @Override + public void consume(String key, AttributeValue value) { + if (first) { + first = false; + } else { + stringBuilder.append(", "); + } + stringBuilder.append(key); + stringBuilder.append("="); + switch (value.getType()) { + case STRING: + stringBuilder.append(value.getStringValue()); + break; + case BOOLEAN: + stringBuilder.append(value.getBooleanValue()); + break; + case LONG: + stringBuilder.append(value.getLongValue()); + break; + case DOUBLE: + stringBuilder.append(value.getDoubleValue()); + break; + case STRING_ARRAY: + stringBuilder.append(value.getStringArrayValue().toString()); + break; + case BOOLEAN_ARRAY: + stringBuilder.append(value.getBooleanArrayValue().toString()); + break; + case LONG_ARRAY: + stringBuilder.append(value.getLongArrayValue().toString()); + break; + case DOUBLE_ARRAY: + stringBuilder.append(value.getDoubleArrayValue().toString()); + break; + } + } + }); + stringBuilder.append("}"); + return stringBuilder.toString(); + } + + private static String renderEvent(Event event) { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(event.getName()); + if (!event.getAttributes().isEmpty()) { + stringBuilder.append(" | "); + stringBuilder.append(renderAttributes(event.getAttributes())); + } + return stringBuilder.toString(); + } + + /** + * Emits HTML body content to the {@link PrintStream} {@code out}. Content emitted by this + * function should be enclosed by tag. + * + * @param queryMap the map containing URL query parameters.s + * @param out the {@link PrintStream} {@code out}. + */ + private void emitHtmlBody(Map queryMap, PrintStream out) + throws UnsupportedEncodingException { + if (dataAggregator == null) { + out.print("OpenTelemetry implementation not available."); + return; + } + // Link to OpenTelemetry Logo + out.print( + ""); + out.print("

TraceZ Summary

"); + emitSummaryTable(out); + // spanName will be null if the query parameter doesn't exist in the URL + String spanName = queryMap.get(PARAM_SPAN_NAME); + if (spanName != null) { + // Convert spanName with URL encoding + spanName = URLEncoder.encode(spanName, "UTF-8"); + // Show detailed information for the corresponding span + String typeStr = queryMap.get(PARAM_SAMPLE_TYPE); + if (typeStr != null) { + List spans = null; + SampleType type = SampleType.fromString(typeStr); + if (type == SampleType.UNKNOWN) { + // Type of unknown is garbage value + return; + } else if (type == SampleType.RUNNING) { + // Display running span + spans = dataAggregator.getRunningSpans(spanName); + Collections.sort(spans, new SpanDataComparator(/* incremental= */ true)); + } else { + String subtypeStr = queryMap.get(PARAM_SAMPLE_SUB_TYPE); + if (subtypeStr != null) { + int subtype = Integer.parseInt(subtypeStr); + if (type == SampleType.LATENCY) { + if (subtype < 0 || subtype >= LatencyBoundary.values().length) { + // N/A or out-of-bound check for latency based subtype, valid values: [0, 8] + return; + } + // Display latency based span + LatencyBoundary latencyBoundary = LatencyBoundary.values()[subtype]; + spans = + dataAggregator.getOkSpans( + spanName, + latencyBoundary.getLatencyLowerBound(), + latencyBoundary.getLatencyUpperBound()); + Collections.sort(spans, new SpanDataComparator(/* incremental= */ false)); + } else { + if (subtype < 0 || subtype >= CanonicalCode.values().length) { + // N/A or out-of-bound cueck for error based subtype, valid values: [0, 15] + return; + } + // Display error based span + spans = dataAggregator.getErrorSpans(spanName); + Collections.sort(spans, new SpanDataComparator(/* incremental= */ false)); + } + } + } + out.print("

Span Details

"); + emitSpanNameAndCount(out, spanName, spans == null ? 0 : spans.size(), type); + + if (spans != null) { + Formatter formatter = new Formatter(out, Locale.US); + emitSpanDetails(out, formatter, spans); + } + } + } + } + + @Override + public void emitHtml(Map queryMap, OutputStream outputStream) { + // PrintStream for emiting HTML contents + try (PrintStream out = new PrintStream(outputStream, /* autoFlush= */ false, "UTF-8")) { + out.print(""); + out.print(""); + out.print(""); + out.print(""); + out.print( + ""); + out.print( + ""); + out.print( + ""); + out.print("TraceZ"); + emitHtmlStyle(out); + out.print(""); + out.print(""); + try { + emitHtmlBody(queryMap, out); + } catch (Throwable t) { + out.print("Error while generating HTML: " + t.toString()); + logger.log(Level.WARNING, "error while generating HTML", t); + } + out.print(""); + out.print(""); + } catch (Throwable t) { + logger.log(Level.WARNING, "error while generating HTML", t); + } + } + + private static String latencyBoundaryToString(LatencyBoundary latencyBoundary) { + switch (latencyBoundary) { + case ZERO_MICROSx10: + return ">0us"; + case MICROSx10_MICROSx100: + return ">10us"; + case MICROSx100_MILLIx1: + return ">100us"; + case MILLIx1_MILLIx10: + return ">1ms"; + case MILLIx10_MILLIx100: + return ">10ms"; + case MILLIx100_SECONDx1: + return ">100ms"; + case SECONDx1_SECONDx10: + return ">1s"; + case SECONDx10_SECONDx100: + return ">10s"; + case SECONDx100_MAX: + return ">100s"; + } + throw new IllegalArgumentException("No value string available for: " + latencyBoundary); + } + + private static ImmutableMap buildLatencyBoundaryStringMap() { + Map latencyBoundaryMap = new HashMap<>(); + for (LatencyBoundary latencyBoundary : LatencyBoundary.values()) { + latencyBoundaryMap.put(latencyBoundary, latencyBoundaryToString(latencyBoundary)); + } + return ImmutableMap.copyOf(latencyBoundaryMap); + } + + private static final class EventComparator implements Comparator, Serializable { + private static final long serialVersionUID = 0; + + @Override + public int compare(Event e1, Event e2) { + return Long.compare(e1.getEpochNanos(), e2.getEpochNanos()); + } + } + + private static final class SpanDataComparator implements Comparator, Serializable { + private static final long serialVersionUID = 0; + private final boolean incremental; + + /** + * Returns a new {@code SpanDataComparator}. + * + * @param incremental {@code true} if sorting spans incrementally + */ + private SpanDataComparator(boolean incremental) { + this.incremental = incremental; + } + + @Override + public int compare(SpanData s1, SpanData s2) { + return incremental + ? Long.compare(s1.getStartEpochNanos(), s2.getStartEpochNanos()) + : Long.compare(s2.getStartEpochNanos(), s1.getEndEpochNanos()); + } + } +} diff --git a/sdk_extensions/zpages/src/main/java/io/opentelemetry/sdk/extensions/zpages/ZPageHandler.java b/sdk_extensions/zpages/src/main/java/io/opentelemetry/sdk/extensions/zpages/ZPageHandler.java new file mode 100644 index 0000000000..a4a54e9a70 --- /dev/null +++ b/sdk_extensions/zpages/src/main/java/io/opentelemetry/sdk/extensions/zpages/ZPageHandler.java @@ -0,0 +1,45 @@ +/* + * Copyright 2020, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.sdk.extensions.zpages; + +import java.io.OutputStream; +import java.util.Map; + +/** + * The main interface for all zPages. All zPages should implement this interface to allow the HTTP + * server implementation to support these pages. + */ +public abstract class ZPageHandler { + + /** + * Returns the URL path that should be used to register this zPage to the HTTP server. + * + * @return the URL path that should be used to register this zPage to the HTTP server. + */ + public abstract String getUrlPath(); + + /** + * Emits the generated HTML page to the {@code outputStream}. + * + * @param queryMap the map of the URL query parameters. + * @param outputStream the output for the generated HTML page. + */ + public abstract void emitHtml(Map queryMap, OutputStream outputStream); + + /** Package protected constructor to disallow users to extend this class. */ + ZPageHandler() {} +} diff --git a/sdk_extensions/zpages/src/main/java/io/opentelemetry/sdk/extensions/zpages/ZPageHttpHandler.java b/sdk_extensions/zpages/src/main/java/io/opentelemetry/sdk/extensions/zpages/ZPageHttpHandler.java new file mode 100644 index 0000000000..37d88734ea --- /dev/null +++ b/sdk_extensions/zpages/src/main/java/io/opentelemetry/sdk/extensions/zpages/ZPageHttpHandler.java @@ -0,0 +1,78 @@ +/* + * Copyright 2020, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.sdk.extensions.zpages; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableMap; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import java.io.IOException; +import java.net.URI; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** An {@link HttpHanlder} that will be used to render HTML pages using any {@code ZPageHandler}. */ +final class ZPageHttpHandler implements HttpHandler { + // Splitter for splitting URL query parameters + private static final Splitter QUERY_SPLITTER = Splitter.on("&").trimResults(); + // Splitter for splitting URL query parameters' key value + private static final Splitter QUERY_KEYVAL_SPLITTER = Splitter.on("=").trimResults(); + // The corresponding ZPageHandler for the zPage (e.g. TracezZPageHandler) + private final ZPageHandler zpageHandler; + + /** Constructs a new {@code ZPageHttpHandler}. */ + ZPageHttpHandler(ZPageHandler zpageHandler) { + this.zpageHandler = zpageHandler; + } + + /** + * Build a query map from the {@code uri}. + * + * @param uri the {@link URI} for buiding the query map + * @return the query map built based on the @{code uri} + */ + @VisibleForTesting + static ImmutableMap parseQueryMap(URI uri) { + String queryStrings = uri.getQuery(); + if (queryStrings == null) { + return ImmutableMap.of(); + } + Map queryMap = new HashMap(); + for (String param : QUERY_SPLITTER.split(queryStrings)) { + List keyValuePair = QUERY_KEYVAL_SPLITTER.splitToList(param); + if (keyValuePair.size() > 1) { + queryMap.put(keyValuePair.get(0), keyValuePair.get(1)); + } else { + queryMap.put(keyValuePair.get(0), ""); + } + } + return ImmutableMap.copyOf(queryMap); + } + + @Override + public final void handle(HttpExchange httpExchange) throws IOException { + try { + httpExchange.sendResponseHeaders(200, 0); + zpageHandler.emitHtml( + parseQueryMap(httpExchange.getRequestURI()), httpExchange.getResponseBody()); + } finally { + httpExchange.close(); + } + } +} diff --git a/sdk_extensions/zpages/src/main/java/io/opentelemetry/sdk/extensions/zpages/ZPageLogo.java b/sdk_extensions/zpages/src/main/java/io/opentelemetry/sdk/extensions/zpages/ZPageLogo.java new file mode 100644 index 0000000000..e50e49a9bf --- /dev/null +++ b/sdk_extensions/zpages/src/main/java/io/opentelemetry/sdk/extensions/zpages/ZPageLogo.java @@ -0,0 +1,62 @@ +/* + * Copyright 2020, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.sdk.extensions.zpages; + +import com.google.common.io.BaseEncoding; +import com.google.common.io.ByteStreams; +import java.io.InputStream; +import java.util.logging.Level; +import java.util.logging.Logger; + +final class ZPageLogo { + private static final Logger logger = Logger.getLogger(ZPageLogo.class.getName()); + + private ZPageLogo() {} + + /** + * Get OpenTelemetry logo in base64 encoding. + * + * @return OpenTelemetry logo in base64 encoding. + */ + public static String getLogoBase64() { + try { + InputStream in = ZPageLogo.class.getClassLoader().getResourceAsStream("logo.png"); + byte[] bytes = ByteStreams.toByteArray(in); + return BaseEncoding.base64().encode(bytes); + } catch (Throwable t) { + logger.log(Level.WARNING, "error while getting OpenTelemetry Logo", t); + return ""; + } + } + + /** + * Get OpenTelemetry favicon in base64 encoding. + * + * @return OpenTelemetry favicon in base64 encoding. + */ + public static String getFaviconBase64() { + try { + + InputStream in = ZPageLogo.class.getClassLoader().getResourceAsStream("favicon.png"); + byte[] bytes = ByteStreams.toByteArray(in); + return BaseEncoding.base64().encode(bytes); + } catch (Throwable t) { + logger.log(Level.WARNING, "error while getting OpenTelemetry Logo", t); + return ""; + } + } +} diff --git a/sdk_extensions/zpages/src/main/java/io/opentelemetry/sdk/extensions/zpages/ZPageServer.java b/sdk_extensions/zpages/src/main/java/io/opentelemetry/sdk/extensions/zpages/ZPageServer.java new file mode 100644 index 0000000000..94d3493a9c --- /dev/null +++ b/sdk_extensions/zpages/src/main/java/io/opentelemetry/sdk/extensions/zpages/ZPageServer.java @@ -0,0 +1,168 @@ +/* + * Copyright 2020, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.sdk.extensions.zpages; + +import static com.google.common.base.Preconditions.checkState; + +import com.google.common.annotations.VisibleForTesting; +import com.sun.net.httpserver.HttpServer; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.trace.TracerSdkProvider; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.annotation.Nullable; +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.ThreadSafe; + +/** + * A collection of HTML pages to display stats and trace data and allow library configuration + * control. + * + *

Example usage with private {@link HttpServer} + * + *

{@code
+ * public class Main {
+ *   public static void main(String[] args) throws Exception {
+ *     ZPageServer.startHttpServerAndRegisterAllPages(8000);
+ *     ... // do work
+ *   }
+ * }
+ * }
+ * + *

Example usage with shared {@link HttpServer} + * + *

{@code
+ * public class Main {
+ *   public static void main(String[] args) throws Exception {
+ *     HttpServer server = HttpServer.create(new InetSocketAddress(8000), 10);
+ *     ZPageServer.registerAllPagesToHttpServer(server);
+ *     server.start();
+ *     ... // do work
+ *   }
+ * }
+ * }
+ */ +@ThreadSafe +public final class ZPageServer { + // The maximum number of queued incoming connections allowed on the HttpServer listening socket. + private static final int HTTPSERVER_BACKLOG = 5; + // Length of time to wait for the HttpServer to stop + private static final int HTTPSERVER_STOP_DELAY = 1; + // Tracez SpanProcessor and DataAggregator for constructing TracezZPageHandler + private static final TracezSpanProcessor tracezSpanProcessor = + TracezSpanProcessor.newBuilder().build(); + private static final TracezDataAggregator tracezDataAggregator = + new TracezDataAggregator(tracezSpanProcessor); + // Handler for /tracez page + private static final ZPageHandler tracezZPageHandler = + new TracezZPageHandler(tracezDataAggregator); + + private static final Object mutex = new Object(); + private static final AtomicBoolean isTracezSpanProcesserAdded = new AtomicBoolean(false); + + @GuardedBy("mutex") + @Nullable + private static HttpServer server; + + /** Function that adds the {@link TracezSpanProcessor} to the {@link tracerSdkProvider}. */ + private static void addTracezSpanProcessor() { + if (isTracezSpanProcesserAdded.compareAndSet(false, true)) { + TracerSdkProvider tracerProvider = OpenTelemetrySdk.getTracerProvider(); + tracerProvider.addSpanProcessor(tracezSpanProcessor); + } + } + + /** + * Registers a {@code ZPageHandler} for tracing debug to the server. The page displays information + * about all running spans and all sampled spans based on latency and error. + * + *

It displays a summary table which contains one row for each span name and data about number + * of running and sampled spans. + * + *

Clicking on a cell in the table with a number that is greater than zero will display + * detailed information about that span. + * + *

This method will add the TracezSpanProcessor to the tracerProvider, it should only be called + * once. + */ + static void registerTracezZPageHandler(HttpServer server) { + addTracezSpanProcessor(); + server.createContext(tracezZPageHandler.getUrlPath(), new ZPageHttpHandler(tracezZPageHandler)); + } + + /** + * Registers all zPages to the given {@link HttpServer} {@code server}. + * + * @param server the server that exports the zPages. + */ + public static void registerAllPagesToHttpServer(HttpServer server) { + // For future zPages, register them to the server in here + registerTracezZPageHandler(server); + } + + /** Method for stopping the {@link HttpServer} {@code server}. */ + private static void stop() { + synchronized (mutex) { + if (server == null) { + return; + } + server.stop(HTTPSERVER_STOP_DELAY); + server = null; + } + } + + /** + * Starts a private {@link HttpServer} and registers all zPages to it. When the JVM shuts down the + * server is stopped. + * + *

Users can only call this function once per process. + * + * @param port the port used to bind the {@link HttpServer} {@code server} + * @throws IllegalStateException if the server is already started. + * @throws IOException if the server cannot bind to the specified port. + */ + public static void startHttpServerAndRegisterAllPages(int port) throws IOException { + synchronized (mutex) { + checkState(server == null, "The HttpServer is already started."); + server = HttpServer.create(new InetSocketAddress(port), HTTPSERVER_BACKLOG); + ZPageServer.registerAllPagesToHttpServer(server); + server.start(); + } + + Runtime.getRuntime() + .addShutdownHook( + new Thread() { + @Override + public void run() { + ZPageServer.stop(); + } + }); + } + + /** + * Returns the boolean indicating if TracezSpanProcessor is added. For testing purpose only. + * + * @return the boolean indicating if TracezSpanProcessor is added. + */ + @VisibleForTesting + static boolean getIsTracezSpanProcesserAdded() { + return isTracezSpanProcesserAdded.get(); + } + + private ZPageServer() {} +} diff --git a/sdk_extensions/zpages/src/main/java/io/opentelemetry/sdk/extensions/zpages/ZPageStyle.java b/sdk_extensions/zpages/src/main/java/io/opentelemetry/sdk/extensions/zpages/ZPageStyle.java new file mode 100644 index 0000000000..d7926b29bc --- /dev/null +++ b/sdk_extensions/zpages/src/main/java/io/opentelemetry/sdk/extensions/zpages/ZPageStyle.java @@ -0,0 +1,43 @@ +/* + * Copyright 2020, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.sdk.extensions.zpages; + +/** This class contains the unified CSS styles for all zPages. */ +final class ZPageStyle { + private ZPageStyle() {} + + /** Style here will be applied to the generated HTML pages for all zPages. */ + static String style = + "body{font-family: \"Roboto\", sans-serif; font-size: 14px;" + + "background-color: #fff;}" + + "h1{color: #363636; text-align: center; margin-bottom 20px;}" + + "h2{color: #363636; text-align: center; margin-top: 30px;}" + + "p{padding: 0 0.5em; color: #363636;}" + + "tr.bg-color{background-color: #4b5fab;}" + + "table{margin: 0 auto;}" + + "th{padding: 0 1em; line-height: 2.0}" + + "td{padding: 0 1em; line-height: 2.0}" + + ".border-right-white{border-right: 1px solid #fff;}" + + ".border-left-white{border-left: 1px solid #fff;}" + + ".border-left-dark{border-left: 1px solid #363636;}" + + "th.header-text{color: #fff; line-height: 3.0;}" + + ".align-center{text-align: center;}" + + ".align-right{text-align: right;}" + + "pre.no-margin{margin: 0;}" + + "pre.wrap-text{white-space:pre-wrap;}" + + "td.bg-white{background-color: #fff;}"; +} diff --git a/sdk_extensions/zpages/src/main/resources/favicon.png b/sdk_extensions/zpages/src/main/resources/favicon.png new file mode 100644 index 0000000000..91f2be7b2e Binary files /dev/null and b/sdk_extensions/zpages/src/main/resources/favicon.png differ diff --git a/sdk_extensions/zpages/src/main/resources/logo.png b/sdk_extensions/zpages/src/main/resources/logo.png new file mode 100644 index 0000000000..f225db4f3b Binary files /dev/null and b/sdk_extensions/zpages/src/main/resources/logo.png differ diff --git a/sdk_extensions/zpages/src/test/java/io/opentelemetry/sdk/extensions/zpages/TracezDataAggregatorTest.java b/sdk_extensions/zpages/src/test/java/io/opentelemetry/sdk/extensions/zpages/TracezDataAggregatorTest.java new file mode 100644 index 0000000000..2502fce2b5 --- /dev/null +++ b/sdk_extensions/zpages/src/test/java/io/opentelemetry/sdk/extensions/zpages/TracezDataAggregatorTest.java @@ -0,0 +1,320 @@ +/* + * Copyright 2020, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.sdk.extensions.zpages; + +import static com.google.common.truth.Truth.assertThat; + +import io.opentelemetry.sdk.internal.TestClock; +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.TracerSdkProvider; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.trace.Span; +import io.opentelemetry.trace.Status; +import io.opentelemetry.trace.Status.CanonicalCode; +import io.opentelemetry.trace.Tracer; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link TracezDataAggregator}. */ +@RunWith(JUnit4.class) +public final class TracezDataAggregatorTest { + private static final String SPAN_NAME_ONE = "one"; + private static final String SPAN_NAME_TWO = "two"; + private final TestClock testClock = TestClock.create(); + private final TracerSdkProvider tracerSdkProvider = + TracerSdkProvider.builder().setClock(testClock).build(); + private final Tracer tracer = tracerSdkProvider.get("TracezDataAggregatorTest"); + private final TracezSpanProcessor spanProcessor = TracezSpanProcessor.newBuilder().build(); + private final TracezDataAggregator dataAggregator = new TracezDataAggregator(spanProcessor); + + @Before + public void setup() { + tracerSdkProvider.addSpanProcessor(spanProcessor); + } + + @Test + public void getSpanNames_noSpans() { + assertThat(dataAggregator.getSpanNames()).isEmpty(); + } + + @Test + public void getSpanNames_twoSpanNames() { + Span span1 = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + Span span2 = tracer.spanBuilder(SPAN_NAME_TWO).startSpan(); + Span span3 = tracer.spanBuilder(SPAN_NAME_TWO).startSpan(); + /* getSpanNames should return a set with 2 span names */ + Set names = dataAggregator.getSpanNames(); + assertThat(names).containsExactly(SPAN_NAME_ONE, SPAN_NAME_TWO); + span1.end(); + span2.end(); + span3.end(); + /* getSpanNames should still return a set with 2 span names */ + names = dataAggregator.getSpanNames(); + assertThat(names).containsExactly(SPAN_NAME_ONE, SPAN_NAME_TWO); + } + + @Test + public void getRunningSpanCounts_noSpans() { + /* getRunningSpanCounts should return a an empty map */ + Map counts = dataAggregator.getRunningSpanCounts(); + assertThat(counts).isEmpty(); + } + + @Test + public void getRunningSpanCounts_oneSpanName() { + Span span1 = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + Span span2 = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + Span span3 = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + /* getRunningSpanCounts should return a map with 1 span name */ + Map counts = dataAggregator.getRunningSpanCounts(); + assertThat(counts.get(SPAN_NAME_ONE)).isEqualTo(3); + span1.end(); + span2.end(); + span3.end(); + /* getRunningSpanCounts should return a map with no span names */ + counts = dataAggregator.getRunningSpanCounts(); + assertThat(counts).isEmpty(); + } + + @Test + public void getRunningSpanCounts_twoSpanNames() { + Span span1 = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + Span span2 = tracer.spanBuilder(SPAN_NAME_TWO).startSpan(); + /* getRunningSpanCounts should return a map with 2 different span names */ + Map counts = dataAggregator.getRunningSpanCounts(); + assertThat(counts.get(SPAN_NAME_ONE)).isEqualTo(1); + assertThat(counts.get(SPAN_NAME_TWO)).isEqualTo(1); + + span1.end(); + /* getRunningSpanCounts should return a map with 1 unique span name */ + counts = dataAggregator.getRunningSpanCounts(); + assertThat(counts.get(SPAN_NAME_ONE)).isNull(); + assertThat(counts.get(SPAN_NAME_TWO)).isEqualTo(1); + + span2.end(); + /* getRunningSpanCounts should return a map with no span names */ + counts = dataAggregator.getRunningSpanCounts(); + assertThat(counts).isEmpty(); + } + + @Test + public void getRunningSpans_noSpans() { + /* getRunningSpans should return an empty List */ + assertThat(dataAggregator.getRunningSpans(SPAN_NAME_ONE)).isEmpty(); + assertThat(dataAggregator.getRunningSpans(SPAN_NAME_TWO)).isEmpty(); + } + + @Test + public void getRunningSpans_oneSpanName() { + Span span1 = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + Span span2 = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + Span span3 = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + /* getRunningSpans should return a List with all 3 spans */ + List spans = dataAggregator.getRunningSpans(SPAN_NAME_ONE); + assertThat(spans) + .containsExactly( + ((ReadableSpan) span1).toSpanData(), + ((ReadableSpan) span2).toSpanData(), + ((ReadableSpan) span3).toSpanData()); + span1.end(); + span2.end(); + span3.end(); + /* getRunningSpans should return an empty List */ + assertThat(dataAggregator.getRunningSpans(SPAN_NAME_ONE)).isEmpty(); + } + + @Test + public void getRunningSpans_twoSpanNames() { + Span span1 = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + Span span2 = tracer.spanBuilder(SPAN_NAME_TWO).startSpan(); + /* getRunningSpans should return a List with only the corresponding span */ + assertThat(dataAggregator.getRunningSpans(SPAN_NAME_ONE)) + .containsExactly(((ReadableSpan) span1).toSpanData()); + assertThat(dataAggregator.getRunningSpans(SPAN_NAME_TWO)) + .containsExactly(((ReadableSpan) span2).toSpanData()); + span1.end(); + span2.end(); + /* getRunningSpans should return an empty List for each span name */ + assertThat(dataAggregator.getRunningSpans(SPAN_NAME_ONE)).isEmpty(); + assertThat(dataAggregator.getRunningSpans(SPAN_NAME_TWO)).isEmpty(); + } + + @Test + public void getSpanLatencyCounts_noSpans() { + /* getSpanLatencyCounts should return a an empty map */ + Map> counts = dataAggregator.getSpanLatencyCounts(); + assertThat(counts).isEmpty(); + } + + @Test + public void getSpanLatencyCounts_noCompletedSpans() { + /* getSpanLatencyCounts should return a an empty map */ + Span span = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + Map> counts = dataAggregator.getSpanLatencyCounts(); + span.end(); + assertThat(counts).isEmpty(); + } + + @Test + public void getSpanLatencyCounts_oneSpanPerLatencyBucket() { + for (LatencyBoundary bucket : LatencyBoundary.values()) { + Span span = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + testClock.advanceNanos(bucket.getLatencyLowerBound()); + span.end(); + } + /* getSpanLatencyCounts should return 1 span per latency bucket */ + Map> counts = dataAggregator.getSpanLatencyCounts(); + for (LatencyBoundary bucket : LatencyBoundary.values()) { + assertThat(counts.get(SPAN_NAME_ONE).get(bucket)).isEqualTo(1); + } + } + + @Test + public void getOkSpans_noSpans() { + /* getOkSpans should return an empty List */ + assertThat(dataAggregator.getOkSpans(SPAN_NAME_ONE, 0, Long.MAX_VALUE)).isEmpty(); + assertThat(dataAggregator.getOkSpans(SPAN_NAME_TWO, 0, Long.MAX_VALUE)).isEmpty(); + } + + @Test + public void getOkSpans_oneSpanNameWithDifferentLatencies() { + Span span1 = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + Span span2 = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + /* getOkSpans should return an empty List */ + assertThat(dataAggregator.getOkSpans(SPAN_NAME_ONE, 0, Long.MAX_VALUE)).isEmpty(); + span1.end(); + testClock.advanceNanos(1000); + span2.end(); + /* getOkSpans should return a List with both spans */ + List spans = dataAggregator.getOkSpans(SPAN_NAME_ONE, 0, Long.MAX_VALUE); + assertThat(spans) + .containsExactly(((ReadableSpan) span1).toSpanData(), ((ReadableSpan) span2).toSpanData()); + /* getOkSpans should return a List with only the first span */ + spans = dataAggregator.getOkSpans(SPAN_NAME_ONE, 0, 1000); + assertThat(spans).containsExactly(((ReadableSpan) span1).toSpanData()); + /* getOkSpans should return a List with only the second span */ + spans = dataAggregator.getOkSpans(SPAN_NAME_ONE, 1000, Long.MAX_VALUE); + assertThat(spans).containsExactly(((ReadableSpan) span2).toSpanData()); + } + + @Test + public void getOkSpans_twoSpanNames() { + Span span1 = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + Span span2 = tracer.spanBuilder(SPAN_NAME_TWO).startSpan(); + /* getOkSpans should return an empty List for each span name */ + assertThat(dataAggregator.getOkSpans(SPAN_NAME_ONE, 0, Long.MAX_VALUE)).isEmpty(); + assertThat(dataAggregator.getOkSpans(SPAN_NAME_TWO, 0, Long.MAX_VALUE)).isEmpty(); + span1.end(); + span2.end(); + /* getOkSpans should return a List with only the corresponding span */ + assertThat(dataAggregator.getOkSpans(SPAN_NAME_ONE, 0, Long.MAX_VALUE)) + .containsExactly(((ReadableSpan) span1).toSpanData()); + assertThat(dataAggregator.getOkSpans(SPAN_NAME_TWO, 0, Long.MAX_VALUE)) + .containsExactly(((ReadableSpan) span2).toSpanData()); + } + + @Test + public void getErrorSpanCounts_noSpans() { + Map counts = dataAggregator.getErrorSpanCounts(); + assertThat(counts).isEmpty(); + } + + @Test + public void getErrorSpanCounts_noCompletedSpans() { + /* getErrorSpanCounts should return a an empty map */ + Span span = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + Map counts = dataAggregator.getErrorSpanCounts(); + span.setStatus(Status.UNKNOWN); + span.end(); + assertThat(counts).isEmpty(); + } + + @Test + public void getErrorSpanCounts_oneSpanPerErrorCode() { + for (CanonicalCode errorCode : CanonicalCode.values()) { + if (!errorCode.equals(CanonicalCode.OK)) { + Span span = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + span.setStatus(errorCode.toStatus()); + span.end(); + } + } + /* getErrorSpanCounts should return a map with CanonicalCode.values().length - 1 spans, as every + code, expect OK, represents an error */ + Map errorCounts = dataAggregator.getErrorSpanCounts(); + assertThat(errorCounts.get(SPAN_NAME_ONE)).isEqualTo(CanonicalCode.values().length - 1); + } + + @Test + public void getErrorSpanCounts_twoSpanNames() { + Span span1 = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + span1.setStatus(Status.UNKNOWN); + span1.end(); + Span span2 = tracer.spanBuilder(SPAN_NAME_TWO).startSpan(); + span2.setStatus(Status.UNKNOWN); + span2.end(); + /* getErrorSpanCounts should return a map with 2 different span names */ + Map errorCounts = dataAggregator.getErrorSpanCounts(); + assertThat(errorCounts.get(SPAN_NAME_ONE)).isEqualTo(1); + assertThat(errorCounts.get(SPAN_NAME_TWO)).isEqualTo(1); + } + + @Test + public void getErrorSpans_noSpans() { + /* getErrorSpans should return an empty List */ + assertThat(dataAggregator.getErrorSpans(SPAN_NAME_ONE)).isEmpty(); + assertThat(dataAggregator.getErrorSpans(SPAN_NAME_TWO)).isEmpty(); + } + + @Test + public void getErrorSpans_oneSpanNameWithDifferentErrors() { + Span span1 = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + Span span2 = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + /* getErrorSpans should return an empty List */ + assertThat(dataAggregator.getErrorSpans(SPAN_NAME_ONE)).isEmpty(); + span1.setStatus(Status.UNKNOWN); + span1.end(); + span2.setStatus(Status.ABORTED); + span2.end(); + /* getErrorSpans should return a List with both spans */ + List errorSpans = dataAggregator.getErrorSpans(SPAN_NAME_ONE); + assertThat(errorSpans) + .containsExactly(((ReadableSpan) span1).toSpanData(), ((ReadableSpan) span2).toSpanData()); + } + + @Test + public void getErrorSpans_twoSpanNames() { + Span span1 = tracer.spanBuilder(SPAN_NAME_ONE).startSpan(); + Span span2 = tracer.spanBuilder(SPAN_NAME_TWO).startSpan(); + /* getErrorSpans should return an empty List for each span name */ + assertThat(dataAggregator.getErrorSpans(SPAN_NAME_ONE)).isEmpty(); + assertThat(dataAggregator.getErrorSpans(SPAN_NAME_TWO)).isEmpty(); + span1.setStatus(Status.UNKNOWN); + span1.end(); + span2.setStatus(Status.UNKNOWN); + span2.end(); + /* getErrorSpans should return a List with only the corresponding span */ + assertThat(dataAggregator.getErrorSpans(SPAN_NAME_ONE)) + .containsExactly(((ReadableSpan) span1).toSpanData()); + assertThat(dataAggregator.getErrorSpans(SPAN_NAME_TWO)) + .containsExactly(((ReadableSpan) span2).toSpanData()); + } +} diff --git a/sdk_extensions/zpages/src/test/java/io/opentelemetry/sdk/extensions/zpages/TracezSpanProcessorTest.java b/sdk_extensions/zpages/src/test/java/io/opentelemetry/sdk/extensions/zpages/TracezSpanProcessorTest.java new file mode 100644 index 0000000000..db5a2d9dff --- /dev/null +++ b/sdk_extensions/zpages/src/test/java/io/opentelemetry/sdk/extensions/zpages/TracezSpanProcessorTest.java @@ -0,0 +1,141 @@ +/* + * Copyright 2020, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.sdk.extensions.zpages; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.when; + +import io.opentelemetry.sdk.trace.ReadableSpan; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.trace.SpanContext; +import io.opentelemetry.trace.SpanId; +import io.opentelemetry.trace.Status; +import io.opentelemetry.trace.TraceFlags; +import io.opentelemetry.trace.TraceId; +import io.opentelemetry.trace.TraceState; +import java.util.Collection; +import java.util.Properties; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** Unit tests for {@link TracezSpanProcessor}. */ +@RunWith(JUnit4.class) +public final class TracezSpanProcessorTest { + private static final String SPAN_NAME = "span"; + private static final SpanContext SAMPLED_SPAN_CONTEXT = + SpanContext.create( + TraceId.getInvalid(), + SpanId.getInvalid(), + TraceFlags.builder().setIsSampled(true).build(), + TraceState.builder().build()); + private static final SpanContext NOT_SAMPLED_SPAN_CONTEXT = SpanContext.getInvalid(); + private static final Status SPAN_STATUS = Status.UNKNOWN; + + private static void assertSpanCacheSizes( + TracezSpanProcessor spanProcessor, int runningSpanCacheSize, int completedSpanCacheSize) { + Collection runningSpans = spanProcessor.getRunningSpans(); + Collection completedSpans = spanProcessor.getCompletedSpans(); + assertThat(runningSpans.size()).isEqualTo(runningSpanCacheSize); + assertThat(completedSpans.size()).isEqualTo(completedSpanCacheSize); + } + + @Mock private ReadableSpan readableSpan; + @Mock private SpanData spanData; + + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Test + public void onStart_sampledSpan_inCache() { + TracezSpanProcessor spanProcessor = TracezSpanProcessor.newBuilder().build(); + /* Return a sampled span, which should be added to the running cache by default */ + when(readableSpan.getSpanContext()).thenReturn(SAMPLED_SPAN_CONTEXT); + spanProcessor.onStart(readableSpan); + assertSpanCacheSizes(spanProcessor, 1, 0); + } + + @Test + public void onEnd_sampledSpan_inCache() { + TracezSpanProcessor spanProcessor = TracezSpanProcessor.newBuilder().build(); + /* Return a sampled span, which should be added to the completed cache upon ending */ + when(readableSpan.getSpanContext()).thenReturn(SAMPLED_SPAN_CONTEXT); + when(readableSpan.getName()).thenReturn(SPAN_NAME); + spanProcessor.onStart(readableSpan); + when(readableSpan.toSpanData()).thenReturn(spanData); + when(spanData.getStatus()).thenReturn(SPAN_STATUS); + spanProcessor.onEnd(readableSpan); + assertSpanCacheSizes(spanProcessor, 0, 1); + } + + @Test + public void onStart_notSampledSpan_inCache() { + TracezSpanProcessor spanProcessor = TracezSpanProcessor.newBuilder().build(); + /* Return a non-sampled span, which should not be added to the running cache by default */ + when(readableSpan.getSpanContext()).thenReturn(NOT_SAMPLED_SPAN_CONTEXT); + spanProcessor.onStart(readableSpan); + assertSpanCacheSizes(spanProcessor, 1, 0); + } + + @Test + public void onEnd_notSampledSpan_notInCache() { + TracezSpanProcessor spanProcessor = TracezSpanProcessor.newBuilder().build(); + /* Return a non-sampled span, which should not be added to the running cache by default */ + when(readableSpan.getSpanContext()).thenReturn(NOT_SAMPLED_SPAN_CONTEXT); + spanProcessor.onStart(readableSpan); + spanProcessor.onEnd(readableSpan); + assertSpanCacheSizes(spanProcessor, 0, 0); + } + + @Test + public void build_sampledFlagTrue_notInCache() { + /* Initialize a TraceZSpanProcessor that only looks at sampled spans */ + Properties properties = new Properties(); + properties.setProperty("otel.zpages.export.sampled", "true"); + TracezSpanProcessor spanProcessor = + TracezSpanProcessor.newBuilder().readProperties(properties).build(); + + /* Return a non-sampled span, which should not be added to the completed cache */ + when(readableSpan.getSpanContext()).thenReturn(NOT_SAMPLED_SPAN_CONTEXT); + spanProcessor.onStart(readableSpan); + assertSpanCacheSizes(spanProcessor, 1, 0); + spanProcessor.onEnd(readableSpan); + assertSpanCacheSizes(spanProcessor, 0, 0); + } + + @Test + public void build_sampledFlagFalse_inCache() { + /* Initialize a TraceZSpanProcessor that looks at all spans */ + Properties properties = new Properties(); + properties.setProperty("otel.zpages.export.sampled", "false"); + TracezSpanProcessor spanProcessor = + TracezSpanProcessor.newBuilder().readProperties(properties).build(); + + /* Return a non-sampled span, which should be added to the caches */ + when(readableSpan.getSpanContext()).thenReturn(NOT_SAMPLED_SPAN_CONTEXT); + when(readableSpan.getName()).thenReturn(SPAN_NAME); + spanProcessor.onStart(readableSpan); + assertSpanCacheSizes(spanProcessor, 1, 0); + when(readableSpan.toSpanData()).thenReturn(spanData); + when(spanData.getStatus()).thenReturn(SPAN_STATUS); + spanProcessor.onEnd(readableSpan); + assertSpanCacheSizes(spanProcessor, 0, 1); + } +} diff --git a/sdk_extensions/zpages/src/test/java/io/opentelemetry/sdk/extensions/zpages/TracezZPageHandlerTest.java b/sdk_extensions/zpages/src/test/java/io/opentelemetry/sdk/extensions/zpages/TracezZPageHandlerTest.java new file mode 100644 index 0000000000..22d59e9452 --- /dev/null +++ b/sdk_extensions/zpages/src/test/java/io/opentelemetry/sdk/extensions/zpages/TracezZPageHandlerTest.java @@ -0,0 +1,291 @@ +/* + * Copyright 2020, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.sdk.extensions.zpages; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.sdk.internal.TestClock; +import io.opentelemetry.sdk.trace.TracerSdkProvider; +import io.opentelemetry.trace.EndSpanOptions; +import io.opentelemetry.trace.Span; +import io.opentelemetry.trace.Status.CanonicalCode; +import io.opentelemetry.trace.Tracer; +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** Unit tests for {@link TracezZPageHandler}. */ +@RunWith(JUnit4.class) +public final class TracezZPageHandlerTest { + private static final String FINISHED_SPAN_ONE = "FinishedSpanOne"; + private static final String FINISHED_SPAN_TWO = "FinishedSpanTwo"; + private static final String RUNNING_SPAN = "RunningSpan"; + private static final String LATENCY_SPAN = "LatencySpan"; + private static final String ERROR_SPAN = "ErrorSpan"; + private final TestClock testClock = TestClock.create(); + private final TracerSdkProvider tracerSdkProvider = + TracerSdkProvider.builder().setClock(testClock).build(); + private final Tracer tracer = tracerSdkProvider.get("TracezZPageHandlerTest"); + private final TracezSpanProcessor spanProcessor = TracezSpanProcessor.newBuilder().build(); + private final TracezDataAggregator dataAggregator = new TracezDataAggregator(spanProcessor); + private final Map queryMap = ImmutableMap.of(); + + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Before + public void setup() { + tracerSdkProvider.addSpanProcessor(spanProcessor); + } + + @Test + public void summaryTable_emitRowForEachSpan() { + OutputStream output = new ByteArrayOutputStream(); + Span finishedSpan1 = tracer.spanBuilder(FINISHED_SPAN_ONE).startSpan(); + Span finishedSpan2 = tracer.spanBuilder(FINISHED_SPAN_TWO).startSpan(); + finishedSpan1.end(); + finishedSpan2.end(); + + Span runningSpan = tracer.spanBuilder(RUNNING_SPAN).startSpan(); + + Span latencySpan = tracer.spanBuilder(LATENCY_SPAN).setStartTimestamp(1L).startSpan(); + EndSpanOptions endOptions = EndSpanOptions.builder().setEndTimestamp(10002L).build(); + latencySpan.end(endOptions); + + Span errorSpan = tracer.spanBuilder(ERROR_SPAN).startSpan(); + errorSpan.setStatus(CanonicalCode.INVALID_ARGUMENT.toStatus()); + errorSpan.end(); + + TracezZPageHandler tracezZPageHandler = new TracezZPageHandler(dataAggregator); + tracezZPageHandler.emitHtml(queryMap, output); + + // Emit a row for all types of spans + assertThat(output.toString()).contains(FINISHED_SPAN_ONE); + assertThat(output.toString()).contains(FINISHED_SPAN_TWO); + assertThat(output.toString()).contains(RUNNING_SPAN); + assertThat(output.toString()).contains(LATENCY_SPAN); + assertThat(output.toString()).contains(ERROR_SPAN); + + runningSpan.end(); + } + + @Test + public void summaryTable_linkForRunningSpans() { + OutputStream output = new ByteArrayOutputStream(); + Span runningSpan1 = tracer.spanBuilder(RUNNING_SPAN).startSpan(); + Span runningSpan2 = tracer.spanBuilder(RUNNING_SPAN).startSpan(); + Span runningSpan3 = tracer.spanBuilder(RUNNING_SPAN).startSpan(); + Span finishedSpan = tracer.spanBuilder(FINISHED_SPAN_ONE).startSpan(); + finishedSpan.end(); + + TracezZPageHandler tracezZPageHandler = new TracezZPageHandler(dataAggregator); + tracezZPageHandler.emitHtml(queryMap, output); + + // Link for running span with 3 running + assertThat(output.toString()) + .contains("href=\"?zspanname=" + RUNNING_SPAN + "&ztype=0&zsubtype=0\">3"); + // No link for finished spans + assertThat(output.toString()) + .doesNotContain("href=\"?zspanname=" + FINISHED_SPAN_ONE + "&ztype=0&subtype=0\""); + + runningSpan1.end(); + runningSpan2.end(); + runningSpan3.end(); + } + + @Test + public void summaryTable_linkForLatencyBasedSpans_NoneForEmptyBoundary() { + OutputStream output = new ByteArrayOutputStream(); + TracezZPageHandler tracezZPageHandler = new TracezZPageHandler(dataAggregator); + tracezZPageHandler.emitHtml(queryMap, output); + + // No link for boundary 0 + assertThat(output.toString()) + .doesNotContain("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=0\""); + // No link for boundary 1 + assertThat(output.toString()) + .doesNotContain("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=1\""); + // No link for boundary 2 + assertThat(output.toString()) + .doesNotContain("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=2\""); + // No link for boundary 3 + assertThat(output.toString()) + .doesNotContain("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=3\""); + // No link for boundary 4 + assertThat(output.toString()) + .doesNotContain("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=4\""); + // No link for boundary 5 + assertThat(output.toString()) + .doesNotContain("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=5\""); + // No link for boundary 6 + assertThat(output.toString()) + .doesNotContain("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=6\""); + // No link for boundary 7 + assertThat(output.toString()) + .doesNotContain("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=7\""); + // No link for boundary 8 + assertThat(output.toString()) + .doesNotContain("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=8\""); + } + + @Test + public void summaryTable_linkForLatencyBasedSpans_OnePerBoundary() { + OutputStream output = new ByteArrayOutputStream(); + // Boundary 0, >1us + Span latencySpanSubtype0 = tracer.spanBuilder(LATENCY_SPAN).setStartTimestamp(1L).startSpan(); + EndSpanOptions endOptions0 = EndSpanOptions.builder().setEndTimestamp(1002L).build(); + latencySpanSubtype0.end(endOptions0); + // Boundary 1, >10us + Span latencySpanSubtype1 = tracer.spanBuilder(LATENCY_SPAN).setStartTimestamp(1L).startSpan(); + EndSpanOptions endOptions1 = EndSpanOptions.builder().setEndTimestamp(10002L).build(); + latencySpanSubtype1.end(endOptions1); + // Boundary 2, >100us + Span latencySpanSubtype2 = tracer.spanBuilder(LATENCY_SPAN).setStartTimestamp(1L).startSpan(); + EndSpanOptions endOptions2 = EndSpanOptions.builder().setEndTimestamp(100002L).build(); + latencySpanSubtype2.end(endOptions2); + // Boundary 3, >1ms + Span latencySpanSubtype3 = tracer.spanBuilder(LATENCY_SPAN).setStartTimestamp(1L).startSpan(); + EndSpanOptions endOptions3 = EndSpanOptions.builder().setEndTimestamp(1000002L).build(); + latencySpanSubtype3.end(endOptions3); + // Boundary 4, >10ms + Span latencySpanSubtype4 = tracer.spanBuilder(LATENCY_SPAN).setStartTimestamp(1L).startSpan(); + EndSpanOptions endOptions4 = EndSpanOptions.builder().setEndTimestamp(10000002L).build(); + latencySpanSubtype4.end(endOptions4); + // Boundary 5, >100ms + Span latencySpanSubtype5 = tracer.spanBuilder(LATENCY_SPAN).setStartTimestamp(1L).startSpan(); + EndSpanOptions endOptions5 = EndSpanOptions.builder().setEndTimestamp(100000002L).build(); + latencySpanSubtype5.end(endOptions5); + // Boundary 6, >1s + Span latencySpanSubtype6 = tracer.spanBuilder(LATENCY_SPAN).setStartTimestamp(1L).startSpan(); + EndSpanOptions endOptions6 = EndSpanOptions.builder().setEndTimestamp(1000000002L).build(); + latencySpanSubtype6.end(endOptions6); + // Boundary 7, >10s + Span latencySpanSubtype7 = tracer.spanBuilder(LATENCY_SPAN).setStartTimestamp(1L).startSpan(); + EndSpanOptions endOptions7 = EndSpanOptions.builder().setEndTimestamp(10000000002L).build(); + latencySpanSubtype7.end(endOptions7); + // Boundary 8, >100s + Span latencySpanSubtype8 = tracer.spanBuilder(LATENCY_SPAN).setStartTimestamp(1L).startSpan(); + EndSpanOptions endOptions8 = EndSpanOptions.builder().setEndTimestamp(100000000002L).build(); + latencySpanSubtype8.end(endOptions8); + + TracezZPageHandler tracezZPageHandler = new TracezZPageHandler(dataAggregator); + tracezZPageHandler.emitHtml(queryMap, output); + + // Link for boundary 0 + assertThat(output.toString()) + .contains("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=0\">1"); + // Link for boundary 1 + assertThat(output.toString()) + .contains("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=1\">1"); + // Link for boundary 2 + assertThat(output.toString()) + .contains("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=2\">1"); + // Link for boundary 3 + assertThat(output.toString()) + .contains("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=3\">1"); + // Link for boundary 4 + assertThat(output.toString()) + .contains("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=4\">1"); + // Link for boundary 5 + assertThat(output.toString()) + .contains("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=5\">1"); + // Link for boundary 6 + assertThat(output.toString()) + .contains("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=6\">1"); + // Link for boundary 7 + assertThat(output.toString()) + .contains("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=7\">1"); + // Link for boundary 8 + assertThat(output.toString()) + .contains("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=8\">1"); + } + + @Test + public void summaryTable_linkForLatencyBasedSpans_MultipleForOneBoundary() { + OutputStream output = new ByteArrayOutputStream(); + // 4 samples in boundary 5, >100ms + Span latencySpan100ms1 = tracer.spanBuilder(LATENCY_SPAN).setStartTimestamp(1L).startSpan(); + EndSpanOptions endOptions1 = EndSpanOptions.builder().setEndTimestamp(112931232L).build(); + latencySpan100ms1.end(endOptions1); + Span latencySpan100ms2 = tracer.spanBuilder(LATENCY_SPAN).setStartTimestamp(1L).startSpan(); + EndSpanOptions endOptions2 = EndSpanOptions.builder().setEndTimestamp(138694322L).build(); + latencySpan100ms2.end(endOptions2); + Span latencySpan100ms3 = tracer.spanBuilder(LATENCY_SPAN).setStartTimestamp(1L).startSpan(); + EndSpanOptions endOptions3 = EndSpanOptions.builder().setEndTimestamp(154486482L).build(); + latencySpan100ms3.end(endOptions3); + Span latencySpan100ms4 = tracer.spanBuilder(LATENCY_SPAN).setStartTimestamp(1L).startSpan(); + EndSpanOptions endOptions4 = EndSpanOptions.builder().setEndTimestamp(194892582L).build(); + latencySpan100ms4.end(endOptions4); + + TracezZPageHandler tracezZPageHandler = new TracezZPageHandler(dataAggregator); + tracezZPageHandler.emitHtml(queryMap, output); + + // Link for boundary 5 with 4 samples + assertThat(output.toString()) + .contains("href=\"?zspanname=" + LATENCY_SPAN + "&ztype=1&zsubtype=5\">4"); + } + + @Test + public void summaryTable_linkForErrorSpans() { + OutputStream output = new ByteArrayOutputStream(); + Span errorSpan1 = tracer.spanBuilder(ERROR_SPAN).startSpan(); + Span errorSpan2 = tracer.spanBuilder(ERROR_SPAN).startSpan(); + Span errorSpan3 = tracer.spanBuilder(ERROR_SPAN).startSpan(); + Span finishedSpan = tracer.spanBuilder(FINISHED_SPAN_ONE).startSpan(); + errorSpan1.setStatus(CanonicalCode.CANCELLED.toStatus()); + errorSpan2.setStatus(CanonicalCode.ABORTED.toStatus()); + errorSpan3.setStatus(CanonicalCode.DEADLINE_EXCEEDED.toStatus()); + errorSpan1.end(); + errorSpan2.end(); + errorSpan3.end(); + finishedSpan.end(); + + TracezZPageHandler tracezZPageHandler = new TracezZPageHandler(dataAggregator); + tracezZPageHandler.emitHtml(queryMap, output); + + // Link for error based spans with 3 samples + assertThat(output.toString()) + .contains("href=\"?zspanname=" + ERROR_SPAN + "&ztype=2&zsubtype=0\">3"); + // No link for Status{#OK} spans + assertThat(output.toString()) + .doesNotContain("href=\"?zspanname=" + FINISHED_SPAN_ONE + "&ztype=2&subtype=0\""); + } + + @Test + public void spanDetails_emitSpanNameCorrectly() { + OutputStream output = new ByteArrayOutputStream(); + Map queryMapWithSpanName = new HashMap(); + queryMapWithSpanName.put("zspanname", FINISHED_SPAN_ONE); + queryMapWithSpanName.put("ztype", "1"); + queryMapWithSpanName.put("zsubtype", "2"); + + TracezZPageHandler tracezZPageHandler = new TracezZPageHandler(dataAggregator); + tracezZPageHandler.emitHtml(queryMapWithSpanName, output); + + assertThat(output.toString()).contains("

Span Details

"); + assertThat(output.toString()).contains(" Span Name: " + FINISHED_SPAN_ONE + ""); + assertThat(output.toString()).contains(" Number of latency samples:"); + } +} diff --git a/sdk_extensions/zpages/src/test/java/io/opentelemetry/sdk/extensions/zpages/ZPageHttpHandlerTest.java b/sdk_extensions/zpages/src/test/java/io/opentelemetry/sdk/extensions/zpages/ZPageHttpHandlerTest.java new file mode 100644 index 0000000000..3db82f5171 --- /dev/null +++ b/sdk_extensions/zpages/src/test/java/io/opentelemetry/sdk/extensions/zpages/ZPageHttpHandlerTest.java @@ -0,0 +1,43 @@ +/* + * Copyright 2020, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.sdk.extensions.zpages; + +import static com.google.common.truth.Truth.assertThat; + +import java.net.URI; +import java.net.URISyntaxException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link ZPageHttpHandler}. */ +@RunWith(JUnit4.class) +public final class ZPageHttpHandlerTest { + @Test + public void parseEmptyQuery() throws URISyntaxException { + URI uri = new URI("http://localhost:8000/tracez"); + assertThat(ZPageHttpHandler.parseQueryMap(uri)).isEmpty(); + } + + @Test + public void parseNormalQuery() throws URISyntaxException { + URI uri = + new URI("http://localhost:8000/tracez/tracez?zspanname=Test&ztype=1&zsubtype=5&noval"); + assertThat(ZPageHttpHandler.parseQueryMap(uri)) + .containsExactly("zspanname", "Test", "ztype", "1", "zsubtype", "5", "noval", ""); + } +} diff --git a/sdk_extensions/zpages/src/test/java/io/opentelemetry/sdk/extensions/zpages/ZPageServerTest.java b/sdk_extensions/zpages/src/test/java/io/opentelemetry/sdk/extensions/zpages/ZPageServerTest.java new file mode 100644 index 0000000000..8c713b96ea --- /dev/null +++ b/sdk_extensions/zpages/src/test/java/io/opentelemetry/sdk/extensions/zpages/ZPageServerTest.java @@ -0,0 +1,40 @@ +/* + * Copyright 2020, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.opentelemetry.sdk.extensions.zpages; + +import static com.google.common.truth.Truth.assertThat; + +import com.sun.net.httpserver.HttpServer; +import java.io.IOException; +import java.net.InetSocketAddress; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link ZPageServer}. */ +@RunWith(JUnit4.class) +public final class ZPageServerTest { + @Test + public void tracezSpanProcessorOnlyAddedOnce() throws IOException { + // tracezSpanProcessor is not added yet + assertThat(ZPageServer.getIsTracezSpanProcesserAdded()).isFalse(); + HttpServer server = HttpServer.create(new InetSocketAddress(8888), 5); + ZPageServer.registerAllPagesToHttpServer(server); + // tracezSpanProcessor is added + assertThat(ZPageServer.getIsTracezSpanProcesserAdded()).isTrue(); + } +} diff --git a/settings.gradle b/settings.gradle index 0d4edff62a..39875eef3f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -42,6 +42,7 @@ include ":opentelemetry-all", ":opentelemetry-sdk-extension-otproto", ":opentelemetry-sdk-extension-testbed", ":opentelemetry-sdk-extension-jaeger-remote-sampler", + ":opentelemetry-sdk-extension-zpages", ":opentelemetry-bom" rootProject.children.each {