This commit is contained in:
Paulo Henrique 2026-06-22 23:09:31 +00:00 committed by GitHub
commit d366b16205
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 175 additions and 4 deletions

View file

@ -34,6 +34,7 @@ import (
"os"
"path"
"regexp"
"runtime"
"slices"
"strconv"
"strings"
@ -263,6 +264,7 @@ func (admin *AdminConfig) newAdminHandler(addr NetworkAddress, remote bool, ctx
addRoute("/"+rawConfigKey+"/", handlerLabel, AdminHandlerFunc(handleConfig))
addRoute("/id/", handlerLabel, AdminHandlerFunc(handleConfigID))
addRoute("/stop", handlerLabel, AdminHandlerFunc(handleStop))
addRoute("/status", handlerLabel, AdminHandlerFunc(handleStatus))
// register debugging endpoints
addRouteWithMetrics("/debug/pprof/", handlerLabel, http.HandlerFunc(pprof.Index))
@ -1483,3 +1485,57 @@ var (
localAdminServer, remoteAdminServer *http.Server
identityCertCache *certmagic.Cache
)
// handleStatus returns a snapshot of the current state of the Caddy instance.
func handleStatus(w http.ResponseWriter, r *http.Request) error {
if r.Method != http.MethodGet {
return APIError{
HTTPStatus: http.StatusMethodNotAllowed,
Err: fmt.Errorf("method not allowed"),
}
}
ctx := ActiveContext()
_, fullVer := Version()
var mem runtime.MemStats
runtime.ReadMemStats(&mem)
// Build the base status structure
statusObj := map[string]any{
"version": fullVer,
"uptime_secs": time.Since(ProcessStartTime).Seconds(),
"memory": map[string]any{
"allocated_bytes": mem.Alloc,
"system_bytes": mem.Sys,
},
"goroutines": runtime.NumGoroutine(),
"apps": map[string]any{},
}
// Iterate through running apps if a config is active
if ctx.cfg != nil {
for appName := range ctx.cfg.AppsRaw {
appIntf, err := ctx.App(appName)
if err != nil {
continue // Ignore if the app instance cannot be retrieved
}
// Dynamically check if the App implements the StatusReporter interface
if reporter, ok := appIntf.(StatusReporter); ok {
appStatus, err := reporter.Status()
if err == nil {
statusObj["apps"].(map[string]any)[appName] = appStatus
} else {
statusObj["apps"].(map[string]any)[appName] = map[string]any{"error": err.Error()}
}
} else {
// The app exists but does not support advanced status reporting yet
statusObj["apps"].(map[string]any)[appName] = map[string]any{"status": "running"}
}
}
}
w.Header().Set("Content-Type", "application/json")
return json.NewEncoder(w).Encode(statusObj)
}

View file

@ -100,6 +100,15 @@ type App interface {
Stop() error
}
// StatusReporter can be implemented by Apps or Modules that wish
// to expose runtime status information via the /status admin endpoint.
type StatusReporter interface {
Status() (any, error)
}
// ProcessStartTime records when the Caddy process started.
var ProcessStartTime = time.Now()
// Run runs the given config, replacing any existing config.
func Run(cfg *Config) error {
cfgJSON, err := json.Marshal(cfg)

View file

@ -15,12 +15,12 @@ func TestCommandsAreAvailable(t *testing.T) {
t.Fatal("default factory failed to build")
}
// check that the default factory has 17 commands; it doesn't
// check that the default factory has 18 commands; it doesn't
// include the commands registered through calls to init in
// other packages
cmds := Commands()
if len(cmds) != 17 {
t.Errorf("expected 17 commands, got %d", len(cmds))
if len(cmds) != 18 {
t.Errorf("expected 18 commands, got %d", len(cmds))
}
commandNames := slices.Collect(maps.Keys(cmds))
@ -30,7 +30,7 @@ func TestCommandsAreAvailable(t *testing.T) {
"adapt", "add-package", "build-info", "completion",
"environ", "fmt", "list-modules", "manpage",
"reload", "remove-package", "run", "start",
"stop", "storage", "upgrade", "validate", "version",
"status", "stop", "storage", "upgrade", "validate", "version",
}
if !reflect.DeepEqual(expectedCommandNames, commandNames) {

106
cmd/status.go Normal file
View file

@ -0,0 +1,106 @@
package caddycmd
import (
"encoding/json"
"flag"
"fmt"
"io"
"net/http"
"os"
"strings"
"github.com/caddyserver/caddy/v2"
)
func init() {
RegisterCommand(Command{
Name: "status",
Func: cmdStatus,
Usage: "[--json] [--address <admin-api-address>] [--config <path>] [--adapter <name>]",
Short: "Prints the status of the running Caddy instance",
Flags: func() *flag.FlagSet {
fs := flag.NewFlagSet("status", flag.ExitOnError)
fs.Bool("json", false, "Output raw JSON instead of human-readable text")
fs.String("address", "", "The address to use to reach the admin API endpoint, if not the default")
fs.String("config", "", "Configuration file")
fs.String("adapter", "", "Name of config adapter to apply")
return fs
}(),
})
}
// cmdStatus implements the 'caddy status' command.
func cmdStatus(fl Flags) (int, error) {
useJSON := fl.Bool("json")
addr := fl.String("address")
cfgFile := fl.String("config")
cfgAdapter := fl.String("adapter")
// Determine the admin API address based on provided flags or defaults
adminAddr, err := DetermineAdminAPIAddress(cfgFile, nil, cfgAdapter, addr)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("could not determine admin API address: %v", err)
}
resp, err := AdminAPIRequest(adminAddr, http.MethodGet, "/status", nil, nil)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("failed to query status: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
// Strictly output raw JSON to stdout for CLI pipelines (e.g., jq)
if useJSON {
os.Stdout.Write(body)
fmt.Println()
return caddy.ExitCodeSuccess, nil
}
var status map[string]any
if err := json.Unmarshal(body, &status); err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("failed to parse status JSON: %v\nRaw output: %s", err, string(body))
}
// Format output to be human-readable
fmt.Printf("Caddy %v\n", status["version"])
fmt.Println("Status: Running")
if uptime, ok := status["uptime_secs"].(float64); ok {
u := int64(uptime)
h := u / 3600
m := (u % 3600) / 60
s := u % 60
if h > 0 {
fmt.Printf("Uptime: %dh %dm %ds\n", h, m, s)
} else if m > 0 {
fmt.Printf("Uptime: %dm %ds\n", m, s)
} else {
fmt.Printf("Uptime: %ds\n", s)
}
}
if apps, ok := status["apps"].(map[string]any); ok && len(apps) > 0 {
var appNames []string
for appName := range apps {
appNames = append(appNames, appName)
}
fmt.Printf("\nRunning apps: %s\n", strings.Join(appNames, ", "))
} else {
fmt.Printf("\nRunning apps: none\n")
}
fmt.Println("\nMemory")
if mem, ok := status["memory"].(map[string]any); ok {
allocMB := mem["allocated_bytes"].(float64) / 1024 / 1024
sysMB := mem["system_bytes"].(float64) / 1024 / 1024
fmt.Printf("Allocated: %.0f MB\nSystem: %.0f MB\n", allocMB, sysMB)
}
fmt.Printf("Goroutines: %v\n", status["goroutines"])
return caddy.ExitCodeSuccess, nil
}