From bf28dd4488e681fa937466f3e35e74499a963796 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Sun, 16 Mar 2014 20:13:47 -0700 Subject: [PATCH] More status handler HTML+JSON, more sync status. Change-Id: I0381853191d5b871af649d102b976e592def791f --- pkg/blobserver/registry.go | 4 ++ pkg/server/root.go | 26 ++++++---- pkg/server/status.go | 98 +++++++++++++++++++++++++++++++----- pkg/server/sync.go | 80 ++++++++++++++++++++++------- pkg/serverinit/serverinit.go | 10 ++++ pkg/test/loader.go | 4 ++ 6 files changed, 183 insertions(+), 39 deletions(-) diff --git a/pkg/blobserver/registry.go b/pkg/blobserver/registry.go index 256ce7599..1bc450ea7 100644 --- a/pkg/blobserver/registry.go +++ b/pkg/blobserver/registry.go @@ -42,6 +42,10 @@ type FindHandlerByTyper interface { // construction of all handlers), then prefix and handler will // both be non-nil when err is nil. FindHandlerByType(handlerType string) (prefix string, handler interface{}, err error) + + // AllHandlers returns a map from prefix to handler type, and + // a map from prefix to handler. + AllHandlers() (map[string]string, map[string]interface{}) } type Loader interface { diff --git a/pkg/server/root.go b/pkg/server/root.go index 1ef6dbf2b..730f031e0 100644 --- a/pkg/server/root.go +++ b/pkg/server/root.go @@ -28,6 +28,7 @@ import ( "camlistore.org/pkg/auth" "camlistore.org/pkg/blobserver" + "camlistore.org/pkg/buildinfo" "camlistore.org/pkg/images" "camlistore.org/pkg/jsonconfig" "camlistore.org/pkg/osutil" @@ -48,6 +49,7 @@ type RootHandler struct { BlobRoot string SearchRoot string statusRoot string + Prefix string // root handler's prefix Storage blobserver.Storage // of BlobRoot, or nil @@ -78,6 +80,7 @@ func newRootFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (h http.Handle SearchRoot: conf.OptionalString("searchRoot", ""), OwnerName: conf.OptionalString("ownerName", username), Username: osutil.Username(), + Prefix: ld.MyPrefix(), } root.Stealth = conf.OptionalBool("stealth", false) root.statusRoot = conf.OptionalString("statusRoot", "") @@ -143,16 +146,21 @@ func (rh *RootHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { serveStaticFile(rw, req, Files, "favicon.ico") return } - - configLink := "" - if auth.IsLocalhost(req) && !isDevServer() { - configLink = "

If you're coming from localhost, configure your Camlistore server at /setup.

" + f := func(p string, a ...interface{}) { + fmt.Fprintf(rw, p, a...) } - fmt.Fprintf(rw, "This is camlistored, a "+ - "Camlistore server."+ - "%s"+ - "

To manage your content, access the /ui/.

\n", - configLink) + f("

This is camlistored (%s), a "+ + "Camlistore server.

", buildinfo.Version()) + if auth.IsLocalhost(req) && !isDevServer() { + f("

If you're coming from localhost, configure your Camlistore server at /setup.

") + } + if rh.ui != nil { + f("

To manage your content, access the %s.

", rh.ui.prefix, rh.ui.prefix) + } + if rh.statusRoot != "" { + f("

To view status, see %s", rh.statusRoot, rh.statusRoot) + } + fmt.Fprintf(rw, "") } func isDevServer() bool { diff --git a/pkg/server/status.go b/pkg/server/status.go index df43fdbfe..5d3e0c8c6 100644 --- a/pkg/server/status.go +++ b/pkg/server/status.go @@ -17,16 +17,24 @@ limitations under the License. package server import ( + "encoding/json" + "fmt" + "html" + "log" "net/http" + "strings" "camlistore.org/pkg/blobserver" "camlistore.org/pkg/buildinfo" "camlistore.org/pkg/httputil" + "camlistore.org/pkg/index" "camlistore.org/pkg/jsonconfig" ) // StatusHandler publishes server status information. type StatusHandler struct { + prefix string + handlerFinder blobserver.FindHandlerByTyper } func init() { @@ -37,30 +45,96 @@ func newStatusFromConfig(ld blobserver.Loader, conf jsonconfig.Obj) (h http.Hand if err := conf.Validate(); err != nil { return nil, err } - return &StatusHandler{}, nil + return &StatusHandler{ + prefix: ld.MyPrefix(), + handlerFinder: ld, + }, nil } func (sh *StatusHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { suffix := httputil.PathSuffix(req) - if req.Method != "GET" { - http.Error(rw, "Illegal URL.", http.StatusMethodNotAllowed) + if !httputil.IsGet(req) { + http.Error(rw, "Illegal status method.", http.StatusMethodNotAllowed) return } - if suffix == "status.json" { - sh.serveStatus(rw, req) - return + switch suffix { + case "status.json": + sh.serveStatusJSON(rw, req) + case "": + sh.serveStatusHTML(rw, req) + default: + http.Error(rw, "Illegal status path.", 404) } - http.Error(rw, "Illegal URL.", 404) } -type statusResponse struct { - Version string `json:"version"` +type status struct { + Version string `json:"version"` + Error string `json:"error,omitempty"` + SyncStatus []syncStatus `json:"sync"` + Storage map[string]storageStatus `json:"storage"` + rootPrefix string } -func (sh *StatusHandler) serveStatus(rw http.ResponseWriter, req *http.Request) { - res := &statusResponse{ +type storageStatus struct { + Primary bool `json:"primary,omitempty"` + IsIndex bool `json:"isIndex,omitempty"` + Type string `json:"type"` + ApproxBlobs int `json:"approximateBlobs"` + ApproxBytes int `json:"approximateBytes"` + ImplStatus interface{} `json:"implStatus,omitempty"` +} + +func (sh *StatusHandler) currentStatus() *status { + res := &status{ Version: buildinfo.Version(), + Storage: make(map[string]storageStatus), + } + _, hi, err := sh.handlerFinder.FindHandlerByType("root") + if err != nil { + res.Error = fmt.Sprintf("Error finding root handler: %v", err) + return res + } + rh := hi.(*RootHandler) + res.rootPrefix = rh.Prefix + for _, sh := range rh.sync { + res.SyncStatus = append(res.SyncStatus, sh.currentStatus()) } - httputil.ReturnJSON(rw, res) + types, handlers := sh.handlerFinder.AllHandlers() + + // Storage + for pfx, typ := range types { + if !strings.HasPrefix(typ, "storage-") { + continue + } + h := handlers[pfx] + _, isIndex := h.(*index.Index) + res.Storage[pfx] = storageStatus{ + Type: strings.TrimPrefix(typ, "storage-"), + Primary: pfx == rh.BlobRoot, + IsIndex: isIndex, + } + } + + return res +} + +func (sh *StatusHandler) serveStatusJSON(rw http.ResponseWriter, req *http.Request) { + httputil.ReturnJSON(rw, sh.currentStatus()) +} + +func (sh *StatusHandler) serveStatusHTML(rw http.ResponseWriter, req *http.Request) { + st := sh.currentStatus() + f := func(p string, a ...interface{}) { + fmt.Fprintf(rw, p, a...) + } + f("Status") + f("

Status

") + f("

As JSON: status.json; and the discovery JSON.

", st.rootPrefix) + f("

Not yet pretty HTML UI:

") + js, err := json.MarshalIndent(st, "", " ") + if err != nil { + log.Printf("JSON marshal error: %v", err) + } + f("
%s
", html.EscapeString(string(js))) } diff --git a/pkg/server/sync.go b/pkg/server/sync.go index 8df3e29bd..14fca2c65 100644 --- a/pkg/server/sync.go +++ b/pkg/server/sync.go @@ -86,9 +86,12 @@ type SyncHandler struct { totalErrors int64 vshards []string // validation shards. if 0, validation not running vshardDone int // shards validated - vmissing int64 // missing blobs found during validat - vdestCount int // number of blobs seen on dest during validate - vdestBytes int64 // number of blob bytes seen on dest during validate + vshardErrs []string + vmissing int64 // missing blobs found during validat + vdestCount int // number of blobs seen on dest during validate + vdestBytes int64 // number of blob bytes seen on dest during validate + vsrcCount int // number of blobs seen on src during validate + vsrcBytes int64 // number of blob bytes seen on src during validate } var ( @@ -234,7 +237,6 @@ func newIdleSyncHandler(fromName, toName string) *SyncHandler { } func (sh *SyncHandler) discoveryMap() map[string]interface{} { - // TODO(mpl): more status info return map[string]interface{}{ "from": sh.fromName, "to": sh.toName, @@ -242,6 +244,41 @@ func (sh *SyncHandler) discoveryMap() map[string]interface{} { } } +// syncStatus is a snapshot of the current status, for display by the +// status handler (status.go) in both JSON and HTML forms. +type syncStatus struct { + sh *SyncHandler + + From string `json:"from"` + FromDesc string `json:"fromDesc"` + To string `json:"to"` + ToDesc string `json:"toDesc"` + DestIsIndex bool `json:"destIsIndex,omitempty"` + BlobsToCopy int `json:"blobsToCopy"` + BytesToCopy int64 `json:"bytesToCopy"` + LastCopySecAgo int `json:"lastCopySecondsAgo,omitempty"` +} + +func (sh *SyncHandler) currentStatus() syncStatus { + sh.mu.Lock() + defer sh.mu.Unlock() + ago := 0 + if !sh.recentCopyTime.IsZero() { + ago = int(time.Now().Sub(sh.recentCopyTime).Seconds()) + } + return syncStatus{ + sh: sh, + From: sh.fromName, + FromDesc: storageDesc(sh.from), + To: sh.toName, + ToDesc: storageDesc(sh.to), + DestIsIndex: sh.toIndex, + BlobsToCopy: len(sh.needCopy), + BytesToCopy: sh.bytesRemain, + LastCopySecAgo: ago, + } +} + // readQueueToMemory slurps in the pending queue from disk (or // wherever) to memory. Even with millions of blobs, it's not much // memory. The point of the persistent queue is to survive restarts if @@ -280,6 +317,8 @@ func (sh *SyncHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { return } + // TODO: remove this lock and instead just call currentStatus, + // and transition to using that here. sh.mu.Lock() defer sh.mu.Unlock() f := func(p string, a ...interface{}) { @@ -311,7 +350,7 @@ func (sh *SyncHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { f("

Validation

") if len(sh.vshards) == 0 { - f("Disabled") + f("Validation disabled") token := xsrftoken.Generate(auth.ProcessRandom(), "user", "runFullValidate") f("
", token) } else { @@ -321,9 +360,14 @@ func (sh *SyncHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { sh.vshardDone, len(sh.vshards), 100*float64(sh.vshardDone)/float64(len(sh.vshards))) - f("
  • Blobs found missing + fixed: %d
  • ", sh.vmissing) + f("
  • Source blobs seen: %d
  • ", sh.vsrcCount) + f("
  • Source bytes seen: %d
  • ", sh.vsrcBytes) f("
  • Dest blobs seen: %d
  • ", sh.vdestCount) f("
  • Dest bytes seen: %d
  • ", sh.vdestBytes) + f("
  • Blobs found missing + fixed: %d
  • ", sh.vmissing) + if len(sh.vshardErrs) > 0 { + f("
  • Validation errors: %s
  • ", sh.vshardErrs) + } f("") } @@ -510,11 +554,6 @@ func (sh *SyncHandler) copyBlob(sb blob.SizedRef) (err error) { sh.copying[br] = cs sh.mu.Unlock() - if strings.Contains(storageDesc(sh.to), "bradfitz-camlistore-pt") { - //sh.logf("LIES NOT ACTUALLY COPYING") - //return nil - } - if sb.Size > constants.MaxBlobSize { return fmt.Errorf("blob size %d too large; max blob size is %d", sb.Size, constants.MaxBlobSize) } @@ -651,12 +690,14 @@ func (sh *SyncHandler) runFullValidation() { func (sh *SyncHandler) validateShardPrefix(pfx string) (err error) { defer func() { - if err != nil { - sh.logf("Failed to validate prefix %s: %v", pfx, err) - return - } sh.mu.Lock() - sh.vshardDone++ + if err != nil { + errs := fmt.Sprintf("Failed to validate prefix %s: %v", pfx, err) + sh.logf("%s", errs) + sh.vshardErrs = append(sh.vshardErrs, errs) + } else { + sh.vshardDone++ + } sh.mu.Unlock() }() ctx := context.New() @@ -705,12 +746,15 @@ func (sh *SyncHandler) startValidatePrefix(ctx *context.Context, pfx string, doD if !strings.HasPrefix(sb.Ref.String(), pfx) { return errNotPrefix } + sh.mu.Lock() if doDest { - sh.mu.Lock() sh.vdestCount++ sh.vdestBytes += int64(sb.Size) - sh.mu.Unlock() + } else { + sh.vsrcCount++ + sh.vsrcBytes += int64(sb.Size) } + sh.mu.Unlock() return nil case <-ctx.Done(): return context.ErrCanceled diff --git a/pkg/serverinit/serverinit.go b/pkg/serverinit/serverinit.go index 9402ed5e0..df85bf89e 100644 --- a/pkg/serverinit/serverinit.go +++ b/pkg/serverinit/serverinit.go @@ -208,6 +208,16 @@ func (hl *handlerLoader) FindHandlerByType(htype string) (prefix string, handler return } +func (hl *handlerLoader) AllHandlers() (types map[string]string, handlers map[string]interface{}) { + types = make(map[string]string) + handlers = make(map[string]interface{}) + for pfx, config := range hl.config { + types[pfx] = config.htype + handlers[pfx] = hl.handler[pfx] + } + return +} + func (hl *handlerLoader) setupAll() { for prefix := range hl.config { hl.setupHandler(prefix) diff --git a/pkg/test/loader.go b/pkg/test/loader.go index 3e9295d39..d788f70fb 100644 --- a/pkg/test/loader.go +++ b/pkg/test/loader.go @@ -41,6 +41,10 @@ func (ld *Loader) FindHandlerByType(handlerType string) (prefix string, handler panic("NOIMPL") } +func (ld *Loader) AllHandlers() (map[string]string, map[string]interface{}) { + panic("NOIMPL") +} + func (ld *Loader) MyPrefix() string { return "/lies/" }