| // Copyright 2015 The Vanadium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Package browseserver provides a web interface that can be used to interact with the |
| // vanadium debug interface. |
| package browseserver |
| |
| import ( |
| "bytes" |
| "fmt" |
| "html" |
| "html/template" |
| "io/ioutil" |
| "net" |
| "net/http" |
| "net/url" |
| "os/exec" |
| "path/filepath" |
| "runtime" |
| "sort" |
| "strings" |
| "time" |
| |
| "v.io/v23" |
| "v.io/v23/context" |
| "v.io/v23/naming" |
| "v.io/v23/security" |
| "v.io/v23/services/logreader" |
| "v.io/v23/services/stats" |
| svtrace "v.io/v23/services/vtrace" |
| "v.io/v23/uniqueid" |
| "v.io/v23/verror" |
| "v.io/v23/vom" |
| "v.io/v23/vtrace" |
| "v.io/x/ref/services/debug/debug/browseserver/sbtree" |
| "v.io/x/ref/services/internal/pproflib" |
| ) |
| |
| //go:generate ./gen_assets.sh |
| |
| const browseProfilesPath = "/profiles" |
| |
| const ( |
| allTraceTmpl = "alltrace.html" |
| blessingsTmpl = "blessings.html" |
| chromeTmpl = "chrome.html" |
| globTmpl = "glob.html" |
| logsTmpl = "logs.html" |
| profilesTmpl = "profiles.html" |
| resolveTmpl = "resolve.html" |
| statsTmpl = "stats.html" |
| vtraceTmpl = "vtrace.html" |
| syncbaseTmpl = "syncbase.html" |
| noSyncbaseTmpl = "no_syncbase.html" |
| collectionTmpl = "collection.html" |
| ) |
| |
| // Serve serves the debug interface over http. An HTTP server is started (serving at httpAddr), its |
| // various handlers make rpc calls to the given name to gather debug information. If log is true |
| // we additionally log debug information for these rpc requests. Timeout defines the timeout for the |
| // rpc calls. The HTTPServer will run until the passed context is canceled. |
| func Serve(ctx *context.T, httpAddr, name string, timeout time.Duration, log bool, assetDir string) error { |
| mux, err := CreateServeMux(ctx, timeout, log, assetDir, "") |
| if err != nil { |
| return err |
| } |
| if len(httpAddr) == 0 { |
| httpAddr = "127.0.0.1:0" |
| } |
| ln, err := net.Listen("tcp", httpAddr) |
| if err != nil { |
| return err |
| } |
| go func() { |
| url := "http://" + ln.Addr().String() + "/?n=" + url.QueryEscape(name) |
| fmt.Printf("Visit %s and Ctrl-C to quit\n", url) |
| // Open the browser if we can |
| switch runtime.GOOS { |
| case "linux": |
| exec.Command("xdg-open", url).Start() |
| case "darwin": |
| exec.Command("open", url).Start() |
| } |
| |
| <-ctx.Done() |
| ln.Close() |
| }() |
| return http.Serve(ln, mux) |
| } |
| |
| // CreateServeMux returns a ServeMux object that has handlers set up. |
| func CreateServeMux(ctx *context.T, timeout time.Duration, log bool, assetDir, urlPrefix string) (*http.ServeMux, error) { |
| mux := http.NewServeMux() |
| h, err := newHandler(ctx, timeout, log, assetDir, urlPrefix) |
| if err != nil { |
| return nil, err |
| } |
| mux.Handle("/", &resolveHandler{h}) |
| mux.Handle("/name", &resolveHandler{h}) |
| mux.Handle("/stats", &statsHandler{h}) |
| mux.Handle("/blessings", &blessingsHandler{h}) |
| mux.Handle("/logs", &logsHandler{h}) |
| mux.Handle("/glob", &globHandler{h}) |
| mux.Handle(browseProfilesPath, &profilesHandler{h}) |
| mux.Handle(browseProfilesPath+"/", &profilesHandler{h}) |
| mux.Handle("/vtraces", &allTracesHandler{h}) |
| mux.Handle("/vtrace", &vtraceHandler{h}) |
| mux.Handle("/syncbase", &syncbaseHandler{h}) |
| mux.Handle("/syncbase/collection", &collectionHandler{h}) |
| mux.Handle("/favicon.ico", http.NotFoundHandler()) |
| |
| return mux, nil |
| } |
| |
| type handler struct { |
| ctx *context.T |
| timeout time.Duration |
| log bool |
| urlPrefix string |
| file func(name string) ([]byte, error) |
| cacheMap map[string]*template.Template |
| funcs template.FuncMap |
| } |
| |
| func newHandler(ctx *context.T, timeout time.Duration, log bool, assetDir, urlPrefix string) (*handler, error) { |
| h := &handler{ |
| ctx: ctx, |
| timeout: timeout, |
| log: log, |
| urlPrefix: urlPrefix, |
| } |
| colors := []string{"red", "blue", "green"} |
| pos := 0 |
| h.funcs = template.FuncMap{ |
| "verrorID": verror.ErrorID, |
| "unmarshalPublicKey": security.UnmarshalPublicKey, |
| "endpoint": func(n string) (naming.Endpoint, error) { |
| if naming.Rooted(n) { |
| n, _ = naming.SplitAddressName(n) |
| } |
| return naming.ParseEndpoint(n) |
| }, |
| "endpointName": func(ep naming.Endpoint) string { return ep.Name() }, |
| "goValueFromVOM": func(v *vom.RawBytes) interface{} { |
| var ret interface{} |
| if err := v.ToValue(&ret); err != nil { |
| panic(err) |
| } |
| return ret |
| }, |
| "nextColor": func() string { |
| c := colors[pos] |
| pos = (pos + 1) % len(colors) |
| return c |
| }, |
| } |
| |
| if assetDir == "" { |
| h.file = Asset |
| h.cacheMap = make(map[string]*template.Template) |
| all := []string{chromeTmpl, allTraceTmpl, blessingsTmpl, globTmpl, |
| logsTmpl, profilesTmpl, resolveTmpl, statsTmpl, vtraceTmpl, |
| syncbaseTmpl, noSyncbaseTmpl} |
| for _, tmpl := range all { |
| if _, err := h.template(ctx, tmpl); err != nil { |
| return nil, err |
| } |
| } |
| } else { |
| h.file = func(name string) ([]byte, error) { |
| return ioutil.ReadFile(filepath.Join(assetDir, name)) |
| } |
| } |
| return h, nil |
| } |
| |
| func (h *handler) template(ctx *context.T, name string) (t *template.Template, err error) { |
| if t = h.cacheMap[name]; t != nil { |
| return t, nil |
| } |
| if name == chromeTmpl { |
| t = template.New(name).Funcs(h.funcs) |
| } else { |
| if t, err = h.template(ctx, chromeTmpl); err != nil { |
| return nil, err |
| } |
| if t, err = t.Clone(); err != nil { |
| return nil, err |
| } |
| } |
| src, err := h.file(name) |
| if err != nil { |
| return nil, err |
| } |
| if _, err = t.Parse(string(src)); err != nil { |
| return nil, err |
| } |
| return t, nil |
| } |
| |
| func (h *handler) execute(ctx *context.T, w http.ResponseWriter, r *http.Request, tmplName string, args interface{}) { |
| if h.log { |
| ctx.Infof("DEBUG: %q -- %+v", r.URL, args) |
| } |
| t, err := h.template(ctx, tmplName) |
| if err != nil { |
| fmt.Fprintf(w, "ERROR:%v", err) |
| ctx.Errorf("Error parsing template %q: %v", tmplName, err) |
| return |
| } |
| if err := t.Execute(w, args); err != nil { |
| fmt.Fprintf(w, "ERROR:%v", err) |
| ctx.Errorf("Error executing template %q: %v", tmplName, err) |
| return |
| } |
| } |
| |
| func (h *handler) withTimeout(ctx *context.T) *context.T { |
| ctx, _ = context.WithTimeout(ctx, h.timeout) |
| return ctx |
| } |
| |
| type resolveHandler struct{ *handler } |
| |
| func (h *resolveHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| name := r.FormValue("n") |
| var suffix string |
| ctx, tracer := newTracer(h.ctx) |
| m, err := v23.GetNamespace(ctx).Resolve(h.withTimeout(ctx), name) |
| if m != nil { |
| suffix = m.Name |
| } |
| args := struct { |
| ServerName string |
| CommandLine string |
| Vtrace *Tracer |
| MountEntry *naming.MountEntry |
| Error error |
| }{ |
| ServerName: strings.TrimSuffix(name, suffix), |
| CommandLine: fmt.Sprintf("debug resolve %q", name), |
| Vtrace: tracer, |
| MountEntry: m, |
| Error: err, |
| } |
| h.execute(h.ctx, w, r, "resolve.html", args) |
| } |
| |
| type blessingsHandler struct{ *handler } |
| |
| func (h *blessingsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| name := r.FormValue("n") |
| ctx, tracer := newTracer(h.ctx) |
| call, err := v23.GetClient(ctx).StartCall(h.withTimeout(ctx), name, "DoNotReallyCareAboutTheMethod", nil) |
| args := struct { |
| ServerName string |
| CommandLine string |
| Vtrace *Tracer |
| Error error |
| Blessings security.Blessings |
| Recognized []string |
| CertificateChains [][]security.Certificate |
| }{ |
| ServerName: name, |
| Vtrace: tracer, |
| CommandLine: fmt.Sprintf("vrpc identify %q", name), |
| Error: err, |
| } |
| if call != nil { |
| args.Recognized, args.Blessings = call.RemoteBlessings() |
| args.CertificateChains = security.MarshalBlessings(args.Blessings).CertificateChains |
| // Don't actually care about the RPC, so don't bother waiting on the Finish. |
| defer func() { go call.Finish() }() |
| } |
| h.execute(h.ctx, w, r, "blessings.html", args) |
| } |
| |
| type statsHandler struct{ *handler } |
| |
| func (h *statsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| var ( |
| server = r.FormValue("n") |
| stat = r.FormValue("s") |
| prefix = naming.Join(server, "__debug", "stats") |
| name = naming.Join(prefix, stat) |
| ctx, tracer = newTracer(h.ctx) |
| hasValue = true |
| ) |
| v, err := stats.StatsClient(name).Value(h.withTimeout(ctx)) |
| if verror.ErrorID(err) == verror.ErrNoExist.ID { |
| // Stat does not exist as a value, that's okay. |
| err = nil |
| hasValue = false |
| } |
| var children []string |
| var childrenErrors []error |
| if glob, globErr := v23.GetNamespace(ctx).Glob(h.withTimeout(ctx), naming.Join(name, "*")); globErr == nil { |
| for e := range glob { |
| switch e := e.(type) { |
| case *naming.GlobReplyEntry: |
| children = append(children, strings.TrimPrefix(e.Value.Name, prefix)) |
| case *naming.GlobReplyError: |
| childrenErrors = append(childrenErrors, e.Value.Error) |
| } |
| } |
| if len(children) == 1 && !hasValue { |
| // Single child, save an extra click |
| redirect, err := url.Parse(r.URL.String()) |
| if err == nil { |
| q := redirect.Query() |
| q.Set("n", server) |
| q.Set("s", children[0]) |
| redirect.RawQuery = q.Encode() |
| ctx.Infof("Redirecting from %v to %v", r.URL, redirect) |
| redirectString := redirect.String() |
| if h.urlPrefix != "" { |
| redirectString = fmt.Sprintf("%s/%s", h.urlPrefix, redirectString) |
| } |
| http.Redirect(w, r, redirectString, http.StatusTemporaryRedirect) |
| return |
| } |
| } |
| } else if err == nil { |
| err = globErr |
| } |
| args := struct { |
| ServerName string |
| CommandLine string |
| Vtrace *Tracer |
| StatName string |
| Value *vom.RawBytes |
| Children []string |
| ChildrenErrors []error |
| Globbed bool |
| Error error |
| }{ |
| ServerName: server, |
| Vtrace: tracer, |
| StatName: stat, |
| Value: v, |
| Error: err, |
| Children: children, |
| ChildrenErrors: childrenErrors, |
| Globbed: len(children)+len(childrenErrors) > 0, |
| } |
| if hasValue { |
| args.CommandLine = fmt.Sprintf("debug stats watch %q", name) |
| } else { |
| args.CommandLine = fmt.Sprintf("debug glob %q", naming.Join(name, "*")) |
| } |
| h.execute(h.ctx, w, r, "stats.html", args) |
| } |
| |
| type logsHandler struct{ *handler } |
| |
| func (h *logsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| var ( |
| server = r.FormValue("n") |
| log = r.FormValue("l") |
| prefix = naming.Join(server, "__debug", "logs") |
| name = naming.Join(prefix, log) |
| path = r.URL.Path |
| list = func() bool { |
| for _, a := range r.Header[http.CanonicalHeaderKey("Accept")] { |
| if a == "text/event-stream" { |
| return true |
| } |
| } |
| return false |
| }() |
| ctx, _ = newTracer(h.ctx) |
| ) |
| // The logs handler streams result to the web browser because there |
| // have been cases where there are ~1 million log files, so doing this |
| // streaming thing will make the UI more responsive. |
| // |
| // For the same reason, avoid setting a timeout. |
| if len(log) == 0 && list { |
| w.Header().Add("Content-Type", "text/event-stream") |
| glob, err := v23.GetNamespace(ctx).Glob(ctx, naming.Join(name, "*")) |
| if err != nil { |
| writeErrorEvent(w, err) |
| return |
| } |
| flusher, _ := w.(http.Flusher) |
| for e := range glob { |
| switch e := e.(type) { |
| case *naming.GlobReplyEntry: |
| logfile := strings.TrimPrefix(e.Value.Name, prefix+"/") |
| writeEvent(w, fmt.Sprintf( |
| `<a href="%s?n=%s&l=%s">%s</a>`, |
| path, |
| url.QueryEscape(server), |
| url.QueryEscape(logfile), |
| html.EscapeString(logfile))) |
| case *naming.GlobReplyError: |
| writeErrorEvent(w, e.Value.Error) |
| } |
| if flusher != nil { |
| flusher.Flush() |
| } |
| } |
| return |
| } |
| if len(log) == 0 { |
| args := struct { |
| ServerName string |
| CommandLine string |
| Vtrace *Tracer |
| }{ |
| ServerName: server, |
| CommandLine: fmt.Sprintf("debug glob %q", naming.Join(name, "*")), |
| } |
| h.execute(h.ctx, w, r, "logs.html", args) |
| return |
| } |
| w.Header().Add("Content-Type", "text/plain") |
| stream, err := logreader.LogFileClient(name).ReadLog(ctx, 0, logreader.AllEntries, true) |
| if err != nil { |
| fmt.Fprintf(w, "ERROR(%v): %v\n", verror.ErrorID(err), err) |
| return |
| } |
| var ( |
| entries = make(chan logreader.LogEntry) |
| abortRPC = make(chan bool) |
| abortHTTP <-chan bool |
| errch = make(chan error, 1) // At most one write on this channel, avoid blocking any goroutines |
| ) |
| if notifier, ok := w.(http.CloseNotifier); ok { |
| abortHTTP = notifier.CloseNotify() |
| } |
| go func() { |
| // writes to: entries, errch |
| // reads from: abortRPC |
| defer stream.Finish() |
| defer close(entries) |
| iterator := stream.RecvStream() |
| for iterator.Advance() { |
| select { |
| case entries <- iterator.Value(): |
| case <-abortRPC: |
| return |
| } |
| } |
| if err := iterator.Err(); err != nil { |
| errch <- err |
| } |
| }() |
| // reads from: entries, errch, abortHTTP |
| // writes to: abortRPC |
| defer close(abortRPC) |
| for { |
| select { |
| case e, more := <-entries: |
| if !more { |
| return |
| } |
| fmt.Fprintln(w, e.Line) |
| case err := <-errch: |
| fmt.Fprintf(w, "ERROR(%v): %v\n", verror.ErrorID(err), err) |
| return |
| case <-abortHTTP: |
| return |
| } |
| } |
| } |
| |
| type globHandler struct{ *handler } |
| |
| func (h *globHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| type entry struct { |
| Suffix string |
| Error error |
| } |
| var ( |
| server = r.FormValue("n") |
| suffix = r.FormValue("s") |
| pattern = naming.Join(server, suffix, "*") |
| ctx, tracer = newTracer(h.ctx) |
| entries []entry |
| ) |
| ch, err := v23.GetNamespace(ctx).Glob(h.withTimeout(ctx), pattern) |
| if err != nil { |
| entries = append(entries, entry{Error: err}) |
| } |
| if ch != nil { |
| for e := range ch { |
| switch e := e.(type) { |
| case *naming.GlobReplyEntry: |
| entries = append(entries, entry{Suffix: strings.TrimPrefix(e.Value.Name, server)}) |
| case *naming.GlobReplyError: |
| entries = append(entries, entry{Error: e.Value.Error}) |
| } |
| } |
| } |
| args := struct { |
| ServerName string |
| CommandLine string |
| Vtrace *Tracer |
| Pattern string |
| Entries []entry |
| }{ |
| ServerName: server, |
| CommandLine: fmt.Sprintf("debug glob %q", pattern), |
| Vtrace: tracer, |
| Pattern: pattern, |
| Entries: entries, |
| } |
| h.execute(h.ctx, w, r, "glob.html", args) |
| } |
| |
| type profilesHandler struct{ *handler } |
| |
| func (h *profilesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| var ( |
| server = r.FormValue("n") |
| name = naming.Join(server, "__debug", "pprof") |
| ) |
| if len(server) == 0 { |
| w.WriteHeader(http.StatusBadRequest) |
| fmt.Fprintf(w, "Must specify a server with the URL query parameter 'n'") |
| return |
| } |
| if path := strings.TrimSuffix(r.URL.Path, "/"); strings.HasSuffix(path, strings.TrimSuffix(browseProfilesPath, "/")) { |
| urlPrefix := fmt.Sprintf("%s/pprof", strings.TrimPrefix(browseProfilesPath, "/")) |
| args := struct { |
| ServerName string |
| CommandLine string |
| Vtrace *Tracer |
| URLPrefix string |
| }{ |
| ServerName: server, |
| CommandLine: fmt.Sprintf("debug pprof run %q", name), |
| URLPrefix: urlPrefix, |
| } |
| h.execute(h.ctx, w, r, "profiles.html", args) |
| return |
| } |
| pproflib.PprofProxy(h.ctx, browseProfilesPath, name).ServeHTTP(w, r) |
| } |
| |
| const ( |
| allTracesLabel = ">0s" |
| oneMsTraceLabel = ">1ms" |
| tenMsTraceLabel = ">10ms" |
| hundredMsTraceLabel = ">100ms" |
| oneSecTraceLabel = ">1s" |
| tenSecTraceLabel = ">10s" |
| hundredSecTraceLabel = ">100s" |
| ) |
| |
| var ( |
| labelToCutoff = map[string]float64{ |
| allTracesLabel: 0, |
| oneMsTraceLabel: 0.001, |
| tenMsTraceLabel: 0.01, |
| hundredMsTraceLabel: 0.1, |
| oneSecTraceLabel: 1, |
| tenSecTraceLabel: 10, |
| hundredSecTraceLabel: 100, |
| } |
| sortedLabels = []string{allTracesLabel, oneMsTraceLabel, tenMsTraceLabel, hundredMsTraceLabel, oneSecTraceLabel, tenSecTraceLabel, hundredSecTraceLabel} |
| ) |
| |
| type traceWithStart struct { |
| Id string |
| Start time.Time |
| } |
| |
| type traceSort []traceWithStart |
| |
| func (t traceSort) Len() int { return len(t) } |
| func (t traceSort) Less(i, j int) bool { return t[i].Start.After(t[j].Start) } |
| func (t traceSort) Swap(i, j int) { t[i], t[j] = t[j], t[i] } |
| |
| func bucketTraces(ctx *context.T, name string) (map[string]traceSort, error) { |
| stub := svtrace.StoreClient(name) |
| |
| call, err := stub.AllTraces(ctx) |
| if err != nil { |
| return nil, err |
| } |
| stream := call.RecvStream() |
| var missingTraces []string |
| bucketedTraces := map[string]traceSort{} |
| for stream.Advance() { |
| trace := stream.Value() |
| node := vtrace.BuildTree(&trace) |
| if node == nil { |
| missingTraces = append(missingTraces, trace.Id.String()) |
| continue |
| } |
| startTime := findStartTime(node) |
| endTime := findEndTime(node) |
| traceNode := traceWithStart{Id: trace.Id.String(), Start: startTime} |
| duration := endTime.Sub(startTime).Seconds() |
| if endTime.IsZero() || startTime.IsZero() { |
| // Set the duration so something small but greater than zero so it shows up in |
| // the first bucket. |
| duration = 0.0001 |
| } |
| for l, cutoff := range labelToCutoff { |
| if duration >= cutoff { |
| bucketedTraces[l] = append(bucketedTraces[l], traceNode) |
| } |
| } |
| } |
| |
| for _, v := range bucketedTraces { |
| sort.Sort(v) |
| } |
| if err := stream.Err(); err != nil { |
| return bucketedTraces, err |
| } |
| if err := call.Finish(); err != nil { |
| return bucketedTraces, err |
| } |
| if len(missingTraces) > 0 { |
| return bucketedTraces, fmt.Errorf("missing information for: %s", strings.Join(missingTraces, ",")) |
| } |
| return bucketedTraces, nil |
| } |
| |
| type allTracesHandler struct{ *handler } |
| |
| func (a *allTracesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| var ( |
| server = r.FormValue("n") |
| name = naming.Join(server, "__debug", "vtrace") |
| ) |
| if len(server) == 0 { |
| w.WriteHeader(http.StatusBadRequest) |
| fmt.Fprintf(w, "Must specify a server with the URL query parameter 'n'") |
| return |
| } |
| ctx, tracer := newTracer(a.ctx) |
| buckets, err := bucketTraces(ctx, name) |
| |
| data := struct { |
| Buckets map[string]traceSort |
| SortedLabels []string |
| Err error |
| ServerName string |
| CommandLine string |
| Vtrace *Tracer |
| }{ |
| Buckets: buckets, |
| SortedLabels: sortedLabels, |
| Err: err, |
| CommandLine: fmt.Sprintf("debug vtraces %q", name), |
| ServerName: server, |
| Vtrace: tracer, |
| } |
| a.execute(a.ctx, w, r, "alltrace.html", data) |
| } |
| |
| type divTree struct { |
| Id string |
| // Start is a value from 0-100 which is the percentage of time into the parent span's duration |
| // that this span started. |
| Start int |
| // Width is a value from 0-100 which is the percentage of time in the parent span's duration this span |
| // took |
| Width int |
| Name string |
| Annotations []annotation |
| Children []divTree |
| } |
| |
| type annotation struct { |
| // Position is a value from 0-100 which is the percentage of time into the span's duration that this |
| // annotation occured. |
| Position int |
| Msg string |
| } |
| |
| func convertToTree(n *vtrace.Node, parentStart time.Time, parentEnd time.Time) *divTree { |
| // If either start of end is missing, use the parent start/end. |
| startTime := n.Span.Start |
| if startTime.IsZero() { |
| startTime = parentStart |
| } |
| |
| endTime := n.Span.End |
| if endTime.IsZero() { |
| endTime = parentEnd |
| } |
| |
| parentDuration := parentEnd.Sub(parentStart).Seconds() |
| start := int(100 * startTime.Sub(parentStart).Seconds() / parentDuration) |
| end := int(100 * endTime.Sub(parentStart).Seconds() / parentDuration) |
| width := end - start |
| if width == 0 { |
| width = 1 |
| } |
| top := &divTree{ |
| Id: n.Span.Id.String(), |
| Start: start, |
| Width: width, |
| Name: n.Span.Name, |
| } |
| |
| top.Annotations = make([]annotation, len(n.Span.Annotations)) |
| for i, a := range n.Span.Annotations { |
| top.Annotations[i].Msg = a.Message |
| if a.When.IsZero() { |
| top.Annotations[i].Position = 0 |
| continue |
| } |
| top.Annotations[i].Position = int(100*a.When.Sub(parentStart).Seconds()/parentDuration) - start |
| } |
| |
| top.Children = make([]divTree, len(n.Children)) |
| for i, c := range n.Children { |
| top.Children[i] = *convertToTree(c, startTime, endTime) |
| } |
| return top |
| |
| } |
| |
| // findStartTime returns the start time of a node. The start time is defined as either the span start if it exists |
| // or the timestamp of the first annotation/sub span. |
| func findStartTime(n *vtrace.Node) time.Time { |
| if !n.Span.Start.IsZero() { |
| return n.Span.Start |
| } |
| var startTime time.Time |
| for _, a := range n.Span.Annotations { |
| startTime = a.When |
| if !startTime.IsZero() { |
| break |
| } |
| } |
| for _, c := range n.Children { |
| childStartTime := findStartTime(c) |
| if startTime.IsZero() || (!childStartTime.IsZero() && startTime.After(childStartTime)) { |
| startTime = childStartTime |
| } |
| if !startTime.IsZero() { |
| break |
| } |
| } |
| return startTime |
| } |
| |
| // findEndTime returns the end time of a node. The end time is defined as either the span end if it exists |
| // or the timestamp of the last annotation/sub span. |
| func findEndTime(n *vtrace.Node) time.Time { |
| if !n.Span.End.IsZero() { |
| return n.Span.End |
| } |
| |
| size := len(n.Span.Annotations) |
| var endTime time.Time |
| for i := range n.Span.Annotations { |
| endTime = n.Span.Annotations[size-1-i].When |
| if !endTime.IsZero() { |
| break |
| } |
| } |
| |
| size = len(n.Children) |
| for i := range n.Children { |
| childEndTime := findEndTime(n.Children[size-1-i]) |
| if endTime.IsZero() || (!childEndTime.IsZero() && childEndTime.After(endTime)) { |
| endTime = childEndTime |
| } |
| if !endTime.IsZero() { |
| break |
| } |
| } |
| return endTime |
| } |
| |
| type vtraceHandler struct{ *handler } |
| |
| func (v *vtraceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| var ( |
| server = r.FormValue("n") |
| traceId = r.FormValue("t") |
| name = naming.Join(server, "__debug", "vtrace") |
| ) |
| |
| if len(server) == 0 { |
| w.WriteHeader(http.StatusBadRequest) |
| fmt.Fprintf(w, "Must specify a server with the URL query parameter 'n'") |
| return |
| } |
| |
| stub := svtrace.StoreClient(name) |
| ctx, tracer := newTracer(v.ctx) |
| id, err := uniqueid.FromHexString(traceId) |
| |
| if err != nil { |
| w.WriteHeader(http.StatusBadRequest) |
| fmt.Fprintf(w, "Invalid trace id %s: %v", traceId, err) |
| return |
| } |
| |
| trace, err := stub.Trace(ctx, id) |
| if err != nil { |
| w.WriteHeader(http.StatusNotFound) |
| fmt.Fprintf(w, "Unknown trace id: %s", traceId) |
| return |
| } |
| |
| var buf bytes.Buffer |
| |
| vtrace.FormatTrace(&buf, &trace, nil) |
| node := vtrace.BuildTree(&trace) |
| |
| if node == nil { |
| w.WriteHeader(http.StatusInternalServerError) |
| fmt.Fprintf(w, "coud not find root span for trace") |
| return |
| } |
| |
| tree := convertToTree(node, findStartTime(node), findEndTime(node)) |
| data := struct { |
| Id string |
| Root *divTree |
| ServerName string |
| CommandLine string |
| DebugTrace string |
| Vtrace *Tracer |
| }{ |
| Id: traceId, |
| Root: tree, |
| ServerName: server, |
| CommandLine: fmt.Sprintf("debug vtraces %q", name), |
| Vtrace: tracer, |
| DebugTrace: buf.String(), |
| } |
| v.execute(v.ctx, w, r, "vtrace.html", data) |
| } |
| |
| // The syncbaseHandler handles the main Syncbase viewer page is linked from the |
| // nav bar of the Debug browser. It displays a list of databases and their |
| // collections, and has links to the detailed collection page. |
| type syncbaseHandler struct{ *handler } |
| |
| func internalServerError(w http.ResponseWriter, doing string, err error) { |
| w.WriteHeader(http.StatusInternalServerError) |
| fmt.Fprintf(w, "Problem %s: %v", doing, err) |
| } |
| |
| func badRequest(w http.ResponseWriter, problem string) { |
| w.WriteHeader(http.StatusBadRequest) |
| fmt.Fprintf(w, problem) |
| } |
| |
| func (h *syncbaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| var ( |
| server = r.FormValue("n") |
| ) |
| ctx, tracer := newTracer(h.ctx) |
| if len(server) == 0 { |
| badRequest(w, "Must specify a server with the URL query parameter 'n'") |
| return |
| } |
| |
| sbTree, err := sbtree.AssembleSyncbaseTree(ctx, server) |
| |
| if err != nil { |
| if err == sbtree.NoSyncbaseError { |
| // Error because no Syncbase, send to no-syncbase page. |
| args := struct { |
| ServerName string |
| CommandLine string |
| Vtrace *Tracer |
| }{ |
| ServerName: server, |
| CommandLine: "(no command line)", |
| Vtrace: tracer, |
| } |
| h.execute(h.ctx, w, r, noSyncbaseTmpl, args) |
| } else { |
| // Some other error. |
| internalServerError(w, "getting syncbase information", err) |
| } |
| return |
| } |
| |
| args := struct { |
| ServerName string |
| CommandLine string |
| Vtrace *Tracer |
| Tree *sbtree.SyncbaseTree |
| }{ |
| ServerName: server, |
| CommandLine: fmt.Sprintf(`debug glob "%s/*"`, server), |
| Vtrace: tracer, |
| Tree: sbTree, |
| } |
| h.execute(h.ctx, w, r, syncbaseTmpl, args) |
| } |
| |
| // collectionHandler handles the Collections details page that is linked off the |
| // main Syncbase viewer page. |
| type collectionHandler struct{ *handler } |
| |
| const keysPerPage = 7 |
| |
| func (h *collectionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| var ( |
| server = r.FormValue("n") |
| dbBlessing = r.FormValue("db") |
| dbName = r.FormValue("dn") |
| collBlessing = r.FormValue("cb") |
| collName = r.FormValue("cn") |
| firstKey = r.FormValue("firstkey") |
| ) |
| ctx, tracer := newTracer(h.ctx) |
| if len(server) == 0 { |
| badRequest(w, "Must specify a server with the URL query parameter 'n'") |
| return |
| } |
| |
| collTree := sbtree.AssembleCollectionTree( |
| ctx, server, |
| dbBlessing, dbName, |
| collBlessing, collName, |
| firstKey, keysPerPage) |
| |
| // Assemble data and send it to the template to generate HTML |
| args := struct { |
| ServerName string |
| CommandLine string |
| Vtrace *Tracer |
| Tree *sbtree.CollectionTree |
| }{ |
| ServerName: server, |
| CommandLine: "(no command line)", |
| Vtrace: tracer, |
| Tree: collTree, |
| } |
| h.execute(h.ctx, w, r, collectionTmpl, args) |
| } |
| |
| func writeEvent(w http.ResponseWriter, data string) { |
| fmt.Fprintf(w, "data: %s\n\n", strings.TrimSpace(data)) |
| } |
| |
| func writeErrorEvent(w http.ResponseWriter, err error) { |
| id := fmt.Sprintf("%v", verror.ErrorID(err)) |
| writeEvent(w, fmt.Sprintf("ERROR(%v): %v", html.EscapeString(id), html.EscapeString(err.Error()))) |
| } |
| |
| // Tracer forces collection of a trace rooted at the call to newTracer. |
| type Tracer struct { |
| ctx *context.T |
| span vtrace.Span |
| } |
| |
| func newTracer(ctx *context.T) (*context.T, *Tracer) { |
| ctx, span := vtrace.WithNewTrace(ctx) |
| vtrace.ForceCollect(ctx, 0) |
| return ctx, &Tracer{ctx, span} |
| } |
| |
| func (t *Tracer) String() string { |
| if t == nil { |
| return "" |
| } |
| tr := vtrace.GetStore(t.ctx).TraceRecord(t.span.Trace()) |
| if len(tr.Spans) == 0 { |
| // Do not bother with empty traces |
| return "" |
| } |
| var buf bytes.Buffer |
| // nil as the time.Location is fine because the HTTP "server" time is |
| // the same as that of the "client" (typically a browser on localhost). |
| vtrace.FormatTrace(&buf, vtrace.GetStore(t.ctx).TraceRecord(t.span.Trace()), nil) |
| return buf.String() |
| } |