Пример #1
0
func InitStaticRouter(staticDir, staticETag string,
	mgr *cbgt.Manager) *mux.Router {
	prefix := ""
	if mgr != nil {
		prefix = mgr.Options()["urlPrefix"]
	}

	hfsStaticX := http.FileServer(assetFS())

	router := mux.NewRouter()
	router.StrictSlash(true)

	router.Handle(prefix+"/",
		http.RedirectHandler(prefix+"/index.html", 302))
	router.Handle(prefix+"/index.html",
		http.RedirectHandler(prefix+"/staticx/index.html", 302))
	router.Handle(prefix+"/static/partials/index/start.html",
		http.RedirectHandler(prefix+"/staticx/partials/index/start.html", 302))

	router = rest.InitStaticRouterEx(router,
		staticDir, staticETag, []string{
			prefix + "/indexes",
			prefix + "/nodes",
			prefix + "/monitor",
			prefix + "/manage",
			prefix + "/logs",
			prefix + "/debug",
		}, http.RedirectHandler(prefix+"/staticx/index.html", 302), mgr)

	router.PathPrefix(prefix + "/staticx/").Handler(
		http.StripPrefix(prefix+"/staticx/", hfsStaticX))

	return router
}
Пример #2
0
func BlevePIndexImplInitRouter(r *mux.Router, phase string,
	mgr *cbgt.Manager) {
	prefix := ""
	if mgr != nil {
		prefix = mgr.Options()["urlPrefix"]
	}

	if phase == "static.before" {
		staticBleveMapping := http.FileServer(bleveMappingUI.AssetFS())

		r.PathPrefix(prefix + "/static-bleve-mapping/").Handler(
			http.StripPrefix(prefix+"/static-bleve-mapping/",
				staticBleveMapping))

		bleveMappingUI.RegisterHandlers(r, prefix+"/api")
	}

	if phase == "manager.after" {
		// Using standard bleveHttp handlers for /api/pindex-bleve endpoints.
		//
		listIndexesHandler := bleveHttp.NewListIndexesHandler()
		r.Handle(prefix+"/api/pindex-bleve",
			listIndexesHandler).Methods("GET")

		getIndexHandler := bleveHttp.NewGetIndexHandler()
		getIndexHandler.IndexNameLookup = rest.PIndexNameLookup
		r.Handle(prefix+"/api/pindex-bleve/{pindexName}",
			getIndexHandler).Methods("GET")

		docCountHandler := bleveHttp.NewDocCountHandler("")
		docCountHandler.IndexNameLookup = rest.PIndexNameLookup
		r.Handle(prefix+"/api/pindex-bleve/{pindexName}/count",
			docCountHandler).Methods("GET")

		searchHandler := bleveHttp.NewSearchHandler("")
		searchHandler.IndexNameLookup = rest.PIndexNameLookup
		r.Handle(prefix+"/api/pindex-bleve/{pindexName}/query",
			searchHandler).Methods("POST")

		docGetHandler := bleveHttp.NewDocGetHandler("")
		docGetHandler.IndexNameLookup = rest.PIndexNameLookup
		docGetHandler.DocIDLookup = rest.DocIDLookup
		r.Handle(prefix+"/api/pindex-bleve/{pindexName}/doc/{docID}",
			docGetHandler).Methods("GET")

		debugDocHandler := bleveHttp.NewDebugDocumentHandler("")
		debugDocHandler.IndexNameLookup = rest.PIndexNameLookup
		debugDocHandler.DocIDLookup = rest.DocIDLookup
		r.Handle(prefix+"/api/pindex-bleve/{pindexName}/docDebug/{docID}",
			debugDocHandler).Methods("GET")

		listFieldsHandler := bleveHttp.NewListFieldsHandler("")
		listFieldsHandler.IndexNameLookup = rest.PIndexNameLookup
		r.Handle(prefix+"/api/pindex-bleve/{pindexName}/fields",
			listFieldsHandler).Methods("GET")
	}
}
Пример #3
0
// InitStaticRouterEx is like InitStaticRouter, but with optional
// manager parameter for more options.
func InitStaticRouterEx(r *mux.Router, staticDir, staticETag string,
	pages []string, pagesHandler http.Handler,
	mgr *cbgt.Manager) *mux.Router {
	prefix := ""
	if mgr != nil {
		prefix = mgr.Options()["urlPrefix"]
	}

	PIndexTypesInitRouter(r, "static.before", mgr)

	var s http.FileSystem
	if staticDir != "" {
		if _, err := os.Stat(staticDir); err == nil {
			log.Printf("http: serving assets from staticDir: %s", staticDir)
			s = http.Dir(staticDir)
		}
	}
	if s == nil {
		log.Printf("http: serving assets from embedded data")
		s = AssetFS()
	}

	r.PathPrefix(prefix + "/static/").Handler(
		http.StripPrefix(prefix+"/static/",
			ETagFileHandler{http.FileServer(s), staticETag}))

	// Bootstrap UI insists on loading templates from this path.
	r.PathPrefix(prefix + "/template/").Handler(
		http.StripPrefix(prefix+"/template/",
			ETagFileHandler{http.FileServer(s), staticETag}))

	// If client ask for any of the pages, redirect.
	for _, p := range pages {
		if pagesHandler != nil {
			r.PathPrefix(p).Handler(pagesHandler)
		} else {
			r.PathPrefix(p).Handler(RewriteURL("/", http.FileServer(s)))
		}
	}

	r.Handle(prefix+"/index.html",
		http.RedirectHandler(prefix+"/static/index.html", 302))
	r.Handle(prefix+"/",
		http.RedirectHandler(prefix+"/static/index.html", 302))

	PIndexTypesInitRouter(r, "static.after", mgr)

	return r
}
Пример #4
0
func NewQueryHandler(mgr *cbgt.Manager) *QueryHandler {
	slowQueryLogTimeout := time.Duration(0)
	slowQueryLogTimeoutV := mgr.Options()["slowQueryLogTimeout"]
	if slowQueryLogTimeoutV != "" {
		d, err := time.ParseDuration(slowQueryLogTimeoutV)
		if err == nil {
			slowQueryLogTimeout = d
		}
	}

	return &QueryHandler{
		mgr:                 mgr,
		slowQueryLogTimeout: slowQueryLogTimeout,
	}
}
Пример #5
0
func CheckAPIAuth(mgr *cbgt.Manager,
	w http.ResponseWriter, req *http.Request, path string) (allowed bool) {
	authType := ""
	if mgr != nil && mgr.Options() != nil {
		authType = mgr.Options()["authType"]
	}

	if authType == "" {
		return true
	}

	if authType != "cbauth" {
		return false
	}

	creds, err := cbauth.AuthWebCreds(req)
	if err != nil {
		http.Error(w, fmt.Sprintf("rest_auth: cbauth.AuthWebCreds,"+
			" err: %v ", err), 403)
		return false
	}

	perms, err := preparePerms(mgr, req, req.Method, path)
	if err != nil {
		http.Error(w, fmt.Sprintf("rest_auth: preparePerm,"+
			" err: %v ", err), 403)
		return false
	}

	for _, perm := range perms {
		allowed, err = creds.IsAllowed(perm)
		if err != nil {
			http.Error(w, fmt.Sprintf("rest_auth: cbauth.IsAllowed,"+
				" err: %v ", err), 403)
			return false
		}

		if !allowed {
			cbauth.SendForbidden(w, perm)
			return false
		}
	}

	return true
}
Пример #6
0
func bleveIndexTargets(mgr *cbgt.Manager, indexName, indexUUID string,
	ensureCanRead bool, consistencyParams *cbgt.ConsistencyParams,
	cancelCh <-chan bool, collector BleveIndexCollector) error {
	planPIndexNodeFilter := cbgt.PlanPIndexNodeOk
	if ensureCanRead {
		planPIndexNodeFilter = cbgt.PlanPIndexNodeCanRead
	}

	localPIndexes, remotePlanPIndexes, err :=
		mgr.CoveringPIndexes(indexName, indexUUID, planPIndexNodeFilter,
			"queries")
	if err != nil {
		return fmt.Errorf("bleve: bleveIndexTargets, err: %v", err)
	}

	prefix := mgr.Options()["urlPrefix"]

	for _, remotePlanPIndex := range remotePlanPIndexes {
		baseURL := "http://" + remotePlanPIndex.NodeDef.HostPort +
			prefix + "/api/pindex/" + remotePlanPIndex.PlanPIndex.Name
		collector.Add(&IndexClient{
			name:        fmt.Sprintf("IndexClient - %s", baseURL),
			QueryURL:    baseURL + "/query",
			CountURL:    baseURL + "/count",
			Consistency: consistencyParams,
			// TODO: Propagate auth to remote client.
		})
	}

	// TODO: Should kickoff remote queries concurrently before we wait.

	return cbgt.ConsistencyWaitGroup(indexName, consistencyParams,
		cancelCh, localPIndexes,
		func(localPIndex *cbgt.PIndex) error {
			bindex, ok := localPIndex.Impl.(bleve.Index)
			if !ok || bindex == nil ||
				!strings.HasPrefix(localPIndex.IndexType, "fulltext-index") {
				return fmt.Errorf("bleve: wrong type, localPIndex: %#v",
					localPIndex)
			}
			collector.Add(bindex)
			return nil
		})
}
Пример #7
0
// NewRESTRouter creates a mux.Router initialized with the REST API
// and web UI routes.  See also InitStaticRouter and InitRESTRouter if
// you need finer control of the router initialization.
func NewRESTRouter(versionMain string, mgr *cbgt.Manager,
	staticDir, staticETag string, mr *cbgt.MsgRing,
	assetDir func(name string) ([]string, error),
	asset func(name string) ([]byte, error)) (
	*mux.Router, map[string]RESTMeta, error) {
	prefix := mgr.Options()["urlPrefix"]

	r := mux.NewRouter()
	r.StrictSlash(true)

	r = InitStaticRouterEx(r,
		staticDir, staticETag, []string{
			prefix + "/indexes",
			prefix + "/nodes",
			prefix + "/monitor",
			prefix + "/manage",
			prefix + "/logs",
			prefix + "/debug",
		}, nil, mgr)

	return InitRESTRouter(r, versionMain, mgr,
		staticDir, staticETag, mr, assetDir, asset)
}
Пример #8
0
func InitStaticRouter(staticDir, staticETag string,
	mgr *cbgt.Manager) *mux.Router {

	router := mux.NewRouter()
	router.StrictSlash(true)

	showUI := true
	if mgr != nil && mgr.Options()["hideUI"] != "" {
		hideUI, err := strconv.ParseBool(mgr.Options()["hideUI"])
		if err == nil && hideUI {
			showUI = false
		}
	}

	if showUI {
		prefix := ""
		if mgr != nil {
			prefix = mgr.Options()["urlPrefix"]
		}

		hfsStaticX := http.FileServer(assetFS())

		router.Handle(prefix+"/",
			http.RedirectHandler(prefix+"/index.html", 302))
		router.Handle(prefix+"/index.html",
			http.RedirectHandler(prefix+"/staticx/index.html", 302))
		router.Handle(prefix+"/static/partials/index/start.html",
			http.RedirectHandler(prefix+"/staticx/partials/index/start.html", 302))

		router = rest.InitStaticRouterEx(router,
			staticDir, staticETag, []string{
				prefix + "/indexes",
				prefix + "/nodes",
				prefix + "/monitor",
				prefix + "/manage",
				prefix + "/logs",
				prefix + "/debug",
			}, http.RedirectHandler(prefix+"/staticx/index.html", 302), mgr)

		router.PathPrefix(prefix + "/staticx/").Handler(
			http.StripPrefix(prefix+"/staticx/", hfsStaticX))
	}

	return router
}
Пример #9
0
func QueryBlevePIndexImpl(mgr *cbgt.Manager, indexName, indexUUID string,
	req []byte, res io.Writer) error {
	queryCtlParams := cbgt.QueryCtlParams{
		Ctl: cbgt.QueryCtl{
			Timeout: cbgt.QUERY_CTL_DEFAULT_TIMEOUT_MS,
		},
	}

	err := json.Unmarshal(req, &queryCtlParams)
	if err != nil {
		return fmt.Errorf("bleve: QueryBlevePIndexImpl"+
			" parsing queryCtlParams, req: %s, err: %v", req, err)
	}

	searchRequest := &bleve.SearchRequest{}

	err = json.Unmarshal(req, searchRequest)
	if err != nil {
		return fmt.Errorf("bleve: QueryBlevePIndexImpl"+
			" parsing searchRequest, req: %s, err: %v", req, err)
	}

	err = searchRequest.Query.Validate()
	if err != nil {
		return err
	}

	v, exists := mgr.Options()["bleveMaxResultWindow"]
	if exists {
		bleveMaxResultWindow, err := strconv.Atoi(v)
		if err != nil {
			return err
		}

		if searchRequest.From+searchRequest.Size > bleveMaxResultWindow {
			return fmt.Errorf("bleve: bleveMaxResultWindow exceeded,"+
				" from: %d, size: %d, bleveMaxResultWindow: %d",
				searchRequest.From, searchRequest.Size, bleveMaxResultWindow)
		}
	}

	cancelCh := cbgt.TimeoutCancelChan(queryCtlParams.Ctl.Timeout)

	alias, err := bleveIndexAlias(mgr, indexName, indexUUID, true,
		queryCtlParams.Ctl.Consistency, cancelCh)
	if err != nil {
		return err
	}

	doneCh := make(chan struct{})

	var searchResult *bleve.SearchResult

	go func() {
		searchResult, err = alias.Search(searchRequest)

		close(doneCh)
	}()

	select {
	case <-cancelCh:
		err = fmt.Errorf("pindex_bleve: query timeout")

	case <-doneCh:
		if searchResult != nil {
			rest.MustEncode(res, searchResult)
		}
	}

	return err
}
Пример #10
0
func initNsServerCaching(mgr *cbgt.Manager) {
	runSourcePartitionSeqsOnce.Do(func() {
		go RunSourcePartitionSeqs(mgr.Options(), nil)
		go RunRecentInfoCache(mgr)
	})
}
Пример #11
0
// InitRESTRouter initializes a mux.Router with REST API routes with
// extra option.
func InitRESTRouterEx(r *mux.Router, versionMain string,
	mgr *cbgt.Manager, staticDir, staticETag string,
	mr *cbgt.MsgRing,
	assetDir func(name string) ([]string, error),
	asset func(name string) ([]byte, error),
	options map[string]interface{}) (
	*mux.Router, map[string]RESTMeta, error) {
	var authHandler func(http.Handler) http.Handler

	mapRESTPathStats := map[string]*RESTPathStats{} // Keyed by path spec.

	if options != nil {
		if v, ok := options["auth"]; ok {
			authHandler, ok = v.(func(http.Handler) http.Handler)
			if !ok {
				return nil, nil, fmt.Errorf("rest: auth function invalid")
			}
		}

		if v, ok := options["mapRESTPathStats"]; ok {
			mapRESTPathStats, ok = v.(map[string]*RESTPathStats)
			if !ok {
				return nil, nil, fmt.Errorf("rest: mapRESTPathStats invalid")
			}
		}
	}

	prefix := mgr.Options()["urlPrefix"]

	PIndexTypesInitRouter(r, "manager.before", mgr)

	meta := map[string]RESTMeta{}

	handle := func(path string, method string, h http.Handler,
		opts map[string]string) {
		opts["_path"] = path
		if a, ok := h.(RESTOpts); ok {
			a.RESTOpts(opts)
		}
		prefixPath := prefix + path
		restMeta := RESTMeta{prefixPath, method, opts}
		meta[prefixPath+" "+RESTMethodOrds[method]+method] = restMeta
		h = &HandlerWithRESTMeta{
			h:         h,
			RESTMeta:  &restMeta,
			pathStats: mapRESTPathStats[path],
			focusName: PathFocusName(path),
		}
		if authHandler != nil {
			h = authHandler(h)
		}
		r.Handle(prefixPath, h).Methods(method).Name(prefixPath)
	}

	handle("/api/index", "GET", NewListIndexHandler(mgr),
		map[string]string{
			"_category":          "Indexing|Index definition",
			"_about":             `Returns all index definitions as JSON.`,
			"version introduced": "0.0.1",
		})
	handle("/api/index/{indexName}", "PUT", NewCreateIndexHandler(mgr),
		map[string]string{
			"_category":          "Indexing|Index definition",
			"_about":             `Creates/updates an index definition.`,
			"version introduced": "0.0.1",
		})
	handle("/api/index/{indexName}", "DELETE", NewDeleteIndexHandler(mgr),
		map[string]string{
			"_category":          "Indexing|Index definition",
			"_about":             `Deletes an index definition.`,
			"version introduced": "0.0.1",
		})
	handle("/api/index/{indexName}", "GET", NewGetIndexHandler(mgr),
		map[string]string{
			"_category":          "Indexing|Index definition",
			"_about":             `Returns the definition of an index as JSON.`,
			"version introduced": "0.0.1",
		})

	if mgr == nil || mgr.TagsMap() == nil || mgr.TagsMap()["queryer"] {
		handle("/api/index/{indexName}/count", "GET",
			NewCountHandler(mgr),
			map[string]string{
				"_category":          "Indexing|Index querying",
				"_about":             `Returns the count of indexed documents.`,
				"version introduced": "0.0.1",
			})
		handle("/api/index/{indexName}/query", "POST",
			NewQueryHandler(mgr,
				mapRESTPathStats["/api/index/{indexName}/query"]),
			map[string]string{
				"_category":          "Indexing|Index querying",
				"_about":             `Queries an index.`,
				"version introduced": "0.2.0",
			})
	}

	handle("/api/index/{indexName}/planFreezeControl/{op}", "POST",
		NewIndexControlHandler(mgr, "planFreeze", map[string]bool{
			"freeze":   true,
			"unfreeze": true,
		}),
		map[string]string{
			"_category": "Indexing|Index management",
			"_about":    `Freeze the assignment of index partitions to nodes.`,
			"param: op": "required, string, URL path parameter\n\n" +
				`Allowed values for op are "freeze" or "unfreeze".`,
			"version introduced": "0.0.1",
		})
	handle("/api/index/{indexName}/ingestControl/{op}", "POST",
		NewIndexControlHandler(mgr, "write", map[string]bool{
			"pause":  true,
			"resume": true,
		}),
		map[string]string{
			"_category": "Indexing|Index management",
			"_about": `Pause index updates and maintenance (no more
                          ingesting document mutations).`,
			"param: op": "required, string, URL path parameter\n\n" +
				`Allowed values for op are "pause" or "resume".`,
			"version introduced": "0.0.1",
		})
	handle("/api/index/{indexName}/queryControl/{op}", "POST",
		NewIndexControlHandler(mgr, "read", map[string]bool{
			"allow":    true,
			"disallow": true,
		}),
		map[string]string{
			"_category": "Indexing|Index management",
			"_about":    `Disallow queries on an index.`,
			"param: op": "required, string, URL path parameter\n\n" +
				`Allowed values for op are "allow" or "disallow".`,
			"version introduced": "0.0.1",
		})

	if mgr == nil || mgr.TagsMap() == nil || mgr.TagsMap()["pindex"] {
		handle("/api/pindex", "GET",
			NewListPIndexHandler(mgr),
			map[string]string{
				"_category":          "x/Advanced|x/Index partition definition",
				"version introduced": "0.0.1",
			})
		handle("/api/pindex/{pindexName}", "GET",
			NewGetPIndexHandler(mgr),
			map[string]string{
				"_category":          "x/Advanced|x/Index partition definition",
				"version introduced": "0.0.1",
			})
		handle("/api/pindex/{pindexName}/count", "GET",
			NewCountPIndexHandler(mgr),
			map[string]string{
				"_category":          "x/Advanced|x/Index partition querying",
				"version introduced": "0.0.1",
			})
		handle("/api/pindex/{pindexName}/query", "POST",
			NewQueryPIndexHandler(mgr),
			map[string]string{
				"_category":          "x/Advanced|x/Index partition querying",
				"version introduced": "0.2.0",
			})
	}

	handle("/api/managerOptions", "PUT", NewManagerOptions(mgr),
		map[string]string{
			"_category":          "Node|Node configuration",
			"_about":             "Set the options for the manager",
			"version introduced": "4.2.0",
		})

	handle("/api/cfg", "GET", NewCfgGetHandler(mgr),
		map[string]string{
			"_category": "Node|Node configuration",
			"_about": `Returns the node's current view
                       of the cluster's configuration as JSON.`,
			"version introduced": "0.0.1",
		})

	handle("/api/cfgRefresh", "POST", NewCfgRefreshHandler(mgr),
		map[string]string{
			"_category": "Node|Node configuration",
			"_about": `Requests the node to refresh its configuration
                       from the configuration provider.`,
			"version introduced": "0.0.1",
		})

	handle("/api/log", "GET", NewLogGetHandler(mgr, mr),
		map[string]string{
			"_category": "Node|Node diagnostics",
			"_about": `Returns recent log messages
                       and key events for the node as JSON.`,
			"version introduced": "0.0.1",
		})

	handle("/api/manager", "GET", NewManagerHandler(mgr),
		map[string]string{
			"_category":          "Node|Node configuration",
			"_about":             `Returns runtime config information about this node.`,
			"version introduced": "0.4.0",
		})

	handle("/api/managerKick", "POST", NewManagerKickHandler(mgr),
		map[string]string{
			"_category": "Node|Node configuration",
			"_about": `Forces the node to replan resource assignments
                       (by running the planner, if enabled) and to update
                       its runtime state to reflect the latest plan
                       (by running the janitor, if enabled).`,
			"version introduced": "0.0.1",
		})

	handle("/api/managerMeta", "GET", NewManagerMetaHandler(mgr, meta),
		map[string]string{
			"_category": "Node|Node configuration",
			"_about": `Returns information on the node's capabilities,
                       including available indexing and storage options as JSON,
                       and is intended to help management tools and web UI's
                       to be more dynamically metadata driven.`,
			"version introduced": "0.0.1",
		})

	handle("/api/runtime", "GET",
		NewRuntimeGetHandler(versionMain, mgr),
		map[string]string{
			"_category": "Node|Node diagnostics",
			"_about": `Returns information on the node's software,
                       such as version strings and slow-changing
                       runtime settings as JSON.`,
			"version introduced": "0.0.1",
		})

	handle("/api/runtime/args", "GET",
		http.HandlerFunc(RESTGetRuntimeArgs),
		map[string]string{
			"_category": "Node|Node diagnostics",
			"_about": `Returns information on the node's command-line,
                       parameters, environment variables and
                       O/S process values as JSON.`,
			"version introduced": "0.0.1",
		})

	handle("/api/diag", "GET",
		NewDiagGetHandler(versionMain, mgr, mr, assetDir, asset),
		map[string]string{
			"_category": "Node|Node diagnostics",
			"_about": `Returns full set of diagnostic information
                        from the node in one shot as JSON.  That is, the
                        /api/diag response will be the union of the responses
                        from the other REST API diagnostic and monitoring
                        endpoints from the node, and is intended to make
                        production support easier.`,
			"version introduced": "0.0.1",
		})

	handle("/api/ping", "GET", &NoopHandler{},
		map[string]string{
			"_category":          "Node|Node diagnostics",
			"_about":             `Returns an empty body as a quick aliveness check.`,
			"version introduced": "5.0.0",
		})

	handle("/api/runtime/gc", "POST",
		http.HandlerFunc(RESTPostRuntimeGC),
		map[string]string{
			"_category":          "Node|Node management",
			"_about":             `Requests the node to perform a GC.`,
			"version introduced": "0.0.1",
		})

	handle("/api/runtime/profile/cpu", "POST",
		http.HandlerFunc(RESTProfileCPU),
		map[string]string{
			"_category": "Node|Node diagnostics",
			"_about": `Requests the node to capture local
                       cpu usage profiling information.`,
			"version introduced": "0.0.1",
		})

	handle("/api/runtime/profile/memory", "POST",
		http.HandlerFunc(RESTProfileMemory),
		map[string]string{
			"_category": "Node|Node diagnostics",
			"_about": `Requests the node to capture lcoal
                       memory usage profiling information.`,
			"version introduced": "0.0.1",
		})

	handle("/api/runtime/stats", "GET",
		http.HandlerFunc(RESTGetRuntimeStats),
		map[string]string{
			"_category": "Node|Node monitoring",
			"_about": `Returns information on the node's
                       low-level runtime stats as JSON.`,
			"version introduced": "0.0.1",
		})

	handle("/api/runtime/statsMem", "GET",
		http.HandlerFunc(RESTGetRuntimeStatsMem),
		map[string]string{
			"_category": "Node|Node monitoring",
			"_about": `Returns information on the node's
                       low-level GC and memory related runtime stats as JSON.`,
			"version introduced": "0.0.1",
		})

	handle("/api/stats", "GET", NewStatsHandler(mgr),
		map[string]string{
			"_category": "Indexing|Index monitoring",
			"_about": `Returns indexing and data related metrics,
                       timings and counters from the node as JSON.`,
			"version introduced": "0.0.1",
		})

	// TODO: If we ever implement cluster-wide index stats, we should
	// have it under /api/index/{indexName}/stats GET endpoint.
	//
	handle("/api/stats/index/{indexName}", "GET", NewStatsHandler(mgr),
		map[string]string{
			"_category": "Indexing|Index monitoring",
			"_about": `Returns metrics, timings and counters
                       for a single index from the node as JSON.`,
			"version introduced": "0.0.1",
		})

	handle("/api/stats/sourceStats/{indexName}", "GET",
		NewSourceStatsHandler(mgr),
		map[string]string{
			"_category": "Indexing|Index monitoring",
			"_about": `Returns data source specific stats
                       for an index as JSON.`,
			"version introduced": "4.2.0",
		})

	handle("/api/stats/sourcePartitionSeqs/{indexName}", "GET",
		NewSourcePartitionSeqsHandler(mgr),
		map[string]string{
			"_category": "Indexing|Index monitoring",
			"_about": `Returns data source partiton seqs
                       for an index as JSON.`,
			"version introduced": "4.2.0",
		})

	PIndexTypesInitRouter(r, "manager.after", mgr)

	return r, meta, nil
}