268 lines
7.5 KiB
Go
268 lines
7.5 KiB
Go
// Copyright The OpenTelemetry Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
// 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 "go.opentelemetry.io/contrib/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
|
|
|
|
maxRequestBodySize = 1 << 20 // 1MB
|
|
)
|
|
|
|
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")
|
|
|
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestBodySize)
|
|
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
|
|
}
|