Files
alloy/internal/component/pyroscope/java/java.go
korniltsev-grafanista 612bbb3ef1 pyroscope.java: allow custom asprof distributions instead of embeded one. (#4452)
* pyroscope.java: decouple archive extraction from the profiler, remove Profiler(use Distribution).

* playground

* fix args

* cleanup

* todo

* review fix

* review fixes

* pyroscope.java: add integration tests (#4454)

* pyroscope.java: add integration tests

fix package name

Revert "pyroscope.java: Fix java log level parameter (#4440)"

This reverts commit 4909877427.

move the helper to pyroscope package

* second integration test

* revert compose tests

* revert unneeded changes

* fix buildtag

* fix buildtag

* improve start time for pyroscope container

* skip integration test if it's not pyoroscope job

* update makefile

* pyroscope.java: Fix java log level parameter (#4440)

* pyroscope.java: Fix java log level parameter

The version bundled of the async profiler has no loglevel parameter:

```
ts=2025-09-16T08:16:50.898924708Z level=error component_path=/profiling.feature component_id=pyroscope.java.java_pods pid=1184752 err="failed to start: asprof failed to run: asprof failed to run /tmp/alloy-asprof-ae0261b1093f2bc4df44a87300fef98dcdebccb5/bin/asprof: exit status 1  Unrecognized option: --loglevel\n"
```

* Quiet is not a valid argument for the async-profiler cli

It can only be used for attaching using agent

* remove comments

* fix

---------

Co-authored-by: Christian Simon <simon@swine.de>

---------

Co-authored-by: Christian Simon <simon@swine.de>
2025-10-09 15:28:27 +00:00

190 lines
5.2 KiB
Go

//go:build (linux || darwin) && (amd64 || arm64)
package java
import (
"context"
"fmt"
"os"
"sort"
"strconv"
"sync"
"time"
"github.com/go-kit/log"
"github.com/grafana/alloy/internal/component"
"github.com/grafana/alloy/internal/component/discovery"
"github.com/grafana/alloy/internal/component/pyroscope"
"github.com/grafana/alloy/internal/component/pyroscope/java/asprof"
"github.com/grafana/alloy/internal/featuregate"
"github.com/grafana/alloy/internal/runtime/logging/level"
"github.com/prometheus/client_golang/prometheus"
)
const (
LabelProcessID = "__process_pid__"
)
func init() {
component.Register(component.Registration{
Name: "pyroscope.java",
Stability: featuregate.StabilityGenerallyAvailable,
Args: Arguments{},
Build: func(opts component.Options, args component.Arguments) (component.Component, error) {
return New(opts.Logger, opts.Registerer, opts.ID, args.(Arguments))
},
})
}
func New(logger log.Logger, reg prometheus.Registerer, id string, a Arguments) (*Component, error) {
if os.Getuid() != 0 {
return nil, fmt.Errorf("java profiler: must be run as root")
}
var (
dist asprof.Distribution
err error
)
if a.Dist != "" {
dist, err = asprof.NewExtractedDistribution(a.Dist)
if err != nil {
return nil, fmt.Errorf("invalid asprof dist: %w", err)
}
_ = logger.Log("msg", "using extracted asprof dist", "dist", a.Dist)
} else {
dist, err = asprof.ExtractDistribution(asprof.EmbeddedArchive, a.TmpDir, asprof.EmbeddedArchive.DistName())
if err != nil {
return nil, fmt.Errorf("extract asprof: %w", err)
}
_ = logger.Log("msg", "using embedded asprof dist")
}
forwardTo := pyroscope.NewFanout(a.ForwardTo, id, reg)
c := &Component{
logger: logger,
args: a,
forwardTo: forwardTo,
profiler: dist,
pid2process: make(map[int]*profilingLoop),
}
c.updateTargets(a)
return c, nil
}
type debugInfo struct {
ProfiledTargets []*debugInfoProfiledTarget `alloy:"profiled_targets,block"`
}
type debugInfoBytesPerType struct {
Type string `alloy:"type,attr"`
Bytes int64 `alloy:"bytes,attr"`
}
type debugInfoProfiledTarget struct {
TotalBytes int64 `alloy:"total_bytes,attr,optional"`
TotalSamples int64 `alloy:"total_samples,attr,optional"`
LastProfiled time.Time `alloy:"last_profiled,attr,optional"`
LastError time.Time `alloy:"last_error,attr,optional"`
LastProfileBytesPerType map[string]int64 `alloy:"last_profile_bytes_per_type,attr,optional"`
ErrorMsg string `alloy:"error_msg,attr,optional"`
PID int `alloy:"pid,attr"`
Target discovery.Target `alloy:"target,attr"`
}
var (
_ component.DebugComponent = (*Component)(nil)
_ component.Component = (*Component)(nil)
)
type Component struct {
logger log.Logger
args Arguments
forwardTo *pyroscope.Fanout
mutex sync.Mutex
pid2process map[int]*profilingLoop
profiler asprof.Distribution
}
func (j *Component) Run(ctx context.Context) error {
defer func() {
j.stop()
}()
<-ctx.Done()
return nil
}
func (j *Component) DebugInfo() interface{} {
j.mutex.Lock()
defer j.mutex.Unlock()
var di debugInfo
di.ProfiledTargets = make([]*debugInfoProfiledTarget, 0, len(j.pid2process))
for _, proc := range j.pid2process {
di.ProfiledTargets = append(di.ProfiledTargets, proc.debugInfo())
}
// sort by pid
sort.Slice(di.ProfiledTargets, func(i, j int) bool {
return di.ProfiledTargets[i].PID < di.ProfiledTargets[j].PID
})
return &di
}
func (j *Component) Update(args component.Arguments) error {
newArgs := args.(Arguments)
j.forwardTo.UpdateChildren(newArgs.ForwardTo)
j.updateTargets(newArgs)
return nil
}
func (j *Component) updateTargets(args Arguments) {
j.mutex.Lock()
defer j.mutex.Unlock()
j.args = args
active := make(map[int]struct{})
for _, target := range args.Targets {
pidStr, ok := target.Get(LabelProcessID)
if !ok {
_ = level.Error(j.logger).Log("msg", "could not find PID label", "pid", pidStr)
continue
}
pid64, err := strconv.ParseInt(pidStr, 10, 32)
if err != nil {
_ = level.Error(j.logger).Log("msg", "could not convert process ID to a 32 bit integer", "pid", pidStr, "err", err)
continue
}
pid := int(pid64)
_ = level.Debug(j.logger).Log("msg", "active target",
"target", fmt.Sprintf("%+v", target),
"pid", pid)
proc := j.pid2process[pid]
if proc == nil {
proc = newProfilingLoop(pid, target, j.logger, j.profiler, j.forwardTo, j.args.ProfilingConfig)
_ = level.Debug(j.logger).Log("msg", "new process", "target", fmt.Sprintf("%+v", target))
j.pid2process[pid] = proc
} else {
proc.update(target, j.args.ProfilingConfig)
}
active[pid] = struct{}{}
}
for pid := range j.pid2process {
if _, ok := active[pid]; ok {
continue
}
_ = level.Debug(j.logger).Log("msg", "inactive target", "pid", pid)
_ = j.pid2process[pid].Close()
delete(j.pid2process, pid)
}
}
func (j *Component) stop() {
_ = level.Debug(j.logger).Log("msg", "stopping")
j.mutex.Lock()
defer j.mutex.Unlock()
for _, proc := range j.pid2process {
proc.Close()
_ = level.Debug(j.logger).Log("msg", "stopped", "pid", proc.pid)
delete(j.pid2process, proc.pid)
}
}