Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions cmd/kepler/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,11 +229,18 @@ func createServices(logger *slog.Logger, cfg *config.Config) ([]service.Service,
prometheus.WithCollectors(collectors),
)

return []service.Service{
services := []service.Service{
promExporter,
apiServer,
pm,
}, nil
}

if cfg.EnablePprof {
pprof := server.NewPprof(apiServer)
services = append(services, pprof)
}

return services, nil
}

func createPowerMonitor(logger *slog.Logger, cfg *config.Config) (*monitor.PowerMonitor, error) {
Expand Down
19 changes: 14 additions & 5 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,23 @@ type (
}

Config struct {
Log Log `yaml:"log"`
Host Host `yaml:"host"`
Dev Dev `yaml:"dev"` // WARN: do not expose dev settings as flags
Log Log `yaml:"log"`
Host Host `yaml:"host"`
Dev Dev `yaml:"dev"` // WARN: do not expose dev settings as flags
EnablePprof bool `yaml:"enable-pprof"`
Copy link
Collaborator

@sthaha sthaha Apr 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: What do you think about (separate PR / task)

debug:
  enable-pprof: true #
  collectors:  # so that we group all debugging under debug
   - go
   - process

}
)

const (
// Flags
LogLevelFlag = "log.level"
LogFormatFlag = "log.format"
LogLevelFlag = "log.level"
LogFormatFlag = "log.format"

HostSysFSFlag = "host.sysfs"
HostProcFSFlag = "host.procfs"

EnablePprofFlag = "enable.pprof"

// WARN: dev settings shouldn't be exposed as flags as flags are intended for end users
)

Expand Down Expand Up @@ -123,6 +127,7 @@ func RegisterFlags(app *kingpin.Application) ConfigUpdaterFn {
logFormat := app.Flag(LogFormatFlag, "Logging format: text or json").Default("text").Enum("text", "json")
hostSysFS := app.Flag(HostSysFSFlag, "Host sysfs path").Default("/sys").ExistingDir()
hostProcFS := app.Flag(HostProcFSFlag, "Host procfs path").Default("/proc").ExistingDir()
enablePprof := app.Flag(EnablePprofFlag, "Enable pprof").Default("false").Bool()
return func(cfg *Config) error {
// Logging settings
if flagsSet[LogLevelFlag] {
Expand All @@ -141,6 +146,10 @@ func RegisterFlags(app *kingpin.Application) ConfigUpdaterFn {
cfg.Host.ProcFS = *hostProcFS
}

if flagsSet[EnablePprofFlag] {
cfg.EnablePprof = *enablePprof
}

cfg.sanitize()
return cfg.Validate()
}
Expand Down
33 changes: 33 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -379,3 +379,36 @@ func TestConfigString(t *testing.T) {
})
}
}

func TestEnablePprof(t *testing.T) {
tt := []struct {
name string
args []string
enabled bool
}{{
name: "enable pprof with flag",
args: []string{"--enable.pprof"},
enabled: true,
}, {
name: "disable pprof no flag",
args: []string{"--log.level=debug"},
enabled: false,
}, {
name: "disable pprof with flag",
args: []string{"--no-enable.pprof"},
enabled: false,
}}

for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
app := kingpin.New("test", "Test application")
updateConfig := RegisterFlags(app)
_, parseErr := app.Parse(tc.args)
assert.NoError(t, parseErr, "unexpected flag parsing error")
cfg := DefaultConfig()
err := updateConfig(cfg)
assert.NoError(t, err, "unexpected config update error")
assert.Equal(t, cfg.EnablePprof, tc.enabled, "unexpected flag value")
})
}
}
45 changes: 45 additions & 0 deletions internal/server/pprof.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-FileCopyrightText: 2025 The Kepler Authors
// SPDX-License-Identifier: Apache-2.0

package server

import (
"context"
"net/http"
"net/http/pprof"

"github.com/sustainable-computing-io/kepler/internal/service"
)

type pp struct {
api APIService
}

var _ service.Service = (*pp)(nil)
var _ service.Initializer = (*pp)(nil)

func NewPprof(api APIService) *pp {
return &pp{
api: api,
}
}

func (p *pp) Name() string {
return "pprof"
}

func (p *pp) Init(ctx context.Context) error {
return p.api.Register("/debug/pprof/", "pprof", "Profiling Data", handlers())
}

func handlers() http.Handler {
mux := http.NewServeMux()

mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)

return mux
}
103 changes: 103 additions & 0 deletions internal/server/pprof_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// SPDX-FileCopyrightText: 2025 The Kepler Authors
// SPDX-License-Identifier: Apache-2.0

package server

import (
"context"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"net/http/pprof"
)

// MockAPIService is an implementation of the APIService interface for testing.
type MockAPIService struct {
mock.Mock
}

func (m *MockAPIService) Register(path, name, description string, handler http.Handler) error {
args := m.Called(path, name, description, handler)
return args.Error(0)
}

func (m *MockAPIService) Name() string {
return "mockApiService"
}

// TestNewPprof tests the NewPprof constructor.
func TestNewPprof(t *testing.T) {
api := &MockAPIService{}
p := NewPprof(api)

assert.NotNil(t, p, "NewPprof should return a non-nil pointer")
assert.Equal(t, api, p.api, "NewPprof should set the api field correctly")
}

// TestPprofName tests the Name method.
func TestPprofName(t *testing.T) {
api := &MockAPIService{}
p := NewPprof(api)

name := p.Name()
assert.Equal(t, "pprof", name, "Name should return 'pprof'")
}

// TestPprofInit_Success tests the Init method when the API registration succeeds.
func TestPprofInit_Success(t *testing.T) {
api := &MockAPIService{}
p := NewPprof(api)

// Set up mock expectation
api.On("Register", "/debug/pprof/", "pprof", "Profiling Data", mock.AnythingOfType("*http.ServeMux")).Return(nil)

err := p.Init(context.Background())
assert.NoError(t, err, "Init should not return an error when registration succeeds")
api.AssertExpectations(t)
}

// TestPprofInit_Failure tests the Init method when the API registration fails.
func TestPprofInit_Failure(t *testing.T) {
api := &MockAPIService{}
p := NewPprof(api)

// Set up mock expectation
expectedErr := assert.AnError
api.On("Register", "/debug/pprof/", "pprof", "Profiling Data", mock.AnythingOfType("*http.ServeMux")).Return(expectedErr)

err := p.Init(context.Background())
assert.Error(t, err, "Init should return an error when registration fails")
assert.Equal(t, expectedErr, err, "Init should return the expected error")
api.AssertExpectations(t)
}

// TestPprofHandlers tests the handlers function to ensure it registers the correct pprof endpoints.
func TestPprofHandlers(t *testing.T) {
handler := handlers()
mux, ok := handler.(*http.ServeMux)
assert.True(t, ok, "handlers should return an http.ServeMux")

// Test cases for each pprof endpoint
tests := []struct {
path string
handlerFunc http.HandlerFunc
}{
{"/debug/pprof/", pprof.Index},
{"/debug/pprof/cmdline", pprof.Cmdline},
{"/debug/pprof/profile", pprof.Profile},
{"/debug/pprof/symbol", pprof.Symbol},
{"/debug/pprof/trace", pprof.Trace},
}

for _, tt := range tests {
t.Run(tt.path, func(t *testing.T) {
req := httptest.NewRequest("GET", tt.path, nil)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
assert.NotEqual(t, http.StatusNotFound, rr.Code, "Handler for %s should be registered", tt.path)
})
}
}
1 change: 1 addition & 0 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ func (s *APIServer) Shutdown() error {
}

func (s *APIServer) Register(endpoint, summary, description string, handler http.Handler) error {
s.logger.Debug("Endpoint Registered", "endpoint", endpoint)
s.mux.Handle(endpoint, handler)
s.endpointDescription += fmt.Sprintf("<li> <a href=\"%s\"> %s </a> %s </li>\n", endpoint, summary, description)
return nil
Expand Down