Skip to content

Commit

Permalink
feat(httphandler): Implement debug/pprof endpoints (#131)
Browse files Browse the repository at this point in the history
  • Loading branch information
irvinlim authored Feb 26, 2023
1 parent 32a3ef2 commit 1ec355a
Show file tree
Hide file tree
Showing 17 changed files with 1,257 additions and 133 deletions.
48 changes: 42 additions & 6 deletions apis/config/v1alpha1/managerconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,15 +146,23 @@ type HTTPSpec struct {

// Metrics controls metrics serving.
// +optional
Metrics *MetricsSpec `json:"metrics,omitempty"`
Metrics *HTTPMetricsSpec `json:"metrics,omitempty"`

// Health controls health status serving.
// +optional
Health *HealthSpec `json:"health,omitempty"`
Health *HTTPHealthSpec `json:"health,omitempty"`

// Debug controls debug endpoint serving.
// +optional
Debug *HTTPDebugSpec `json:"debug,omitempty"`

// Pprof controls HTTP serving of runtime profiling data (i.e. pprof).
// +optional
Pprof *HTTPPprofSpec `json:"pprof,omitempty"`
}

type MetricsSpec struct {
// Enabled is whether the controller manager enables serving Prometheus metrics.
type HTTPMetricsSpec struct {
// Enabled is whether the HTTP server enables serving Prometheus metrics.
//
// Default: true
// +optional
Expand All @@ -167,8 +175,8 @@ type MetricsSpec struct {
MetricsPath string `json:"metricsPath,omitempty"`
}

type HealthSpec struct {
// Enabled is whether the controller manager enables serving health probes.
type HTTPHealthSpec struct {
// Enabled is whether the HTTP server enables serving health probes.
//
// Default: true
// +optional
Expand All @@ -187,6 +195,34 @@ type HealthSpec struct {
LivenessProbePath string `json:"livenessProbePath,omitempty"`
}

type HTTPDebugSpec struct {
// Enabled is whether the HTTP server enables debug endpoints.
//
// Default: false
// +optional
Enabled *bool `json:"enabled,omitempty"`

// BasePath is the base path for debug endpoints.
//
// Default: /debug
// +optional
BasePath string `json:"basePath,omitempty"`
}

type HTTPPprofSpec struct {
// Enabled is whether the HTTP server enables pprof endpoints.
//
// Default: false
// +optional
Enabled *bool `json:"enabled,omitempty"`

// IndexPath is the index path for pprof endpoints.
//
// Default: /debug/pprof
// +optional
IndexPath string `json:"indexPath,omitempty"`
}

type ExecutionControllerConcurrencySpec struct {
// Control the concurrency for the Job controller.
//
Expand Down
122 changes: 86 additions & 36 deletions apis/config/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion cmd/execution-controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ func main() {

// Start HTTP server in background.
go func() {
if err := httphandler.ListenAndServe(ctx, options.HTTP, mgr); err != nil {
if err := httphandler.ListenAndServeHTTP(ctx, options.HTTP, mgr, &options, ctrlContext.Configs()); err != nil {
klog.Fatalf("cannot start http handlers: %v", err)
}
}()
Expand Down
2 changes: 1 addition & 1 deletion cmd/execution-webhook/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ func main() {

// Start HTTP server in background.
go func() {
if err := httphandler.ListenAndServe(ctx, options.HTTP, mgr); err != nil {
if err := httphandler.ListenAndServeHTTP(ctx, options.HTTP, mgr, &options, ctrlContext.Configs()); err != nil {
klog.Fatalf("cannot start http handlers: %v", err)
}
}()
Expand Down
125 changes: 125 additions & 0 deletions pkg/runtime/httphandler/debug.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Copyright 2022 The Furiko Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package httphandler

import (
"net/http"
"path"

"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/json"
"k8s.io/klog/v2"

configv1alpha1 "github.com/furiko-io/furiko/apis/config/v1alpha1"
"github.com/furiko-io/furiko/pkg/runtime/controllercontext"
)

const (
defaultDebugBasePath = "/debug"
)

// DebugHandler is a delegate to fetch debug information.
type DebugHandler interface {
GetBootstrapConfig() runtime.Object
GetDynamicConfig() (map[configv1alpha1.ConfigName]runtime.Object, error)
}

type defaultDebugHandler struct {
bootstrapConfig runtime.Object
configsGetter controllercontext.Configs
}

var _ DebugHandler = (*defaultDebugHandler)(nil)

func (h *defaultDebugHandler) GetBootstrapConfig() runtime.Object {
return h.bootstrapConfig
}

func (h *defaultDebugHandler) GetDynamicConfig() (map[configv1alpha1.ConfigName]runtime.Object, error) {
return h.configsGetter.AllConfigs()
}

// ServeDebug adds debug handlers to the given serve mux.
func ServeDebug(mux *http.ServeMux, cfg *configv1alpha1.HTTPDebugSpec, handler DebugHandler) {
if cfg == nil {
return
}
if enabled := cfg.Enabled; enabled == nil || !*enabled {
return
}

basePath := cfg.BasePath
if basePath == "" {
basePath = defaultDebugBasePath
}

// Clean the path to remove any trailing slashes.
basePath = path.Clean(basePath)

server := newDebugServer(handler)
mux.HandleFunc(basePath+"/config/bootstrap", server.HandleBootstrapConfig())
mux.HandleFunc(basePath+"/config/dynamic", server.HandleDynamicConfig())

klog.V(4).Infof("httphandler: added http handler for health probes")
}

type debugServer struct {
handler DebugHandler
}

func newDebugServer(handler DebugHandler) *debugServer {
return &debugServer{
handler: handler,
}
}

func (s *debugServer) HandleBootstrapConfig() http.HandlerFunc {
return s.makeJSONHandler(s.handler.GetBootstrapConfig())
}

func (s *debugServer) HandleDynamicConfig() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
configs, err := s.handler.GetDynamicConfig()
if err != nil {
s.makeErrorHandler(err).ServeHTTP(w, r)
return
}
s.makeJSONHandler(configs).ServeHTTP(w, r)
}
}

func (s *debugServer) makeJSONHandler(data interface{}) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
body, err := json.Marshal(data)
if err != nil {
s.makeErrorHandler(err).ServeHTTP(w, r)
return
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write(body)
}
}

func (s *debugServer) makeErrorHandler(err error) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
body, _ := json.Marshal(map[string]interface{}{
"error": err.Error(),
})
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write(body)
}
}
Loading

0 comments on commit 1ec355a

Please sign in to comment.