Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP pyroscope java #5985

Merged
merged 6 commits into from
Jan 5, 2024
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
265 changes: 133 additions & 132 deletions component/all/all.go

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion component/discovery/process/discover.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ const (
labelProcessID = "__process_pid__"
labelProcessExe = "__meta_process_exe"
labelProcessCwd = "__meta_process_cwd"
labelProcessCommandline = "__meta_process_commandline"
labelProcessContainerID = "__container_id__"
)

type process struct {
pid string
exe string
cwd string
commandline string
containerID string
}

Expand Down Expand Up @@ -76,6 +78,11 @@ func discover(l log.Logger) ([]process, error) {
loge(int(p.Pid), err)
continue
}
commandline, err := p.Cmdline()
if err != nil {
loge(int(p.Pid), err)
continue
}

containerID, err := getLinuxProcessContainerID(l, spid)
if err != nil {
Expand All @@ -86,9 +93,10 @@ func discover(l log.Logger) ([]process, error) {
pid: spid,
exe: exe,
cwd: cwd,
commandline: commandline,
containerID: containerID,
})
_ = level.Debug(l).Log("msg", "found process", "pid", p.Pid, "exe", exe, "cwd", cwd, "container_id", containerID)
//_ = level.Debug(l).Log("msg", "found process", "pid", p.Pid, "exe", exe, "cwd", cwd, "container_id", containerID)
}

return res, nil
Expand Down
128 changes: 128 additions & 0 deletions component/pyroscope/java/asprof/asprof.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package asprof

import (
"bytes"
_ "embed"
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"sync"

"k8s.io/utils/path"
)

// option1: embed the tar.gz file todo make this a module outside of agent repo
// option2: distribute the tar.gz file with the agent docker image

type Profiler struct {
tmpDir string
unpackOnce sync.Once

mutex sync.Mutex
unpackError error
}

func NewProfiler(tmpDir string) *Profiler {
return &Profiler{tmpDir: tmpDir}
}

type Distribution struct {
targz []byte
fname string
version int
extractedDir string
}

const tmpDirMarker = "grafana-agent-asprof"

func (d *Distribution) AsprofPath() string {
if d.version < 300 {
return filepath.Join(d.extractedDir, "bin/profiler.sh")
}
return filepath.Join(d.extractedDir, "bin/asprof")
}

func (p *Profiler) Extract() error {
p.unpackOnce.Do(func() {
for _, d := range AllDistributions() {

Check failure on line 49 in component/pyroscope/java/asprof/asprof.go

View workflow job for this annotation

GitHub Actions / run_tests

undefined: AllDistributions
err := d.Extract(p.tmpDir, tmpDirMarker)
if err != nil {
p.unpackError = err
break
}
}
})
return p.unpackError
}

func (p *Profiler) Execute(dist *Distribution, argv []string) (string, string, error) {
stdout := bytes.NewBuffer(nil)
stderr := bytes.NewBuffer(nil)

exe := dist.AsprofPath()
cmd := exec.Command(exe, argv...)
cmd.Stdout = stdout
cmd.Stderr = stderr
err := cmd.Start()
if err != nil {
return stdout.String(), stderr.String(), fmt.Errorf("asprof failed to start %s: %w", exe, err)
}
err = cmd.Wait()
if err != nil {
return stdout.String(), stderr.String(), fmt.Errorf("asprof failed to run %s: %w", exe, err)
}
return stdout.String(), stderr.String(), nil
}

func (p *Profiler) CopyLib(dist *Distribution, pid int) error {
p.mutex.Lock()
defer p.mutex.Unlock()
src := dist.LibPath()

Check failure on line 82 in component/pyroscope/java/asprof/asprof.go

View workflow job for this annotation

GitHub Actions / run_tests

dist.LibPath undefined (type *Distribution has no field or method LibPath)

libBytes, err := os.ReadFile(src)
if err != nil {
return err
}

dst := ProcessPath(dist.LibPath(), pid)

Check failure on line 89 in component/pyroscope/java/asprof/asprof.go

View workflow job for this annotation

GitHub Actions / run_tests

undefined: ProcessPath

Check failure on line 89 in component/pyroscope/java/asprof/asprof.go

View workflow job for this annotation

GitHub Actions / run_tests

dist.LibPath undefined (type *Distribution has no field or method LibPath)
targetExists, err := path.Exists(path.CheckSymlinkOnly, dst)
if err != nil {
return err
}
if targetExists {
targetLibBytes, err := os.ReadFile(dst)
if err != nil {
return err
}
if !bytes.Equal(libBytes, targetLibBytes) {
return fmt.Errorf("file %s already exists and is different", dst)
}
return nil
} else {
fd, err := os.OpenFile(dst, os.O_CREATE|os.O_EXCL, 0644)
if err != nil {
return fmt.Errorf("failed to create file %s: %w", dst, err)
}
defer fd.Close()
//todo check fd was not manipulated with symlinks
n, err := fd.Write(libBytes)
if err != nil {
return fmt.Errorf("failed to write to file %s: %w", dst, err)
}
if n != len(libBytes) {
return fmt.Errorf("failed to write to file %s %d", dst, n)
}
return nil
}
}

type ProcFile struct {
Path string
PID int
}

func (f *ProcFile) ProcRootPath() string {
return filepath.Join("/proc", strconv.Itoa(f.PID), "root", f.Path)
}
18 changes: 18 additions & 0 deletions component/pyroscope/java/asprof/asprof_amd64.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//go:build linux && amd64

package asprof

import _ "embed"

//go:embed async-profiler-3.0-ea-linux-x64.tar.gz
var glibcDistribution []byte
var glibcDistributionName = "async-profiler-3.0-ea-linux-x64"

// todo
var muslDistribution []byte
var muslDistributionName = "TODO"

func (p *Profiler) TargetLibPath(dist *Distribution, pid int) string {
f := ProcFile{Path: dist.LibPath(), PID: pid}

Check failure on line 16 in component/pyroscope/java/asprof/asprof_amd64.go

View workflow job for this annotation

GitHub Actions / run_tests

dist.LibPath undefined (type *Distribution has no field or method LibPath)
return f.ProcRootPath()
}
16 changes: 16 additions & 0 deletions component/pyroscope/java/asprof/asprof_arm64.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//go:build linux && arm64

package asprof

import _ "embed"

var glibcDistribution []byte
var glibcDistributionName = "TODO"

var muslDistribution []byte
var muslDistributionName = "TODO"

func (p *Profiler) TargetLibPath(dist *Distribution, pid int) string {
f := ProcFile{Path: dist.LibPath(), PID: pid}
return f.ProcRootPath()
}
33 changes: 33 additions & 0 deletions component/pyroscope/java/asprof/asprof_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//go:build darwin

package asprof

import (
_ "embed"
"path/filepath"
)

//go:embed async-profiler-3.0-ea-macos-arm64.tar.gz
var distribution []byte

var macDist = &Distribution{
targz: distribution,
fname: "async-profiler-3.0-ea-macos-arm64.tar.gz",
version: 300,
}

func AllDistributions() []*Distribution {
return []*Distribution{macDist}
}

func DistributionForProcess(pid int) *Distribution {
return macDist
}

func (d *Distribution) LibPath() string {
return filepath.Join(d.extractedDir, "lib/libasyncProfiler.dylib")
}

func ProcessPath(path string, pid int) string {
return path
}
74 changes: 74 additions & 0 deletions component/pyroscope/java/asprof/asprof_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//go:build darwin || linux

package asprof

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestDistributionExtract(t *testing.T) {
d := *DistributionForProcess(os.Getpid())
tmpDir := t.TempDir()
t.Log(tmpDir)

err := d.Extract(tmpDir, tmpDirMarker)
assert.NoError(t, err)
assert.NotEmpty(t, d.extractedDir)
assert.FileExists(t, d.AsprofPath())
assert.FileExists(t, d.LibPath())
libStat1, err := os.Stat(d.LibPath())
assert.NoError(t, err)

d = *DistributionForProcess(os.Getpid()) // extracting second time should just verify
err = d.Extract(tmpDir, tmpDirMarker)
assert.NoError(t, err)
assert.NotEmpty(t, d.extractedDir)
assert.FileExists(t, filepath.Join(d.AsprofPath()))
assert.FileExists(t, filepath.Join(d.LibPath()))
libStat2, err := os.Stat(d.LibPath())
require.NoError(t, err)
require.Equal(t, libStat1.ModTime(), libStat2.ModTime())

file, err := os.OpenFile(d.AsprofPath(), os.O_RDWR, 0)
require.NoError(t, err)
defer file.Close()
file.Write([]byte("hello")) //modify binary
file.Close()

d = *DistributionForProcess(os.Getpid()) // extracting second time should just verify
err = d.Extract(tmpDir, tmpDirMarker)
assert.Error(t, err)
assert.Contains(t, err.Error(), "already exists and is different")
assert.Empty(t, d.extractedDir)

}

func TestDistributionExtractRace(t *testing.T) {
tmpDir := t.TempDir()
race = func(stage, extra string) {
switch stage {
case "mkdir dist":
err := os.MkdirAll(filepath.Join(tmpDir, extra), 0755)
assert.NoError(t, err)
default:
t.Fatal("unexpected stage")
}
}
defer func() {
race = func(stage, extra string) {

}
}()
d := *DistributionForProcess(os.Getpid())
t.Log(tmpDir)
err := d.Extract(tmpDir, tmpDirMarker)
assert.Error(t, err)
assert.Empty(t, d.extractedDir)
assert.NoFileExists(t, filepath.Join(d.AsprofPath()))
assert.NoFileExists(t, filepath.Join(d.LibPath()))
}
Binary file not shown.
Binary file not shown.
Loading
Loading