[management,signal,proxy] add pyroscope profiling (#7536)

This commit is contained in:
Pascal Fischer
2026-09-23 18:01:35 +02:00
committed by GitHub
parent 40dffc69ae
commit 7009add7a9
16 changed files with 614 additions and 30 deletions
+127
View File
@@ -0,0 +1,127 @@
package profiling
import (
"errors"
"fmt"
"net/netip"
"net/url"
"os"
"strings"
"sync/atomic"
"github.com/caarlos0/env/v11"
"github.com/grafana/pyroscope-go"
log "github.com/sirupsen/logrus"
)
var errNotConfigured = errors.New("pyroscope not configured")
var started atomic.Bool
type config struct {
Address string `env:"NB_PYROSCOPE_ADDRESS"`
User string `env:"NB_PYROSCOPE_USER,notEmpty"`
Password string `env:"NB_PYROSCOPE_PASSWORD,notEmpty"`
}
func Start(applicationName string) func() {
noop := func() {}
cfg, err := loadConfig()
switch {
case errors.Is(err, errNotConfigured):
log.Info("pyroscope not configured, continuous profiling disabled")
return noop
case err != nil:
log.Errorf("failed to load pyroscope config: %v", err)
return noop
}
// pprof allows one CPU profile per process, so a second profiler (e.g. the
// signal server inside the combined binary) would only log errors.
if !started.CompareAndSwap(false, true) {
log.Warnf("continuous profiling already running in this process, not starting it for %s", applicationName)
return noop
}
tags := map[string]string{}
if hostname, err := os.Hostname(); err == nil {
tags["instance"] = hostname
} else {
log.Warnf("failed to resolve hostname for profile tags: %v", err)
}
profiler, err := pyroscope.Start(pyroscope.Config{
ApplicationName: applicationName,
ServerAddress: cfg.Address,
BasicAuthUser: cfg.User,
BasicAuthPassword: cfg.Password,
Logger: log.StandardLogger(),
Tags: tags,
ProfileTypes: []pyroscope.ProfileType{
pyroscope.ProfileCPU,
pyroscope.ProfileAllocObjects,
pyroscope.ProfileAllocSpace,
pyroscope.ProfileInuseObjects,
pyroscope.ProfileInuseSpace,
},
})
if err != nil {
started.Store(false)
log.Errorf("failed to start continuous profiling: %v", err)
return noop
}
return func() {
_ = profiler.Stop()
started.Store(false)
}
}
func loadConfig() (config, error) {
var cfg config
if err := env.Parse(&cfg); err != nil {
if cfg.Address == "" {
return cfg, errNotConfigured
}
return cfg, fmt.Errorf("failed to parse pyroscope config: %w", err)
}
if cfg.Address == "" {
return cfg, errNotConfigured
}
if err := validateAddress(cfg.Address); err != nil {
return cfg, err
}
return cfg, nil
}
// validateAddress refuses to send the basic-auth credentials in plaintext to
// anything but a loopback or private endpoint.
func validateAddress(address string) error {
u, err := url.Parse(address)
if err != nil {
return fmt.Errorf("invalid pyroscope address %q: %w", address, err)
}
switch u.Scheme {
case "https":
return nil
case "http":
if isLocalOrPrivate(u.Hostname()) {
return nil
}
return fmt.Errorf("insecure pyroscope address %q: use https for non-local endpoints", address)
default:
return fmt.Errorf("pyroscope address %q must use http or https", address)
}
}
func isLocalOrPrivate(host string) bool {
if host == "localhost" || strings.HasSuffix(host, ".localhost") {
return true
}
ip, err := netip.ParseAddr(host)
return err == nil && (ip.IsLoopback() || ip.IsPrivate())
}