diff --git a/Makefile b/Makefile
index e7ef845c988d..6f6fabd3c359 100644
--- a/Makefile
+++ b/Makefile
@@ -1442,7 +1442,7 @@ ui-watch ui-watch-secure: $(UI_CCL_DLLS) pkg/ui/yarn.opt.installed
ui-clean: ## Remove build artifacts.
find pkg/ui/distccl/assets pkg/ui/distoss/assets -mindepth 1 -not -name .gitkeep -delete
rm -rf pkg/ui/assets.ccl.installed pkg/ui/assets.oss.installed
- rm -rf pkg/ui/dist/*
+ rm -rf pkg/ui/dist_vendor/*
rm -f $(UI_PROTOS_CCL) $(UI_PROTOS_OSS)
rm -f pkg/ui/workspaces/db-console/*manifest.json
rm -rf pkg/ui/workspaces/cluster-ui/dist
diff --git a/pkg/server/debug/BUILD.bazel b/pkg/server/debug/BUILD.bazel
index 6f731cd5e7a5..38120156700d 100644
--- a/pkg/server/debug/BUILD.bazel
+++ b/pkg/server/debug/BUILD.bazel
@@ -29,6 +29,8 @@ go_library(
"//pkg/util/log/severity",
"//pkg/util/stop",
"//pkg/util/timeutil",
+ "//pkg/util/tracing",
+ "//pkg/util/tracing/tracingui",
"//pkg/util/uint128",
"@com_github_cockroachdb_errors//:errors",
"@com_github_cockroachdb_pebble//tool",
diff --git a/pkg/server/debug/server.go b/pkg/server/debug/server.go
index 788b99b47b3c..15cdef61d07d 100644
--- a/pkg/server/debug/server.go
+++ b/pkg/server/debug/server.go
@@ -32,6 +32,8 @@ import (
"github.com/cockroachdb/cockroach/pkg/storage"
"github.com/cockroachdb/cockroach/pkg/util/log"
"github.com/cockroachdb/cockroach/pkg/util/stop"
+ "github.com/cockroachdb/cockroach/pkg/util/tracing"
+ "github.com/cockroachdb/cockroach/pkg/util/tracing/tracingui"
"github.com/cockroachdb/errors"
pebbletool "github.com/cockroachdb/pebble/tool"
metrics "github.com/rcrowley/go-metrics"
@@ -61,14 +63,18 @@ var _ = func() *settings.StringSetting {
// Server serves the /debug/* family of tools.
type Server struct {
- st *cluster.Settings
- mux *http.ServeMux
- spy logSpy
+ ambientCtx log.AmbientContext
+ st *cluster.Settings
+ mux *http.ServeMux
+ spy logSpy
}
// NewServer sets up a debug server.
func NewServer(
- st *cluster.Settings, hbaConfDebugFn http.HandlerFunc, profiler pprofui.Profiler,
+ ambientContext log.AmbientContext,
+ st *cluster.Settings,
+ hbaConfDebugFn http.HandlerFunc,
+ profiler pprofui.Profiler,
) *Server {
mux := http.NewServeMux()
@@ -137,9 +143,10 @@ func NewServer(
})
return &Server{
- st: st,
- mux: mux,
- spy: spy,
+ ambientCtx: ambientContext,
+ st: st,
+ mux: mux,
+ spy: spy,
}
}
@@ -232,6 +239,12 @@ func (ds *Server) RegisterClosedTimestampSideTransport(
})
}
+// RegisterTracez registers the /debug/tracez handler, which renders snapshots
+// of active spans.
+func (ds *Server) RegisterTracez(tr *tracing.Tracer) {
+ tracingui.RegisterHTTPHandlers(ds.ambientCtx, ds.mux, tr)
+}
+
// ServeHTTP serves various tools under the /debug endpoint.
func (ds *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
handler, _ := ds.mux.Handler(r)
diff --git a/pkg/server/server.go b/pkg/server/server.go
index b66783293f9c..4203ed2fa1d7 100644
--- a/pkg/server/server.go
+++ b/pkg/server/server.go
@@ -834,7 +834,7 @@ func NewServer(cfg Config, stopper *stop.Stopper) (*Server, error) {
}
sStatus.setStmtDiagnosticsRequester(sqlServer.execCfg.StmtDiagnosticsRecorder)
sStatus.baseStatusServer.sqlServer = sqlServer
- debugServer := debug.NewServer(st, sqlServer.pgServer.HBADebugFn(), sStatus)
+ debugServer := debug.NewServer(cfg.BaseConfig.AmbientCtx, st, sqlServer.pgServer.HBADebugFn(), sStatus)
node.InitLogger(sqlServer.execCfg)
*lateBoundServer = Server{
@@ -1958,6 +1958,7 @@ func (s *Server) PreStart(ctx context.Context) error {
return errors.Wrapf(err, "failed to register engines with debug server")
}
s.debug.RegisterClosedTimestampSideTransport(s.ctSender, s.node.storeCfg.ClosedTimestampReceiver)
+ s.debug.RegisterTracez(s.cfg.Tracer)
s.ctSender.Run(ctx, state.nodeID)
diff --git a/pkg/server/tenant.go b/pkg/server/tenant.go
index c74b18a3913b..bd7c9a2809bc 100644
--- a/pkg/server/tenant.go
+++ b/pkg/server/tenant.go
@@ -211,7 +211,7 @@ func StartTenant(
)
mux := http.NewServeMux()
- debugServer := debug.NewServer(args.Settings, s.pgServer.HBADebugFn(), s.execCfg.SQLStatusServer)
+ debugServer := debug.NewServer(baseCfg.AmbientCtx, args.Settings, s.pgServer.HBADebugFn(), s.execCfg.SQLStatusServer)
mux.Handle("/", debugServer)
mux.Handle("/_status/", gwMux)
mux.HandleFunc("/health", func(w http.ResponseWriter, req *http.Request) {
diff --git a/pkg/ui/.gitignore b/pkg/ui/.gitignore
index 0d5500eded18..5900689c9692 100644
--- a/pkg/ui/.gitignore
+++ b/pkg/ui/.gitignore
@@ -14,6 +14,8 @@ assets.oss.installed
# Generated intermediates
.cache-loader
dist*/**
+dist_vendor/**
+!dist_vendor/.gitkeep
!dist*/_empty_assets/assets/index.html
!distccl/assets/.gitkeep
!distccl/distccl.go
diff --git a/pkg/ui/BUILD.bazel b/pkg/ui/BUILD.bazel
index 862ffdf0aed6..fb5de323c399 100644
--- a/pkg/ui/BUILD.bazel
+++ b/pkg/ui/BUILD.bazel
@@ -1,8 +1,17 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
+load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary")
go_library(
name = "ui",
- srcs = ["ui.go"],
+ srcs = [
+ "tracing_ui.go",
+ "ui.go",
+ ],
+ embedsrcs = [
+ "dist_vendor/list.min.js",
+ "templates/tracing/html_template.html",
+ "dist_vendor/.gitkeep",
+ ],
importpath = "github.com/cockroachdb/cockroach/pkg/ui",
visibility = ["//visibility:public"],
deps = [
@@ -35,3 +44,11 @@ EOF
""",
visibility = ["//pkg/ui:__subpackages__"],
)
+
+genrule(
+ name = "listjs",
+ srcs = ["@npm//:node_modules/list.js/dist/list.min.js"],
+ outs = ["dist_vendor/list.min.js"],
+ cmd = "cp ./$(location @npm//:node_modules/list.js/dist/list.min.js) $@",
+ tools = ["@npm//list.js"],
+)
diff --git a/pkg/ui/dist_vendor/.gitkeep b/pkg/ui/dist_vendor/.gitkeep
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/pkg/ui/templates/tracing/html_template.html b/pkg/ui/templates/tracing/html_template.html
new file mode 100644
index 000000000000..f66b7a4d6ad0
--- /dev/null
+++ b/pkg/ui/templates/tracing/html_template.html
@@ -0,0 +1,140 @@
+
+
+
+
+
+
+
+
+
+Take a snapshot of current operations
+
+Stored snapshots (ID: capture time):
+{{$id := .SnapshotID}}
+{{range $i, $s := .AllSnapshots}}
+
+ {{if eq $s.ID $id}}
+ [current] {{$s.ID}}: {{formatTimeNoMillis .CapturedAt}}
+ {{else}}
+ {{$s.ID}}: {{formatTimeNoMillis .CapturedAt}}
+ {{end}}
+
+{{end}}
+
+
+Spans currently open: {{len .SpansList.Spans}}. Snapshot captured at: {{formatTime .CapturedAt}} UTC. Page generated at: {{formatTime .Now}} UTC.
+{{if ne .Err nil}}
+There was an error producing this snapshot; it might be incomplete: {{.Err}}
+{{end}}
+
+
+
+
+
diff --git a/pkg/ui/tracing_ui.go b/pkg/ui/tracing_ui.go
new file mode 100644
index 000000000000..57a912dac5b8
--- /dev/null
+++ b/pkg/ui/tracing_ui.go
@@ -0,0 +1,36 @@
+// Copyright 2021 The Cockroach Authors.
+//
+// Use of this software is governed by the Business Source License
+// included in the file licenses/BSL.txt.
+//
+// As of the Change Date specified in that file, in accordance with
+// the Business Source License, use of this software will be governed
+// by the Apache License, Version 2.0, included in the file
+// licenses/APL.txt.
+
+package ui
+
+import (
+ "embed"
+ "io/fs"
+)
+
+// This file deals with embedding assets used by /debug/tracez.
+
+//go:embed dist_vendor/*
+var vendorFiles embed.FS
+
+// VendorFS exposes the list.js package.
+var VendorFS fs.FS
+
+//go:embed templates/tracing/html_template.html
+// SpansTableTemplateSrc contains a template used by /debug/tracez
+var SpansTableTemplateSrc string
+
+func init() {
+ var err error
+ VendorFS, err = fs.Sub(vendorFiles, "dist_vendor")
+ if err != nil {
+ panic(err)
+ }
+}
diff --git a/pkg/ui/workspaces/db-console/BUILD.bazel b/pkg/ui/workspaces/db-console/BUILD.bazel
index 75b99f44106d..494064a475c6 100644
--- a/pkg/ui/workspaces/db-console/BUILD.bazel
+++ b/pkg/ui/workspaces/db-console/BUILD.bazel
@@ -130,6 +130,7 @@ DEPENDENCIES = [
"@npm//highlight.js",
"@npm//lodash",
"@npm//long",
+ "@npm//list.js",
"@npm//mini-create-react-context",
"@npm//moment",
"@npm//nvd3",
diff --git a/pkg/ui/workspaces/db-console/package.json b/pkg/ui/workspaces/db-console/package.json
index 1d1f0d06e2af..600d687f10b7 100644
--- a/pkg/ui/workspaces/db-console/package.json
+++ b/pkg/ui/workspaces/db-console/package.json
@@ -29,6 +29,7 @@
"connected-react-router": "^6.9.1",
"create-react-context": "^0.3.0",
"highlight.js": "^10.6.0",
+ "list.js": "^2.3.1",
"lodash": "^4.17.21",
"long": "^4.0.0",
"mini-create-react-context": "^0.3.2",
diff --git a/pkg/ui/workspaces/db-console/src/views/reports/containers/debug/index.tsx b/pkg/ui/workspaces/db-console/src/views/reports/containers/debug/index.tsx
index 4b827609d132..6bcd38d4dc89 100644
--- a/pkg/ui/workspaces/db-console/src/views/reports/containers/debug/index.tsx
+++ b/pkg/ui/workspaces/db-console/src/views/reports/containers/debug/index.tsx
@@ -330,6 +330,7 @@ export default function Debug() {
+
{
new RemoveBrokenDependenciesPlugin(),
new CopyWebpackPlugin([
{ from: path.resolve(__dirname, "favicon.ico"), to: "favicon.ico" },
+ {
+ from: path.resolve(
+ !isBazelBuild ? __dirname : "",
+ !isBazelBuild ? "../.." : "",
+ "node_modules/list.js/dist/list.min.js",
+ ),
+ to: path.resolve(__dirname, "../../dist_vendor/list.min.js"),
+ },
]),
// use WebpackBar instead of webpack dashboard to fit multiple webpack dev server outputs (db-console and cluster-ui)
new WebpackBar({
diff --git a/pkg/ui/yarn-vendor b/pkg/ui/yarn-vendor
index b7cef07d6748..dfceabccc717 160000
--- a/pkg/ui/yarn-vendor
+++ b/pkg/ui/yarn-vendor
@@ -1 +1 @@
-Subproject commit b7cef07d6748b5af824fb1c1af90cc17f3009c56
+Subproject commit dfceabccc717aff312f17423089652e9e697355b
diff --git a/pkg/ui/yarn.lock b/pkg/ui/yarn.lock
index 0295930e2b57..c0198cdf4cb7 100644
--- a/pkg/ui/yarn.lock
+++ b/pkg/ui/yarn.lock
@@ -11461,6 +11461,13 @@ linkify-it@^2.0.0:
dependencies:
uc.micro "^1.0.1"
+list.js@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/list.js/-/list.js-2.3.1.tgz#48961989ffe52b0505e352f7a521f819f51df7e7"
+ integrity sha512-jnmm7DYpKtH3DxtO1E2VNCC9Gp7Wrp/FWA2JxQrZUhVJ2RCQBd57pCN6W5w6jpsfWZV0PCAbTX2NOPgyFeeZZg==
+ dependencies:
+ string-natural-compare "^2.0.2"
+
listr-silent-renderer@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e"
@@ -16399,6 +16406,11 @@ string-length@^3.1.0:
astral-regex "^1.0.0"
strip-ansi "^5.2.0"
+string-natural-compare@^2.0.2:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-2.0.3.tgz#9dbe1dd65490a5fe14f7a5c9bc686fc67cb9c6e4"
+ integrity sha512-4Kcl12rNjc+6EKhY8QyDVuQTAlMWwRiNbsxnVwBUKFr7dYPQuXVrtNU4sEkjF9LHY0AY6uVbB3ktbkIH4LC+BQ==
+
string-replace-webpack-plugin@^0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/string-replace-webpack-plugin/-/string-replace-webpack-plugin-0.1.3.tgz#73c657e759d66cfe80ae1e0cf091aa256d0e715c"
diff --git a/pkg/util/tracing/BUILD.bazel b/pkg/util/tracing/BUILD.bazel
index fa4c8a23bc83..3210ab249ec2 100644
--- a/pkg/util/tracing/BUILD.bazel
+++ b/pkg/util/tracing/BUILD.bazel
@@ -14,6 +14,7 @@ go_library(
"tags.go",
"test_utils.go",
"tracer.go",
+ "tracer_snapshots.go",
"utils.go",
],
importpath = "github.com/cockroachdb/cockroach/pkg/util/tracing",
diff --git a/pkg/util/tracing/tracer.go b/pkg/util/tracing/tracer.go
index 5634f71f7159..91021a7ad676 100644
--- a/pkg/util/tracing/tracer.go
+++ b/pkg/util/tracing/tracer.go
@@ -28,6 +28,7 @@ import (
"github.com/cockroachdb/cockroach/pkg/util/iterutil"
"github.com/cockroachdb/cockroach/pkg/util/netutil/addr"
"github.com/cockroachdb/cockroach/pkg/util/randutil"
+ "github.com/cockroachdb/cockroach/pkg/util/ring"
"github.com/cockroachdb/cockroach/pkg/util/syncutil"
"github.com/cockroachdb/cockroach/pkg/util/timeutil"
"github.com/cockroachdb/cockroach/pkg/util/tracing/tracingpb"
@@ -59,6 +60,9 @@ const (
// maxLogsPerSpanExternal limits the number of logs in a Span for external
// tracers (net/trace, OpenTelemetry); use a comfortable limit.
maxLogsPerSpanExternal = 1000
+ // maxSnapshots limits the number of snapshots that a Tracer will hold in
+ // memory. Beyond this limit, each new snapshot evicts the oldest one.
+ maxSnapshots = 10
)
// These constants are used to form keys to represent tracing context
@@ -239,7 +243,14 @@ type Tracer struct {
// activeSpans is a map that references all non-Finish'ed local root spans,
// i.e. those for which no WithParent() option was supplied.
- activeSpansRegistry *spanRegistry
+ activeSpansRegistry *SpanRegistry
+ snapshotsMu struct {
+ syncutil.Mutex
+ // snapshots stores the activeSpansRegistry snapshots taken during the
+ // Tracer's lifetime. The ring buffer will contain snapshots with contiguous
+ // IDs, from the oldest one to + maxSnapshots - 1.
+ snapshots ring.Buffer // snapshotWithID
+ }
testingMu syncutil.Mutex // protects testingRecordAsyncSpans
testingRecordAsyncSpans bool // see TestingRecordAsyncSpans
@@ -263,7 +274,7 @@ type Tracer struct {
stack string
}
-// spanRegistry is a map that references all non-Finish'ed local root spans,
+// SpanRegistry is a map that references all non-Finish'ed local root spans,
// i.e. those for which no WithLocalParent() option was supplied. The
// map is keyed on the span ID, which is deterministically unique.
//
@@ -272,30 +283,30 @@ type Tracer struct {
//
// The map can be introspected by `Tracer.VisitSpans`. A Span can also be
// retrieved from its ID by `Tracer.GetActiveSpanByID`.
-type spanRegistry struct {
+type SpanRegistry struct {
mu struct {
syncutil.Mutex
m map[tracingpb.SpanID]*crdbSpan
}
}
-func makeSpanRegistry() *spanRegistry {
- r := &spanRegistry{}
+func makeSpanRegistry() *SpanRegistry {
+ r := &SpanRegistry{}
r.mu.m = make(map[tracingpb.SpanID]*crdbSpan)
return r
}
-func (r *spanRegistry) removeSpanLocked(id tracingpb.SpanID) {
+func (r *SpanRegistry) removeSpanLocked(id tracingpb.SpanID) {
delete(r.mu.m, id)
}
-func (r *spanRegistry) addSpan(s *crdbSpan) {
+func (r *SpanRegistry) addSpan(s *crdbSpan) {
r.mu.Lock()
defer r.mu.Unlock()
r.addSpanLocked(s)
}
-func (r *spanRegistry) addSpanLocked(s *crdbSpan) {
+func (r *SpanRegistry) addSpanLocked(s *crdbSpan) {
// Ensure that the registry does not grow unboundedly in case there is a leak.
// When the registry reaches max size, each new span added kicks out an
// arbitrary existing span. We rely on map iteration order here to make this
@@ -310,7 +321,7 @@ func (r *spanRegistry) addSpanLocked(s *crdbSpan) {
}
// getSpanByID looks up a span in the registry. Returns nil if not found.
-func (r *spanRegistry) getSpanByID(id tracingpb.SpanID) RegistrySpan {
+func (r *SpanRegistry) getSpanByID(id tracingpb.SpanID) RegistrySpan {
r.mu.Lock()
defer r.mu.Unlock()
crdbSpan, ok := r.mu.m[id]
@@ -321,7 +332,10 @@ func (r *spanRegistry) getSpanByID(id tracingpb.SpanID) RegistrySpan {
return crdbSpan
}
-func (r *spanRegistry) visitSpans(visitor func(span RegistrySpan) error) error {
+// VisitSpans calls the visitor callback for every local root span in the
+// registry. Iterations stops when the visitor returns an error. If that error
+// is iterutils.StopIteration(), then VisitSpans() returns nil.
+func (r *SpanRegistry) VisitSpans(visitor func(span RegistrySpan) error) error {
r.mu.Lock()
sl := make([]*crdbSpan, 0, len(r.mu.m))
for _, sp := range r.mu.m {
@@ -343,7 +357,7 @@ func (r *spanRegistry) visitSpans(visitor func(span RegistrySpan) error) error {
// testingAll returns (pointers to) all the spans in the registry, in an
// arbitrary order. Since spans can generally finish at any point and use of a
// finished span is not permitted, this method is only suitable for tests.
-func (r *spanRegistry) testingAll() []*crdbSpan {
+func (r *SpanRegistry) testingAll() []*crdbSpan {
r.mu.Lock()
defer r.mu.Unlock()
res := make([]*crdbSpan, 0, len(r.mu.m))
@@ -358,7 +372,7 @@ func (r *spanRegistry) testingAll() []*crdbSpan {
// removing the parent from the registry, the children are accessible in the
// registry through that parent; if we didn't do this swap when the parent is
// removed, the children would not be part of the registry anymore.
-func (r *spanRegistry) swap(parentID tracingpb.SpanID, children []*crdbSpan) {
+func (r *SpanRegistry) swap(parentID tracingpb.SpanID, children []*crdbSpan) {
r.mu.Lock()
defer r.mu.Unlock()
r.removeSpanLocked(parentID)
@@ -1138,7 +1152,7 @@ func (t *Tracer) GetActiveSpanByID(spanID tracingpb.SpanID) RegistrySpan {
// VisitSpans invokes the visitor with all active Spans. The function will
// gracefully exit if the visitor returns iterutil.StopIteration().
func (t *Tracer) VisitSpans(visitor func(span RegistrySpan) error) error {
- return t.activeSpansRegistry.visitSpans(visitor)
+ return t.activeSpansRegistry.VisitSpans(visitor)
}
// TestingRecordAsyncSpans is a test-only helper that configures
@@ -1179,6 +1193,11 @@ func (t *Tracer) PanicOnUseAfterFinish() bool {
return t.panicOnUseAfterFinish
}
+// SpanRegistry exports the registry containing all currently-open spans.
+func (t *Tracer) SpanRegistry() *SpanRegistry {
+ return t.activeSpansRegistry
+}
+
// ForkSpan forks the current span, if any[1]. Forked spans "follow from" the
// original, and are typically used to trace operations that may outlive the
// parent (think async tasks). See the package-level documentation for more
diff --git a/pkg/util/tracing/tracer_snapshots.go b/pkg/util/tracing/tracer_snapshots.go
new file mode 100644
index 000000000000..8f4ce784202f
--- /dev/null
+++ b/pkg/util/tracing/tracer_snapshots.go
@@ -0,0 +1,195 @@
+// Copyright 2021 The Cockroach Authors.
+//
+// Use of this software is governed by the Business Source License
+// included in the file licenses/BSL.txt.
+//
+// As of the Change Date specified in that file, in accordance with
+// the Business Source License, use of this software will be governed
+// by the Apache License, Version 2.0, included in the file
+// licenses/APL.txt.
+
+package tracing
+
+import (
+ "bufio"
+ "runtime"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/cockroachdb/cockroach/pkg/util/timeutil"
+ "github.com/cockroachdb/errors"
+)
+
+// SpansSnapshot represents a snapshot of all the open spans at a certain point
+// in time.
+type SpansSnapshot struct {
+ // CapturedAt is the time when the snapshot was collected.
+ CapturedAt time.Time
+ // Traces contains the collected traces. Each "local route" corresponds to one
+ // trace. "Detached recording" spans are included in the parent's trace.
+ Traces []Recording
+ // Stacks is a map from groutine ID to the goroutine's stack trace. All
+ // goroutines running at the time when this snapshot is produced are
+ // represented here. A goroutine referenced by a span's GoroutineID field will
+ // not be present if it has finished since the respective span was started.
+ Stacks map[int]string
+ // Err is set if an error was encountered when producing the snapshot. The
+ // snapshot will be incomplete in this case.
+ Err error
+}
+
+// SnapshotID identifies a spans snapshot. The ID can be used to retrieve a
+// specific snapshot from the Tracer.
+type SnapshotID int
+type snapshotWithID struct {
+ ID SnapshotID
+ SpansSnapshot
+}
+
+// SaveSnapshot collects a snapshot of the currently open spans (i.e. the
+// contents of the Tracer's active spans registry) saves it in-memory in the
+// Tracer. The snapshot's ID is returned; the snapshot can be retrieved
+// subsequently through t.GetSnapshot(id).
+//
+// Snapshots also include a dump of all the goroutine stack traces.
+func (t *Tracer) SaveSnapshot() SnapshotID {
+ snap := t.generateSnapshot()
+ t.snapshotsMu.Lock()
+ defer t.snapshotsMu.Unlock()
+
+ snapshots := &t.snapshotsMu.snapshots
+
+ if snapshots.Len() == maxSnapshots {
+ snapshots.RemoveFirst()
+ }
+ var id SnapshotID
+ if snapshots.Len() == 0 {
+ id = 1
+ } else {
+ id = snapshots.GetLast().(snapshotWithID).ID + 1
+ }
+ snapshots.AddLast(snapshotWithID{
+ ID: id,
+ SpansSnapshot: snap,
+ })
+ return id
+}
+
+var errSnapshotTooOld = errors.Newf(
+ "the requested snapshot is too old and has been deleted. "+
+ "Only the last %d snapshots are stored.", maxSnapshots)
+
+var errSnapshotDoesntExist = errors.New("the requested snapshot doesn't exist")
+
+// GetSnapshot returns the snapshot with the given ID. If the ID is below the
+// minimum stored snapshot, then the requested snapshot must have been
+// garbage-collected and errSnapshotTooOld is returned. If the snapshot id is
+// beyond the maximum stored ID, errSnapshotDoesntExist is returned.
+//
+// Note that SpansSpanshot has an Err field through which errors are returned.
+// In these error cases, the snapshot will be incomplete.
+func (t *Tracer) GetSnapshot(id SnapshotID) (SpansSnapshot, error) {
+ t.snapshotsMu.Lock()
+ defer t.snapshotsMu.Unlock()
+ snapshots := &t.snapshotsMu.snapshots
+
+ if snapshots.Len() == 0 {
+ return SpansSnapshot{}, errSnapshotDoesntExist
+ }
+ minID := snapshots.GetFirst().(snapshotWithID).ID
+ if id < minID {
+ return SpansSnapshot{}, errSnapshotTooOld
+ }
+ maxID := snapshots.GetLast().(snapshotWithID).ID
+ if id > maxID {
+ return SpansSnapshot{}, errSnapshotDoesntExist
+ }
+
+ return snapshots.Get(int(id - minID)).(snapshotWithID).SpansSnapshot, nil
+}
+
+// SnapshotInfo represents minimal info about a stored snapshot, as returned by
+// Tracer.GetSnapshots().
+type SnapshotInfo struct {
+ ID SnapshotID
+ CapturedAt time.Time
+}
+
+// GetSnapshots returns info on all stored span snapshots.
+func (t *Tracer) GetSnapshots() []SnapshotInfo {
+ t.snapshotsMu.Lock()
+ defer t.snapshotsMu.Unlock()
+ snapshots := &t.snapshotsMu.snapshots
+
+ res := make([]SnapshotInfo, snapshots.Len())
+ for i := 0; i < snapshots.Len(); i++ {
+ s := snapshots.Get(i).(snapshotWithID)
+ res[i] = SnapshotInfo{
+ ID: s.ID,
+ CapturedAt: s.CapturedAt,
+ }
+ }
+ return res
+}
+
+// generateSnapshot produces a snapshot of all the currently open spans and
+// all the current goroutine stacktraces.
+//
+// Note that SpansSpanshot has an Err field through which errors are returned.
+// In these error cases, the snapshot will be incomplete.
+func (t *Tracer) generateSnapshot() SpansSnapshot {
+ capturedAt := timeutil.Now()
+ // Collect the traces.
+ traces := make([]Recording, 0, 1000)
+ _ = t.SpanRegistry().VisitSpans(func(sp RegistrySpan) error {
+ rec := sp.GetFullRecording(RecordingVerbose)
+ traces = append(traces, rec)
+ return nil
+ })
+
+ // Collect and parse the goroutine stack traces.
+
+ // We don't know how big the traces are, so grow a few times if they don't
+ // fit. Start large, though.
+ var stacks []byte
+ for n := 1 << 20; /* 1mb */ n <= (1 << 29); /* 512mb */ n *= 2 {
+ stacks = make([]byte, n)
+ nbytes := runtime.Stack(stacks, true /* all */)
+ if nbytes < len(stacks) {
+ break
+ }
+ }
+
+ splits := strings.Split(string(stacks), "\n\n")
+ stackMap := make(map[int]string, len(splits))
+ var parseErr error
+ for _, s := range splits {
+ // Parse the goroutine ID. The first line of each stack is expected to look like:
+ // goroutine 115 [chan receive]:
+ scanner := bufio.NewScanner(strings.NewReader(s))
+ scanner.Split(bufio.ScanWords)
+ // Skip the word "goroutine".
+ if !scanner.Scan() {
+ parseErr = errors.Errorf("unexpected end of string")
+ break
+ }
+ // Scan the goroutine ID.
+ if !scanner.Scan() {
+ parseErr = errors.Errorf("unexpected end of string")
+ break
+ }
+ goroutineID, err := strconv.Atoi(scanner.Text())
+ if err != nil {
+ panic(err)
+ }
+ stackMap[goroutineID] = s
+ }
+
+ return SpansSnapshot{
+ CapturedAt: capturedAt,
+ Traces: traces,
+ Stacks: stackMap,
+ Err: parseErr,
+ }
+}
diff --git a/pkg/util/tracing/tracingpb/recorded_span.go b/pkg/util/tracing/tracingpb/recorded_span.go
index b398962c941d..2bb4862a5104 100644
--- a/pkg/util/tracing/tracingpb/recorded_span.go
+++ b/pkg/util/tracing/tracingpb/recorded_span.go
@@ -25,6 +25,25 @@ type TraceID uint64
// SpanID is a probabilistically-unique span id.
type SpanID uint64
+// Recording represents a group of RecordedSpans rooted at a fixed root span, as
+// returned by GetRecording. Spans are sorted by StartTime.
+type Recording []RecordedSpan
+
+// Less implements sort.Interface.
+func (r Recording) Less(i, j int) bool {
+ return r[i].StartTime.Before(r[j].StartTime)
+}
+
+// Swap implements sort.Interface.
+func (r Recording) Swap(i, j int) {
+ r[i], r[j] = r[j], r[i]
+}
+
+// Len implements sort.Interface.
+func (r Recording) Len() int {
+ return len(r)
+}
+
// LogMessageField is the field name used for the log message in a LogRecord.
const LogMessageField = "event"
diff --git a/pkg/util/tracing/tracingui/BUILD.bazel b/pkg/util/tracing/tracingui/BUILD.bazel
new file mode 100644
index 000000000000..b225899b887e
--- /dev/null
+++ b/pkg/util/tracing/tracingui/BUILD.bazel
@@ -0,0 +1,17 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "tracingui",
+ srcs = ["span_registry_ui.go"],
+ importpath = "github.com/cockroachdb/cockroach/pkg/util/tracing/tracingui",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//pkg/ui",
+ "//pkg/util/log",
+ "//pkg/util/sysutil",
+ "//pkg/util/timeutil",
+ "//pkg/util/tracing",
+ "//pkg/util/tracing/tracingpb",
+ "@com_github_cockroachdb_errors//:errors",
+ ],
+)
diff --git a/pkg/util/tracing/tracingui/span_registry_ui.go b/pkg/util/tracing/tracingui/span_registry_ui.go
new file mode 100644
index 000000000000..95e1298533ce
--- /dev/null
+++ b/pkg/util/tracing/tracingui/span_registry_ui.go
@@ -0,0 +1,277 @@
+// Copyright 2021 The Cockroach Authors.
+//
+// Use of this software is governed by the Business Source License
+// included in the file licenses/BSL.txt.
+//
+// As of the Change Date specified in that file, in accordance with
+// the Business Source License, use of this software will be governed
+// by the Apache License, Version 2.0, included in the file
+// licenses/APL.txt.
+
+package tracingui
+
+import (
+ "context"
+ "fmt"
+ "html/template"
+ "net/http"
+ "net/url"
+ "strconv"
+ "time"
+
+ "github.com/cockroachdb/cockroach/pkg/ui"
+ "github.com/cockroachdb/cockroach/pkg/util/log"
+ "github.com/cockroachdb/cockroach/pkg/util/sysutil"
+ "github.com/cockroachdb/cockroach/pkg/util/timeutil"
+ "github.com/cockroachdb/cockroach/pkg/util/tracing"
+ "github.com/cockroachdb/cockroach/pkg/util/tracing/tracingpb"
+ "github.com/cockroachdb/errors"
+)
+
+// This file deals with producing the /debug/tracez page, which lists
+// a snapshot of the spans in the Tracer's active spans registry.
+
+const dateAndtimeFormat = "2006-01-02 15:04:05.000"
+const timeFormat = "15:04:05.000"
+
+// RegisterHTTPHandlers registers the /debug/tracez handlers, and helpers.
+func RegisterHTTPHandlers(ambientCtx log.AmbientContext, mux *http.ServeMux, tr *tracing.Tracer) {
+ fileServer := http.StripPrefix("/debug/assets/", http.FileServer(http.FS(ui.VendorFS)))
+ mux.HandleFunc("/debug/tracez",
+ func(w http.ResponseWriter, req *http.Request) {
+ serveHTTP(ambientCtx.AnnotateCtx(context.Background()), w, req, tr)
+ })
+ mux.HandleFunc("/debug/show-trace",
+ func(w http.ResponseWriter, req *http.Request) {
+ serveHTTPTrace(ambientCtx.AnnotateCtx(req.Context()), w, req, tr)
+ })
+ mux.HandleFunc("/debug/assets/list.min.js", fileServer.ServeHTTP)
+}
+
+type pageData struct {
+ SnapshotID tracing.SnapshotID
+ Now time.Time
+ CapturedAt time.Time
+ Err error
+ AllSnapshots []tracing.SnapshotInfo
+ SpansList spansList
+}
+
+type spansList struct {
+ Spans []tracingpb.RecordedSpan
+ // Stacks contains stack traces for the goroutines referenced by the Spans
+ // through their GoroutineID field.
+ Stacks map[int]string // GoroutineID to stack trace
+}
+
+var spansTableTemplate *template.Template
+
+func init() {
+ spansTableTemplate = template.Must(template.New("spans-list").Funcs(
+ template.FuncMap{
+ "formatTime": formatTime,
+ "formatTimeNoMillis": formatTimeNoMillis,
+ "since": func(t time.Time, capturedAt time.Time) string {
+ return fmt.Sprintf("(%s ago)", formatDuration(capturedAt.Sub(t)))
+ },
+ "timeRaw": func(t time.Time) int64 { return t.UnixMicro() },
+ },
+ ).Parse(ui.SpansTableTemplateSrc))
+}
+
+func formatTime(t time.Time) string {
+ t = t.UTC()
+ if t.Truncate(24*time.Hour) == timeutil.Now().Truncate(24*time.Hour) {
+ return t.Format(timeFormat)
+ }
+ return t.Format(dateAndtimeFormat)
+}
+
+func formatTimeNoMillis(t time.Time) string {
+ t = t.UTC()
+ if t.Truncate(24*time.Hour) == timeutil.Now().Truncate(24*time.Hour) {
+ const format = "15:04:05"
+ return t.Format(format)
+ }
+ const format = "2006-01-02 15:04:05"
+ return t.Format(format)
+}
+
+// formatDuration formats a duration in one of the following formats, depending
+// on its magnitude.
+// 0.001s
+// 1.000s
+// 1m01s
+// 1h05m01s
+// 1d02h05m
+func formatDuration(d time.Duration) string {
+ d = d.Round(time.Millisecond)
+ days := d / (24 * time.Hour)
+ d -= days * 24 * time.Hour
+ h := d / time.Hour
+ d -= h * time.Hour
+ m := d / time.Minute
+ d -= m * time.Minute
+ s := d / time.Second
+ d -= s * time.Second
+ millis := d / time.Millisecond
+ if days != 0 {
+ return fmt.Sprintf("%dd%02dh%02dm", days, h, m)
+ }
+ if h != 0 {
+ return fmt.Sprintf("%dh%02dm%02ds", h, m, s)
+ }
+ if m != 0 {
+ return fmt.Sprintf("%dm%02ds", m, s)
+ }
+ return fmt.Sprintf("%d.%03ds", s, millis)
+}
+
+func serveHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request, tr *tracing.Tracer) {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ var pageErr error
+ if err := r.ParseForm(); err != nil {
+ log.Warningf(ctx, "error parsing /tracez form: %s", err.Error())
+ pageErr = err
+ }
+
+ snapID := r.Form.Get("snap")
+ var snapshot tracing.SpansSnapshot
+ var snapshotID tracing.SnapshotID
+ switch snapID {
+ case "new":
+ // Capture a new snapshot and return a redirect to that snapshot's ID.
+ snapshotID = tr.SaveSnapshot()
+ newURL, err := r.URL.Parse(fmt.Sprintf("?snap=%d", snapshotID))
+ if err != nil {
+ pageErr = err
+ break
+ }
+ http.Redirect(w, r, newURL.String(), http.StatusFound)
+ return
+ case "":
+ default:
+ id, err := strconv.Atoi(snapID)
+ if err != nil {
+ pageErr = errors.Errorf("invalid snapshot ID: %s", snapID)
+ break
+ }
+ snapshotID = tracing.SnapshotID(id)
+ snapshot, err = tr.GetSnapshot(snapshotID)
+ if err != nil {
+ pageErr = err
+ break
+ }
+ }
+
+ // Flatten the recordings.
+ spans := make([]tracingpb.RecordedSpan, 0, len(snapshot.Traces)*3)
+ for _, r := range snapshot.Traces {
+ spans = append(spans, r...)
+ }
+
+ // Copy the stack traces and augment the map.
+ stacks := make(map[int]string, len(snapshot.Stacks))
+ for k, v := range snapshot.Stacks {
+ stacks[k] = v
+ }
+ // Fill in messages for the goroutines for which we don't have a stack trace.
+ for _, s := range spans {
+ gid := int(s.GoroutineID)
+ if _, ok := stacks[gid]; !ok {
+ stacks[gid] = "Goroutine not found. Goroutine must have finished since the span was created."
+ }
+ }
+
+ if pageErr != nil {
+ snapshot.Err = pageErr
+ }
+ err := spansTableTemplate.ExecuteTemplate(w, "spans-list", pageData{
+ Now: timeutil.Now(),
+ CapturedAt: snapshot.CapturedAt,
+ SnapshotID: snapshotID,
+ AllSnapshots: tr.GetSnapshots(),
+ Err: snapshot.Err,
+ SpansList: spansList{
+ Spans: spans,
+ Stacks: stacks,
+ },
+ })
+ if err != nil {
+ // We can get a "connection reset by peer" error if the browser requesting
+ // the page has gone away.
+ if !sysutil.IsErrConnectionReset(err) {
+ log.Warningf(ctx, "error executing tracez template: %s", err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
+ }
+}
+
+func serveHTTPTrace(
+ ctx context.Context, w http.ResponseWriter, r *http.Request, tr *tracing.Tracer,
+) {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ var err error
+ if err = r.ParseForm(); err != nil {
+ goto Error
+ }
+
+ {
+ var traceID tracingpb.TraceID
+ tid := r.Form.Get("trace")
+ if tid == "" {
+ err = errors.Errorf("trace ID missing; use ?trace=")
+ goto Error
+ }
+ var id int
+ id, err = strconv.Atoi(tid)
+ if err != nil {
+ err = errors.Errorf("invalid trace ID: %s", tid)
+ goto Error
+ }
+ traceID = tracingpb.TraceID(id)
+
+ var snapshotID tracing.SnapshotID
+ var snapshot tracing.SpansSnapshot
+ snapID := r.Form.Get("snap")
+ if snapID != "" {
+ var id int
+ id, err = strconv.Atoi(snapID)
+ if err != nil {
+ err = errors.Errorf("invalid snapshot ID: %s", snapID)
+ goto Error
+ }
+ snapshotID = tracing.SnapshotID(id)
+ snapshot, err = tr.GetSnapshot(snapshotID)
+ if err != nil {
+ goto Error
+ }
+ } else {
+ // If no snapshot is specified, we'll take a new one now and redirect to
+ // it.
+ snapshotID = tr.SaveSnapshot()
+ var newURL *url.URL
+ newURL, err = r.URL.Parse(fmt.Sprintf("?trace=%d&snap=%d", traceID, snapshotID))
+ if err != nil {
+ goto Error
+ }
+ http.Redirect(w, r, newURL.String(), http.StatusFound)
+ return
+ }
+
+ for _, r := range snapshot.Traces {
+ if r[0].TraceID == traceID {
+ _, err = w.Write([]byte("" + r.String() + "\n
"))
+ if err != nil {
+ goto Error
+ }
+ return
+ }
+ }
+ err = errors.Errorf("trace %d not found in snapshot", traceID)
+ goto Error
+ }
+
+Error:
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+}