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\"> </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\"> </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\"> </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\"> </td>"); 506 out.write("<td colspan=1 class=\"head\"><b>Running</b></td>\n"); 507 out.write("<td class=\"borderLW\"> </td>"); 508 out.write("<td colspan=9 class=\"head\"><b>Latency Samples</b></td>\n"); 509 out.write("<td class=\"borderRW\"> </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\"> </td>"); 516 out.write("<td colspan=1></td>\n"); 517 out.write("<td class=\"borderLW\"> </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\"> </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