Implemented base zPages classes and TraceZ zPage (#1380)

* Implemented a span processor for the TraceZ zPage

* Implemented aggregation logic for running and latency based spans,
summary table generation with running span logic

* Implemented aggregation logic for error based spans

* Finished implementation of TraceZ zPage summary table

* Finished implementation of HttpHandler, HttpServer, and TraceZ zPage

* Refactored the sdk_contrib folder to follow updated syntax

* Removed duplicate function

* Changed inline base64 images

* Fixed copyright statement

* Modified the TracezSpanProcessor to only allow for a limited number of completed spans (#17)

* Modified the TracezSpanProcessor to only allow for a limited number of completed spans

* Renamed count functions and reduced logic in addToBucket

* Fixed typos and turned SpanProcessor and DataAggregator to package private

* Separated SpanBuckets and LatencyBoundaries from TracezDataAggregator

* Switched to PrintStream, removed bufferWritter, removed factory and changed to package private constructor, added documentation for getTracerzHandler behavior, and other small fixes

* Fixed package name

* Changed way of registering handlers, changed atomicBoolean to final, removed unnecessary lock

* Changed registerTracezZPageHandler to package private

* Fixed javadoc styling and wording

* Added constants for splitters, changed httpserver to compileOnly dependency

* Removed FQNs from backend files

* Removed style errors

* Updated TracezSpanProcessor to maintain a set of span names incrementally

* Added visibleForTesting tag, adjusted HTML expression

* Removed unused functions in the data aggregator

* Made TracezSpanBuckets thread-safe

* Changed test to use Mockitorule, removed unnecessary Formatter, unrolled attribute value

* Changed test with @Mock to use Mockito test runner

* Renamed LatencyBoundaries to LatencyBoundary and cleaned up code

* Replaced the EvictingQueue with a faster SpanBucket class and modified checks in TracezDataAggregatorTest

* Addressed additional comments

* Migrated images to resources, used logger to log errors, and other minor fixes

* Moved a variable to within a class in TracezZPageHandler

* Removed printStackTrace, added test for query parameter

* Changed logger to static field, used log method to log detailed stackTrace

* Made minor fixes

* Resolved unmodifiable list error with getOkSpans and getErrorSpans

* Changed map @Mock to empty map, added more test

* Changed test runner

Co-authored-by: williamhu99 <wilhu@google.com>
Co-authored-by: William Hu <32604217+williamhu99@users.noreply.github.com>
This commit is contained in:
Terry (Tianyu) Wang 2020-07-15 15:51:29 +00:00 committed by GitHub
parent 95bb731953
commit 373fbf4a2e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 2491 additions and 0 deletions

View File

@ -0,0 +1,9 @@
# OpenTelemetry SDK Contrib - zPages
[![Javadocs][javadoc-image]][javadoc-url]
This module contains code for OpenTelemetry's Java zPages.
<!--- TODO: Update javadoc -->
[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

View File

@ -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"
}

View File

@ -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;
}
}

View File

@ -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<ReadableSpan> 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<ReadableSpan> result) {
for (int i = 0; i < bucketSize; i++) {
ReadableSpan span = spans.get(i);
if (span != null) {
result.add(span);
} else {
break;
}
}
}
}

View File

@ -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.
*
* <p>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<String> getSpanNames() {
Set<String> spanNames = new TreeSet<>();
Collection<ReadableSpan> 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<String, Integer> getRunningSpanCounts() {
Collection<ReadableSpan> allRunningSpans = spanProcessor.getRunningSpans();
Map<String, Integer> 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<SpanData> getRunningSpans(String spanName) {
Collection<ReadableSpan> allRunningSpans = spanProcessor.getRunningSpans();
List<SpanData> 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<String, Map<LatencyBoundary, Integer>> getSpanLatencyCounts() {
Map<String, TracezSpanBuckets> completedSpanCache = spanProcessor.getCompletedSpanCache();
Map<String, Map<LatencyBoundary, Integer>> numSpansPerName = new HashMap<>();
for (Entry<String, TracezSpanBuckets> 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<SpanData> getOkSpans(String spanName, long lowerBound, long upperBound) {
Map<String, TracezSpanBuckets> completedSpanCache = spanProcessor.getCompletedSpanCache();
TracezSpanBuckets buckets = completedSpanCache.get(spanName);
if (buckets == null) {
return Collections.emptyList();
}
Collection<ReadableSpan> allOkSpans = buckets.getOkSpans();
List<SpanData> 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<String, Integer> getErrorSpanCounts() {
Map<String, TracezSpanBuckets> completedSpanCache = spanProcessor.getCompletedSpanCache();
Map<String, Integer> numErrorsPerName = new HashMap<>();
for (Entry<String, TracezSpanBuckets> 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<SpanData> getErrorSpans(String spanName) {
Map<String, TracezSpanBuckets> completedSpanCache = spanProcessor.getCompletedSpanCache();
TracezSpanBuckets buckets = completedSpanCache.get(spanName);
if (buckets == null) {
return Collections.emptyList();
}
Collection<ReadableSpan> allErrorSpans = buckets.getErrorSpans();
List<SpanData> errorSpans = new ArrayList<>();
for (ReadableSpan span : allErrorSpans) {
errorSpans.add(span.toSpanData());
}
return Collections.unmodifiableList(errorSpans);
}
}

View File

@ -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<LatencyBoundary, SpanBucket> latencyBuckets;
private final ImmutableMap<CanonicalCode, SpanBucket> errorBuckets;
TracezSpanBuckets() {
ImmutableMap.Builder<LatencyBoundary, SpanBucket> latencyBucketsBuilder =
ImmutableMap.builder();
for (LatencyBoundary bucket : LatencyBoundary.values()) {
latencyBucketsBuilder.put(bucket, new SpanBucket(/* isLatencyBucket= */ true));
}
latencyBuckets = latencyBucketsBuilder.build();
ImmutableMap.Builder<CanonicalCode, SpanBucket> 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<LatencyBoundary, Integer> getLatencyBoundaryToCountMap() {
Map<LatencyBoundary, Integer> latencyCounts = new EnumMap<>(LatencyBoundary.class);
for (LatencyBoundary bucket : LatencyBoundary.values()) {
latencyCounts.put(bucket, latencyBuckets.get(bucket).size());
}
return latencyCounts;
}
List<ReadableSpan> getOkSpans() {
List<ReadableSpan> okSpans = new ArrayList<>();
for (SpanBucket latencyBucket : latencyBuckets.values()) {
latencyBucket.addTo(okSpans);
}
return okSpans;
}
List<ReadableSpan> getErrorSpans() {
List<ReadableSpan> errorSpans = new ArrayList<>();
for (SpanBucket errorBucket : errorBuckets.values()) {
errorBucket.addTo(errorSpans);
}
return errorSpans;
}
List<ReadableSpan> getSpans() {
List<ReadableSpan> spans = new ArrayList<>();
spans.addAll(getOkSpans());
spans.addAll(getErrorSpans());
return spans;
}
}

View File

@ -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.
*
* <p>Configuration options for {@link TracezSpanProcessor} can be read from system properties,
* environment variables, or {@link java.util.Properties} objects.
*
* <p>For system properties and {@link java.util.Properties} objects, {@link TracezSpanProcessor}
* will look for the following names:
*
* <ul>
* <li>{@code otel.zpages.export.sampled}: sets whether only sampled spans should be exported.
* </ul>
*
* <p>For environment variables, {@link TracezSpanProcessor} will look for the following names:
*
* <ul>
* <li>{@code OTEL_ZPAGES_EXPORT_SAMPLED}: sets whether only sampled spans should be exported.
* </ul>
*/
@ThreadSafe
final class TracezSpanProcessor implements SpanProcessor {
private final ConcurrentMap<SpanId, ReadableSpan> runningSpanCache;
private final ConcurrentMap<String, TracezSpanBuckets> 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<ReadableSpan> getRunningSpans() {
return runningSpanCache.values();
}
/**
* Returns a Collection of all completed spans for {@link TracezSpanProcessor}.
*
* @return a Collection of {@link ReadableSpan}.
*/
Collection<ReadableSpan> getCompletedSpans() {
Collection<ReadableSpan> 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<String, TracezSpanBuckets> 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<Builder> {
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:
*
* <ul>
* <li>{@code otel.zpages.export.sampled}: to set whether only sampled spans should be
* exported.
* </ul>
*
* @param configMap {@link Map} holding the configuration values.
* @return this.
*/
@Override
protected Builder fromConfigMap(
Map<String, String> 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.
*
* <p>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);
}
}
}

View File

@ -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<LatencyBoundary, String> 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 <head></head> tag.
*
* @param out the {@link PrintStream} {@code out}.
*/
private static void emitHtmlStyle(PrintStream out) {
out.print("<style>");
out.print(ZPageStyle.style);
out.print("</style>");
}
/**
* 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("<tr class=\"bg-color\">");
out.print("<th colspan=1 class=\"header-text\"><b>Span Name</b></th>");
out.print("<th colspan=1 class=\"header-text border-left-white\"><b>Running</b></th>");
out.print("<th colspan=9 class=\"header-text border-left-white\"><b>Latency Samples</b></th>");
out.print("<th colspan=1 class=\"header-text border-left-white\"><b>Error Samples</b></th>");
out.print("</tr>");
// Second row
out.print("<tr class=\"bg-color\">");
out.print("<th colspan=1></th>");
out.print("<th colspan=1 class=\"border-left-white\"></th>");
for (LatencyBoundary latencyBoundary : LatencyBoundary.values()) {
out.print(
"<th colspan=1 class=\"border-left-white align-center\""
+ "style=\"color: #fff;\"><b>["
+ LATENCY_BOUNDARIES_STRING_MAP.get(latencyBoundary)
+ "]</b></th>");
}
out.print("<th colspan=1 class=\"border-left-white\"></th>");
out.print("</tr>");
}
/**
* 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("<td class=\"align-center border-left-dark\"><a href=\"?");
out.print(PARAM_SPAN_NAME + "=" + URLEncoder.encode(spanName, "UTF-8"));
out.print("&" + PARAM_SAMPLE_TYPE + "=" + type.getValue());
out.print("&" + PARAM_SAMPLE_SUB_TYPE + "=" + subtype);
out.print("\">" + numOfSamples + "</a></td>");
} else if (numOfSamples < 0) {
out.print("<td class=\"align-center border-left-dark\">N/A</td>");
} else {
out.print("<td class=\"align-center border-left-dark\">0</td>");
}
}
/**
* 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("<table style=\"border-spacing: 0; border: 1px solid #363636;\">");
emitSummaryTableHeader(out);
Set<String> spanNames = dataAggregator.getSpanNames();
boolean zebraStripe = false;
Map<String, Integer> runningSpanCounts = dataAggregator.getRunningSpanCounts();
Map<String, Map<LatencyBoundary, Integer>> latencySpanCounts =
dataAggregator.getSpanLatencyCounts();
Map<String, Integer> errorSpanCounts = dataAggregator.getErrorSpanCounts();
for (String spanName : spanNames) {
if (zebraStripe) {
out.print("<tr style=\"background-color: " + ZEBRA_STRIPE_COLOR + "\">");
} else {
out.print("<tr>");
}
zebraStripe = !zebraStripe;
out.print("<td>" + htmlEscaper().escape(spanName) + "</td>");
// 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("</table>");
}
private static void emitSpanNameAndCount(
PrintStream out, String spanName, int count, SampleType type) {
out.print(
"<p class=\"align-center\"><b> Span Name: " + htmlEscaper().escape(spanName) + "</b></p>");
String typeString =
type == SampleType.RUNNING
? "running"
: type == SampleType.LATENCY ? "latency samples" : "error samples";
out.print("<p class=\"align-center\"><b> Number of " + typeString + ": " + count + "</b></p>");
}
private static void emitSpanDetails(
PrintStream out, Formatter formatter, Collection<SpanData> spans) {
out.print("<table style=\"border-spacing: 0; border: 1px solid #363636;\">");
out.print("<tr class=\"bg-color\">");
out.print(
"<td style=\"color: #fff;\"><pre class=\"no-margin wrap-text\"><b>When</b></pre></td>");
out.print(
"<td class=\"border-left-white\" style=\"color: #fff;\">"
+ "<pre class=\"no-margin wrap-text\"><b>Elapsed(s)</b></pre></td>");
out.print("<td class=\"border-left-white\"></td>");
out.print("</tr>");
boolean zebraStripe = false;
for (SpanData span : spans) {
zebraStripe = emitSingleSpan(out, formatter, span, zebraStripe);
}
out.print("</table>");
}
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(
"<tr style=\"background-color: %s;\">", zebraStripe ? ZEBRA_STRIPE_COLOR : "#fff");
formatter.format(
"<td class=\"align-right\"><pre class=\"no-margin wrap-text\"><b>"
+ "%04d/%02d/%02d-%02d:%02d:%02d.%06d</b></pre></td>",
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(
"<td class=\"border-left-dark\"><pre class=\"no-margin wrap-text\"><b>%s</b></pre></td>",
elapsedSecondsStr);
formatter.format(
"<td class=\"border-left-dark\"><pre class=\"no-margin wrap-text\"><b>"
+ "TraceId: <b style=\"color:%s;\">%s</b> "
+ " | SpanId: %s | ParentSpanId: %s</b></pre></td>",
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("</tr>");
zebraStripe = !zebraStripe;
int lastEntryDayOfYear = calendar.get(Calendar.DAY_OF_YEAR);
long lastEpochNanos = span.getStartEpochNanos();
List<Event> timedEvents = new ArrayList<>(span.getEvents());
Collections.sort(timedEvents, new EventComparator());
for (Event event : timedEvents) {
calendar.setTimeInMillis(TimeUnit.NANOSECONDS.toMillis(event.getEpochNanos()));
formatter.format(
"<tr style=\"background-color: %s;\">", zebraStripe ? ZEBRA_STRIPE_COLOR : "#fff");
emitSingleEvent(out, formatter, event, calendar, lastEntryDayOfYear, lastEpochNanos);
out.print("</tr>");
if (calendar.get(Calendar.DAY_OF_YEAR) != lastEntryDayOfYear) {
lastEntryDayOfYear = calendar.get(Calendar.DAY_OF_YEAR);
}
lastEpochNanos = event.getEpochNanos();
zebraStripe = !zebraStripe;
}
formatter.format(
"<tr style=\"background-color: %s;\"><td></td><td class=\"border-left-dark\">"
+ "</td><td class=\"border-left-dark\"><pre class=\"no-margin wrap-text\">",
zebraStripe ? ZEBRA_STRIPE_COLOR : "#fff");
Status status = span.getStatus();
if (status != null) {
formatter.format("%s | ", htmlEscaper().escape(status.toString()));
}
formatter.format("%s</pre></td>", 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("<td class=\"align-right\"><pre class=\"no-margin wrap-text\">");
} else {
formatter.format(
"<td class=\"align-right\"><pre class=\"no-margin wrap-text\">%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</pre></td> "
+ "<td class=\"border-left-dark\"><pre class=\"no-margin wrap-text\">%s</pre></td>"
+ "<td class=\"border-left-dark\"><pre class=\"no-margin wrap-text\">%s</pre></td>",
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<AttributeValue>() {
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 <body></body> tag.
*
* @param queryMap the map containing URL query parameters.s
* @param out the {@link PrintStream} {@code out}.
*/
private void emitHtmlBody(Map<String, String> queryMap, PrintStream out)
throws UnsupportedEncodingException {
if (dataAggregator == null) {
out.print("OpenTelemetry implementation not available.");
return;
}
// Link to OpenTelemetry Logo
out.print(
"<img style=\"height: 90px;\" src=\"data:image/png;base64,"
+ ZPageLogo.getLogoBase64()
+ "\" />");
out.print("<h1>TraceZ Summary</h1>");
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<SpanData> 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("<h2>Span Details</h2>");
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<String, String> queryMap, OutputStream outputStream) {
// PrintStream for emiting HTML contents
try (PrintStream out = new PrintStream(outputStream, /* autoFlush= */ false, "UTF-8")) {
out.print("<!DOCTYPE html>");
out.print("<html lang=\"en\">");
out.print("<head>");
out.print("<meta charset=\"UTF-8\">");
out.print(
"<link rel=\"shortcut icon\" href=\"data:image/png;base64,"
+ ZPageLogo.getFaviconBase64()
+ "\" type=\"image/png\">");
out.print(
"<link href=\"https://fonts.googleapis.com/css?family=Open+Sans:300\""
+ "rel=\"stylesheet\">");
out.print(
"<link href=\"https://fonts.googleapis.com/css?family=Roboto\" rel=\"stylesheet\">");
out.print("<title>TraceZ</title>");
emitHtmlStyle(out);
out.print("</head>");
out.print("<body>");
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("</body>");
out.print("</html>");
} 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<LatencyBoundary, String> buildLatencyBoundaryStringMap() {
Map<LatencyBoundary, String> latencyBoundaryMap = new HashMap<>();
for (LatencyBoundary latencyBoundary : LatencyBoundary.values()) {
latencyBoundaryMap.put(latencyBoundary, latencyBoundaryToString(latencyBoundary));
}
return ImmutableMap.copyOf(latencyBoundaryMap);
}
private static final class EventComparator implements Comparator<Event>, 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<SpanData>, 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());
}
}
}

View File

@ -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<String, String> queryMap, OutputStream outputStream);
/** Package protected constructor to disallow users to extend this class. */
ZPageHandler() {}
}

View File

@ -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<String, String> parseQueryMap(URI uri) {
String queryStrings = uri.getQuery();
if (queryStrings == null) {
return ImmutableMap.of();
}
Map<String, String> queryMap = new HashMap<String, String>();
for (String param : QUERY_SPLITTER.split(queryStrings)) {
List<String> 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();
}
}
}

View File

@ -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 "";
}
}
}

View File

@ -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.
*
* <p>Example usage with private {@link HttpServer}
*
* <pre>{@code
* public class Main {
* public static void main(String[] args) throws Exception {
* ZPageServer.startHttpServerAndRegisterAllPages(8000);
* ... // do work
* }
* }
* }</pre>
*
* <p>Example usage with shared {@link HttpServer}
*
* <pre>{@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
* }
* }
* }</pre>
*/
@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.
*
* <p>It displays a summary table which contains one row for each span name and data about number
* of running and sampled spans.
*
* <p>Clicking on a cell in the table with a number that is greater than zero will display
* detailed information about that span.
*
* <p>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.
*
* <p>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() {}
}

View File

@ -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;}";
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -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<String> 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<String, Integer> 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<String, Integer> 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<String, Integer> 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<SpanData> 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<String, Map<LatencyBoundary, Integer>> 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<String, Map<LatencyBoundary, Integer>> 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<String, Map<LatencyBoundary, Integer>> 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<SpanData> 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<String, Integer> 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<String, Integer> 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<String, Integer> 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<String, Integer> 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<SpanData> 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());
}
}

View File

@ -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<ReadableSpan> runningSpans = spanProcessor.getRunningSpans();
Collection<ReadableSpan> 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);
}
}

View File

@ -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<String, String> 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<String, String> queryMapWithSpanName = new HashMap<String, String>();
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("<h2>Span Details</h2>");
assertThat(output.toString()).contains("<b> Span Name: " + FINISHED_SPAN_ONE + "</b>");
assertThat(output.toString()).contains("<b> Number of latency samples:");
}
}

View File

@ -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", "");
}
}

View File

@ -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();
}
}

View File

@ -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 {