diff --git a/api/endpoints_debug.go b/api/endpoints_debug.go index 9e81dae..163e948 100644 --- a/api/endpoints_debug.go +++ b/api/endpoints_debug.go @@ -2,10 +2,12 @@ package api import ( "bytes" + "context" "fmt" "net/http" "os" "runtime/pprof" + "time" "github.com/safing/portbase/utils/debug" ) @@ -41,6 +43,42 @@ func registerDebugEndpoints() error { return err } + if err := RegisterEndpoint(Endpoint{ + Path: "debug/cpu", + Read: PermitAnyone, + DataFunc: handleCPUProfile, + Name: "Get CPU Profile", + Description: "", + Parameters: []Parameter{{ + Method: http.MethodGet, + Field: "duration", + Value: "10s", + Description: "Specify the formatting style. The default is simple markdown formatting.", + }}, + }); err != nil { + return err + } + + if err := RegisterEndpoint(Endpoint{ + Path: "debug/heap", + Read: PermitAnyone, + DataFunc: handleHeapProfile, + Name: "Get Heap Profile", + Description: "", + }); err != nil { + return err + } + + if err := RegisterEndpoint(Endpoint{ + Path: "debug/allocs", + Read: PermitAnyone, + DataFunc: handleAllocsProfile, + Name: "Get Allocs Profile", + Description: "", + }); err != nil { + return err + } + if err := RegisterEndpoint(Endpoint{ Path: "debug/info", Read: PermitAnyone, @@ -90,6 +128,55 @@ func printStack(_ *Request) (msg string, err error) { return "stack printed to stdout", nil } +// handleCPUProfile returns the CPU profile. +func handleCPUProfile(ar *Request) (data []byte, err error) { + // Parse duration. + duration := 10 * time.Second + if durationOption := ar.Request.URL.Query().Get("duration"); durationOption != "" { + parsedDuration, err := time.ParseDuration(durationOption) + if err != nil { + return nil, fmt.Errorf("failed to parse duration: %w", err) + } + duration = parsedDuration + } + + // Start CPU profiling. + buf := new(bytes.Buffer) + if err := pprof.StartCPUProfile(buf); err != nil { + return nil, fmt.Errorf("failed to start cpu profile: %w", err) + } + + // Wait for the specified duration. + select { + case <-time.After(duration): + case <-ar.Context().Done(): + pprof.StopCPUProfile() + return nil, context.Canceled + } + + // Stop CPU profiling and return data. + pprof.StopCPUProfile() + return buf.Bytes(), nil +} + +// handleHeapProfile returns the Heap profile. +func handleHeapProfile(ar *Request) (data []byte, err error) { + buf := new(bytes.Buffer) + if err := pprof.Lookup("heap").WriteTo(buf, 0); err != nil { + return nil, fmt.Errorf("failed to write heap profile: %w", err) + } + return buf.Bytes(), nil +} + +// handleAllocsProfile returns the Allocs profile. +func handleAllocsProfile(ar *Request) (data []byte, err error) { + buf := new(bytes.Buffer) + if err := pprof.Lookup("allocs").WriteTo(buf, 0); err != nil { + return nil, fmt.Errorf("failed to write allocs profile: %w", err) + } + return buf.Bytes(), nil +} + // debugInfo returns the debugging information for support requests. func debugInfo(ar *Request) (data []byte, err error) { // Create debug information helper. diff --git a/metrics/config.go b/metrics/config.go index 5815b84..c88ea78 100644 --- a/metrics/config.go +++ b/metrics/config.go @@ -14,13 +14,18 @@ var ( instanceOption config.StringOption cfgOptionInstanceOrder = 0 + CfgOptionCommentKey = "core/metrics/comment" + commentOption config.StringOption + cfgOptionCommentOrder = 0 + CfgOptionPushKey = "core/metrics/push" pushOption config.StringOption cfgOptionPushOrder = 0 - pushFlag string instanceFlag string defaultInstance string + commentFlag string + pushFlag string ) func init() { @@ -32,15 +37,16 @@ func init() { } } + flag.StringVar(&instanceFlag, "metrics-instance", defaultInstance, "set the default metrics instance label for all metrics") + flag.StringVar(&commentFlag, "metrics-comment", "", "set the default metrics comment label") flag.StringVar(&pushFlag, "push-metrics", "", "set default URL to push prometheus metrics to") - flag.StringVar(&instanceFlag, "metrics-instance", defaultInstance, "set the default global instance label") } func prepConfig() error { err := config.Register(&config.Option{ Name: "Metrics Instance Name", Key: CfgOptionInstanceKey, - Description: "Define the prometheus instance label for exported metrics. Please note that changing the instance name will reset persisted metrics.", + Description: "Define the prometheus instance label for all exported metrics. Please note that changing the metrics instance name will reset persisted metrics.", Sensitive: true, OptType: config.OptTypeString, ExpertiseLevel: config.ExpertiseLevelExpert, @@ -58,6 +64,26 @@ func prepConfig() error { } instanceOption = config.Concurrent.GetAsString(CfgOptionInstanceKey, instanceFlag) + err = config.Register(&config.Option{ + Name: "Metrics Comment Label", + Key: CfgOptionCommentKey, + Description: "Define a metrics comment label, which is added to the info metric.", + Sensitive: true, + OptType: config.OptTypeString, + ExpertiseLevel: config.ExpertiseLevelExpert, + ReleaseLevel: config.ReleaseLevelStable, + DefaultValue: commentFlag, + RequiresRestart: true, + Annotations: config.Annotations{ + config.DisplayOrderAnnotation: cfgOptionCommentOrder, + config.CategoryAnnotation: "Metrics", + }, + }) + if err != nil { + return err + } + commentOption = config.Concurrent.GetAsString(CfgOptionCommentKey, commentFlag) + err = config.Register(&config.Option{ Name: "Push Prometheus Metrics", Key: CfgOptionPushKey, diff --git a/metrics/metrics_info.go b/metrics/metrics_info.go index 9c82a9a..a03a360 100644 --- a/metrics/metrics_info.go +++ b/metrics/metrics_info.go @@ -23,6 +23,7 @@ func registerInfoMetric() error { "go_arch": runtime.GOARCH, "go_version": runtime.Version(), "go_compiler": runtime.Compiler, + "comment": commentOption(), }, func() float64 { return 1