| // Copyright 2017, OpenCensus Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| // |
| |
| package zpages |
| |
| import ( |
| "fmt" |
| "io" |
| "log" |
| "net/http" |
| "sort" |
| "strconv" |
| "strings" |
| "text/tabwriter" |
| "time" |
| |
| "go.opencensus.io/internal" |
| "go.opencensus.io/trace" |
| ) |
| |
| const ( |
| // spanNameQueryField is the header for span name. |
| spanNameQueryField = "zspanname" |
| // spanTypeQueryField is the header for type (running = 0, latency = 1, error = 2) to display. |
| spanTypeQueryField = "ztype" |
| // spanSubtypeQueryField is the header for sub-type: |
| // * for latency based samples [0, 8] representing the latency buckets, where 0 is the first one; |
| // * for error based samples, 0 means all, otherwise the error code; |
| spanSubtypeQueryField = "zsubtype" |
| // maxTraceMessageLength is the maximum length of a message in tracez output. |
| maxTraceMessageLength = 1024 |
| ) |
| |
| var ( |
| defaultLatencies = [...]time.Duration{ |
| 10 * time.Microsecond, |
| 100 * time.Microsecond, |
| time.Millisecond, |
| 10 * time.Millisecond, |
| 100 * time.Millisecond, |
| time.Second, |
| 10 * time.Second, |
| 100 * time.Second, |
| } |
| canonicalCodes = [...]string{ |
| "OK", |
| "CANCELLED", |
| "UNKNOWN", |
| "INVALID_ARGUMENT", |
| "DEADLINE_EXCEEDED", |
| "NOT_FOUND", |
| "ALREADY_EXISTS", |
| "PERMISSION_DENIED", |
| "RESOURCE_EXHAUSTED", |
| "FAILED_PRECONDITION", |
| "ABORTED", |
| "OUT_OF_RANGE", |
| "UNIMPLEMENTED", |
| "INTERNAL", |
| "UNAVAILABLE", |
| "DATA_LOSS", |
| "UNAUTHENTICATED", |
| } |
| ) |
| |
| func canonicalCodeString(code int32) string { |
| if code < 0 || int(code) >= len(canonicalCodes) { |
| return "error code " + strconv.FormatInt(int64(code), 10) |
| } |
| return canonicalCodes[code] |
| } |
| |
| func tracezHandler(w http.ResponseWriter, r *http.Request) { |
| r.ParseForm() |
| w.Header().Set("Content-Type", "text/html; charset=utf-8") |
| name := r.Form.Get(spanNameQueryField) |
| t, _ := strconv.Atoi(r.Form.Get(spanTypeQueryField)) |
| st, _ := strconv.Atoi(r.Form.Get(spanSubtypeQueryField)) |
| WriteHTMLTracezPage(w, name, t, st) |
| } |
| |
| // WriteHTMLTracezPage writes an HTML document to w containing locally-sampled trace spans. |
| func WriteHTMLTracezPage(w io.Writer, spanName string, spanType, spanSubtype int) { |
| if err := headerTemplate.Execute(w, headerData{Title: "Trace Spans"}); err != nil { |
| log.Printf("zpages: executing template: %v", err) |
| } |
| WriteHTMLTracezSummary(w) |
| WriteHTMLTracezSpans(w, spanName, spanType, spanSubtype) |
| if err := footerTemplate.Execute(w, nil); err != nil { |
| log.Printf("zpages: executing template: %v", err) |
| } |
| } |
| |
| // WriteHTMLTracezSummary writes HTML to w containing a summary of locally-sampled trace spans. |
| // |
| // It includes neither a header nor footer, so you can embed this data in other pages. |
| func WriteHTMLTracezSummary(w io.Writer) { |
| if err := summaryTableTemplate.Execute(w, getSummaryPageData()); err != nil { |
| log.Printf("zpages: executing template: %v", err) |
| } |
| } |
| |
| // WriteHTMLTracezSpans writes HTML to w containing locally-sampled trace spans. |
| // |
| // It includes neither a header nor footer, so you can embed this data in other pages. |
| func WriteHTMLTracezSpans(w io.Writer, spanName string, spanType, spanSubtype int) { |
| if spanName == "" { |
| return |
| } |
| if err := tracesTableTemplate.Execute(w, traceDataFromSpans(spanName, traceSpans(spanName, spanType, spanSubtype))); err != nil { |
| log.Printf("zpages: executing template: %v", err) |
| } |
| } |
| |
| // WriteTextTracezSpans writes formatted text to w containing locally-sampled trace spans. |
| func WriteTextTracezSpans(w io.Writer, spanName string, spanType, spanSubtype int) { |
| spans := traceSpans(spanName, spanType, spanSubtype) |
| data := traceDataFromSpans(spanName, spans) |
| writeTextTraces(w, data) |
| } |
| |
| // WriteTextTracezSummary writes formatted text to w containing a summary of locally-sampled trace spans. |
| func WriteTextTracezSummary(w io.Writer) { |
| w.Write([]byte("Locally sampled spans summary\n\n")) |
| |
| data := getSummaryPageData() |
| if len(data.Rows) == 0 { |
| return |
| } |
| |
| tw := tabwriter.NewWriter(w, 8, 8, 1, ' ', 0) |
| |
| for i, s := range data.Header { |
| if i != 0 { |
| tw.Write([]byte("\t")) |
| } |
| tw.Write([]byte(s)) |
| } |
| tw.Write([]byte("\n")) |
| |
| put := func(x int) { |
| if x == 0 { |
| tw.Write([]byte(".\t")) |
| return |
| } |
| fmt.Fprintf(tw, "%d\t", x) |
| } |
| for _, r := range data.Rows { |
| tw.Write([]byte(r.Name)) |
| tw.Write([]byte("\t")) |
| put(r.Active) |
| for _, l := range r.Latency { |
| put(l) |
| } |
| put(r.Errors) |
| tw.Write([]byte("\n")) |
| } |
| tw.Flush() |
| } |
| |
| // traceData contains data for the trace data template. |
| type traceData struct { |
| Name string |
| Num int |
| Rows []traceRow |
| } |
| |
| type traceRow struct { |
| Fields [3]string |
| trace.SpanContext |
| ParentSpanID trace.SpanID |
| } |
| |
| type events []interface{} |
| |
| func (e events) Len() int { return len(e) } |
| func (e events) Less(i, j int) bool { |
| var ti time.Time |
| switch x := e[i].(type) { |
| case *trace.Annotation: |
| ti = x.Time |
| case *trace.MessageEvent: |
| ti = x.Time |
| } |
| switch x := e[j].(type) { |
| case *trace.Annotation: |
| return ti.Before(x.Time) |
| case *trace.MessageEvent: |
| return ti.Before(x.Time) |
| } |
| return false |
| } |
| |
| func (e events) Swap(i, j int) { e[i], e[j] = e[j], e[i] } |
| |
| func traceRows(s *trace.SpanData) []traceRow { |
| start := s.StartTime |
| |
| lasty, lastm, lastd := start.Date() |
| wholeTime := func(t time.Time) string { |
| return t.Format("2006/01/02-15:04:05") + fmt.Sprintf(".%06d", t.Nanosecond()/1000) |
| } |
| formatTime := func(t time.Time) string { |
| y, m, d := t.Date() |
| if y == lasty && m == lastm && d == lastd { |
| return t.Format(" 15:04:05") + fmt.Sprintf(".%06d", t.Nanosecond()/1000) |
| } |
| lasty, lastm, lastd = y, m, d |
| return wholeTime(t) |
| } |
| |
| lastTime := start |
| formatElapsed := func(t time.Time) string { |
| d := t.Sub(lastTime) |
| lastTime = t |
| u := int64(d / 1000) |
| // There are five cases for duration printing: |
| // -1234567890s |
| // -1234.123456 |
| // .123456 |
| // 12345.123456 |
| // 12345678901s |
| switch { |
| case u < -9999999999: |
| return fmt.Sprintf("%11ds", u/1e6) |
| case u < 0: |
| sec := u / 1e6 |
| u -= sec * 1e6 |
| return fmt.Sprintf("%5d.%06d", sec, -u) |
| case u < 1e6: |
| return fmt.Sprintf(" .%6d", u) |
| case u <= 99999999999: |
| sec := u / 1e6 |
| u -= sec * 1e6 |
| return fmt.Sprintf("%5d.%06d", sec, u) |
| default: |
| return fmt.Sprintf("%11ds", u/1e6) |
| } |
| } |
| |
| firstRow := traceRow{Fields: [3]string{wholeTime(start), "", ""}, SpanContext: s.SpanContext, ParentSpanID: s.ParentSpanID} |
| if s.EndTime.IsZero() { |
| firstRow.Fields[1] = " " |
| } else { |
| firstRow.Fields[1] = formatElapsed(s.EndTime) |
| lastTime = start |
| } |
| out := []traceRow{firstRow} |
| |
| formatAttributes := func(a map[string]interface{}) string { |
| if len(a) == 0 { |
| return "" |
| } |
| var keys []string |
| for key := range a { |
| keys = append(keys, key) |
| } |
| sort.Strings(keys) |
| var s []string |
| for _, key := range keys { |
| val := a[key] |
| switch val.(type) { |
| case string: |
| s = append(s, fmt.Sprintf("%s=%q", key, val)) |
| default: |
| s = append(s, fmt.Sprintf("%s=%v", key, val)) |
| } |
| } |
| return "Attributes:{" + strings.Join(s, ", ") + "}" |
| } |
| |
| if s.Status != (trace.Status{}) { |
| msg := fmt.Sprintf("Status{canonicalCode=%s, description=%q}", |
| canonicalCodeString(s.Status.Code), s.Status.Message) |
| out = append(out, traceRow{Fields: [3]string{"", "", msg}}) |
| } |
| |
| if len(s.Attributes) != 0 { |
| out = append(out, traceRow{Fields: [3]string{"", "", formatAttributes(s.Attributes)}}) |
| } |
| |
| var es events |
| for i := range s.Annotations { |
| es = append(es, &s.Annotations[i]) |
| } |
| for i := range s.MessageEvents { |
| es = append(es, &s.MessageEvents[i]) |
| } |
| sort.Sort(es) |
| for _, e := range es { |
| switch e := e.(type) { |
| case *trace.Annotation: |
| msg := e.Message |
| if len(e.Attributes) != 0 { |
| msg = msg + " " + formatAttributes(e.Attributes) |
| } |
| row := traceRow{Fields: [3]string{ |
| formatTime(e.Time), |
| formatElapsed(e.Time), |
| msg, |
| }} |
| out = append(out, row) |
| case *trace.MessageEvent: |
| row := traceRow{Fields: [3]string{formatTime(e.Time), formatElapsed(e.Time)}} |
| switch e.EventType { |
| case trace.MessageEventTypeSent: |
| row.Fields[2] = fmt.Sprintf("sent message [%d bytes, %d compressed bytes]", e.UncompressedByteSize, e.CompressedByteSize) |
| case trace.MessageEventTypeRecv: |
| row.Fields[2] = fmt.Sprintf("received message [%d bytes, %d compressed bytes]", e.UncompressedByteSize, e.CompressedByteSize) |
| } |
| out = append(out, row) |
| } |
| } |
| for i := range out { |
| if len(out[i].Fields[2]) > maxTraceMessageLength { |
| out[i].Fields[2] = out[i].Fields[2][:maxTraceMessageLength] |
| } |
| } |
| return out |
| } |
| |
| func traceSpans(spanName string, spanType, spanSubtype int) []*trace.SpanData { |
| internalTrace := internal.Trace.(interface { |
| ReportActiveSpans(name string) []*trace.SpanData |
| ReportSpansByError(name string, code int32) []*trace.SpanData |
| ReportSpansByLatency(name string, minLatency, maxLatency time.Duration) []*trace.SpanData |
| }) |
| var spans []*trace.SpanData |
| switch spanType { |
| case 0: // active |
| spans = internalTrace.ReportActiveSpans(spanName) |
| case 1: // latency |
| var min, max time.Duration |
| n := len(defaultLatencies) |
| if spanSubtype == 0 { |
| max = defaultLatencies[0] |
| } else if spanSubtype == n { |
| min, max = defaultLatencies[spanSubtype-1], (1<<63)-1 |
| } else if 0 < spanSubtype && spanSubtype < n { |
| min, max = defaultLatencies[spanSubtype-1], defaultLatencies[spanSubtype] |
| } |
| spans = internalTrace.ReportSpansByLatency(spanName, min, max) |
| case 2: // error |
| spans = internalTrace.ReportSpansByError(spanName, 0) |
| } |
| return spans |
| } |
| |
| func traceDataFromSpans(name string, spans []*trace.SpanData) traceData { |
| data := traceData{ |
| Name: name, |
| Num: len(spans), |
| } |
| for _, s := range spans { |
| data.Rows = append(data.Rows, traceRows(s)...) |
| } |
| return data |
| } |
| |
| func writeTextTraces(w io.Writer, data traceData) { |
| tw := tabwriter.NewWriter(w, 1, 8, 1, ' ', 0) |
| fmt.Fprint(tw, "When\tElapsed(s)\tType\n") |
| for _, r := range data.Rows { |
| tw.Write([]byte(r.Fields[0])) |
| tw.Write([]byte("\t")) |
| tw.Write([]byte(r.Fields[1])) |
| tw.Write([]byte("\t")) |
| tw.Write([]byte(r.Fields[2])) |
| if sc := r.SpanContext; sc != (trace.SpanContext{}) { |
| fmt.Fprintf(tw, "trace_id: %s span_id: %s", sc.TraceID, sc.SpanID) |
| if r.ParentSpanID != (trace.SpanID{}) { |
| fmt.Fprintf(tw, " parent_span_id: %s", r.ParentSpanID) |
| } |
| } |
| tw.Write([]byte("\n")) |
| } |
| tw.Flush() |
| } |
| |
| type summaryPageData struct { |
| Header []string |
| LatencyBucketNames []string |
| Links bool |
| TracesEndpoint string |
| Rows []summaryPageRow |
| } |
| |
| type summaryPageRow struct { |
| Name string |
| Active int |
| Latency []int |
| Errors int |
| } |
| |
| func getSummaryPageData() summaryPageData { |
| data := summaryPageData{ |
| Links: true, |
| TracesEndpoint: "tracez", |
| } |
| internalTrace := internal.Trace.(interface { |
| ReportSpansPerMethod() map[string]internal.PerMethodSummary |
| }) |
| for name, s := range internalTrace.ReportSpansPerMethod() { |
| if len(data.Header) == 0 { |
| data.Header = []string{"Name", "Active"} |
| for _, b := range s.LatencyBuckets { |
| l := b.MinLatency |
| s := fmt.Sprintf(">%v", l) |
| if l == 100*time.Second { |
| s = ">100s" |
| } |
| data.Header = append(data.Header, s) |
| data.LatencyBucketNames = append(data.LatencyBucketNames, s) |
| } |
| data.Header = append(data.Header, "Errors") |
| } |
| row := summaryPageRow{Name: name, Active: s.Active} |
| for _, l := range s.LatencyBuckets { |
| row.Latency = append(row.Latency, l.Size) |
| } |
| for _, e := range s.ErrorBuckets { |
| row.Errors += e.Size |
| } |
| data.Rows = append(data.Rows, row) |
| } |
| sort.Slice(data.Rows, func(i, j int) bool { |
| return data.Rows[i].Name < data.Rows[j].Name |
| }) |
| return data |
| } |