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("
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("