opentelemetry-go-contrib/zpages/tracez.go

262 lines
7.3 KiB
Go

// Copyright The OpenTelemetry Authors
// 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"
"log"
"net/http"
"sort"
"strconv"
"strings"
"time"
"go.opentelemetry.io/otel/attribute"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/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"
// spanLatencyBucketQueryField is the header for latency based samples.
// Default is [0, 8] representing the latency buckets, where 0 is the first one.
spanLatencyBucketQueryField = "zlatencybucket"
// maxTraceMessageLength is the maximum length of a message in tracez output.
maxTraceMessageLength = 1024
)
type summaryTableData struct {
Header []string
LatencyBucketNames []string
Links bool
TracesEndpoint string
Rows []summaryTableRowData
}
type summaryTableRowData struct {
Name string
Active int
Latency []int
Errors int
}
// traceTableData contains data for the trace data template.
type traceTableData struct {
Name string
Num int
Rows []spanRow
}
var _ http.Handler = (*tracezHandler)(nil)
type tracezHandler struct {
sp *SpanProcessor
}
// NewTracezHandler returns an http.Handler that can be used to serve HTTP requests for trace zpages.
func NewTracezHandler(sp *SpanProcessor) http.Handler {
return &tracezHandler{sp: sp}
}
// ServeHTTP implements the http.Handler and is capable of serving "tracez" HTTP requests.
func (th *tracezHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := r.ParseForm(); err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
spanName := r.Form.Get(spanNameQueryField)
spanType, _ := strconv.Atoi(r.Form.Get(spanTypeQueryField))
spanSubtype, _ := strconv.Atoi(r.Form.Get(spanLatencyBucketQueryField))
if err := headerTemplate.Execute(w, headerData{Title: "Trace Spans"}); err != nil {
log.Printf("zpages: executing template: %v", err)
}
if err := summaryTableTemplate.Execute(w, th.getSummaryTableData()); err != nil {
log.Printf("zpages: executing template: %v", err)
}
if spanName != "" {
if err := tracesTableTemplate.Execute(w, th.getTraceTableData(spanName, spanType, spanSubtype)); err != nil {
log.Printf("zpages: executing template: %v", err)
}
}
if err := footerTemplate.Execute(w, nil); err != nil {
log.Printf("zpages: executing template: %v", err)
}
}
func (th *tracezHandler) getTraceTableData(spanName string, spanType, latencyBucket int) traceTableData {
var spans []sdktrace.ReadOnlySpan
switch spanType {
case 0: // active
spans = th.sp.activeSpans(spanName)
case 1: // latency
spans = th.sp.spansByLatency(spanName, latencyBucket)
case 2: // error
spans = th.sp.errorSpans(spanName)
}
data := traceTableData{
Name: spanName,
Num: len(spans),
}
for _, s := range spans {
data.Rows = append(data.Rows, spanRows(s)...)
}
return data
}
func (th *tracezHandler) getSummaryTableData() summaryTableData {
data := summaryTableData{
Links: true,
TracesEndpoint: "tracez",
}
data.Header = []string{"Name", "active"}
// An implicit 0 lower bound latency bucket is always present.
latencyBuckets := append([]time.Duration{0}, defaultBoundaries.durations...)
for _, l := range latencyBuckets {
s := fmt.Sprintf(">%v", l)
data.Header = append(data.Header, s)
data.LatencyBucketNames = append(data.LatencyBucketNames, s)
}
data.Header = append(data.Header, "Errors")
for name, s := range th.sp.spansPerMethod() {
row := summaryTableRowData{Name: name, Active: s.activeSpans, Errors: s.errorSpans, Latency: s.latencySpans}
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
}
type spanRow struct {
Fields [3]string
trace.SpanContext
ParentSpanContext trace.SpanContext
}
type events []sdktrace.Event
func (e events) Len() int { return len(e) }
func (e events) Less(i, j int) bool {
return e[i].Time.Before(e[j].Time)
}
func (e events) Swap(i, j int) { e[i], e[j] = e[j], e[i] }
type attributes []attribute.KeyValue
func (e attributes) Len() int { return len(e) }
func (e attributes) Less(i, j int) bool {
return string(e[i].Key) < string(e[j].Key)
}
func (e attributes) Swap(i, j int) { e[i], e[j] = e[j], e[i] }
func spanRows(s sdktrace.ReadOnlySpan) []spanRow {
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 := spanRow{Fields: [3]string{wholeTime(start), "", ""}, SpanContext: s.SpanContext(), ParentSpanContext: s.Parent()}
if s.EndTime().IsZero() {
firstRow.Fields[1] = " "
} else {
firstRow.Fields[1] = formatElapsed(s.EndTime())
lastTime = start
}
out := []spanRow{firstRow}
formatAttributes := func(a attributes) string {
sort.Sort(a)
var s []string
for i := range a {
s = append(s, fmt.Sprintf("%s=%v", a[i].Key, a[i].Value.Emit()))
}
return "Attributes:{" + strings.Join(s, ", ") + "}"
}
msg := fmt.Sprintf("Status{Code=%s, description=%q}", s.Status().Code.String(), s.Status().Description)
out = append(out, spanRow{Fields: [3]string{"", "", msg}})
if len(s.Attributes()) != 0 {
out = append(out, spanRow{Fields: [3]string{"", "", formatAttributes(s.Attributes())}})
}
es := events(s.Events())
sort.Sort(es)
for _, e := range es {
msg := e.Name
if len(e.Attributes) != 0 {
msg = msg + " " + formatAttributes(e.Attributes)
}
row := spanRow{Fields: [3]string{
formatTime(e.Time),
formatElapsed(e.Time),
msg,
}}
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
}