1// Copyright 2017, OpenCensus Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//
15
16package zpages
17
18import (
19	"fmt"
20	"io"
21	"log"
22	"net/http"
23	"sort"
24	"strconv"
25	"strings"
26	"text/tabwriter"
27	"time"
28
29	"go.opencensus.io/internal"
30	"go.opencensus.io/trace"
31)
32
33const (
34	// spanNameQueryField is the header for span name.
35	spanNameQueryField = "zspanname"
36	// spanTypeQueryField is the header for type (running = 0, latency = 1, error = 2) to display.
37	spanTypeQueryField = "ztype"
38	// spanSubtypeQueryField is the header for sub-type:
39	// * for latency based samples [0, 8] representing the latency buckets, where 0 is the first one;
40	// * for error based samples, 0 means all, otherwise the error code;
41	spanSubtypeQueryField = "zsubtype"
42	// maxTraceMessageLength is the maximum length of a message in tracez output.
43	maxTraceMessageLength = 1024
44)
45
46var (
47	defaultLatencies = [...]time.Duration{
48		10 * time.Microsecond,
49		100 * time.Microsecond,
50		time.Millisecond,
51		10 * time.Millisecond,
52		100 * time.Millisecond,
53		time.Second,
54		10 * time.Second,
55		100 * time.Second,
56	}
57	canonicalCodes = [...]string{
58		"OK",
59		"CANCELLED",
60		"UNKNOWN",
61		"INVALID_ARGUMENT",
62		"DEADLINE_EXCEEDED",
63		"NOT_FOUND",
64		"ALREADY_EXISTS",
65		"PERMISSION_DENIED",
66		"RESOURCE_EXHAUSTED",
67		"FAILED_PRECONDITION",
68		"ABORTED",
69		"OUT_OF_RANGE",
70		"UNIMPLEMENTED",
71		"INTERNAL",
72		"UNAVAILABLE",
73		"DATA_LOSS",
74		"UNAUTHENTICATED",
75	}
76)
77
78func canonicalCodeString(code int32) string {
79	if code < 0 || int(code) >= len(canonicalCodes) {
80		return "error code " + strconv.FormatInt(int64(code), 10)
81	}
82	return canonicalCodes[code]
83}
84
85func tracezHandler(w http.ResponseWriter, r *http.Request) {
86	r.ParseForm()
87	w.Header().Set("Content-Type", "text/html; charset=utf-8")
88	name := r.Form.Get(spanNameQueryField)
89	t, _ := strconv.Atoi(r.Form.Get(spanTypeQueryField))
90	st, _ := strconv.Atoi(r.Form.Get(spanSubtypeQueryField))
91	WriteHTMLTracezPage(w, name, t, st)
92}
93
94// WriteHTMLTracezPage writes an HTML document to w containing locally-sampled trace spans.
95func WriteHTMLTracezPage(w io.Writer, spanName string, spanType, spanSubtype int) {
96	if err := headerTemplate.Execute(w, headerData{Title: "Trace Spans"}); err != nil {
97		log.Printf("zpages: executing template: %v", err)
98	}
99	WriteHTMLTracezSummary(w)
100	WriteHTMLTracezSpans(w, spanName, spanType, spanSubtype)
101	if err := footerTemplate.Execute(w, nil); err != nil {
102		log.Printf("zpages: executing template: %v", err)
103	}
104}
105
106// WriteHTMLTracezSummary writes HTML to w containing a summary of locally-sampled trace spans.
107//
108// It includes neither a header nor footer, so you can embed this data in other pages.
109func WriteHTMLTracezSummary(w io.Writer) {
110	if err := summaryTableTemplate.Execute(w, getSummaryPageData()); err != nil {
111		log.Printf("zpages: executing template: %v", err)
112	}
113}
114
115// WriteHTMLTracezSpans writes HTML to w containing locally-sampled trace spans.
116//
117// It includes neither a header nor footer, so you can embed this data in other pages.
118func WriteHTMLTracezSpans(w io.Writer, spanName string, spanType, spanSubtype int) {
119	if spanName == "" {
120		return
121	}
122	if err := tracesTableTemplate.Execute(w, traceDataFromSpans(spanName, traceSpans(spanName, spanType, spanSubtype))); err != nil {
123		log.Printf("zpages: executing template: %v", err)
124	}
125}
126
127// WriteTextTracezSpans writes formatted text to w containing locally-sampled trace spans.
128func WriteTextTracezSpans(w io.Writer, spanName string, spanType, spanSubtype int) {
129	spans := traceSpans(spanName, spanType, spanSubtype)
130	data := traceDataFromSpans(spanName, spans)
131	writeTextTraces(w, data)
132}
133
134// WriteTextTracezSummary writes formatted text to w containing a summary of locally-sampled trace spans.
135func WriteTextTracezSummary(w io.Writer) {
136	w.Write([]byte("Locally sampled spans summary\n\n"))
137
138	data := getSummaryPageData()
139	if len(data.Rows) == 0 {
140		return
141	}
142
143	tw := tabwriter.NewWriter(w, 8, 8, 1, ' ', 0)
144
145	for i, s := range data.Header {
146		if i != 0 {
147			tw.Write([]byte("\t"))
148		}
149		tw.Write([]byte(s))
150	}
151	tw.Write([]byte("\n"))
152
153	put := func(x int) {
154		if x == 0 {
155			tw.Write([]byte(".\t"))
156			return
157		}
158		fmt.Fprintf(tw, "%d\t", x)
159	}
160	for _, r := range data.Rows {
161		tw.Write([]byte(r.Name))
162		tw.Write([]byte("\t"))
163		put(r.Active)
164		for _, l := range r.Latency {
165			put(l)
166		}
167		put(r.Errors)
168		tw.Write([]byte("\n"))
169	}
170	tw.Flush()
171}
172
173// traceData contains data for the trace data template.
174type traceData struct {
175	Name string
176	Num  int
177	Rows []traceRow
178}
179
180type traceRow struct {
181	Fields [3]string
182	trace.SpanContext
183	ParentSpanID trace.SpanID
184}
185
186type events []interface{}
187
188func (e events) Len() int { return len(e) }
189func (e events) Less(i, j int) bool {
190	var ti time.Time
191	switch x := e[i].(type) {
192	case *trace.Annotation:
193		ti = x.Time
194	case *trace.MessageEvent:
195		ti = x.Time
196	}
197	switch x := e[j].(type) {
198	case *trace.Annotation:
199		return ti.Before(x.Time)
200	case *trace.MessageEvent:
201		return ti.Before(x.Time)
202	}
203	return false
204}
205
206func (e events) Swap(i, j int) { e[i], e[j] = e[j], e[i] }
207
208func traceRows(s *trace.SpanData) []traceRow {
209	start := s.StartTime
210
211	lasty, lastm, lastd := start.Date()
212	wholeTime := func(t time.Time) string {
213		return t.Format("2006/01/02-15:04:05") + fmt.Sprintf(".%06d", t.Nanosecond()/1000)
214	}
215	formatTime := func(t time.Time) string {
216		y, m, d := t.Date()
217		if y == lasty && m == lastm && d == lastd {
218			return t.Format("           15:04:05") + fmt.Sprintf(".%06d", t.Nanosecond()/1000)
219		}
220		lasty, lastm, lastd = y, m, d
221		return wholeTime(t)
222	}
223
224	lastTime := start
225	formatElapsed := func(t time.Time) string {
226		d := t.Sub(lastTime)
227		lastTime = t
228		u := int64(d / 1000)
229		// There are five cases for duration printing:
230		// -1234567890s
231		// -1234.123456
232		//      .123456
233		// 12345.123456
234		// 12345678901s
235		switch {
236		case u < -9999999999:
237			return fmt.Sprintf("%11ds", u/1e6)
238		case u < 0:
239			sec := u / 1e6
240			u -= sec * 1e6
241			return fmt.Sprintf("%5d.%06d", sec, -u)
242		case u < 1e6:
243			return fmt.Sprintf("     .%6d", u)
244		case u <= 99999999999:
245			sec := u / 1e6
246			u -= sec * 1e6
247			return fmt.Sprintf("%5d.%06d", sec, u)
248		default:
249			return fmt.Sprintf("%11ds", u/1e6)
250		}
251	}
252
253	firstRow := traceRow{Fields: [3]string{wholeTime(start), "", ""}, SpanContext: s.SpanContext, ParentSpanID: s.ParentSpanID}
254	if s.EndTime.IsZero() {
255		firstRow.Fields[1] = "            "
256	} else {
257		firstRow.Fields[1] = formatElapsed(s.EndTime)
258		lastTime = start
259	}
260	out := []traceRow{firstRow}
261
262	formatAttributes := func(a map[string]interface{}) string {
263		if len(a) == 0 {
264			return ""
265		}
266		var keys []string
267		for key := range a {
268			keys = append(keys, key)
269		}
270		sort.Strings(keys)
271		var s []string
272		for _, key := range keys {
273			val := a[key]
274			switch val.(type) {
275			case string:
276				s = append(s, fmt.Sprintf("%s=%q", key, val))
277			default:
278				s = append(s, fmt.Sprintf("%s=%v", key, val))
279			}
280		}
281		return "Attributes:{" + strings.Join(s, ", ") + "}"
282	}
283
284	if s.Status != (trace.Status{}) {
285		msg := fmt.Sprintf("Status{canonicalCode=%s, description=%q}",
286			canonicalCodeString(s.Status.Code), s.Status.Message)
287		out = append(out, traceRow{Fields: [3]string{"", "", msg}})
288	}
289
290	if len(s.Attributes) != 0 {
291		out = append(out, traceRow{Fields: [3]string{"", "", formatAttributes(s.Attributes)}})
292	}
293
294	var es events
295	for i := range s.Annotations {
296		es = append(es, &s.Annotations[i])
297	}
298	for i := range s.MessageEvents {
299		es = append(es, &s.MessageEvents[i])
300	}
301	sort.Sort(es)
302	for _, e := range es {
303		switch e := e.(type) {
304		case *trace.Annotation:
305			msg := e.Message
306			if len(e.Attributes) != 0 {
307				msg = msg + "  " + formatAttributes(e.Attributes)
308			}
309			row := traceRow{Fields: [3]string{
310				formatTime(e.Time),
311				formatElapsed(e.Time),
312				msg,
313			}}
314			out = append(out, row)
315		case *trace.MessageEvent:
316			row := traceRow{Fields: [3]string{formatTime(e.Time), formatElapsed(e.Time)}}
317			switch e.EventType {
318			case trace.MessageEventTypeSent:
319				row.Fields[2] = fmt.Sprintf("sent message [%d bytes, %d compressed bytes]", e.UncompressedByteSize, e.CompressedByteSize)
320			case trace.MessageEventTypeRecv:
321				row.Fields[2] = fmt.Sprintf("received message [%d bytes, %d compressed bytes]", e.UncompressedByteSize, e.CompressedByteSize)
322			}
323			out = append(out, row)
324		}
325	}
326	for i := range out {
327		if len(out[i].Fields[2]) > maxTraceMessageLength {
328			out[i].Fields[2] = out[i].Fields[2][:maxTraceMessageLength]
329		}
330	}
331	return out
332}
333
334func traceSpans(spanName string, spanType, spanSubtype int) []*trace.SpanData {
335	internalTrace := internal.Trace.(interface {
336		ReportActiveSpans(name string) []*trace.SpanData
337		ReportSpansByError(name string, code int32) []*trace.SpanData
338		ReportSpansByLatency(name string, minLatency, maxLatency time.Duration) []*trace.SpanData
339	})
340	var spans []*trace.SpanData
341	switch spanType {
342	case 0: // active
343		spans = internalTrace.ReportActiveSpans(spanName)
344	case 1: // latency
345		var min, max time.Duration
346		n := len(defaultLatencies)
347		if spanSubtype == 0 {
348			max = defaultLatencies[0]
349		} else if spanSubtype == n {
350			min, max = defaultLatencies[spanSubtype-1], (1<<63)-1
351		} else if 0 < spanSubtype && spanSubtype < n {
352			min, max = defaultLatencies[spanSubtype-1], defaultLatencies[spanSubtype]
353		}
354		spans = internalTrace.ReportSpansByLatency(spanName, min, max)
355	case 2: // error
356		spans = internalTrace.ReportSpansByError(spanName, 0)
357	}
358	return spans
359}
360
361func traceDataFromSpans(name string, spans []*trace.SpanData) traceData {
362	data := traceData{
363		Name: name,
364		Num:  len(spans),
365	}
366	for _, s := range spans {
367		data.Rows = append(data.Rows, traceRows(s)...)
368	}
369	return data
370}
371
372func writeTextTraces(w io.Writer, data traceData) {
373	tw := tabwriter.NewWriter(w, 1, 8, 1, ' ', 0)
374	fmt.Fprint(tw, "When\tElapsed(s)\tType\n")
375	for _, r := range data.Rows {
376		tw.Write([]byte(r.Fields[0]))
377		tw.Write([]byte("\t"))
378		tw.Write([]byte(r.Fields[1]))
379		tw.Write([]byte("\t"))
380		tw.Write([]byte(r.Fields[2]))
381		if sc := r.SpanContext; sc != (trace.SpanContext{}) {
382			fmt.Fprintf(tw, "trace_id: %s span_id: %s", sc.TraceID, sc.SpanID)
383			if r.ParentSpanID != (trace.SpanID{}) {
384				fmt.Fprintf(tw, " parent_span_id: %s", r.ParentSpanID)
385			}
386		}
387		tw.Write([]byte("\n"))
388	}
389	tw.Flush()
390}
391
392type summaryPageData struct {
393	Header             []string
394	LatencyBucketNames []string
395	Links              bool
396	TracesEndpoint     string
397	Rows               []summaryPageRow
398}
399
400type summaryPageRow struct {
401	Name    string
402	Active  int
403	Latency []int
404	Errors  int
405}
406
407func getSummaryPageData() summaryPageData {
408	data := summaryPageData{
409		Links:          true,
410		TracesEndpoint: "tracez",
411	}
412	internalTrace := internal.Trace.(interface {
413		ReportSpansPerMethod() map[string]internal.PerMethodSummary
414	})
415	for name, s := range internalTrace.ReportSpansPerMethod() {
416		if len(data.Header) == 0 {
417			data.Header = []string{"Name", "Active"}
418			for _, b := range s.LatencyBuckets {
419				l := b.MinLatency
420				s := fmt.Sprintf(">%v", l)
421				if l == 100*time.Second {
422					s = ">100s"
423				}
424				data.Header = append(data.Header, s)
425				data.LatencyBucketNames = append(data.LatencyBucketNames, s)
426			}
427			data.Header = append(data.Header, "Errors")
428		}
429		row := summaryPageRow{Name: name, Active: s.Active}
430		for _, l := range s.LatencyBuckets {
431			row.Latency = append(row.Latency, l.Size)
432		}
433		for _, e := range s.ErrorBuckets {
434			row.Errors += e.Size
435		}
436		data.Rows = append(data.Rows, row)
437	}
438	sort.Slice(data.Rows, func(i, j int) bool {
439		return data.Rows[i].Name < data.Rows[j].Name
440	})
441	return data
442}
443