1 /*
2  * Copyright 2017, OpenCensus Authors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package io.opencensus.contrib.zpages;
18 
19 import static com.google.common.html.HtmlEscapers.htmlEscaper;
20 
21 import com.google.common.base.Charsets;
22 import com.google.common.collect.ImmutableMap;
23 import com.google.common.io.BaseEncoding;
24 import io.opencensus.common.Duration;
25 import io.opencensus.common.Function;
26 import io.opencensus.common.Functions;
27 import io.opencensus.common.Timestamp;
28 import io.opencensus.trace.Annotation;
29 import io.opencensus.trace.AttributeValue;
30 import io.opencensus.trace.SpanContext;
31 import io.opencensus.trace.SpanId;
32 import io.opencensus.trace.Status;
33 import io.opencensus.trace.Status.CanonicalCode;
34 import io.opencensus.trace.Tracer;
35 import io.opencensus.trace.Tracing;
36 import io.opencensus.trace.export.RunningSpanStore;
37 import io.opencensus.trace.export.SampledSpanStore;
38 import io.opencensus.trace.export.SampledSpanStore.ErrorFilter;
39 import io.opencensus.trace.export.SampledSpanStore.LatencyBucketBoundaries;
40 import io.opencensus.trace.export.SampledSpanStore.LatencyFilter;
41 import io.opencensus.trace.export.SpanData;
42 import io.opencensus.trace.export.SpanData.TimedEvent;
43 import io.opencensus.trace.export.SpanData.TimedEvents;
44 import java.io.BufferedWriter;
45 import java.io.OutputStream;
46 import java.io.OutputStreamWriter;
47 import java.io.PrintWriter;
48 import java.io.Serializable;
49 import java.io.UnsupportedEncodingException;
50 import java.net.URLEncoder;
51 import java.util.ArrayList;
52 import java.util.Calendar;
53 import java.util.Collection;
54 import java.util.Collections;
55 import java.util.Comparator;
56 import java.util.Formatter;
57 import java.util.HashMap;
58 import java.util.List;
59 import java.util.Locale;
60 import java.util.Map;
61 import java.util.Set;
62 import java.util.TreeSet;
63 import java.util.concurrent.TimeUnit;
64 
65 /*>>>
66 import org.checkerframework.checker.nullness.qual.Nullable;
67 */
68 
69 // TODO(hailongwen): remove the usage of `NetworkEvent` in the future.
70 /**
71  * HTML page formatter for tracing debug. The page displays information about all active spans and
72  * all sampled spans based on latency and errors.
73  *
74  * <p>It prints a summary table which contains one row for each span name and data about number of
75  * active and sampled spans.
76  */
77 final class TracezZPageHandler extends ZPageHandler {
78   private enum RequestType {
79     RUNNING(0),
80     FINISHED(1),
81     FAILED(2),
82     UNKNOWN(-1);
83 
84     private final int value;
85 
RequestType(int value)86     RequestType(int value) {
87       this.value = value;
88     }
89 
fromString(String str)90     static RequestType fromString(String str) {
91       int value = Integer.parseInt(str);
92       switch (value) {
93         case 0:
94           return RUNNING;
95         case 1:
96           return FINISHED;
97         case 2:
98           return FAILED;
99         default:
100           return UNKNOWN;
101       }
102     }
103 
getValue()104     int getValue() {
105       return value;
106     }
107   }
108 
109   private static final String TRACEZ_URL = "/tracez";
110   private static final Tracer tracer = Tracing.getTracer();
111   // Color to use for zebra-striping.
112   private static final String ZEBRA_STRIPE_COLOR = "#FFF";
113   // Color for sampled traceIds.
114   private static final String SAMPLED_TRACE_ID_COLOR = "#C1272D";
115   // Color for not sampled traceIds
116   private static final String NOT_SAMPLED_TRACE_ID_COLOR = "black";
117   // The header for span name.
118   private static final String HEADER_SPAN_NAME = "zspanname";
119   // The header for type (running = 0, latency = 1, error = 2) to display.
120   private static final String HEADER_SAMPLES_TYPE = "ztype";
121   // The header for sub-type:
122   // * for latency based samples [0, 8] representing the latency buckets, where 0 is the first one;
123   // * for error based samples [0, 15], 0 - means all, otherwise the error code;
124   private static final String HEADER_SAMPLES_SUB_TYPE = "zsubtype";
125   // Map from LatencyBucketBoundaries to the human string displayed on the UI for each bucket.
126   private static final Map<LatencyBucketBoundaries, String> LATENCY_BUCKET_BOUNDARIES_STRING_MAP =
127       buildLatencyBucketBoundariesStringMap();
128   @javax.annotation.Nullable private final RunningSpanStore runningSpanStore;
129   @javax.annotation.Nullable private final SampledSpanStore sampledSpanStore;
130 
TracezZPageHandler( @avax.annotation.Nullable RunningSpanStore runningSpanStore, @javax.annotation.Nullable SampledSpanStore sampledSpanStore)131   private TracezZPageHandler(
132       @javax.annotation.Nullable RunningSpanStore runningSpanStore,
133       @javax.annotation.Nullable SampledSpanStore sampledSpanStore) {
134     this.runningSpanStore = runningSpanStore;
135     this.sampledSpanStore = sampledSpanStore;
136   }
137 
138   /**
139    * Constructs a new {@code TracezZPageHandler}.
140    *
141    * @param runningSpanStore the instance of the {@code RunningSpanStore} to be used.
142    * @param sampledSpanStore the instance of the {@code SampledSpanStore} to be used.
143    * @return a new {@code TracezZPageHandler}.
144    */
create( @avax.annotation.Nullable RunningSpanStore runningSpanStore, @javax.annotation.Nullable SampledSpanStore sampledSpanStore)145   static TracezZPageHandler create(
146       @javax.annotation.Nullable RunningSpanStore runningSpanStore,
147       @javax.annotation.Nullable SampledSpanStore sampledSpanStore) {
148     return new TracezZPageHandler(runningSpanStore, sampledSpanStore);
149   }
150 
151   @Override
getUrlPath()152   public String getUrlPath() {
153     return TRACEZ_URL;
154   }
155 
emitStyle(PrintWriter out)156   private static void emitStyle(PrintWriter out) {
157     out.write("<style>\n");
158     out.write(Style.style);
159     out.write("</style>\n");
160   }
161 
162   @Override
emitHtml(Map<String, String> queryMap, OutputStream outputStream)163   public void emitHtml(Map<String, String> queryMap, OutputStream outputStream) {
164     PrintWriter out =
165         new PrintWriter(new BufferedWriter(new OutputStreamWriter(outputStream, Charsets.UTF_8)));
166     out.write("<!DOCTYPE html>\n");
167     out.write("<html lang=\"en\"><head>\n");
168     out.write("<meta charset=\"utf-8\">\n");
169     out.write("<title>TraceZ</title>\n");
170     out.write("<link rel=\"shortcut icon\" href=\"https://opencensus.io/images/favicon.ico\"/>\n");
171     out.write(
172         "<link href=\"https://fonts.googleapis.com/css?family=Open+Sans:300\""
173             + "rel=\"stylesheet\">\n");
174     out.write(
175         "<link href=\"https://fonts.googleapis.com/css?family=Roboto\"" + "rel=\"stylesheet\">\n");
176     emitStyle(out);
177     out.write("</head>\n");
178     out.write("<body>\n");
179     out.write(
180         "<p class=\"header\">"
181             + "<img class=\"oc\" src=\"https://opencensus.io/img/logo-sm.svg\" />"
182             + "Open<span>Census</span></p>");
183     out.write("<h1>TraceZ Summary</h1>\n");
184 
185     try {
186       emitHtmlBody(queryMap, out);
187     } catch (Throwable t) {
188       out.write("Errors while generate the HTML page " + t);
189     }
190     out.write("</body>\n");
191     out.write("</html>\n");
192     out.close();
193   }
194 
emitHtmlBody(Map<String, String> queryMap, PrintWriter out)195   private void emitHtmlBody(Map<String, String> queryMap, PrintWriter out)
196       throws UnsupportedEncodingException {
197     if (runningSpanStore == null || sampledSpanStore == null) {
198       out.write("OpenCensus implementation not available.");
199       return;
200     }
201     Formatter formatter = new Formatter(out, Locale.US);
202     emitSummaryTable(out, formatter);
203     String spanName = queryMap.get(HEADER_SPAN_NAME);
204     if (spanName != null) {
205       tracer
206           .getCurrentSpan()
207           .addAnnotation(
208               "Render spans.",
209               ImmutableMap.<String, AttributeValue>builder()
210                   .put("SpanName", AttributeValue.stringAttributeValue(spanName))
211                   .build());
212       String typeStr = queryMap.get(HEADER_SAMPLES_TYPE);
213       if (typeStr != null) {
214         List<SpanData> spans = null;
215         RequestType type = RequestType.fromString(typeStr);
216         if (type == RequestType.UNKNOWN) {
217           return;
218         }
219         if (type == RequestType.RUNNING) {
220           // Display running.
221           spans =
222               new ArrayList<>(
223                   runningSpanStore.getRunningSpans(RunningSpanStore.Filter.create(spanName, 0)));
224           // Sort active spans incremental.
225           Collections.sort(spans, new SpanDataComparator(/* incremental= */ true));
226         } else {
227           String subtypeStr = queryMap.get(HEADER_SAMPLES_SUB_TYPE);
228           if (subtypeStr != null) {
229             int subtype = Integer.parseInt(subtypeStr);
230             if (type == RequestType.FAILED) {
231               if (subtype < 0 || subtype >= CanonicalCode.values().length) {
232                 return;
233               }
234               // Display errors. subtype 0 means all.
235               CanonicalCode code = subtype == 0 ? null : CanonicalCode.values()[subtype];
236               spans =
237                   new ArrayList<>(
238                       sampledSpanStore.getErrorSampledSpans(ErrorFilter.create(spanName, code, 0)));
239             } else {
240               if (subtype < 0 || subtype >= LatencyBucketBoundaries.values().length) {
241                 return;
242               }
243               // Display latency.
244               LatencyBucketBoundaries latencyBucketBoundaries =
245                   LatencyBucketBoundaries.values()[subtype];
246               spans =
247                   new ArrayList<>(
248                       sampledSpanStore.getLatencySampledSpans(
249                           LatencyFilter.create(
250                               spanName,
251                               latencyBucketBoundaries.getLatencyLowerNs(),
252                               latencyBucketBoundaries.getLatencyUpperNs(),
253                               0)));
254               // Sort sampled spans decremental.
255               Collections.sort(spans, new SpanDataComparator(/* incremental= */ false));
256             }
257           }
258         }
259         emitSpanNameAndCountPages(formatter, spanName, spans == null ? 0 : spans.size(), type);
260 
261         if (spans != null) {
262           emitSpans(out, formatter, spans);
263           emitLegend(out);
264         }
265       }
266     }
267   }
268 
emitSpanNameAndCountPages( Formatter formatter, String spanName, int returnedNum, RequestType type)269   private static void emitSpanNameAndCountPages(
270       Formatter formatter, String spanName, int returnedNum, RequestType type) {
271     formatter.format("<p><b>Span Name: %s </b></p>%n", htmlEscaper().escape(spanName));
272     formatter.format(
273         "%s Requests %d</b></p>%n",
274         type == RequestType.RUNNING
275             ? "Running"
276             : type == RequestType.FINISHED ? "Finished" : "Failed",
277         returnedNum);
278   }
279 
280   /** Emits the list of SampledRequets with a header. */
emitSpans(PrintWriter out, Formatter formatter, Collection<SpanData> spans)281   private static void emitSpans(PrintWriter out, Formatter formatter, Collection<SpanData> spans) {
282     out.write("<pre>\n");
283     formatter.format("%-23s %18s%n", "When", "Elapsed(s)");
284     out.write("-------------------------------------------\n");
285     for (SpanData span : spans) {
286       tracer
287           .getCurrentSpan()
288           .addAnnotation(
289               "Render span.",
290               ImmutableMap.<String, AttributeValue>builder()
291                   .put(
292                       "SpanId",
293                       AttributeValue.stringAttributeValue(
294                           BaseEncoding.base16()
295                               .lowerCase()
296                               .encode(span.getContext().getSpanId().getBytes())))
297                   .build());
298 
299       emitSingleSpan(formatter, span);
300     }
301     out.write("</pre>\n");
302   }
303 
304   // Emits the internal html for a single {@link SpanData}.
305   @SuppressWarnings("deprecation")
emitSingleSpan(Formatter formatter, SpanData span)306   private static void emitSingleSpan(Formatter formatter, SpanData span) {
307     Calendar calendar = Calendar.getInstance();
308     calendar.setTimeInMillis(TimeUnit.SECONDS.toMillis(span.getStartTimestamp().getSeconds()));
309     long microsField = TimeUnit.NANOSECONDS.toMicros(span.getStartTimestamp().getNanos());
310     String elapsedSecondsStr =
311         span.getEndTimestamp() != null
312             ? String.format(
313                 "%13.6f",
314                 durationToNanos(span.getEndTimestamp().subtractTimestamp(span.getStartTimestamp()))
315                     * 1.0e-9)
316             : String.format("%13s", " ");
317 
318     SpanContext spanContext = span.getContext();
319     formatter.format(
320         "<b>%04d/%02d/%02d-%02d:%02d:%02d.%06d %s     TraceId: <b style=\"color:%s;\">%s</b> "
321             + "SpanId: %s ParentSpanId: %s</b>%n",
322         calendar.get(Calendar.YEAR),
323         calendar.get(Calendar.MONTH) + 1,
324         calendar.get(Calendar.DAY_OF_MONTH),
325         calendar.get(Calendar.HOUR_OF_DAY),
326         calendar.get(Calendar.MINUTE),
327         calendar.get(Calendar.SECOND),
328         microsField,
329         elapsedSecondsStr,
330         spanContext.getTraceOptions().isSampled()
331             ? SAMPLED_TRACE_ID_COLOR
332             : NOT_SAMPLED_TRACE_ID_COLOR,
333         BaseEncoding.base16().lowerCase().encode(spanContext.getTraceId().getBytes()),
334         BaseEncoding.base16().lowerCase().encode(spanContext.getSpanId().getBytes()),
335         BaseEncoding.base16()
336             .lowerCase()
337             .encode(
338                 span.getParentSpanId() == null
339                     ? SpanId.INVALID.getBytes()
340                     : span.getParentSpanId().getBytes()));
341 
342     int lastEntryDayOfYear = calendar.get(Calendar.DAY_OF_YEAR);
343 
344     Timestamp lastTimestampNanos = span.getStartTimestamp();
345     TimedEvents<Annotation> annotations = span.getAnnotations();
346     TimedEvents<io.opencensus.trace.NetworkEvent> networkEvents = span.getNetworkEvents();
347     List<TimedEvent<?>> timedEvents = new ArrayList<TimedEvent<?>>(annotations.getEvents());
348     timedEvents.addAll(networkEvents.getEvents());
349     Collections.sort(timedEvents, new TimedEventComparator());
350     for (TimedEvent<?> event : timedEvents) {
351       // Special printing so that durations smaller than one second
352       // are left padded with blanks instead of '0' characters.
353       // E.g.,
354       //        Number                  Printout
355       //        ---------------------------------
356       //        0.000534                  .   534
357       //        1.000534                 1.000534
358       long deltaMicros =
359           TimeUnit.NANOSECONDS.toMicros(
360               durationToNanos(event.getTimestamp().subtractTimestamp(lastTimestampNanos)));
361       String deltaString;
362       if (deltaMicros >= 1000000) {
363         deltaString = String.format("%.6f", (deltaMicros / 1000000.0));
364       } else {
365         deltaString = String.format(".%6d", deltaMicros);
366       }
367 
368       calendar.setTimeInMillis(
369           TimeUnit.SECONDS.toMillis(event.getTimestamp().getSeconds())
370               + TimeUnit.NANOSECONDS.toMillis(event.getTimestamp().getNanos()));
371       microsField = TimeUnit.NANOSECONDS.toMicros(event.getTimestamp().getNanos());
372 
373       int dayOfYear = calendar.get(Calendar.DAY_OF_YEAR);
374       if (dayOfYear == lastEntryDayOfYear) {
375         formatter.format("%11s", "");
376       } else {
377         formatter.format(
378             "%04d/%02d/%02d-",
379             calendar.get(Calendar.YEAR),
380             calendar.get(Calendar.MONTH) + 1,
381             calendar.get(Calendar.DAY_OF_MONTH));
382         lastEntryDayOfYear = dayOfYear;
383       }
384 
385       formatter.format(
386           "%02d:%02d:%02d.%06d %13s ... %s%n",
387           calendar.get(Calendar.HOUR_OF_DAY),
388           calendar.get(Calendar.MINUTE),
389           calendar.get(Calendar.SECOND),
390           microsField,
391           deltaString,
392           htmlEscaper()
393               .escape(
394                   event.getEvent() instanceof Annotation
395                       ? renderAnnotation((Annotation) event.getEvent())
396                       : renderNetworkEvents(
397                           (io.opencensus.trace.NetworkEvent) castNonNull(event.getEvent()))));
398       lastTimestampNanos = event.getTimestamp();
399     }
400     Status status = span.getStatus();
401     if (status != null) {
402       formatter.format("%44s %s%n", "", htmlEscaper().escape(renderStatus(status)));
403     }
404     formatter.format(
405         "%44s %s%n",
406         "", htmlEscaper().escape(renderAttributes(span.getAttributes().getAttributeMap())));
407   }
408 
409   // TODO(sebright): Remove this method.
410   @SuppressWarnings("nullness")
castNonNull(@avax.annotation.Nullable T arg)411   private static <T> T castNonNull(@javax.annotation.Nullable T arg) {
412     return arg;
413   }
414 
415   // Emits the summary table with links to all samples.
emitSummaryTable(PrintWriter out, Formatter formatter)416   private void emitSummaryTable(PrintWriter out, Formatter formatter)
417       throws UnsupportedEncodingException {
418     if (runningSpanStore == null || sampledSpanStore == null) {
419       return;
420     }
421     RunningSpanStore.Summary runningSpanStoreSummary = runningSpanStore.getSummary();
422     SampledSpanStore.Summary sampledSpanStoreSummary = sampledSpanStore.getSummary();
423 
424     out.write("<table style='border-spacing: 0;\n");
425     out.write("border-left:1px solid #3D3D3D;border-right:1px solid #3D3D3D;'>\n");
426     emitSummaryTableHeader(out, formatter);
427 
428     Set<String> spanNames = new TreeSet<>(runningSpanStoreSummary.getPerSpanNameSummary().keySet());
429     spanNames.addAll(sampledSpanStoreSummary.getPerSpanNameSummary().keySet());
430     boolean zebraColor = true;
431     for (String spanName : spanNames) {
432       out.write("<tr class=\"border\">\n");
433       if (!zebraColor) {
434         out.write("<tr class=\"border\">\n");
435       } else {
436         formatter.format("<tr class=\"border\" style=\"background: %s\">%n", ZEBRA_STRIPE_COLOR);
437       }
438       zebraColor = !zebraColor;
439       formatter.format("<td>%s</td>%n", htmlEscaper().escape(spanName));
440 
441       // Running
442       out.write("<td class=\"borderRL\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
443       RunningSpanStore.PerSpanNameSummary runningSpanStorePerSpanNameSummary =
444           runningSpanStoreSummary.getPerSpanNameSummary().get(spanName);
445 
446       // subtype ignored for running requests.
447       emitSingleCell(
448           out,
449           formatter,
450           spanName,
451           runningSpanStorePerSpanNameSummary == null
452               ? 0
453               : runningSpanStorePerSpanNameSummary.getNumRunningSpans(),
454           RequestType.RUNNING,
455           0);
456 
457       SampledSpanStore.PerSpanNameSummary sampledSpanStorePerSpanNameSummary =
458           sampledSpanStoreSummary.getPerSpanNameSummary().get(spanName);
459 
460       // Latency based samples
461       out.write("<td class=\"borderLC\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
462       Map<LatencyBucketBoundaries, Integer> latencyBucketsSummaries =
463           sampledSpanStorePerSpanNameSummary != null
464               ? sampledSpanStorePerSpanNameSummary.getNumbersOfLatencySampledSpans()
465               : null;
466       int subtype = 0;
467       for (LatencyBucketBoundaries latencyBucketsBoundaries : LatencyBucketBoundaries.values()) {
468         if (latencyBucketsSummaries != null) {
469           int numSamples =
470               latencyBucketsSummaries.containsKey(latencyBucketsBoundaries)
471                   ? latencyBucketsSummaries.get(latencyBucketsBoundaries)
472                   : 0;
473           emitSingleCell(out, formatter, spanName, numSamples, RequestType.FINISHED, subtype++);
474         } else {
475           // numSamples < -1 means "Not Available".
476           emitSingleCell(out, formatter, spanName, -1, RequestType.FINISHED, subtype++);
477         }
478       }
479 
480       // Error based samples.
481       out.write("<td class=\"borderRL\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
482       if (sampledSpanStorePerSpanNameSummary != null) {
483         Map<CanonicalCode, Integer> errorBucketsSummaries =
484             sampledSpanStorePerSpanNameSummary.getNumbersOfErrorSampledSpans();
485         int numErrorSamples = 0;
486         for (Map.Entry<CanonicalCode, Integer> it : errorBucketsSummaries.entrySet()) {
487           numErrorSamples += it.getValue();
488         }
489         // subtype 0 means all;
490         emitSingleCell(out, formatter, spanName, numErrorSamples, RequestType.FAILED, 0);
491       } else {
492         // numSamples < -1 means "Not Available".
493         emitSingleCell(out, formatter, spanName, -1, RequestType.FAILED, 0);
494       }
495 
496       out.write("</tr>\n");
497     }
498     out.write("</table>");
499   }
500 
emitSummaryTableHeader(PrintWriter out, Formatter formatter)501   private static void emitSummaryTableHeader(PrintWriter out, Formatter formatter) {
502     // First line.
503     out.write("<tr class=\"bgcolor\">\n");
504     out.write("<td colspan=1 class=\"head\"><b>Span Name</b></td>\n");
505     out.write("<td class=\"borderRW\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
506     out.write("<td colspan=1 class=\"head\"><b>Running</b></td>\n");
507     out.write("<td class=\"borderLW\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
508     out.write("<td colspan=9 class=\"head\"><b>Latency Samples</b></td>\n");
509     out.write("<td class=\"borderRW\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
510     out.write("<td colspan=1 class=\"head\"><b>Error Samples</b></td>\n");
511     out.write("</tr>\n");
512     // Second line.
513     out.write("<tr class=\"bgcolor\">\n");
514     out.write("<td colspan=1></td>\n");
515     out.write("<td class=\"borderRW\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
516     out.write("<td colspan=1></td>\n");
517     out.write("<td class=\"borderLW\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
518     for (LatencyBucketBoundaries latencyBucketsBoundaries : LatencyBucketBoundaries.values()) {
519       formatter.format(
520           "<td colspan=1 class=\"centerW\"><b>[%s]</b></td>%n",
521           LATENCY_BUCKET_BOUNDARIES_STRING_MAP.get(latencyBucketsBoundaries));
522     }
523     out.write("<td class=\"borderRW\">&nbsp;&nbsp;&nbsp;&nbsp;</td>");
524     out.write("<td colspan=1></td>\n");
525     out.write("</tr>\n");
526   }
527 
528   // If numSamples is greater than 0 then emit a link to see span data, if the numSamples is
529   // negative then print "N/A", otherwise print the text "0".
emitSingleCell( PrintWriter out, Formatter formatter, String spanName, int numSamples, RequestType type, int subtype)530   private static void emitSingleCell(
531       PrintWriter out,
532       Formatter formatter,
533       String spanName,
534       int numSamples,
535       RequestType type,
536       int subtype)
537       throws UnsupportedEncodingException {
538     if (numSamples > 0) {
539       formatter.format(
540           "<td class=\"center\"><a href='?%s=%s&%s=%d&%s=%d'>%d</a></td>%n",
541           HEADER_SPAN_NAME,
542           URLEncoder.encode(spanName, "UTF-8"),
543           HEADER_SAMPLES_TYPE,
544           type.getValue(),
545           HEADER_SAMPLES_SUB_TYPE,
546           subtype,
547           numSamples);
548     } else if (numSamples < 0) {
549       out.write("<td class=\"center\">N/A</td>\n");
550     } else {
551       out.write("<td class=\"center\">0</td>\n");
552     }
553   }
554 
emitLegend(PrintWriter out)555   private static void emitLegend(PrintWriter out) {
556     out.write("<br>\n");
557     out.printf(
558         "<p><b style=\"color:%s;\">TraceId</b> means sampled request. "
559             + "<b style=\"color:%s;\">TraceId</b> means not sampled request.</p>%n",
560         SAMPLED_TRACE_ID_COLOR, NOT_SAMPLED_TRACE_ID_COLOR);
561   }
562 
buildLatencyBucketBoundariesStringMap()563   private static Map<LatencyBucketBoundaries, String> buildLatencyBucketBoundariesStringMap() {
564     Map<LatencyBucketBoundaries, String> ret = new HashMap<>();
565     for (LatencyBucketBoundaries latencyBucketBoundaries : LatencyBucketBoundaries.values()) {
566       ret.put(latencyBucketBoundaries, latencyBucketBoundariesToString(latencyBucketBoundaries));
567     }
568     return Collections.unmodifiableMap(ret);
569   }
570 
durationToNanos(Duration duration)571   private static long durationToNanos(Duration duration) {
572     return TimeUnit.SECONDS.toNanos(duration.getSeconds()) + duration.getNanos();
573   }
574 
latencyBucketBoundariesToString( LatencyBucketBoundaries latencyBucketBoundaries)575   private static String latencyBucketBoundariesToString(
576       LatencyBucketBoundaries latencyBucketBoundaries) {
577     switch (latencyBucketBoundaries) {
578       case ZERO_MICROSx10:
579         return ">0us";
580       case MICROSx10_MICROSx100:
581         return ">10us";
582       case MICROSx100_MILLIx1:
583         return ">100us";
584       case MILLIx1_MILLIx10:
585         return ">1ms";
586       case MILLIx10_MILLIx100:
587         return ">10ms";
588       case MILLIx100_SECONDx1:
589         return ">100ms";
590       case SECONDx1_SECONDx10:
591         return ">1s";
592       case SECONDx10_SECONDx100:
593         return ">10s";
594       case SECONDx100_MAX:
595         return ">100s";
596     }
597     throw new IllegalArgumentException("No value string available for: " + latencyBucketBoundaries);
598   }
599 
600   @SuppressWarnings("deprecation")
renderNetworkEvents(io.opencensus.trace.NetworkEvent networkEvent)601   private static String renderNetworkEvents(io.opencensus.trace.NetworkEvent networkEvent) {
602     StringBuilder stringBuilder = new StringBuilder();
603     if (networkEvent.getType() == io.opencensus.trace.NetworkEvent.Type.RECV) {
604       stringBuilder.append("Received message");
605     } else if (networkEvent.getType() == io.opencensus.trace.NetworkEvent.Type.SENT) {
606       stringBuilder.append("Sent message");
607     } else {
608       stringBuilder.append("Unknown");
609     }
610     stringBuilder.append(" id=");
611     stringBuilder.append(networkEvent.getMessageId());
612     stringBuilder.append(" uncompressed_size=");
613     stringBuilder.append(networkEvent.getUncompressedMessageSize());
614     stringBuilder.append(" compressed_size=");
615     stringBuilder.append(networkEvent.getCompressedMessageSize());
616     return stringBuilder.toString();
617   }
618 
renderAnnotation(Annotation annotation)619   private static String renderAnnotation(Annotation annotation) {
620     StringBuilder stringBuilder = new StringBuilder();
621     stringBuilder.append(annotation.getDescription());
622     if (!annotation.getAttributes().isEmpty()) {
623       stringBuilder.append(" ");
624       stringBuilder.append(renderAttributes(annotation.getAttributes()));
625     }
626     return stringBuilder.toString();
627   }
628 
renderStatus(Status status)629   private static String renderStatus(Status status) {
630     return status.toString();
631   }
632 
renderAttributes(Map<String, AttributeValue> attributes)633   private static String renderAttributes(Map<String, AttributeValue> attributes) {
634     StringBuilder stringBuilder = new StringBuilder();
635     stringBuilder.append("Attributes:{");
636     boolean first = true;
637     for (Map.Entry<String, AttributeValue> entry : attributes.entrySet()) {
638       if (first) {
639         first = false;
640         stringBuilder.append(entry.getKey());
641         stringBuilder.append("=");
642         stringBuilder.append(attributeValueToString(entry.getValue()));
643       } else {
644         stringBuilder.append(", ");
645         stringBuilder.append(entry.getKey());
646         stringBuilder.append("=");
647         stringBuilder.append(attributeValueToString(entry.getValue()));
648       }
649     }
650     stringBuilder.append("}");
651     return stringBuilder.toString();
652   }
653 
654   // The return type needs to be nullable when this function is used as an argument to 'match' in
655   // attributeValueToString, because 'match' doesn't allow covariant return types.
656   private static final Function<Object, /*@Nullable*/ String> returnToString =
657       Functions.returnToString();
658 
659   @javax.annotation.Nullable
attributeValueToString(AttributeValue attributeValue)660   private static String attributeValueToString(AttributeValue attributeValue) {
661     return attributeValue.match(
662         returnToString,
663         returnToString,
664         returnToString,
665         returnToString,
666         Functions.</*@Nullable*/ String>returnNull());
667   }
668 
669   private static final class TimedEventComparator
670       implements Comparator<TimedEvent<?>>, Serializable {
671     private static final long serialVersionUID = 0;
672 
673     @Override
compare(TimedEvent<?> o1, TimedEvent<?> o2)674     public int compare(TimedEvent<?> o1, TimedEvent<?> o2) {
675       return o1.getTimestamp().compareTo(o2.getTimestamp());
676     }
677   }
678 
679   private static final class SpanDataComparator implements Comparator<SpanData>, Serializable {
680     private static final long serialVersionUID = 0;
681     private final boolean incremental;
682 
683     /**
684      * Returns a new {@code SpanDataComparator}.
685      *
686      * @param incremental {@code true} if sorted incremental.
687      */
SpanDataComparator(boolean incremental)688     private SpanDataComparator(boolean incremental) {
689       this.incremental = incremental;
690     }
691 
692     @Override
compare(SpanData o1, SpanData o2)693     public int compare(SpanData o1, SpanData o2) {
694       return incremental
695           ? o1.getStartTimestamp().compareTo(o2.getStartTimestamp())
696           : o2.getStartTimestamp().compareTo(o1.getStartTimestamp());
697     }
698   }
699 }
700