Skip to content

Commit

Permalink
feat: add debug service (#177)
Browse files Browse the repository at this point in the history
## Which problem is this PR solving?

More visibility

## Short description of the changes

- Adds debug service that allows you to use pprof to visualize and
analyze profiling data, listening on port 6060
- Also includes pyroscope deltaprof for delta profiles per Liz's comment
on Refinery "On long-lived services like Refinery with large process
uptimes, the number of distinct stacks and code addresses grows in the
profile as time goes on; this causes high numbers of bytes to be scraped
when collecting profiles for timeseries that aren't increasing in value
very often but have been seen at least once in process lifetime."
- Adjusts debug logging to use `LOG_LEVEL=DEBUG` to separate from this
`DEBUG=true` to avoid all the debug logging and allow these to work
independently of each other

## How to verify that this has the expected result

set `DEBUG=true` and get profiling data
  • Loading branch information
JamieDanielson committed Sep 14, 2023
1 parent e9d70a4 commit 47a2d9d
Show file tree
Hide file tree
Showing 6 changed files with 210 additions and 2 deletions.
5 changes: 5 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import (
"github.com/rs/zerolog/log"
)

// TODO hard-coded for now, make configurable
const DebugAddr = "localhost:6060"

var maxcount = flag.Int("c", -1, "Only grab this many packets, then exit")
var statsevery = flag.Int("stats", 1000, "Output statistics every N packets")
var lazy = flag.Bool("lazy", false, "If true, do lazy decoding")
Expand Down Expand Up @@ -59,6 +62,7 @@ type Config struct {
ChannelBufferSize int
MaxBufferedPagesTotal int
MaxBufferedPagesPerConnection int
DebugAddr string
}

func NewConfig() Config {
Expand Down Expand Up @@ -86,6 +90,7 @@ func NewConfig() Config {
ChannelBufferSize: *channelBufferSize,
MaxBufferedPagesTotal: *maxBufferedPagesTotal,
MaxBufferedPagesPerConnection: *maxBufferedPagesPerConnection,
DebugAddr: DebugAddr,
}

// Add filters to only capture common HTTP methods
Expand Down
186 changes: 186 additions & 0 deletions debug/debug_service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package debug

import (
"encoding/json"
"fmt"
"html/template"
"net"
"net/http"
"net/http/pprof"
"os"
"runtime"
"strconv"
"sync"
"syscall"

"github.com/honeycombio/ebpf-agent/config"
deltaprof "github.com/pyroscope-io/godeltaprof/http/pprof"
metrics "github.com/rcrowley/go-metrics"
"github.com/rcrowley/go-metrics/exp"
"github.com/rs/zerolog/log"
)

const addr = "localhost:6060"

// injectable debug service
type DebugService struct {
mux *http.ServeMux
urls []string
expVars map[string]interface{}
mutex sync.RWMutex
Config config.Config
}

func (s *DebugService) Start() error {
// Enables 1% mutex profiling, and 1s block profiling
// Values from github.com/DataDog/go-profiler-notes/blob/main/block.md#usage
runtime.SetBlockProfileRate(1000000)
runtime.SetMutexProfileFraction(100)

s.expVars = make(map[string]interface{})

s.mux = http.NewServeMux()

// Add to the mux but don't add an index entry.
s.mux.HandleFunc("/", s.indexHandler)

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

s.HandleFunc("/debug/pprof/delta_heap", deltaprof.Heap)
s.HandleFunc("/debug/pprof/delta_block", deltaprof.Block)
s.HandleFunc("/debug/pprof/delta_mutex", deltaprof.Mutex)

s.HandleFunc("/debug/vars", s.expvarHandler)
s.Handle("/debug/metrics", exp.ExpHandler(metrics.DefaultRegistry))
s.Publish("cmdline", os.Args)
s.Publish("memstats", Func(memstats))

go func() {
configAddr := s.Config.DebugAddr
if configAddr != "" {
host, portStr, _ := net.SplitHostPort(configAddr)
addr := net.JoinHostPort(host, portStr)
log.Info().
Str("addr", addr).
Msg("Debug service listening")

err := http.ListenAndServe(addr, s.mux)
log.Debug().
Err(err).
Msg("debug http server error")
} else {
// Prefer to listen on addr, but will try to bind to the next 9 ports
// in case you have multiple services running on the same host.
for i := 0; i < 10; i++ {
host, portStr, _ := net.SplitHostPort(addr)
port, _ := strconv.Atoi(portStr)
port += i
addr := net.JoinHostPort(host, fmt.Sprint(port))

log.Info().
Str("addr", addr).
Msg("Debug service listening")

err := http.ListenAndServe(addr, s.mux)
log.Debug().
Err(err).
Msg("debug http server error")

if err, ok := err.(*net.OpError); ok {
if err, ok := err.Err.(*os.SyscallError); ok {
if err.Err == syscall.EADDRINUSE {
// address already in use, try another
continue
}
}
}
break
}
}
}()

return nil
}

// Use Handle and HandleFunc to add new services on the internal debugging port.
func (s *DebugService) Handle(pattern string, handler http.Handler) {
s.mutex.Lock()
defer s.mutex.Unlock()

s.urls = append(s.urls, pattern)
s.mux.Handle(pattern, handler)
}

func (s *DebugService) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) {
s.mutex.Lock()
defer s.mutex.Unlock()

s.urls = append(s.urls, pattern)
s.mux.HandleFunc(pattern, handler)
}

// Publish an expvar at /debug/vars, possibly using Func
func (s *DebugService) Publish(name string, v interface{}) {
s.mutex.Lock()
defer s.mutex.Unlock()
if _, existing := s.expVars[name]; existing {
log.Panic().Msg("Reuse of exported var name: " + name)
}
s.expVars[name] = v
}

func (s *DebugService) indexHandler(w http.ResponseWriter, req *http.Request) {
s.mutex.RLock()
defer s.mutex.RUnlock()

if err := indexTmpl.Execute(w, s.urls); err != nil {
log.Debug().Err(err).Msg("error rendering debug index")
}
}

var indexTmpl = template.Must(template.New("index").Parse(`
<html>
<head>
<title>Debug Index</title>
</head>
<body>
<h2>Index</h2>
<table>
{{range .}}
<tr><td><a href="{{.}}?debug=1">{{.}}</a>
{{end}}
</table>
</body>
</html>
`))

func (s *DebugService) expvarHandler(w http.ResponseWriter, r *http.Request) {
s.mutex.RLock()
defer s.mutex.RUnlock()

w.Header().Set("Content-Type", "application/json; charset=utf-8")
values := make(map[string]interface{}, len(s.expVars))
for k, v := range s.expVars {
if f, ok := v.(Func); ok {
v = f()
}
values[k] = v
}
b, err := json.MarshalIndent(values, "", " ")
if err != nil {
log.Debug().Err(err).Msg("error encoding expvars")
}
w.Write(b)
}

func memstats() interface{} {
stats := new(runtime.MemStats)
runtime.ReadMemStats(stats)
return *stats
}

type Func func() interface{}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pyroscope-io/godeltaprof v0.1.2
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pyroscope-io/godeltaprof v0.1.2 h1:MdlEmYELd5w+lvIzmZvXGNMVzW2Qc9jDMuJaPOR75g4=
github.com/pyroscope-io/godeltaprof v0.1.2/go.mod h1:psMITXp90+8pFenXkKIpNhrfmI9saQnPbba27VIaiQE=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c=
Expand Down
7 changes: 6 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/honeycombio/ebpf-agent/assemblers"
"github.com/honeycombio/ebpf-agent/config"
"github.com/honeycombio/ebpf-agent/debug"
"github.com/honeycombio/ebpf-agent/utils"
"github.com/honeycombio/libhoney-go"
"github.com/rs/zerolog"
Expand All @@ -29,9 +30,13 @@ const defaultEndpoint = "https://api.honeycomb.io"
func main() {
// Set logging level
zerolog.SetGlobalLevel(zerolog.InfoLevel)
if os.Getenv("DEBUG") == "true" {
if os.Getenv("LOG_LEVEL") == "DEBUG" {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
}
if os.Getenv("DEBUG") == "true" {
debug := &debug.DebugService{}
debug.Start()
}
// TODO: add a flag to enable human readable logs
// log.Logger = log.Output(zerolog.NewConsoleWriter())

Expand Down
8 changes: 7 additions & 1 deletion smoke-tests/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ spec:
image: hny/ebpf-agent:local
# image: ghcr.io/honeycombio/ebpf-agent:latest
imagePullPolicy: IfNotPresent
# uncomment this to enable profiling listener on port 6060
# ports:
# - containerPort: 6060
env:
- name: HONEYCOMB_API_KEY
valueFrom:
Expand All @@ -89,7 +92,10 @@ spec:
key: api-key
- name: HONEYCOMB_DATASET
value: hny-ebpf-agent
## uncomment this to enable debug logs
## uncomment this to enable debug log level
# - name: LOG_LEVEL
# value: "DEBUG"
## uncomment this to enable profiling
# - name: DEBUG
# value: "true"
args:
Expand Down

0 comments on commit 47a2d9d

Please sign in to comment.