1
2
3
4
5
63 package trace
64
65 import (
66 "bytes"
67 "context"
68 "fmt"
69 "html/template"
70 "io"
71 "log"
72 "net"
73 "net/http"
74 "net/url"
75 "runtime"
76 "sort"
77 "strconv"
78 "sync"
79 "sync/atomic"
80 "time"
81
82 "golang.org/x/net/internal/timeseries"
83 )
84
85
86
87 var DebugUseAfterFinish = false
88
89
90 const (
91 debugRequestsPath = "/debug/requests"
92 debugEventsPath = "/debug/events"
93 )
94
95
96
97
98
99
100
101
102
103
104
105 var AuthRequest = func(req *http.Request) (any, sensitive bool) {
106
107
108 host, _, err := net.SplitHostPort(req.RemoteAddr)
109 if err != nil {
110 host = req.RemoteAddr
111 }
112 switch host {
113 case "localhost", "127.0.0.1", "::1":
114 return true, true
115 default:
116 return false, false
117 }
118 }
119
120 func init() {
121 _, pat := http.DefaultServeMux.Handler(&http.Request{URL: &url.URL{Path: debugRequestsPath}})
122 if pat == debugRequestsPath {
123 panic("/debug/requests is already registered. You may have two independent copies of " +
124 "golang.org/x/net/trace in your binary, trying to maintain separate state. This may " +
125 "involve a vendored copy of golang.org/x/net/trace.")
126 }
127
128
129
130 http.HandleFunc(debugRequestsPath, Traces)
131 http.HandleFunc(debugEventsPath, Events)
132 }
133
134
135
136 func NewContext(ctx context.Context, tr Trace) context.Context {
137 return context.WithValue(ctx, contextKey, tr)
138 }
139
140
141 func FromContext(ctx context.Context) (tr Trace, ok bool) {
142 tr, ok = ctx.Value(contextKey).(Trace)
143 return
144 }
145
146
147
148
149
150
151 func Traces(w http.ResponseWriter, req *http.Request) {
152 any, sensitive := AuthRequest(req)
153 if !any {
154 http.Error(w, "not allowed", http.StatusUnauthorized)
155 return
156 }
157 w.Header().Set("Content-Type", "text/html; charset=utf-8")
158 Render(w, req, sensitive)
159 }
160
161
162
163
164
165
166 func Events(w http.ResponseWriter, req *http.Request) {
167 any, sensitive := AuthRequest(req)
168 if !any {
169 http.Error(w, "not allowed", http.StatusUnauthorized)
170 return
171 }
172 w.Header().Set("Content-Type", "text/html; charset=utf-8")
173 RenderEvents(w, req, sensitive)
174 }
175
176
177
178
179
180 func Render(w io.Writer, req *http.Request, sensitive bool) {
181 data := &struct {
182 Families []string
183 ActiveTraceCount map[string]int
184 CompletedTraces map[string]*family
185
186
187 Traces traceList
188 Family string
189 Bucket int
190 Expanded bool
191 Traced bool
192 Active bool
193 ShowSensitive bool
194
195 Histogram template.HTML
196 HistogramWindow string
197
198
199
200 Total int
201 }{
202 CompletedTraces: completedTraces,
203 }
204
205 data.ShowSensitive = sensitive
206 if req != nil {
207
208
209 if req.FormValue("show_sensitive") == "0" {
210 data.ShowSensitive = false
211 }
212
213 if exp, err := strconv.ParseBool(req.FormValue("exp")); err == nil {
214 data.Expanded = exp
215 }
216 if exp, err := strconv.ParseBool(req.FormValue("rtraced")); err == nil {
217 data.Traced = exp
218 }
219 }
220
221 completedMu.RLock()
222 data.Families = make([]string, 0, len(completedTraces))
223 for fam := range completedTraces {
224 data.Families = append(data.Families, fam)
225 }
226 completedMu.RUnlock()
227 sort.Strings(data.Families)
228
229
230
231 data.ActiveTraceCount = make(map[string]int, len(data.Families))
232 activeMu.RLock()
233 for fam, s := range activeTraces {
234 data.ActiveTraceCount[fam] = s.Len()
235 }
236 activeMu.RUnlock()
237
238 var ok bool
239 data.Family, data.Bucket, ok = parseArgs(req)
240 switch {
241 case !ok:
242
243 case data.Bucket == -1:
244 data.Active = true
245 n := data.ActiveTraceCount[data.Family]
246 data.Traces = getActiveTraces(data.Family)
247 if len(data.Traces) < n {
248 data.Total = n
249 }
250 case data.Bucket < bucketsPerFamily:
251 if b := lookupBucket(data.Family, data.Bucket); b != nil {
252 data.Traces = b.Copy(data.Traced)
253 }
254 default:
255 if f := getFamily(data.Family, false); f != nil {
256 var obs timeseries.Observable
257 f.LatencyMu.RLock()
258 switch o := data.Bucket - bucketsPerFamily; o {
259 case 0:
260 obs = f.Latency.Minute()
261 data.HistogramWindow = "last minute"
262 case 1:
263 obs = f.Latency.Hour()
264 data.HistogramWindow = "last hour"
265 case 2:
266 obs = f.Latency.Total()
267 data.HistogramWindow = "all time"
268 }
269 f.LatencyMu.RUnlock()
270 if obs != nil {
271 data.Histogram = obs.(*histogram).html()
272 }
273 }
274 }
275
276 if data.Traces != nil {
277 defer data.Traces.Free()
278 sort.Sort(data.Traces)
279 }
280
281 completedMu.RLock()
282 defer completedMu.RUnlock()
283 if err := pageTmpl().ExecuteTemplate(w, "Page", data); err != nil {
284 log.Printf("net/trace: Failed executing template: %v", err)
285 }
286 }
287
288 func parseArgs(req *http.Request) (fam string, b int, ok bool) {
289 if req == nil {
290 return "", 0, false
291 }
292 fam, bStr := req.FormValue("fam"), req.FormValue("b")
293 if fam == "" || bStr == "" {
294 return "", 0, false
295 }
296 b, err := strconv.Atoi(bStr)
297 if err != nil || b < -1 {
298 return "", 0, false
299 }
300
301 return fam, b, true
302 }
303
304 func lookupBucket(fam string, b int) *traceBucket {
305 f := getFamily(fam, false)
306 if f == nil || b < 0 || b >= len(f.Buckets) {
307 return nil
308 }
309 return f.Buckets[b]
310 }
311
312 type contextKeyT string
313
314 var contextKey = contextKeyT("golang.org/x/net/trace.Trace")
315
316
317 type Trace interface {
318
319
320
321 LazyLog(x fmt.Stringer, sensitive bool)
322
323
324
325
326 LazyPrintf(format string, a ...interface{})
327
328
329 SetError()
330
331
332
333
334
335 SetRecycler(f func(interface{}))
336
337
338
339 SetTraceInfo(traceID, spanID uint64)
340
341
342
343
344 SetMaxEvents(m int)
345
346
347
348 Finish()
349 }
350
351 type lazySprintf struct {
352 format string
353 a []interface{}
354 }
355
356 func (l *lazySprintf) String() string {
357 return fmt.Sprintf(l.format, l.a...)
358 }
359
360
361 func New(family, title string) Trace {
362 tr := newTrace()
363 tr.ref()
364 tr.Family, tr.Title = family, title
365 tr.Start = time.Now()
366 tr.maxEvents = maxEventsPerTrace
367 tr.events = tr.eventsBuf[:0]
368
369 activeMu.RLock()
370 s := activeTraces[tr.Family]
371 activeMu.RUnlock()
372 if s == nil {
373 activeMu.Lock()
374 s = activeTraces[tr.Family]
375 if s == nil {
376 s = new(traceSet)
377 activeTraces[tr.Family] = s
378 }
379 activeMu.Unlock()
380 }
381 s.Add(tr)
382
383
384
385
386
387
388 completedMu.RLock()
389 if _, ok := completedTraces[tr.Family]; !ok {
390 go allocFamily(tr.Family)
391 }
392 completedMu.RUnlock()
393
394 return tr
395 }
396
397 func (tr *trace) Finish() {
398 elapsed := time.Since(tr.Start)
399 tr.mu.Lock()
400 tr.Elapsed = elapsed
401 tr.mu.Unlock()
402
403 if DebugUseAfterFinish {
404 buf := make([]byte, 4<<10)
405 n := runtime.Stack(buf, false)
406 tr.finishStack = buf[:n]
407 }
408
409 activeMu.RLock()
410 m := activeTraces[tr.Family]
411 activeMu.RUnlock()
412 m.Remove(tr)
413
414 f := getFamily(tr.Family, true)
415 tr.mu.RLock()
416 for _, b := range f.Buckets {
417 if b.Cond.match(tr) {
418 b.Add(tr)
419 }
420 }
421 tr.mu.RUnlock()
422
423
424 h := new(histogram)
425 h.addMeasurement(elapsed.Nanoseconds() / 1e3)
426 f.LatencyMu.Lock()
427 f.Latency.Add(h)
428 f.LatencyMu.Unlock()
429
430 tr.unref()
431 }
432
433 const (
434 bucketsPerFamily = 9
435 tracesPerBucket = 10
436 maxActiveTraces = 20
437 maxEventsPerTrace = 10
438 numHistogramBuckets = 38
439 )
440
441 var (
442
443 activeMu sync.RWMutex
444 activeTraces = make(map[string]*traceSet)
445
446
447 completedMu sync.RWMutex
448 completedTraces = make(map[string]*family)
449 )
450
451 type traceSet struct {
452 mu sync.RWMutex
453 m map[*trace]bool
454
455
456
457
458
459
460 }
461
462 func (ts *traceSet) Len() int {
463 ts.mu.RLock()
464 defer ts.mu.RUnlock()
465 return len(ts.m)
466 }
467
468 func (ts *traceSet) Add(tr *trace) {
469 ts.mu.Lock()
470 if ts.m == nil {
471 ts.m = make(map[*trace]bool)
472 }
473 ts.m[tr] = true
474 ts.mu.Unlock()
475 }
476
477 func (ts *traceSet) Remove(tr *trace) {
478 ts.mu.Lock()
479 delete(ts.m, tr)
480 ts.mu.Unlock()
481 }
482
483
484 func (ts *traceSet) FirstN(n int) traceList {
485 ts.mu.RLock()
486 defer ts.mu.RUnlock()
487
488 if n > len(ts.m) {
489 n = len(ts.m)
490 }
491 trl := make(traceList, 0, n)
492
493
494 if n == len(ts.m) {
495 for tr := range ts.m {
496 tr.ref()
497 trl = append(trl, tr)
498 }
499 sort.Sort(trl)
500 return trl
501 }
502
503
504
505 for tr := range ts.m {
506
507
508 if len(trl) < n {
509 tr.ref()
510 trl = append(trl, tr)
511 if len(trl) == n {
512
513 sort.Sort(trl)
514 }
515 continue
516 }
517 if tr.Start.After(trl[n-1].Start) {
518 continue
519 }
520
521
522 tr.ref()
523 i := sort.Search(n, func(i int) bool { return trl[i].Start.After(tr.Start) })
524 trl[n-1].unref()
525 copy(trl[i+1:], trl[i:])
526 trl[i] = tr
527 }
528
529 return trl
530 }
531
532 func getActiveTraces(fam string) traceList {
533 activeMu.RLock()
534 s := activeTraces[fam]
535 activeMu.RUnlock()
536 if s == nil {
537 return nil
538 }
539 return s.FirstN(maxActiveTraces)
540 }
541
542 func getFamily(fam string, allocNew bool) *family {
543 completedMu.RLock()
544 f := completedTraces[fam]
545 completedMu.RUnlock()
546 if f == nil && allocNew {
547 f = allocFamily(fam)
548 }
549 return f
550 }
551
552 func allocFamily(fam string) *family {
553 completedMu.Lock()
554 defer completedMu.Unlock()
555 f := completedTraces[fam]
556 if f == nil {
557 f = newFamily()
558 completedTraces[fam] = f
559 }
560 return f
561 }
562
563
564 type family struct {
565
566 Buckets [bucketsPerFamily]*traceBucket
567
568
569 LatencyMu sync.RWMutex
570 Latency *timeseries.MinuteHourSeries
571 }
572
573 func newFamily() *family {
574 return &family{
575 Buckets: [bucketsPerFamily]*traceBucket{
576 {Cond: minCond(0)},
577 {Cond: minCond(50 * time.Millisecond)},
578 {Cond: minCond(100 * time.Millisecond)},
579 {Cond: minCond(200 * time.Millisecond)},
580 {Cond: minCond(500 * time.Millisecond)},
581 {Cond: minCond(1 * time.Second)},
582 {Cond: minCond(10 * time.Second)},
583 {Cond: minCond(100 * time.Second)},
584 {Cond: errorCond{}},
585 },
586 Latency: timeseries.NewMinuteHourSeries(func() timeseries.Observable { return new(histogram) }),
587 }
588 }
589
590
591
592 type traceBucket struct {
593 Cond cond
594
595
596 mu sync.RWMutex
597 buf [tracesPerBucket]*trace
598 start int
599 length int
600 }
601
602 func (b *traceBucket) Add(tr *trace) {
603 b.mu.Lock()
604 defer b.mu.Unlock()
605
606 i := b.start + b.length
607 if i >= tracesPerBucket {
608 i -= tracesPerBucket
609 }
610 if b.length == tracesPerBucket {
611
612 b.buf[i].unref()
613 b.start++
614 if b.start == tracesPerBucket {
615 b.start = 0
616 }
617 }
618 b.buf[i] = tr
619 if b.length < tracesPerBucket {
620 b.length++
621 }
622 tr.ref()
623 }
624
625
626
627
628
629
630 func (b *traceBucket) Copy(tracedOnly bool) traceList {
631 b.mu.RLock()
632 defer b.mu.RUnlock()
633
634 trl := make(traceList, 0, b.length)
635 for i, x := 0, b.start; i < b.length; i++ {
636 tr := b.buf[x]
637 if !tracedOnly || tr.spanID != 0 {
638 tr.ref()
639 trl = append(trl, tr)
640 }
641 x++
642 if x == b.length {
643 x = 0
644 }
645 }
646 return trl
647 }
648
649 func (b *traceBucket) Empty() bool {
650 b.mu.RLock()
651 defer b.mu.RUnlock()
652 return b.length == 0
653 }
654
655
656 type cond interface {
657 match(t *trace) bool
658 String() string
659 }
660
661 type minCond time.Duration
662
663 func (m minCond) match(t *trace) bool { return t.Elapsed >= time.Duration(m) }
664 func (m minCond) String() string { return fmt.Sprintf("≥%gs", time.Duration(m).Seconds()) }
665
666 type errorCond struct{}
667
668 func (e errorCond) match(t *trace) bool { return t.IsError }
669 func (e errorCond) String() string { return "errors" }
670
671 type traceList []*trace
672
673
674 func (trl traceList) Free() {
675 for _, t := range trl {
676 t.unref()
677 }
678 }
679
680
681 func (trl traceList) Len() int { return len(trl) }
682 func (trl traceList) Less(i, j int) bool { return trl[i].Start.After(trl[j].Start) }
683 func (trl traceList) Swap(i, j int) { trl[i], trl[j] = trl[j], trl[i] }
684
685
686 type event struct {
687 When time.Time
688 Elapsed time.Duration
689 NewDay bool
690 Recyclable bool
691 Sensitive bool
692 What interface{}
693 }
694
695
696
697 func (e event) WhenString() string {
698 if e.NewDay {
699 return e.When.Format("2006/01/02 15:04:05.000000")
700 }
701 return e.When.Format("15:04:05.000000")
702 }
703
704
705
706 type discarded int
707
708 func (d *discarded) String() string {
709 return fmt.Sprintf("(%d events discarded)", int(*d))
710 }
711
712
713
714 type trace struct {
715
716 Family string
717
718
719 Title string
720
721
722 Start time.Time
723
724 mu sync.RWMutex
725 events []event
726 maxEvents int
727 recycler func(interface{})
728 IsError bool
729 Elapsed time.Duration
730 traceID uint64
731 spanID uint64
732
733 refs int32
734 disc discarded
735
736 finishStack []byte
737
738 eventsBuf [4]event
739 }
740
741 func (tr *trace) reset() {
742
743 tr.Family = ""
744 tr.Title = ""
745 tr.Start = time.Time{}
746
747 tr.mu.Lock()
748 tr.Elapsed = 0
749 tr.traceID = 0
750 tr.spanID = 0
751 tr.IsError = false
752 tr.maxEvents = 0
753 tr.events = nil
754 tr.recycler = nil
755 tr.mu.Unlock()
756
757 tr.refs = 0
758 tr.disc = 0
759 tr.finishStack = nil
760 for i := range tr.eventsBuf {
761 tr.eventsBuf[i] = event{}
762 }
763 }
764
765
766
767
768 func (tr *trace) delta(t time.Time) (time.Duration, bool) {
769 if len(tr.events) == 0 {
770 return t.Sub(tr.Start), false
771 }
772 prev := tr.events[len(tr.events)-1].When
773 return t.Sub(prev), prev.Day() != t.Day()
774 }
775
776 func (tr *trace) addEvent(x interface{}, recyclable, sensitive bool) {
777 if DebugUseAfterFinish && tr.finishStack != nil {
778 buf := make([]byte, 4<<10)
779 n := runtime.Stack(buf, false)
780 log.Printf("net/trace: trace used after finish:\nFinished at:\n%s\nUsed at:\n%s", tr.finishStack, buf[:n])
781 }
782
783
796
797 e := event{When: time.Now(), What: x, Recyclable: recyclable, Sensitive: sensitive}
798 tr.mu.Lock()
799 e.Elapsed, e.NewDay = tr.delta(e.When)
800 if len(tr.events) < tr.maxEvents {
801 tr.events = append(tr.events, e)
802 } else {
803
804 di := int((tr.maxEvents - 1) / 2)
805 if d, ok := tr.events[di].What.(*discarded); ok {
806 (*d)++
807 } else {
808
809
810 tr.disc = 2
811 if tr.recycler != nil && tr.events[di].Recyclable {
812 go tr.recycler(tr.events[di].What)
813 }
814 tr.events[di].What = &tr.disc
815 }
816
817
818 tr.events[di].When = tr.events[di+1].When
819
820 if tr.recycler != nil && tr.events[di+1].Recyclable {
821 go tr.recycler(tr.events[di+1].What)
822 }
823 copy(tr.events[di+1:], tr.events[di+2:])
824 tr.events[tr.maxEvents-1] = e
825 }
826 tr.mu.Unlock()
827 }
828
829 func (tr *trace) LazyLog(x fmt.Stringer, sensitive bool) {
830 tr.addEvent(x, true, sensitive)
831 }
832
833 func (tr *trace) LazyPrintf(format string, a ...interface{}) {
834 tr.addEvent(&lazySprintf{format, a}, false, false)
835 }
836
837 func (tr *trace) SetError() {
838 tr.mu.Lock()
839 tr.IsError = true
840 tr.mu.Unlock()
841 }
842
843 func (tr *trace) SetRecycler(f func(interface{})) {
844 tr.mu.Lock()
845 tr.recycler = f
846 tr.mu.Unlock()
847 }
848
849 func (tr *trace) SetTraceInfo(traceID, spanID uint64) {
850 tr.mu.Lock()
851 tr.traceID, tr.spanID = traceID, spanID
852 tr.mu.Unlock()
853 }
854
855 func (tr *trace) SetMaxEvents(m int) {
856 tr.mu.Lock()
857
858 if len(tr.events) == 0 && m > 3 {
859 tr.maxEvents = m
860 }
861 tr.mu.Unlock()
862 }
863
864 func (tr *trace) ref() {
865 atomic.AddInt32(&tr.refs, 1)
866 }
867
868 func (tr *trace) unref() {
869 if atomic.AddInt32(&tr.refs, -1) == 0 {
870 tr.mu.RLock()
871 if tr.recycler != nil {
872
873 go func(f func(interface{}), es []event) {
874 for _, e := range es {
875 if e.Recyclable {
876 f(e.What)
877 }
878 }
879 }(tr.recycler, tr.events)
880 }
881 tr.mu.RUnlock()
882
883 freeTrace(tr)
884 }
885 }
886
887 func (tr *trace) When() string {
888 return tr.Start.Format("2006/01/02 15:04:05.000000")
889 }
890
891 func (tr *trace) ElapsedTime() string {
892 tr.mu.RLock()
893 t := tr.Elapsed
894 tr.mu.RUnlock()
895
896 if t == 0 {
897
898 t = time.Since(tr.Start)
899 }
900 return fmt.Sprintf("%.6f", t.Seconds())
901 }
902
903 func (tr *trace) Events() []event {
904 tr.mu.RLock()
905 defer tr.mu.RUnlock()
906 return tr.events
907 }
908
909 var traceFreeList = make(chan *trace, 1000)
910
911
912 func newTrace() *trace {
913 select {
914 case tr := <-traceFreeList:
915 return tr
916 default:
917 return new(trace)
918 }
919 }
920
921
922
923 func freeTrace(tr *trace) {
924 if DebugUseAfterFinish {
925 return
926 }
927 tr.reset()
928 select {
929 case traceFreeList <- tr:
930 default:
931 }
932 }
933
934 func elapsed(d time.Duration) string {
935 b := []byte(fmt.Sprintf("%.6f", d.Seconds()))
936
937
938
939 if d < time.Second {
940 dot := bytes.IndexByte(b, '.')
941 for i := 0; i < dot; i++ {
942 b[i] = ' '
943 }
944 for i := dot + 1; i < len(b); i++ {
945 if b[i] == '0' {
946 b[i] = ' '
947 } else {
948 break
949 }
950 }
951 }
952
953 return string(b)
954 }
955
956 var pageTmplCache *template.Template
957 var pageTmplOnce sync.Once
958
959 func pageTmpl() *template.Template {
960 pageTmplOnce.Do(func() {
961 pageTmplCache = template.Must(template.New("Page").Funcs(template.FuncMap{
962 "elapsed": elapsed,
963 "add": func(a, b int) int { return a + b },
964 }).Parse(pageHTML))
965 })
966 return pageTmplCache
967 }
968
969 const pageHTML = `
970 {{template "Prolog" .}}
971 {{template "StatusTable" .}}
972 {{template "Epilog" .}}
973
974 {{define "Prolog"}}
975 <html>
976 <head>
977 <title>/debug/requests</title>
978 <style type="text/css">
979 body {
980 font-family: sans-serif;
981 }
982 table#tr-status td.family {
983 padding-right: 2em;
984 }
985 table#tr-status td.active {
986 padding-right: 1em;
987 }
988 table#tr-status td.latency-first {
989 padding-left: 1em;
990 }
991 table#tr-status td.empty {
992 color: #aaa;
993 }
994 table#reqs {
995 margin-top: 1em;
996 }
997 table#reqs tr.first {
998 {{if $.Expanded}}font-weight: bold;{{end}}
999 }
1000 table#reqs td {
1001 font-family: monospace;
1002 }
1003 table#reqs td.when {
1004 text-align: right;
1005 white-space: nowrap;
1006 }
1007 table#reqs td.elapsed {
1008 padding: 0 0.5em;
1009 text-align: right;
1010 white-space: pre;
1011 width: 10em;
1012 }
1013 address {
1014 font-size: smaller;
1015 margin-top: 5em;
1016 }
1017 </style>
1018 </head>
1019 <body>
1020
1021 <h1>/debug/requests</h1>
1022 {{end}} {{/* end of Prolog */}}
1023
1024 {{define "StatusTable"}}
1025 <table id="tr-status">
1026 {{range $fam := .Families}}
1027 <tr>
1028 <td class="family">{{$fam}}</td>
1029
1030 {{$n := index $.ActiveTraceCount $fam}}
1031 <td class="active {{if not $n}}empty{{end}}">
1032 {{if $n}}<a href="?fam={{$fam}}&b=-1{{if $.Expanded}}&exp=1{{end}}">{{end}}
1033 [{{$n}} active]
1034 {{if $n}}</a>{{end}}
1035 </td>
1036
1037 {{$f := index $.CompletedTraces $fam}}
1038 {{range $i, $b := $f.Buckets}}
1039 {{$empty := $b.Empty}}
1040 <td {{if $empty}}class="empty"{{end}}>
1041 {{if not $empty}}<a href="?fam={{$fam}}&b={{$i}}{{if $.Expanded}}&exp=1{{end}}">{{end}}
1042 [{{.Cond}}]
1043 {{if not $empty}}</a>{{end}}
1044 </td>
1045 {{end}}
1046
1047 {{$nb := len $f.Buckets}}
1048 <td class="latency-first">
1049 <a href="?fam={{$fam}}&b={{$nb}}">[minute]</a>
1050 </td>
1051 <td>
1052 <a href="?fam={{$fam}}&b={{add $nb 1}}">[hour]</a>
1053 </td>
1054 <td>
1055 <a href="?fam={{$fam}}&b={{add $nb 2}}">[total]</a>
1056 </td>
1057
1058 </tr>
1059 {{end}}
1060 </table>
1061 {{end}} {{/* end of StatusTable */}}
1062
1063 {{define "Epilog"}}
1064 {{if $.Traces}}
1065 <hr />
1066 <h3>Family: {{$.Family}}</h3>
1067
1068 {{if or $.Expanded $.Traced}}
1069 <a href="?fam={{$.Family}}&b={{$.Bucket}}">[Normal/Summary]</a>
1070 {{else}}
1071 [Normal/Summary]
1072 {{end}}
1073
1074 {{if or (not $.Expanded) $.Traced}}
1075 <a href="?fam={{$.Family}}&b={{$.Bucket}}&exp=1">[Normal/Expanded]</a>
1076 {{else}}
1077 [Normal/Expanded]
1078 {{end}}
1079
1080 {{if not $.Active}}
1081 {{if or $.Expanded (not $.Traced)}}
1082 <a href="?fam={{$.Family}}&b={{$.Bucket}}&rtraced=1">[Traced/Summary]</a>
1083 {{else}}
1084 [Traced/Summary]
1085 {{end}}
1086 {{if or (not $.Expanded) (not $.Traced)}}
1087 <a href="?fam={{$.Family}}&b={{$.Bucket}}&exp=1&rtraced=1">[Traced/Expanded]</a>
1088 {{else}}
1089 [Traced/Expanded]
1090 {{end}}
1091 {{end}}
1092
1093 {{if $.Total}}
1094 <p><em>Showing <b>{{len $.Traces}}</b> of <b>{{$.Total}}</b> traces.</em></p>
1095 {{end}}
1096
1097 <table id="reqs">
1098 <caption>
1099 {{if $.Active}}Active{{else}}Completed{{end}} Requests
1100 </caption>
1101 <tr><th>When</th><th>Elapsed (s)</th></tr>
1102 {{range $tr := $.Traces}}
1103 <tr class="first">
1104 <td class="when">{{$tr.When}}</td>
1105 <td class="elapsed">{{$tr.ElapsedTime}}</td>
1106 <td>{{$tr.Title}}</td>
1107 {{/* TODO: include traceID/spanID */}}
1108 </tr>
1109 {{if $.Expanded}}
1110 {{range $tr.Events}}
1111 <tr>
1112 <td class="when">{{.WhenString}}</td>
1113 <td class="elapsed">{{elapsed .Elapsed}}</td>
1114 <td>{{if or $.ShowSensitive (not .Sensitive)}}... {{.What}}{{else}}<em>[redacted]</em>{{end}}</td>
1115 </tr>
1116 {{end}}
1117 {{end}}
1118 {{end}}
1119 </table>
1120 {{end}} {{/* if $.Traces */}}
1121
1122 {{if $.Histogram}}
1123 <h4>Latency (µs) of {{$.Family}} over {{$.HistogramWindow}}</h4>
1124 {{$.Histogram}}
1125 {{end}} {{/* if $.Histogram */}}
1126
1127 </body>
1128 </html>
1129 {{end}} {{/* end of Epilog */}}
1130 `
1131
View as plain text