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:
parent
95bb731953
commit
373fbf4a2e
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {}
|
||||
}
|
||||
|
|
@ -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 |
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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:");
|
||||
}
|
||||
}
|
||||
|
|
@ -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", "");
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue