blob: 330022c23e70b19987ba3c55cb15f61d6676703e [file] [log] [blame] [edit]
// 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
}