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
1 change: 1 addition & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ type Config struct {
APIKey string `yaml:"api_key" env:"API_KEY" desc:"API key for authenticating requests to the Outpost API." required:"Y"`
APIJWTSecret string `yaml:"api_jwt_secret" env:"API_JWT_SECRET" desc:"Secret key for signing and verifying JWTs if JWT authentication is used for the API." required:"Y"`
GinMode string `yaml:"gin_mode" env:"GIN_MODE" desc:"Sets the Gin framework mode (e.g., 'debug', 'release', 'test'). See Gin documentation for details." required:"N"`
PprofEnabled bool `yaml:"pprof_enabled" env:"PPROF_ENABLED" desc:"If true, exposes Go pprof profiling endpoints under /debug/pprof/ on the service's HTTP port (every service type). Unauthenticated; enable only when the port is not publicly reachable." required:"N" default:"false"`

// Application
DeploymentID string `yaml:"deployment_id" env:"DEPLOYMENT_ID" desc:"Optional deployment identifier for multi-tenancy. Enables multiple deployments to share the same infrastructure while maintaining data isolation." required:"N"`
Expand Down
3 changes: 3 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ func TestDefaultValues(t *testing.T) {

// Test only fields that have explicit defaults
assert.Equal(t, 3333, cfg.APIPort)
assert.False(t, cfg.PprofEnabled)
assert.Equal(t, "127.0.0.1", cfg.Redis.Host)
assert.Equal(t, 6379, cfg.Redis.Port)
assert.Equal(t, "outpost", cfg.MQs.RabbitMQ.Exchange)
Expand Down Expand Up @@ -110,6 +111,7 @@ retry_interval_seconds: 60
max_destinations_per_tenant: 50
delivery_timeout_seconds: 10
aes_encryption_secret: test-secret
pprof_enabled: true
`),
},
envVars: map[string]string{
Expand All @@ -133,6 +135,7 @@ aes_encryption_secret: test-secret
assert.Equal(t, 5, cfg.LogMaxConcurrency)
assert.Equal(t, 60, cfg.RetryIntervalSeconds)
assert.Equal(t, 50, cfg.MaxDestinationsPerTenant)
assert.True(t, cfg.PprofEnabled)
assert.Equal(t, 10, cfg.DeliveryTimeoutSeconds)
assert.Equal(t, "test-secret", cfg.AESEncryptionSecret)
// assert.Equal(t, "localhost:9000", cfg.ClickHouse.Addr)
Expand Down
2 changes: 1 addition & 1 deletion internal/services/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func (b *ServiceBuilder) BuildWorkers() (*worker.WorkerSupervisor, error) {

// Create base router with health check that all services will extend
b.logger.Debug("creating base router with health check")
baseRouter := NewBaseRouter(b.supervisor, b.cfg.GinMode)
baseRouter := NewBaseRouter(b.supervisor, b.cfg.GinMode, b.cfg.PprofEnabled)

if serviceType == config.ServiceTypeAPI || serviceType == config.ServiceTypeAll {
if err := b.BuildAPIWorkers(baseRouter); err != nil {
Expand Down
20 changes: 19 additions & 1 deletion internal/services/health.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package services

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

"github.com/gin-gonic/gin"
"github.com/hookdeck/outpost/internal/worker"
Expand All @@ -26,7 +27,7 @@ func HealthHandler(supervisor *worker.WorkerSupervisor) gin.HandlerFunc {
// TODO: Rethink API versioning strategy in the future.
// For now, we expose health check at both /healthz and /api/v1/healthz for backwards compatibility.
// The /api/v1 prefix is hardcoded here but should be part of a broader versioning approach.
func NewBaseRouter(supervisor *worker.WorkerSupervisor, ginMode string) *gin.Engine {
func NewBaseRouter(supervisor *worker.WorkerSupervisor, ginMode string, pprofEnabled bool) *gin.Engine {
gin.SetMode(ginMode)
r := gin.New()
r.Use(gin.Recovery())
Expand All @@ -35,5 +36,22 @@ func NewBaseRouter(supervisor *worker.WorkerSupervisor, ginMode string) *gin.Eng
r.GET("/healthz", healthHandler)
r.GET("/api/v1/healthz", healthHandler)

if pprofEnabled {
registerPprof(r)
}

return r
}

// registerPprof mounts net/http/pprof under /debug/pprof/. The handlers are
// unauthenticated, so this is opt-in via config.
func registerPprof(r *gin.Engine) {
r.GET("/debug/pprof/", gin.WrapF(pprof.Index))
r.GET("/debug/pprof/cmdline", gin.WrapF(pprof.Cmdline))
r.GET("/debug/pprof/profile", gin.WrapF(pprof.Profile))
r.GET("/debug/pprof/symbol", gin.WrapF(pprof.Symbol))
r.GET("/debug/pprof/trace", gin.WrapF(pprof.Trace))
r.GET("/debug/pprof/:name", func(c *gin.Context) {
pprof.Handler(c.Param("name")).ServeHTTP(c.Writer, c.Request)
})
}
40 changes: 40 additions & 0 deletions internal/services/health_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package services_test

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

"github.com/hookdeck/outpost/internal/logging"
"github.com/hookdeck/outpost/internal/services"
"github.com/hookdeck/outpost/internal/worker"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestNewBaseRouter_Pprof(t *testing.T) {
logger, err := logging.NewLogger(logging.WithLogLevel("error"))
require.NoError(t, err)
supervisor := worker.NewWorkerSupervisor(logger)

get := func(r http.Handler, path string) int {
rec := httptest.NewRecorder()
r.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil))
return rec.Code
}

t.Run("disabled by default", func(t *testing.T) {
r := services.NewBaseRouter(supervisor, "test", false)
assert.Equal(t, http.StatusNotFound, get(r, "/debug/pprof/"))
assert.Equal(t, http.StatusNotFound, get(r, "/debug/pprof/heap"))
assert.Equal(t, http.StatusOK, get(r, "/healthz"))
})

t.Run("enabled", func(t *testing.T) {
r := services.NewBaseRouter(supervisor, "test", true)
assert.Equal(t, http.StatusOK, get(r, "/debug/pprof/"))
assert.Equal(t, http.StatusOK, get(r, "/debug/pprof/heap"))
assert.Equal(t, http.StatusOK, get(r, "/debug/pprof/goroutine"))
assert.Equal(t, http.StatusOK, get(r, "/healthz"))
})
}
Loading