From 532b52b7522b16c6287269b31d7190a787a2a112 Mon Sep 17 00:00:00 2001 From: Christian Simon Date: Mon, 29 Jan 2024 11:01:34 +0000 Subject: [PATCH 01/19] Implement first iteration of a go analyzer This also adds a test binary that can be run against local containers --- .../discovery/process/analyze/analyze.go | 55 +++++++++++++++++ component/discovery/process/analyze/go.go | 35 +++++++++++ component/discovery/process/discover.go | 22 +++---- component/discovery/process/join_test.go | 28 ++++----- .../discovery/process/list-processes/main.go | 61 +++++++++++++++++++ component/discovery/process/process.go | 2 +- 6 files changed, 177 insertions(+), 26 deletions(-) create mode 100644 component/discovery/process/analyze/analyze.go create mode 100644 component/discovery/process/analyze/go.go create mode 100644 component/discovery/process/list-processes/main.go diff --git a/component/discovery/process/analyze/analyze.go b/component/discovery/process/analyze/analyze.go new file mode 100644 index 000000000000..fb7d4a290ced --- /dev/null +++ b/component/discovery/process/analyze/analyze.go @@ -0,0 +1,55 @@ +package analyze + +import ( + "fmt" + "io" + "os" + "path/filepath" + + "github.com/go-kit/log" + "github.com/go-kit/log/level" +) + +type analyserFunc func(pid string, reader io.ReaderAt, labels map[string]string) error + +func PID(logger log.Logger, pid string) (map[string]string, error) { + m := make(map[string]string) + + procPath := filepath.Join("/proc", pid) + exePath := filepath.Join(procPath, "exe") + + // check if executable exists + _, err := os.Stat(exePath) + if err != nil { + if !os.IsNotExist(err) { + return nil, err + } + + // resolve path relative to mount + // TODO:simonswine, don't think this actually needed, double check + fmt.Println("relative to mount") + dest, err := os.Readlink(filepath.Join(procPath, "exe")) + if err != nil { + return nil, err + } + + exePath = filepath.Join(procPath, "root", dest) + } + + // get path to executable + f, err := os.Open(exePath) + if err != nil { + return nil, err + } + defer f.Close() + + for _, a := range []analyserFunc{analyzeGo} { + if err := a(pid, f, m); err == io.EOF { + break + } else if err != nil { + level.Warn(logger).Log("msg", "error during", "func", "todo", "err", err) + } + } + + return m, nil +} diff --git a/component/discovery/process/analyze/go.go b/component/discovery/process/analyze/go.go new file mode 100644 index 000000000000..e56c86dfa494 --- /dev/null +++ b/component/discovery/process/analyze/go.go @@ -0,0 +1,35 @@ +package analyze + +import ( + "debug/buildinfo" + "io" +) + +const ( + LabelGo = "__meta_process_go__" + LabelGoVersion = "__meta_process_go_version__" + LabelGoModulePath = "__meta_process_go_module_path__" + LabelGoModuleVersion = "__meta_process_go_module_version__" +) + +func analyzeGo(pid string, reader io.ReaderAt, m map[string]string) error { + info, err := buildinfo.Read(reader) + if err != nil { + return err + } + + m[LabelGo] = "true" + + if info.GoVersion != "" { + m[LabelGoVersion] = info.GoVersion + } + + if info.Main.Path != "" { + m[LabelGoModulePath] = info.Main.Path + } + if info.Main.Version != "" { + m[LabelGoModuleVersion] = info.Main.Version + } + + return io.EOF +} diff --git a/component/discovery/process/discover.go b/component/discovery/process/discover.go index 70bcd907cf65..ccf0f2473190 100644 --- a/component/discovery/process/discover.go +++ b/component/discovery/process/discover.go @@ -26,8 +26,8 @@ const ( labelProcessContainerID = "__container_id__" ) -type process struct { - pid string +type Process struct { + PID string exe string cwd string commandline string @@ -36,11 +36,11 @@ type process struct { uid string } -func (p process) String() string { - return fmt.Sprintf("pid=%s exe=%s cwd=%s commandline=%s containerID=%s", p.pid, p.exe, p.cwd, p.commandline, p.containerID) +func (p Process) String() string { + return fmt.Sprintf("pid=%s exe=%s cwd=%s commandline=%s containerID=%s", p.PID, p.exe, p.cwd, p.commandline, p.containerID) } -func convertProcesses(ps []process) []discovery.Target { +func convertProcesses(ps []Process) []discovery.Target { var res []discovery.Target for _, p := range ps { t := convertProcess(p) @@ -49,9 +49,9 @@ func convertProcesses(ps []process) []discovery.Target { return res } -func convertProcess(p process) discovery.Target { +func convertProcess(p Process) discovery.Target { t := make(discovery.Target, 5) - t[labelProcessID] = p.pid + t[labelProcessID] = p.PID if p.exe != "" { t[labelProcessExe] = p.exe } @@ -73,12 +73,12 @@ func convertProcess(p process) discovery.Target { return t } -func discover(l log.Logger, cfg *DiscoverConfig) ([]process, error) { +func Discover(l log.Logger, cfg *DiscoverConfig) ([]Process, error) { processes, err := gopsutil.Processes() if err != nil { return nil, fmt.Errorf("failed to list processes: %w", err) } - res := make([]process, 0, len(processes)) + res := make([]Process, 0, len(processes)) loge := func(pid int, e error) { if errors.Is(e, unix.ESRCH) { return @@ -139,8 +139,8 @@ func discover(l log.Logger, cfg *DiscoverConfig) ([]process, error) { continue } } - res = append(res, process{ - pid: spid, + res = append(res, Process{ + PID: spid, exe: exe, cwd: cwd, commandline: commandline, diff --git a/component/discovery/process/join_test.go b/component/discovery/process/join_test.go index 8ddd7dc7cdf9..c5762f13e502 100644 --- a/component/discovery/process/join_test.go +++ b/component/discovery/process/join_test.go @@ -18,20 +18,20 @@ func TestJoin(t *testing.T) { }{ { []discovery.Target{ - convertProcess(process{ - pid: "239", + convertProcess(Process{ + PID: "239", exe: "/bin/foo", cwd: "/", containerID: "7edda1de1e0d1d366351e478359cf5fa16bb8ab53063a99bb119e56971bfb7e2", }), - convertProcess(process{ - pid: "240", + convertProcess(Process{ + PID: "240", exe: "/bin/bar", cwd: "/tmp", containerID: "7edda1de1e0d1d366351e478359cf5fa16bb8ab53063a99bb119e56971bfb7e2", }), - convertProcess(process{ - pid: "241", + convertProcess(Process{ + PID: "241", exe: "/bin/bash", cwd: "/opt", containerID: "", @@ -85,28 +85,28 @@ func TestJoin(t *testing.T) { }, { []discovery.Target{ - convertProcess(process{ - pid: "239", + convertProcess(Process{ + PID: "239", exe: "/bin/foo", cwd: "/", containerID: "7edda1de1e0d1d366351e478359cf5fa16bb8ab53063a99bb119e56971bfb7e2", }), - convertProcess(process{ - pid: "240", + convertProcess(Process{ + PID: "240", exe: "/bin/bar", cwd: "/", containerID: "", }), }, []discovery.Target{}, []discovery.Target{ - convertProcess(process{ - pid: "239", + convertProcess(Process{ + PID: "239", exe: "/bin/foo", cwd: "/", containerID: "7edda1de1e0d1d366351e478359cf5fa16bb8ab53063a99bb119e56971bfb7e2", }), - convertProcess(process{ - pid: "240", + convertProcess(Process{ + PID: "240", exe: "/bin/bar", cwd: "/", containerID: "", diff --git a/component/discovery/process/list-processes/main.go b/component/discovery/process/list-processes/main.go new file mode 100644 index 000000000000..b38c3b2fb133 --- /dev/null +++ b/component/discovery/process/list-processes/main.go @@ -0,0 +1,61 @@ +package main + +import ( + "os" + "sort" + + "github.com/go-kit/log" + "github.com/go-kit/log/level" + + "github.com/grafana/agent/component/discovery/process" + "github.com/grafana/agent/component/discovery/process/analyze" +) + +var logger = log.NewLogfmtLogger(os.Stderr) + +func run() error { + + processes, err := process.Discover(logger, &process.DiscoverConfig{}) + if err != nil { + return err + } + + var ( + keys = make([]string, 16) + attributes = make([]interface{}, 16) + ) + + for _, p := range processes { + m, err := analyze.PID(logger, p.PID) + if err != nil { + level.Error(logger).Log("msg", "error analyzing process", "pid", p.PID, "err", err) + continue + } + + attributes = attributes[:4] + attributes[0] = "msg" + attributes[1] = "found process" + attributes[2] = "pid" + attributes[3] = p.PID + + keys = keys[:0] + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, k := range keys { + attributes = append(attributes, k, m[k]) + } + + level.Info(logger).Log(attributes...) + } + + return nil +} + +func main() { + if err := run(); err != nil { + level.Error(logger).Log("msg", "failed to discover processes", "err", err) + } +} diff --git a/component/discovery/process/process.go b/component/discovery/process/process.go index a32077ece804..d44718aea822 100644 --- a/component/discovery/process/process.go +++ b/component/discovery/process/process.go @@ -43,7 +43,7 @@ type Component struct { func (c *Component) Run(ctx context.Context) error { doDiscover := func() error { - processes, err := discover(c.l, &c.args.DiscoverConfig) + processes, err := Discover(c.l, &c.args.DiscoverConfig) if err != nil { return err } From 699845271a2dfcf6c5bc52289a41fa3ee747d842 Mon Sep 17 00:00:00 2001 From: Tolya Korniltsev Date: Mon, 29 Jan 2024 21:08:53 +0700 Subject: [PATCH 02/19] gosdk and build setting, use analyze in the discovery --- component/discovery/process/analyze/go.go | 47 +++++++++++++++++-- component/discovery/process/discover.go | 12 +++++ .../discovery/process/list-processes/main.go | 10 +--- 3 files changed, 56 insertions(+), 13 deletions(-) diff --git a/component/discovery/process/analyze/go.go b/component/discovery/process/analyze/go.go index e56c86dfa494..0308c2240cf5 100644 --- a/component/discovery/process/analyze/go.go +++ b/component/discovery/process/analyze/go.go @@ -3,13 +3,23 @@ package analyze import ( "debug/buildinfo" "io" + "regexp" + "strings" ) const ( - LabelGo = "__meta_process_go__" - LabelGoVersion = "__meta_process_go_version__" - LabelGoModulePath = "__meta_process_go_module_path__" - LabelGoModuleVersion = "__meta_process_go_module_version__" + LabelGo = "__meta_process_go__" + LabelGoVersion = "__meta_process_go_version__" + LabelGoModulePath = "__meta_process_go_module_path__" + LabelGoModuleVersion = "__meta_process_go_module_version__" + LabelGoSdk = "__meta_process_go_sdk__" + LabelGoSdkVersion = "__meta_process_go_sdk_version__" + LabelGoDeltaProf = "__meta_process_go_godeltaprof__" + LabelGoDeltaProfVersion = "__meta_process_go_godeltaprof_version__" + LabelGoBuildSettingPrefix = "__meta_process_go_build_setting_" + + goSdkModule = "github.com/grafana/pyroscope-go" + godeltaprofModule = "github.com/grafana/pyroscope-go/godeltaprof" ) func analyzeGo(pid string, reader io.ReaderAt, m map[string]string) error { @@ -23,7 +33,6 @@ func analyzeGo(pid string, reader io.ReaderAt, m map[string]string) error { if info.GoVersion != "" { m[LabelGoVersion] = info.GoVersion } - if info.Main.Path != "" { m[LabelGoModulePath] = info.Main.Path } @@ -31,5 +40,33 @@ func analyzeGo(pid string, reader io.ReaderAt, m map[string]string) error { m[LabelGoModuleVersion] = info.Main.Version } + for _, setting := range info.Settings { + if setting.Key == "vcs.revision" { + k := sanitizeLabelName(setting.Key) + m[LabelGoBuildSettingPrefix+k] = setting.Value + } + } + + for _, dep := range info.Deps { + switch dep.Path { + case goSdkModule: + m[LabelGoSdk] = "true" + m[LabelGoSdkVersion] = dep.Version + case godeltaprofModule: + m[LabelGoDeltaProf] = "true" + m[LabelGoDeltaProfVersion] = dep.Version + default: + //todo should we optionally/configurable include all deps? + continue + } + } + return io.EOF } + +var sanitizeRe = regexp.MustCompile("[^a-zA-Z0-9_]") + +func sanitizeLabelName(s string) string { + s = sanitizeRe.ReplaceAllString(s, "_") + return strings.ToLower(s) +} diff --git a/component/discovery/process/discover.go b/component/discovery/process/discover.go index ccf0f2473190..cd7aea27d84c 100644 --- a/component/discovery/process/discover.go +++ b/component/discovery/process/discover.go @@ -12,6 +12,7 @@ import ( "github.com/go-kit/log" "github.com/go-kit/log/level" "github.com/grafana/agent/component/discovery" + "github.com/grafana/agent/component/discovery/process/analyze" gopsutil "github.com/shirou/gopsutil/v3/process" "golang.org/x/sys/unix" ) @@ -34,6 +35,7 @@ type Process struct { containerID string username string uid string + Analysis map[string]string } func (p Process) String() string { @@ -70,6 +72,10 @@ func convertProcess(p Process) discovery.Target { if p.uid != "" { t[labelProcessUID] = p.uid } + for k, v := range p.Analysis { + t[k] = v + } + return t } @@ -139,6 +145,11 @@ func Discover(l log.Logger, cfg *DiscoverConfig) ([]Process, error) { continue } } + m, err := analyze.PID(l, spid) //todo do not analyze same process and or binary twice + if err != nil { + level.Error(l).Log("msg", "error analyzing process", "pid", spid, "err", err) + continue + } res = append(res, Process{ PID: spid, exe: exe, @@ -147,6 +158,7 @@ func Discover(l log.Logger, cfg *DiscoverConfig) ([]Process, error) { containerID: containerID, username: username, uid: uid, + Analysis: m, }) } diff --git a/component/discovery/process/list-processes/main.go b/component/discovery/process/list-processes/main.go index b38c3b2fb133..0b52b69abfd0 100644 --- a/component/discovery/process/list-processes/main.go +++ b/component/discovery/process/list-processes/main.go @@ -8,7 +8,6 @@ import ( "github.com/go-kit/log/level" "github.com/grafana/agent/component/discovery/process" - "github.com/grafana/agent/component/discovery/process/analyze" ) var logger = log.NewLogfmtLogger(os.Stderr) @@ -26,11 +25,6 @@ func run() error { ) for _, p := range processes { - m, err := analyze.PID(logger, p.PID) - if err != nil { - level.Error(logger).Log("msg", "error analyzing process", "pid", p.PID, "err", err) - continue - } attributes = attributes[:4] attributes[0] = "msg" @@ -39,13 +33,13 @@ func run() error { attributes[3] = p.PID keys = keys[:0] - for k := range m { + for k := range p.Analysis { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { - attributes = append(attributes, k, m[k]) + attributes = append(attributes, k, p.Analysis[k]) } level.Info(logger).Log(attributes...) From d0c993cb40dff045b6903fd06d2bd0efa6ac5a89 Mon Sep 17 00:00:00 2001 From: Tolya Korniltsev Date: Mon, 29 Jan 2024 21:18:07 +0700 Subject: [PATCH 03/19] the more the better --- component/discovery/process/analyze/go.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/component/discovery/process/analyze/go.go b/component/discovery/process/analyze/go.go index 0308c2240cf5..39e92dbd28df 100644 --- a/component/discovery/process/analyze/go.go +++ b/component/discovery/process/analyze/go.go @@ -41,10 +41,8 @@ func analyzeGo(pid string, reader io.ReaderAt, m map[string]string) error { } for _, setting := range info.Settings { - if setting.Key == "vcs.revision" { - k := sanitizeLabelName(setting.Key) - m[LabelGoBuildSettingPrefix+k] = setting.Value - } + k := sanitizeLabelName(setting.Key) + m[LabelGoBuildSettingPrefix+k] = setting.Value } for _, dep := range info.Deps { From 9f35d2c0f5dd2bb632615c26057dd4c4a63bec52 Mon Sep 17 00:00:00 2001 From: Christian Simon Date: Mon, 29 Jan 2024 16:58:37 +0000 Subject: [PATCH 04/19] Discover cpython runtime and python version --- .../discovery/process/analyze/analyze.go | 2 +- component/discovery/process/analyze/python.go | 47 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 component/discovery/process/analyze/python.go diff --git a/component/discovery/process/analyze/analyze.go b/component/discovery/process/analyze/analyze.go index fb7d4a290ced..83a8dd678aa4 100644 --- a/component/discovery/process/analyze/analyze.go +++ b/component/discovery/process/analyze/analyze.go @@ -43,7 +43,7 @@ func PID(logger log.Logger, pid string) (map[string]string, error) { } defer f.Close() - for _, a := range []analyserFunc{analyzeGo} { + for _, a := range []analyserFunc{analyzeGo, analyzePython} { if err := a(pid, f, m); err == io.EOF { break } else if err != nil { diff --git a/component/discovery/process/analyze/python.go b/component/discovery/process/analyze/python.go new file mode 100644 index 000000000000..ccf2d976e25a --- /dev/null +++ b/component/discovery/process/analyze/python.go @@ -0,0 +1,47 @@ +package analyze + +import ( + "debug/elf" + "io" + "strings" +) + +const ( + LabelPython = "__meta_process_python__" + LabelPythonVersion = "__meta_process_python_version__" + + libpythonPrefix = "libpython" +) + +func analyzePython(pid string, reader io.ReaderAt, m map[string]string) error { + e, err := elf.NewFile(reader) + if err != nil { + return err + } + defer e.Close() + + libs, err := e.ImportedLibraries() + if err != nil { + return err + } + + var pythonVersion string + for _, lib := range libs { + if strings.HasPrefix(lib, libpythonPrefix) { + pythonVersion = lib[len(libpythonPrefix):] + pos := strings.Index(pythonVersion, ".so") + if pos < 0 { + continue + } + pythonVersion = pythonVersion[:pos] + break + } + } + if pythonVersion == "" { + return nil + } + m[LabelPython] = "true" + m[LabelPythonVersion] = pythonVersion + + return nil +} From b6eaf65f14cadc6f8eb14621d004fbc45eb5bf86 Mon Sep 17 00:00:00 2001 From: Christian Simon Date: Mon, 29 Jan 2024 17:31:03 +0000 Subject: [PATCH 05/19] Add dotnet analyzer --- .../discovery/process/analyze/analyze.go | 13 ++- component/discovery/process/analyze/dotnet.go | 86 +++++++++++++++++++ go.mod | 3 + go.sum | 3 + 4 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 component/discovery/process/analyze/dotnet.go diff --git a/component/discovery/process/analyze/analyze.go b/component/discovery/process/analyze/analyze.go index 83a8dd678aa4..305f7b34a1de 100644 --- a/component/discovery/process/analyze/analyze.go +++ b/component/discovery/process/analyze/analyze.go @@ -10,7 +10,12 @@ import ( "github.com/go-kit/log/level" ) -type analyserFunc func(pid string, reader io.ReaderAt, labels map[string]string) error +// analyzerFunc is called with a particular pid and a reader into its binary. +// +// If an error occurs analyzing the binary/process information it is returned. +// If there is strong evidence that this process has been detected, the +// analyzer can return io.EOF and it will skip all following analyzers. +type analyzerFunc func(pid string, reader io.ReaderAt, labels map[string]string) error func PID(logger log.Logger, pid string) (map[string]string, error) { m := make(map[string]string) @@ -43,7 +48,11 @@ func PID(logger log.Logger, pid string) (map[string]string, error) { } defer f.Close() - for _, a := range []analyserFunc{analyzeGo, analyzePython} { + for _, a := range []analyzerFunc{ + analyzeGo, + analyzePython, + analyzeDotNet, + } { if err := a(pid, f, m); err == io.EOF { break } else if err != nil { diff --git a/component/discovery/process/analyze/dotnet.go b/component/discovery/process/analyze/dotnet.go new file mode 100644 index 000000000000..7013557d3406 --- /dev/null +++ b/component/discovery/process/analyze/dotnet.go @@ -0,0 +1,86 @@ +package analyze + +import ( + "io" + "path/filepath" + "strconv" + "strings" + + "github.com/prometheus/procfs" + "github.com/pyroscope-io/dotnetdiag" +) + +const ( + LabelDotNet = "__meta_process_dotnet__" + LabelDotNetArch = "__meta_process_dotnet_arch__" + LabelDotNetOS = "__meta_process_dotnet_os__" + LabelDotNetVersion = "__meta_process_dotnet_version__" + LabelDotNetDiagnosticSocket = "__meta_process_dotnet_diagnostic_socket__" + LabelDotNetCommandLine = "__meta_process_dotnet_command_line__" + LabelDotNetAssemblyName = "__meta_process_dotnet_assembly_name__" +) + +func analyzeDotNet(pid string, reader io.ReaderAt, m map[string]string) error { + // small hack: the per process api procfs.Proc doesn't support reading NetUnix, so i am using the global one + procPath := filepath.Join("/proc", pid) + procph, err := procfs.NewFS(procPath) + if err != nil { + return err + } + netunix, err := procph.NetUNIX() + if err != nil { + return err + } + sockets := map[string]*procfs.NetUNIXLine{} + for _, sock := range netunix.Rows { + if !strings.HasPrefix(filepath.Base(sock.Path), "dotnet-diagnostic-") { + continue + } + sockets[strconv.FormatUint(sock.Inode, 10)] = sock + } + + // now get the inodes for the fds of the process and see if they match + pidInt, err := strconv.Atoi(pid) + if err != nil { + return err + } + procp, err := procfs.NewProc(pidInt) + if err != nil { + return err + } + fdinfo, err := procp.FileDescriptorsInfo() + if err != nil { + return err + } + unixSocket := "" + for _, fd := range fdinfo { + sock, found := sockets[fd.Ino] + if !found { + continue + } + unixSocket = filepath.Join(procPath, "root", sock.Path) + break + } + + // bail if no unix socket found + if unixSocket == "" { + return nil + } + + // connect to the dotnet socket and retrieve metadata + ddc := dotnetdiag.NewClient(unixSocket) + info, err := ddc.ProcessInfo2() + if err != nil { + return err + } + + m[LabelDotNet] = "true" + m[LabelDotNetCommandLine] = info.CommandLine + m[LabelDotNetOS] = info.OS + m[LabelDotNetArch] = info.Arch + m[LabelDotNetAssemblyName] = info.AssemblyName + m[LabelDotNetVersion] = info.RuntimeVersion + m[LabelDotNetDiagnosticSocket] = unixSocket + + return io.EOF +} diff --git a/go.mod b/go.mod index c9800748482b..305a0b6cc6f1 100644 --- a/go.mod +++ b/go.mod @@ -614,6 +614,7 @@ require ( github.com/open-telemetry/opentelemetry-collector-contrib/processor/filterprocessor v0.87.0 github.com/open-telemetry/opentelemetry-collector-contrib/receiver/prometheusreceiver v0.87.0 github.com/open-telemetry/opentelemetry-collector-contrib/receiver/vcenterreceiver v0.87.0 + github.com/pyroscope-io/dotnetdiag v0.0.0-00010101000000-000000000000 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.42.0 k8s.io/apimachinery v0.28.3 ) @@ -751,3 +752,5 @@ exclude ( ) replace github.com/github/smimesign => github.com/grafana/smimesign v0.2.1-0.20220408144937-2a5adf3481d3 + +replace github.com/pyroscope-io/dotnetdiag => github.com/simonswine/dotnetdiag v1.2.2-0.20240117150745-9dda1ccf4db2 diff --git a/go.sum b/go.sum index 6710608f0f42..f59c5ca4e563 100644 --- a/go.sum +++ b/go.sum @@ -207,6 +207,7 @@ github.com/Mellanox/rdmamap v0.0.0-20191106181932-7c3c4763a6ee/go.mod h1:jDA6v0T github.com/Microsoft/ApplicationInsights-Go v0.4.2/go.mod h1:CukZ/G66zxXtI+h/VcVn3eVVDGDHfXM2zVILF7bMmsg= github.com/Microsoft/go-winio v0.4.3/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/Microsoft/go-winio v0.4.9/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= +github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= @@ -2100,6 +2101,8 @@ github.com/sijms/go-ora/v2 v2.7.6/go.mod h1:EHxlY6x7y9HAsdfumurRfTd+v8NrEOTR3Xl4 github.com/simonpasquier/klog-gokit v0.3.0/go.mod h1:+SUlDQNrhVtGt2FieaqNftzzk8P72zpWlACateWxA9k= github.com/simonpasquier/klog-gokit/v3 v3.3.0 h1:HMzH999kO5gEgJTaWWO+xjncW5oycspcsBnjn9b853Q= github.com/simonpasquier/klog-gokit/v3 v3.3.0/go.mod h1:uSbnWC3T7kt1dQyY9sjv0Ao1SehMAJdVnUNSKhjaDsg= +github.com/simonswine/dotnetdiag v1.2.2-0.20240117150745-9dda1ccf4db2 h1:f4AkKjudhHgmR0UYxWmWkQFUq1KdyFGxcHZHcSr3LL0= +github.com/simonswine/dotnetdiag v1.2.2-0.20240117150745-9dda1ccf4db2/go.mod h1:eFUEHCp4eD1TgcXMlJihC+R4MrqGf7nTRdWxNADbDHA= github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= From 95986f99490754ccd0cd17fd7a5040be5014cf3a Mon Sep 17 00:00:00 2001 From: Aleksandar Petrov <8142643+aleks-p@users.noreply.github.com> Date: Tue, 30 Jan 2024 17:45:50 -0400 Subject: [PATCH 06/19] Add java analyzer --- .../discovery/process/analyze/analyze.go | 1 + component/discovery/process/analyze/java.go | 155 ++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 component/discovery/process/analyze/java.go diff --git a/component/discovery/process/analyze/analyze.go b/component/discovery/process/analyze/analyze.go index 305f7b34a1de..00f62604ce13 100644 --- a/component/discovery/process/analyze/analyze.go +++ b/component/discovery/process/analyze/analyze.go @@ -52,6 +52,7 @@ func PID(logger log.Logger, pid string) (map[string]string, error) { analyzeGo, analyzePython, analyzeDotNet, + analyzeJava, } { if err := a(pid, f, m); err == io.EOF { break diff --git a/component/discovery/process/analyze/java.go b/component/discovery/process/analyze/java.go new file mode 100644 index 000000000000..ed308b9b01bb --- /dev/null +++ b/component/discovery/process/analyze/java.go @@ -0,0 +1,155 @@ +package analyze + +import ( + "bufio" + "errors" + "io" + "os" + "os/exec" + "path" + "strconv" + "strings" + + "github.com/prometheus/procfs" +) + +const ( + labelJava = "__meta_process_java__" + labelJavaVersion = "__meta_process_java_version__" + labelJavaVersionDate = "__meta_process_java_version_date__" + labelJavaClasspath = "__meta_process_java_classpath__" + LabelJavaHome = "__meta_process_java_home__" + labelJavaVMFlags = "__meta_process_java_vm_flags__" + labelJavaVMType = "__meta_process_java_vm_type__" + labelJavaOsName = "__meta_process_java_os_name__" + labelJavaOsArch = "__meta_process_java_os_arch__" +) + +type jvmInfo struct { + classpath string + javaHome string + javaVersion string + javaVersionDate string + vmFlags string + vmType string + osName string + osArch string +} + +func analyzeJava(pid string, reader io.ReaderAt, m map[string]string) error { + jInfo, err := getInfoFromJcmd(pid) + if err != nil { + jInfo, _ = getInfoFromReleaseFile(pid) + } + if jInfo == nil { + return nil + } + m[labelJava] = "true" + if jInfo.classpath != "" { + m[labelJavaClasspath] = jInfo.classpath + } + if jInfo.javaHome != "" { + m[LabelJavaHome] = jInfo.javaHome + } + if jInfo.javaVersion != "" { + m[labelJavaVersion] = jInfo.javaVersion + } + if jInfo.javaVersionDate != "" { + m[labelJavaVersionDate] = jInfo.javaVersionDate + } + if jInfo.vmFlags != "" { + m[labelJavaVMFlags] = jInfo.vmFlags + } + if jInfo.vmType != "" { + m[labelJavaVMType] = jInfo.vmType + } + if jInfo.osName != "" { + m[labelJavaOsName] = jInfo.osName + } + if jInfo.osArch != "" { + m[labelJavaOsArch] = jInfo.osArch + } + return nil +} + +func getInfoFromJcmd(pid string) (*jvmInfo, error) { + cmd := exec.Command("jcmd", pid, "VM.system_properties") + rawOutput, err := cmd.CombinedOutput() + if err != nil { + return nil, err + } + output := string(rawOutput) + props := strings.Split(output, "\n") + j := &jvmInfo{ + vmType: "jdk", + } + for _, p := range props { + writeValue(p, "java.home", &j.javaHome) + writeValue(p, "java.class.path", &j.classpath) + writeValue(p, "os.name", &j.osName) + writeValue(p, "os.arch", &j.osArch) + writeValue(p, "java.version", &j.javaVersion) + writeValue(p, "java.version.date", &j.javaVersionDate) + } + cmd = exec.Command("jcmd", pid, "VM.flags") + rawOutput, err = cmd.CombinedOutput() + if err != nil { + return j, nil + } + output = string(rawOutput) + parts := strings.Split(output, "\n") + if len(parts) > 1 { + j.vmFlags = parts[1] + } + return j, nil +} + +func getInfoFromReleaseFile(pid string) (*jvmInfo, error) { + pidn, _ := strconv.Atoi(pid) + proc, err := procfs.NewProc(pidn) + if err != nil { + return nil, err + } + + envVars, err := proc.Environ() + if err != nil { + return nil, err + } + javaHome := "" + for _, e := range envVars { + writeValue(e, "JAVA_HOME", &javaHome) + if javaHome != "" { + break + } + } + + if javaHome == "" { + return nil, errors.New("java.home not found") + } + + file, err := os.Open(path.Join(javaHome, "release")) + if err != nil { + return nil, err + } + + scanner := bufio.NewScanner(file) + j := &jvmInfo{ + javaHome: javaHome, + } + + for scanner.Scan() { + p := scanner.Text() + writeValue(p, "JAVA_VERSION", &j.javaVersion) + writeValue(p, "JAVA_VERSION_DATE", &j.javaVersionDate) + writeValue(p, "OS_ARCH", &j.osArch) + writeValue(p, "OS_NAME", &j.osName) + writeValue(p, "IMAGE_TYPE", &j.vmType) + } + return j, nil +} + +func writeValue(p, n string, dest *string) { + if strings.HasPrefix(p, n+"=") { + *dest = strings.Trim(p[len(n)+1:], "\"") + } +} From 664b346049dbf36ce50a121d5d3ae567902fc6dd Mon Sep 17 00:00:00 2001 From: Aleksandar Petrov <8142643+aleks-p@users.noreply.github.com> Date: Tue, 30 Jan 2024 17:57:00 -0400 Subject: [PATCH 07/19] Improve java analyzer --- component/discovery/process/analyze/java.go | 30 ++++++++++++++------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/component/discovery/process/analyze/java.go b/component/discovery/process/analyze/java.go index ed308b9b01bb..34110f6fadce 100644 --- a/component/discovery/process/analyze/java.go +++ b/component/discovery/process/analyze/java.go @@ -37,14 +37,32 @@ type jvmInfo struct { } func analyzeJava(pid string, reader io.ReaderAt, m map[string]string) error { + pidn, _ := strconv.Atoi(pid) + proc, err := procfs.NewProc(pidn) + if err != nil { + return err + } + + cmdLine, err := proc.CmdLine() + isJava := false + for _, c := range cmdLine { + if strings.HasPrefix(c, "java") || strings.HasSuffix(c, "java") { + isJava = true + break + } + } + + if !isJava { + return nil + } + m[labelJava] = "true" jInfo, err := getInfoFromJcmd(pid) if err != nil { - jInfo, _ = getInfoFromReleaseFile(pid) + jInfo, _ = getInfoFromReleaseFile(proc) } if jInfo == nil { return nil } - m[labelJava] = "true" if jInfo.classpath != "" { m[labelJavaClasspath] = jInfo.classpath } @@ -104,13 +122,7 @@ func getInfoFromJcmd(pid string) (*jvmInfo, error) { return j, nil } -func getInfoFromReleaseFile(pid string) (*jvmInfo, error) { - pidn, _ := strconv.Atoi(pid) - proc, err := procfs.NewProc(pidn) - if err != nil { - return nil, err - } - +func getInfoFromReleaseFile(proc procfs.Proc) (*jvmInfo, error) { envVars, err := proc.Environ() if err != nil { return nil, err From 2e51bc433c272b523ca03f0d3695a255069fea85 Mon Sep 17 00:00:00 2001 From: Tolya Korniltsev Date: Wed, 31 Jan 2024 13:04:22 +0700 Subject: [PATCH 08/19] naive c++ detection by looking into linked dynamic libraries --- .../discovery/process/analyze/analyze.go | 1 + component/discovery/process/analyze/cpp.go | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 component/discovery/process/analyze/cpp.go diff --git a/component/discovery/process/analyze/analyze.go b/component/discovery/process/analyze/analyze.go index 00f62604ce13..16c2e8c28e92 100644 --- a/component/discovery/process/analyze/analyze.go +++ b/component/discovery/process/analyze/analyze.go @@ -50,6 +50,7 @@ func PID(logger log.Logger, pid string) (map[string]string, error) { for _, a := range []analyzerFunc{ analyzeGo, + analyzeCpp, analyzePython, analyzeDotNet, analyzeJava, diff --git a/component/discovery/process/analyze/cpp.go b/component/discovery/process/analyze/cpp.go new file mode 100644 index 000000000000..6eb4bf55f043 --- /dev/null +++ b/component/discovery/process/analyze/cpp.go @@ -0,0 +1,33 @@ +package analyze + +import ( + "debug/elf" + "io" + "strings" +) + +const ( + LabelCPP = "__meta_process_cpp__" +) + +func analyzeCpp(pid string, reader io.ReaderAt, m map[string]string) error { + e, err := elf.NewFile(reader) + if err != nil { + return err + } + defer e.Close() + + libs, err := e.ImportedLibraries() + if err != nil { + return err + } + + for _, lib := range libs { + if strings.Contains(lib, "libc++") || strings.Contains(lib, "libstdc++") { + m[LabelCPP] = "true" + break + } + } + + return nil +} From f7e6005afb47fa83a382cec5b2beebcb0a6366cf Mon Sep 17 00:00:00 2001 From: Aleksandar Petrov <8142643+aleks-p@users.noreply.github.com> Date: Wed, 31 Jan 2024 09:15:29 -0400 Subject: [PATCH 09/19] Remove dependencies towards jcmd and the target file system --- component/discovery/process/analyze/java.go | 135 +------------------- 1 file changed, 2 insertions(+), 133 deletions(-) diff --git a/component/discovery/process/analyze/java.go b/component/discovery/process/analyze/java.go index 34110f6fadce..349049a24524 100644 --- a/component/discovery/process/analyze/java.go +++ b/component/discovery/process/analyze/java.go @@ -1,12 +1,7 @@ package analyze import ( - "bufio" - "errors" "io" - "os" - "os/exec" - "path" "strconv" "strings" @@ -14,28 +9,9 @@ import ( ) const ( - labelJava = "__meta_process_java__" - labelJavaVersion = "__meta_process_java_version__" - labelJavaVersionDate = "__meta_process_java_version_date__" - labelJavaClasspath = "__meta_process_java_classpath__" - LabelJavaHome = "__meta_process_java_home__" - labelJavaVMFlags = "__meta_process_java_vm_flags__" - labelJavaVMType = "__meta_process_java_vm_type__" - labelJavaOsName = "__meta_process_java_os_name__" - labelJavaOsArch = "__meta_process_java_os_arch__" + labelJava = "__meta_process_java__" ) -type jvmInfo struct { - classpath string - javaHome string - javaVersion string - javaVersionDate string - vmFlags string - vmType string - osName string - osArch string -} - func analyzeJava(pid string, reader io.ReaderAt, m map[string]string) error { pidn, _ := strconv.Atoi(pid) proc, err := procfs.NewProc(pidn) @@ -46,7 +22,7 @@ func analyzeJava(pid string, reader io.ReaderAt, m map[string]string) error { cmdLine, err := proc.CmdLine() isJava := false for _, c := range cmdLine { - if strings.HasPrefix(c, "java") || strings.HasSuffix(c, "java") { + if strings.Contains(c, "java") { isJava = true break } @@ -56,112 +32,5 @@ func analyzeJava(pid string, reader io.ReaderAt, m map[string]string) error { return nil } m[labelJava] = "true" - jInfo, err := getInfoFromJcmd(pid) - if err != nil { - jInfo, _ = getInfoFromReleaseFile(proc) - } - if jInfo == nil { - return nil - } - if jInfo.classpath != "" { - m[labelJavaClasspath] = jInfo.classpath - } - if jInfo.javaHome != "" { - m[LabelJavaHome] = jInfo.javaHome - } - if jInfo.javaVersion != "" { - m[labelJavaVersion] = jInfo.javaVersion - } - if jInfo.javaVersionDate != "" { - m[labelJavaVersionDate] = jInfo.javaVersionDate - } - if jInfo.vmFlags != "" { - m[labelJavaVMFlags] = jInfo.vmFlags - } - if jInfo.vmType != "" { - m[labelJavaVMType] = jInfo.vmType - } - if jInfo.osName != "" { - m[labelJavaOsName] = jInfo.osName - } - if jInfo.osArch != "" { - m[labelJavaOsArch] = jInfo.osArch - } return nil } - -func getInfoFromJcmd(pid string) (*jvmInfo, error) { - cmd := exec.Command("jcmd", pid, "VM.system_properties") - rawOutput, err := cmd.CombinedOutput() - if err != nil { - return nil, err - } - output := string(rawOutput) - props := strings.Split(output, "\n") - j := &jvmInfo{ - vmType: "jdk", - } - for _, p := range props { - writeValue(p, "java.home", &j.javaHome) - writeValue(p, "java.class.path", &j.classpath) - writeValue(p, "os.name", &j.osName) - writeValue(p, "os.arch", &j.osArch) - writeValue(p, "java.version", &j.javaVersion) - writeValue(p, "java.version.date", &j.javaVersionDate) - } - cmd = exec.Command("jcmd", pid, "VM.flags") - rawOutput, err = cmd.CombinedOutput() - if err != nil { - return j, nil - } - output = string(rawOutput) - parts := strings.Split(output, "\n") - if len(parts) > 1 { - j.vmFlags = parts[1] - } - return j, nil -} - -func getInfoFromReleaseFile(proc procfs.Proc) (*jvmInfo, error) { - envVars, err := proc.Environ() - if err != nil { - return nil, err - } - javaHome := "" - for _, e := range envVars { - writeValue(e, "JAVA_HOME", &javaHome) - if javaHome != "" { - break - } - } - - if javaHome == "" { - return nil, errors.New("java.home not found") - } - - file, err := os.Open(path.Join(javaHome, "release")) - if err != nil { - return nil, err - } - - scanner := bufio.NewScanner(file) - j := &jvmInfo{ - javaHome: javaHome, - } - - for scanner.Scan() { - p := scanner.Text() - writeValue(p, "JAVA_VERSION", &j.javaVersion) - writeValue(p, "JAVA_VERSION_DATE", &j.javaVersionDate) - writeValue(p, "OS_ARCH", &j.osArch) - writeValue(p, "OS_NAME", &j.osName) - writeValue(p, "IMAGE_TYPE", &j.vmType) - } - return j, nil -} - -func writeValue(p, n string, dest *string) { - if strings.HasPrefix(p, n+"=") { - *dest = strings.Trim(p[len(n)+1:], "\"") - } -} From 507cc897b0960e65f4bd8955e7e6af529e7365f4 Mon Sep 17 00:00:00 2001 From: Aleksandar Petrov <8142643+aleks-p@users.noreply.github.com> Date: Wed, 31 Jan 2024 10:16:41 -0400 Subject: [PATCH 10/19] Improve java discovery --- component/discovery/process/analyze/java.go | 27 +++++++++++++-------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/component/discovery/process/analyze/java.go b/component/discovery/process/analyze/java.go index 349049a24524..315fc4f31ee0 100644 --- a/component/discovery/process/analyze/java.go +++ b/component/discovery/process/analyze/java.go @@ -19,18 +19,25 @@ func analyzeJava(pid string, reader io.ReaderAt, m map[string]string) error { return err } - cmdLine, err := proc.CmdLine() - isJava := false - for _, c := range cmdLine { - if strings.Contains(c, "java") { - isJava = true - break - } + executable, err := proc.Executable() + if err != nil { + return err } + if strings.HasSuffix(executable, "java") { + m[labelJava] = "true" + } else { + cmdLine, err := proc.CmdLine() + if err != nil { + return err + } - if !isJava { - return nil + for _, c := range cmdLine { + if strings.HasPrefix(c, "java") { + m[labelJava] = "true" + break + } + } } - m[labelJava] = "true" + return nil } From 125e0b2d4be1d3682ea34cfbfe0f63ef978cafc6 Mon Sep 17 00:00:00 2001 From: Tolya Korniltsev Date: Thu, 1 Feb 2024 16:17:08 +0700 Subject: [PATCH 11/19] analyze cache --- .../discovery/process/analyze/analyze.go | 54 +++---- .../process/analyze/cache/buildid.go | 105 +++++++++++++ .../discovery/process/analyze/cache/cache.go | 145 ++++++++++++++++++ .../process/analyze/cache/cache_test.go | 93 +++++++++++ .../process/analyze/cache/stat_linux.go | 27 ++++ component/discovery/process/analyze/cpp.go | 13 +- component/discovery/process/analyze/dotnet.go | 12 +- component/discovery/process/analyze/go.go | 5 +- component/discovery/process/analyze/java.go | 8 +- component/discovery/process/analyze/python.go | 12 +- component/discovery/process/discover.go | 12 +- .../discovery/process/list-processes/main.go | 9 +- component/discovery/process/process.go | 5 +- 13 files changed, 422 insertions(+), 78 deletions(-) create mode 100644 component/discovery/process/analyze/cache/buildid.go create mode 100644 component/discovery/process/analyze/cache/cache.go create mode 100644 component/discovery/process/analyze/cache/cache_test.go create mode 100644 component/discovery/process/analyze/cache/stat_linux.go diff --git a/component/discovery/process/analyze/analyze.go b/component/discovery/process/analyze/analyze.go index 16c2e8c28e92..52dc6e3e34e0 100644 --- a/component/discovery/process/analyze/analyze.go +++ b/component/discovery/process/analyze/analyze.go @@ -1,53 +1,35 @@ package analyze import ( - "fmt" + "debug/elf" "io" - "os" - "path/filepath" "github.com/go-kit/log" "github.com/go-kit/log/level" ) +type Results struct { + Labels map[string]string +} + +type Input struct { + PID uint32 + PIDs string + File io.ReaderAt + ElfFile *elf.File +} + // analyzerFunc is called with a particular pid and a reader into its binary. // // If an error occurs analyzing the binary/process information it is returned. // If there is strong evidence that this process has been detected, the // analyzer can return io.EOF and it will skip all following analyzers. -type analyzerFunc func(pid string, reader io.ReaderAt, labels map[string]string) error +type analyzerFunc func(input Input, analysis *Results) error -func PID(logger log.Logger, pid string) (map[string]string, error) { - m := make(map[string]string) - - procPath := filepath.Join("/proc", pid) - exePath := filepath.Join(procPath, "exe") - - // check if executable exists - _, err := os.Stat(exePath) - if err != nil { - if !os.IsNotExist(err) { - return nil, err - } - - // resolve path relative to mount - // TODO:simonswine, don't think this actually needed, double check - fmt.Println("relative to mount") - dest, err := os.Readlink(filepath.Join(procPath, "exe")) - if err != nil { - return nil, err - } - - exePath = filepath.Join(procPath, "root", dest) +func Analyze(logger log.Logger, input Input) *Results { + res := &Results{ + Labels: make(map[string]string), } - - // get path to executable - f, err := os.Open(exePath) - if err != nil { - return nil, err - } - defer f.Close() - for _, a := range []analyzerFunc{ analyzeGo, analyzeCpp, @@ -55,12 +37,12 @@ func PID(logger log.Logger, pid string) (map[string]string, error) { analyzeDotNet, analyzeJava, } { - if err := a(pid, f, m); err == io.EOF { + if err := a(input, res); err == io.EOF { break } else if err != nil { level.Warn(logger).Log("msg", "error during", "func", "todo", "err", err) } } - return m, nil + return res } diff --git a/component/discovery/process/analyze/cache/buildid.go b/component/discovery/process/analyze/cache/buildid.go new file mode 100644 index 000000000000..56a561a04df1 --- /dev/null +++ b/component/discovery/process/analyze/cache/buildid.go @@ -0,0 +1,105 @@ +package cache + +import ( + "bytes" + "debug/elf" + "encoding/hex" + "errors" + "fmt" +) + +// copypaste from https://github.com/grafana/pyroscope/blob/8a7fe2b80c219bfda9be685ff27ca1dee4218a42/ebpf/symtab/elf/buildid.go#L31 + +//type BuildID struct { +// ID string +// Typ string +//} +// +//func GNUBuildID(s string) BuildID { +// return BuildID{ID: s, Typ: "gnu"} +//} +//func GoBuildID(s string) BuildID { +// return BuildID{ID: s, Typ: "go"} +//} +// +//func (b *BuildID) Empty() bool { +// return b.ID == "" || b.Typ == "" +//} +// +//func (b *BuildID) GNU() bool { +// return b.Typ == "gnu" +//} + +var ( + ErrNoBuildIDSection = fmt.Errorf("build ID section not found") +) + +func BuildID(f *elf.File) (string, error) { + id, err := GNUBuildID(f) + if err != nil && !errors.Is(err, ErrNoBuildIDSection) { + return "", err + } + if id != "" { + return id, nil + } + id, err = GoBuildID(f) + if err != nil && !errors.Is(err, ErrNoBuildIDSection) { + return "", err + } + if id != "" { + return id, nil + } + + return "", ErrNoBuildIDSection +} + +var goBuildIDSep = []byte("/") + +func GoBuildID(f *elf.File) (string, error) { + buildIDSection := f.Section(".note.go.buildid") + if buildIDSection == nil { + return "", ErrNoBuildIDSection + } + + data, err := buildIDSection.Data() + if err != nil { + return "", fmt.Errorf("reading .note.go.buildid %w", err) + } + if len(data) < 17 { + return "", fmt.Errorf(".note.gnu.build-id is too small") + } + + data = data[16 : len(data)-1] + if len(data) < 40 || bytes.Count(data, goBuildIDSep) < 2 { + return "", fmt.Errorf("wrong .note.go.buildid ") + } + id := string(data) + if id == "redacted" { + return "", fmt.Errorf("blacklisted .note.go.buildid ") + } + return id, nil +} + +func GNUBuildID(f *elf.File) (string, error) { + buildIDSection := f.Section(".note.gnu.build-id") + if buildIDSection == nil { + return "", ErrNoBuildIDSection + } + + data, err := buildIDSection.Data() + if err != nil { + return "", fmt.Errorf("reading .note.gnu.build-id %w", err) + } + if len(data) < 16 { + return "", fmt.Errorf(".note.gnu.build-id is too small") + } + if !bytes.Equal([]byte("GNU"), data[12:15]) { + return "", fmt.Errorf(".note.gnu.build-id is not a GNU build-id") + } + rawBuildID := data[16:] + if len(rawBuildID) != 20 && len(rawBuildID) != 8 { // 8 is xxhash, for example in Container-Optimized OS + return "", fmt.Errorf(".note.gnu.build-id has wrong size ") + } + buildIDHex := hex.EncodeToString(rawBuildID) + return buildIDHex, nil +} diff --git a/component/discovery/process/analyze/cache/cache.go b/component/discovery/process/analyze/cache/cache.go new file mode 100644 index 000000000000..1b91bad51835 --- /dev/null +++ b/component/discovery/process/analyze/cache/cache.go @@ -0,0 +1,145 @@ +package cache + +import ( + "debug/elf" + "os" + "path/filepath" + "strconv" + + "github.com/go-kit/log" + "github.com/grafana/agent/component/discovery/process/analyze" +) + +type Cache struct { + l log.Logger + pids map[uint32]*Entry + stats map[Stat]*Entry + buildIDs map[string]*analyze.Results +} + +func New(logger log.Logger) *Cache { + return &Cache{ + l: logger, + pids: make(map[uint32]*Entry), + stats: make(map[Stat]*Entry), + buildIDs: make(map[string]*analyze.Results), + } +} + +type Entry struct { + Results *analyze.Results + Stat Stat + BuildID string +} + +func (c *Cache) GetPID(pid uint32) *Entry { + return c.pids[pid] +} + +func (c *Cache) Put(pid uint32, a *Entry) { + c.pids[pid] = a + if a.Stat.Inode != 0 && a.Stat.Dev != 0 { + c.stats[a.Stat] = a + } + if a.BuildID != "" { + c.buildIDs[a.BuildID] = a.Results + } +} + +func (c *Cache) GetStat(s Stat) *Entry { + return c.stats[s] +} + +func (c *Cache) GetBuildID(buildID string) *analyze.Results { + if buildID == "" { + return nil + } + return c.buildIDs[buildID] +} +func (c *Cache) AnalyzePID(pid string) (*analyze.Results, error) { + ipid, _ := strconv.Atoi(pid) + exePath := filepath.Join("/proc", pid, "exe") + return c.AnalyzePIDPath(uint32(ipid), pid, exePath) +} +func (c *Cache) AnalyzePIDPath(pid uint32, pidS string, exePath string) (*analyze.Results, error) { + + e := c.GetPID(pid) + if e != nil { + return e.Results, nil + } + + // check if executable exists + fi, err := os.Stat(exePath) + if err != nil { + return nil, err + } + st := StatFromFileInfo(fi) + e = c.GetStat(st) + if e != nil { + c.Put(pid, e) + return e.Results, nil + } + + // get path to executable + f, err := os.Open(exePath) + if err != nil { + return nil, err + } + defer f.Close() + ef, err := elf.NewFile(f) + if err != nil { + return nil, err + } + defer ef.Close() + + buildID, _ := BuildID(ef) + r := c.GetBuildID(buildID) + if r != nil { + c.Put(pid, &Entry{ + Results: r, + Stat: st, + BuildID: buildID, + }) + return r, nil + } + + r = analyze.Analyze(c.l, analyze.Input{ + PID: pid, + PIDs: pidS, + File: f, + ElfFile: ef, + }) + + c.Put(pid, &Entry{ + Results: r, + Stat: st, + BuildID: buildID, + }) + return r, nil +} + +func (c *Cache) GC(active map[uint32]struct{}) { + for pid := range c.pids { + if _, ok := active[pid]; !ok { + delete(c.pids, pid) + } + } + reachableStats := make(map[Stat]struct{}) + reachableBuildIDs := make(map[string]struct{}) + for _, e := range c.pids { + reachableStats[e.Stat] = struct{}{} + if e.BuildID != "" { + reachableBuildIDs[e.BuildID] = struct{}{} + } + } + for s := range c.stats { + if _, ok := reachableStats[s]; !ok { + delete(c.stats, s) + } + } + for id := range c.buildIDs { + if _, ok := reachableBuildIDs[id]; !ok { + delete(c.buildIDs, id) + } + } +} diff --git a/component/discovery/process/analyze/cache/cache_test.go b/component/discovery/process/analyze/cache/cache_test.go new file mode 100644 index 000000000000..893bd24e296c --- /dev/null +++ b/component/discovery/process/analyze/cache/cache_test.go @@ -0,0 +1,93 @@ +package cache + +import ( + "io" + "os" + "testing" + + "github.com/grafana/agent/pkg/util" + "github.com/stretchr/testify/require" +) + +func copyFile(t *testing.T, src, dst string) { + t.Helper() + s, err := os.Open(src) + if err != nil { + t.Fatal(err) + } + defer s.Close() + d, err := os.Create(dst) + if err != nil { + t.Fatal(err) + } + defer d.Close() + _, err = io.Copy(d, s) + if err != nil { + t.Fatal(err) + } +} + +func TestCache(t *testing.T) { + d := t.TempDir() + copyFile(t, "/proc/self/exe", d+"/exe1") + copyFile(t, "/proc/self/exe", d+"/exe2") + err := os.Symlink(d+"/exe1", d+"/exe1-symlink") + require.NoError(t, err) + + l := util.TestLogger(t) + c := New(l) + r1, err := c.AnalyzePIDPath(1, "1", d+"/exe1") + require.NoError(t, err) + r2, err := c.AnalyzePIDPath(1, "1", d+"/exe1") + require.NoError(t, err) + require.True(t, r1 == r2) + + r3, err := c.AnalyzePIDPath(2, "2", d+"/exe1-symlink") + require.NoError(t, err) + require.True(t, r1 == r3) + + require.Equal(t, 2, len(c.pids)) + require.Equal(t, 1, len(c.stats)) + require.Equal(t, 1, len(c.buildIDs)) + + r4, err := c.AnalyzePIDPath(3, "3", d+"/exe2") + require.NoError(t, err) + require.True(t, r1 == r4) + + require.Equal(t, 3, len(c.pids)) + require.Equal(t, 2, len(c.stats)) + require.Equal(t, 1, len(c.buildIDs)) + + c.GC(map[uint32]struct{}{1: {}, 2: {}, 3: {}}) + + require.Equal(t, 3, len(c.pids)) + require.Equal(t, 2, len(c.stats)) + require.Equal(t, 1, len(c.buildIDs)) + + c.GC(map[uint32]struct{}{2: {}, 3: {}}) + + require.Equal(t, 2, len(c.pids)) + require.Equal(t, 2, len(c.stats)) + require.Equal(t, 1, len(c.buildIDs)) + + r3, err = c.AnalyzePIDPath(2, "2", d+"/exe1-symlink") + require.NoError(t, err) + require.True(t, r1 == r3) + + r4, err = c.AnalyzePIDPath(3, "3", d+"/exe2") + require.NoError(t, err) + require.True(t, r1 == r4) + + c.GC(map[uint32]struct{}{3: {}}) + + require.Equal(t, 1, len(c.pids)) + require.Equal(t, 1, len(c.stats)) + require.Equal(t, 1, len(c.buildIDs)) + + c.GC(map[uint32]struct{}{}) + + require.Equal(t, 0, len(c.pids)) + require.Equal(t, 0, len(c.stats)) + require.Equal(t, 0, len(c.buildIDs)) + +} diff --git a/component/discovery/process/analyze/cache/stat_linux.go b/component/discovery/process/analyze/cache/stat_linux.go new file mode 100644 index 000000000000..eefe469bda9c --- /dev/null +++ b/component/discovery/process/analyze/cache/stat_linux.go @@ -0,0 +1,27 @@ +//go:build linux + +package cache + +import ( + "os" + "syscall" +) + +// copypaste from https://github.com/grafana/pyroscope/blob/8a7fe2b80c219bfda9be685ff27ca1dee4218a42/ebpf/symtab/stat_linux.go#L14-L13 + +type Stat struct { + Dev uint64 + Inode uint64 +} + +func StatFromFileInfo(file os.FileInfo) Stat { + sys := file.Sys() + sysStat, ok := sys.(*syscall.Stat_t) + if !ok || sysStat == nil { + return Stat{} + } + return Stat{ + Dev: sysStat.Dev, + Inode: sysStat.Ino, + } +} diff --git a/component/discovery/process/analyze/cpp.go b/component/discovery/process/analyze/cpp.go index 6eb4bf55f043..ad7d892841ac 100644 --- a/component/discovery/process/analyze/cpp.go +++ b/component/discovery/process/analyze/cpp.go @@ -1,8 +1,6 @@ package analyze import ( - "debug/elf" - "io" "strings" ) @@ -10,14 +8,9 @@ const ( LabelCPP = "__meta_process_cpp__" ) -func analyzeCpp(pid string, reader io.ReaderAt, m map[string]string) error { - e, err := elf.NewFile(reader) - if err != nil { - return err - } - defer e.Close() - - libs, err := e.ImportedLibraries() +func analyzeCpp(input Input, a *Results) error { + m := a.Labels + libs, err := input.ElfFile.ImportedLibraries() if err != nil { return err } diff --git a/component/discovery/process/analyze/dotnet.go b/component/discovery/process/analyze/dotnet.go index 7013557d3406..5f6749162dbf 100644 --- a/component/discovery/process/analyze/dotnet.go +++ b/component/discovery/process/analyze/dotnet.go @@ -20,9 +20,10 @@ const ( LabelDotNetAssemblyName = "__meta_process_dotnet_assembly_name__" ) -func analyzeDotNet(pid string, reader io.ReaderAt, m map[string]string) error { +func analyzeDotNet(input Input, a *Results) error { + m := a.Labels // small hack: the per process api procfs.Proc doesn't support reading NetUnix, so i am using the global one - procPath := filepath.Join("/proc", pid) + procPath := filepath.Join("/proc", input.PIDs) procph, err := procfs.NewFS(procPath) if err != nil { return err @@ -40,11 +41,8 @@ func analyzeDotNet(pid string, reader io.ReaderAt, m map[string]string) error { } // now get the inodes for the fds of the process and see if they match - pidInt, err := strconv.Atoi(pid) - if err != nil { - return err - } - procp, err := procfs.NewProc(pidInt) + + procp, err := procfs.NewProc(int(input.PID)) if err != nil { return err } diff --git a/component/discovery/process/analyze/go.go b/component/discovery/process/analyze/go.go index 39e92dbd28df..d9153ed834e8 100644 --- a/component/discovery/process/analyze/go.go +++ b/component/discovery/process/analyze/go.go @@ -22,8 +22,9 @@ const ( godeltaprofModule = "github.com/grafana/pyroscope-go/godeltaprof" ) -func analyzeGo(pid string, reader io.ReaderAt, m map[string]string) error { - info, err := buildinfo.Read(reader) +func analyzeGo(input Input, a *Results) error { + m := a.Labels + info, err := buildinfo.Read(input.File) // it reads elf second time if err != nil { return err } diff --git a/component/discovery/process/analyze/java.go b/component/discovery/process/analyze/java.go index 315fc4f31ee0..505939417956 100644 --- a/component/discovery/process/analyze/java.go +++ b/component/discovery/process/analyze/java.go @@ -1,8 +1,6 @@ package analyze import ( - "io" - "strconv" "strings" "github.com/prometheus/procfs" @@ -12,9 +10,9 @@ const ( labelJava = "__meta_process_java__" ) -func analyzeJava(pid string, reader io.ReaderAt, m map[string]string) error { - pidn, _ := strconv.Atoi(pid) - proc, err := procfs.NewProc(pidn) +func analyzeJava(input Input, a *Results) error { + m := a.Labels + proc, err := procfs.NewProc(int(input.PID)) if err != nil { return err } diff --git a/component/discovery/process/analyze/python.go b/component/discovery/process/analyze/python.go index ccf2d976e25a..1be439f23a33 100644 --- a/component/discovery/process/analyze/python.go +++ b/component/discovery/process/analyze/python.go @@ -1,8 +1,6 @@ package analyze import ( - "debug/elf" - "io" "strings" ) @@ -13,14 +11,10 @@ const ( libpythonPrefix = "libpython" ) -func analyzePython(pid string, reader io.ReaderAt, m map[string]string) error { - e, err := elf.NewFile(reader) - if err != nil { - return err - } - defer e.Close() +func analyzePython(input Input, a *Results) error { + m := a.Labels - libs, err := e.ImportedLibraries() + libs, err := input.ElfFile.ImportedLibraries() if err != nil { return err } diff --git a/component/discovery/process/discover.go b/component/discovery/process/discover.go index cd7aea27d84c..5de4e938e767 100644 --- a/component/discovery/process/discover.go +++ b/component/discovery/process/discover.go @@ -13,6 +13,7 @@ import ( "github.com/go-kit/log/level" "github.com/grafana/agent/component/discovery" "github.com/grafana/agent/component/discovery/process/analyze" + analCache "github.com/grafana/agent/component/discovery/process/analyze/cache" gopsutil "github.com/shirou/gopsutil/v3/process" "golang.org/x/sys/unix" ) @@ -35,7 +36,7 @@ type Process struct { containerID string username string uid string - Analysis map[string]string + Analysis *analyze.Results } func (p Process) String() string { @@ -72,14 +73,14 @@ func convertProcess(p Process) discovery.Target { if p.uid != "" { t[labelProcessUID] = p.uid } - for k, v := range p.Analysis { + for k, v := range p.Analysis.Labels { t[k] = v } return t } -func Discover(l log.Logger, cfg *DiscoverConfig) ([]Process, error) { +func Discover(l log.Logger, cfg *DiscoverConfig, cache *analCache.Cache) ([]Process, error) { processes, err := gopsutil.Processes() if err != nil { return nil, fmt.Errorf("failed to list processes: %w", err) @@ -94,6 +95,7 @@ func Discover(l log.Logger, cfg *DiscoverConfig) ([]Process, error) { } _ = level.Error(l).Log("msg", "failed to get process info", "err", e, "pid", pid) } + active := make(map[uint32]struct{}) for _, p := range processes { spid := fmt.Sprintf("%d", p.Pid) var ( @@ -145,7 +147,7 @@ func Discover(l log.Logger, cfg *DiscoverConfig) ([]Process, error) { continue } } - m, err := analyze.PID(l, spid) //todo do not analyze same process and or binary twice + m, err := cache.AnalyzePID(spid) if err != nil { level.Error(l).Log("msg", "error analyzing process", "pid", spid, "err", err) continue @@ -160,7 +162,9 @@ func Discover(l log.Logger, cfg *DiscoverConfig) ([]Process, error) { uid: uid, Analysis: m, }) + active[uint32(p.Pid)] = struct{}{} } + cache.GC(active) return res, nil } diff --git a/component/discovery/process/list-processes/main.go b/component/discovery/process/list-processes/main.go index 0b52b69abfd0..fd939e0d81d8 100644 --- a/component/discovery/process/list-processes/main.go +++ b/component/discovery/process/list-processes/main.go @@ -6,6 +6,7 @@ import ( "github.com/go-kit/log" "github.com/go-kit/log/level" + analCache "github.com/grafana/agent/component/discovery/process/analyze/cache" "github.com/grafana/agent/component/discovery/process" ) @@ -13,8 +14,8 @@ import ( var logger = log.NewLogfmtLogger(os.Stderr) func run() error { - - processes, err := process.Discover(logger, &process.DiscoverConfig{}) + cache := analCache.New(logger) + processes, err := process.Discover(logger, &process.DiscoverConfig{}, cache) if err != nil { return err } @@ -33,13 +34,13 @@ func run() error { attributes[3] = p.PID keys = keys[:0] - for k := range p.Analysis { + for k := range p.Analysis.Labels { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { - attributes = append(attributes, k, p.Analysis[k]) + attributes = append(attributes, k, p.Analysis.Labels[k]) } level.Info(logger).Log(attributes...) diff --git a/component/discovery/process/process.go b/component/discovery/process/process.go index d44718aea822..83cd5aae6ec6 100644 --- a/component/discovery/process/process.go +++ b/component/discovery/process/process.go @@ -9,6 +9,7 @@ import ( "github.com/go-kit/log" "github.com/grafana/agent/component" "github.com/grafana/agent/component/discovery" + analCache "github.com/grafana/agent/component/discovery/process/analyze/cache" ) func init() { @@ -29,6 +30,7 @@ func New(opts component.Options, args Arguments) (*Component, error) { onStateChange: opts.OnStateChange, argsUpdates: make(chan Arguments), args: args, + analCache: analCache.New(opts.Logger), } return c, nil } @@ -39,11 +41,12 @@ type Component struct { processes []discovery.Target argsUpdates chan Arguments args Arguments + analCache *analCache.Cache } func (c *Component) Run(ctx context.Context) error { doDiscover := func() error { - processes, err := Discover(c.l, &c.args.DiscoverConfig) + processes, err := Discover(c.l, &c.args.DiscoverConfig, c.analCache) if err != nil { return err } From b88c025986c1095a7449f8fa4716477027902b69 Mon Sep 17 00:00:00 2001 From: Aleksandar Petrov <8142643+aleks-p@users.noreply.github.com> Date: Wed, 31 Jan 2024 16:33:30 -0400 Subject: [PATCH 12/19] Analyze java processes using JVM dynamic attach --- component/discovery/process/analyze/java.go | 276 +++++++++++++++++++- 1 file changed, 272 insertions(+), 4 deletions(-) diff --git a/component/discovery/process/analyze/java.go b/component/discovery/process/analyze/java.go index 505939417956..a17731fbc1ac 100644 --- a/component/discovery/process/analyze/java.go +++ b/component/discovery/process/analyze/java.go @@ -1,15 +1,43 @@ package analyze import ( + "bufio" + "errors" + "fmt" + "golang.org/x/sys/unix" + "os" + "os/signal" + "strconv" "strings" + "syscall" + "time" "github.com/prometheus/procfs" ) const ( - labelJava = "__meta_process_java__" + labelJava = "__meta_process_java__" + labelJavaVersion = "__meta_process_java_version__" + labelJavaVersionDate = "__meta_process_java_version_date__" + labelJavaClasspath = "__meta_process_java_classpath__" + LabelJavaHome = "__meta_process_java_home__" + labelJavaVMFlags = "__meta_process_java_vm_flags__" + labelJavaVMType = "__meta_process_java_vm_type__" + labelJavaOsName = "__meta_process_java_os_name__" + labelJavaOsArch = "__meta_process_java_os_arch__" ) +type jvmInfo struct { + classpath string + javaHome string + javaVersion string + javaVersionDate string + vmFlags string + vmType string + osName string + osArch string +} + func analyzeJava(input Input, a *Results) error { m := a.Labels proc, err := procfs.NewProc(int(input.PID)) @@ -21,21 +49,261 @@ func analyzeJava(input Input, a *Results) error { if err != nil { return err } + isJava := false if strings.HasSuffix(executable, "java") { - m[labelJava] = "true" + isJava = true } else { cmdLine, err := proc.CmdLine() if err != nil { return err } - for _, c := range cmdLine { if strings.HasPrefix(c, "java") { - m[labelJava] = "true" + isJava = true break } } } + if !isJava { + return nil + } + m[labelJava] = "true" + jInfo, err := getInfoFromJcmd(int(input.PID)) + if err != nil { + return nil + } + if jInfo.classpath != "" { + m[labelJavaClasspath] = jInfo.classpath + } + if jInfo.javaHome != "" { + m[LabelJavaHome] = jInfo.javaHome + } + if jInfo.javaVersion != "" { + m[labelJavaVersion] = jInfo.javaVersion + } + if jInfo.javaVersionDate != "" { + m[labelJavaVersionDate] = jInfo.javaVersionDate + } + if jInfo.vmFlags != "" { + m[labelJavaVMFlags] = jInfo.vmFlags + } + if jInfo.vmType != "" { + m[labelJavaVMType] = jInfo.vmType + } + if jInfo.osName != "" { + m[labelJavaOsName] = jInfo.osName + } + if jInfo.osArch != "" { + m[labelJavaOsArch] = jInfo.osArch + } return nil } + +func getInfoFromJcmd(pid int) (*jvmInfo, error) { + output, err := attachAndRunJcmdCommand(pid, "VM.system_properties") + if err != nil { + return nil, err + } + props := strings.Split(output, "\n") + j := &jvmInfo{ + vmType: "jdk", + } + for _, p := range props { + writeValue(p, "java.home", &j.javaHome) + writeValue(p, "java.class.path", &j.classpath) + writeValue(p, "os.name", &j.osName) + writeValue(p, "os.arch", &j.osArch) + writeValue(p, "java.version", &j.javaVersion) + writeValue(p, "java.version.date", &j.javaVersionDate) + } + output, err = attachAndRunJcmdCommand(pid, "VM.flags") + if err != nil { + return j, nil + } + parts := strings.Split(output, "\n") + if len(parts) > 1 { + j.vmFlags = parts[1] + } + return j, nil +} + +func writeValue(p, n string, dest *string) { + if strings.HasPrefix(p, n+"=") { + *dest = strings.Trim(p[len(n)+1:], "\"") + } +} + +func attachAndRunJcmdCommand(pid int, cmd string) (string, error) { + agentUid := uint32(os.Geteuid()) + agentGid := uint32(os.Getegid()) + targetUid, targetGid, nsPid, err := getProcessInfo(pid) + if err != nil { + return "", err + } + enterNS(pid, "net") + enterNS(pid, "ipc") + enterNS(pid, "mnt") + + if (agentGid != targetGid && syscall.Setegid(int(targetGid)) != nil) || + (agentUid != targetUid && syscall.Seteuid(int(targetUid)) != nil) { + return "", errors.New("failed to change credentials to match the target process") + } + + tmpPath, err := getTmpPath(pid) + if err != nil { + return "", err + } + + signal.Ignore(syscall.SIGPIPE) + + if !checkSocket(nsPid, tmpPath) { + if err = attachToJvm(pid, nsPid, tmpPath); err != nil { + return "", err + } + } + + fd, err := connectSocket(nsPid, tmpPath) + if err != nil { + return "", err + } + defer unix.Close(fd) + + return sendRequest(fd, "jcmd", cmd) +} + +func getProcessInfo(pid int) (uid, gid uint32, nspid int, err error) { + path := fmt.Sprintf("/proc/%d/status", pid) + statusFile, err := os.Open(path) + if err != nil { + return 0, 0, 0, err + } + defer statusFile.Close() + + scanner := bufio.NewScanner(statusFile) + for scanner.Scan() { + line := scanner.Text() + fields := strings.Fields(line) + + switch fields[0] { + case "Uid:": + uid64, err := strconv.ParseUint(fields[1], 10, 32) + if err != nil { + return 0, 0, 0, err + } + uid = uint32(uid64) + case "Gid:": + gid64, err := strconv.ParseUint(fields[1], 10, 32) + if err != nil { + return 0, 0, 0, err + } + gid = uint32(gid64) + case "NStgid:": + // PID namespaces can be nested; the last one is the innermost one + for _, s := range fields[1:] { + nspid, err = strconv.Atoi(s) + if err != nil { + return 0, 0, 0, err + } + } + default: + } + } + return uid, gid, nspid, nil +} + +func enterNS(pid int, nsType string) bool { + path := fmt.Sprintf("/proc/%d/ns/%s", pid, nsType) + selfPath := fmt.Sprintf("/proc/self/ns/%s", nsType) + + var oldNSStat, newNSStat syscall.Stat_t + if err := syscall.Stat(selfPath, &oldNSStat); err == nil { + if err := syscall.Stat(path, &newNSStat); err == nil { + if oldNSStat.Ino != newNSStat.Ino { + newNS, err := syscall.Open(path, syscall.O_RDONLY, 0) + _ = syscall.Close(newNS) + if err != nil { + return false + } + } + } + } + return true +} + +func getTmpPath(pid int) (path string, err error) { + path = fmt.Sprintf("/proc/%d/root/tmp", pid) + var stats syscall.Stat_t + return path, syscall.Stat(path, &stats) +} + +func checkSocket(pid int, tmpPath string) bool { + path := fmt.Sprintf("%s/.java_pid%d", tmpPath, pid) + + var stats syscall.Stat_t + return syscall.Stat(path, &stats) == nil && (stats.Mode&unix.S_IFSOCK) != 0 +} + +func attachToJvm(pid, nspid int, tmpPath string) error { + path := fmt.Sprintf("%s/.attach_pid%d", tmpPath, nspid) + file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, os.FileMode(0660)) + if err != nil { + return err + } + defer file.Close() + defer os.Remove(path) + + err = syscall.Kill(pid, syscall.SIGQUIT) + if err != nil { + return err + } + + var ts = time.Millisecond * 20 + attached := false + for !attached && ts.Nanoseconds() < int64(500*time.Millisecond) { + time.Sleep(ts) + attached = checkSocket(nspid, tmpPath) + ts += 20 * time.Millisecond + } + return err +} + +func connectSocket(pid int, tmpPath string) (int, error) { + fd, err := unix.Socket(unix.AF_UNIX, unix.SOCK_STREAM, 0) + if err != nil { + return -1, err + } + addr := unix.SockaddrUnix{ + Name: fmt.Sprintf("%s/.java_pid%d", tmpPath, pid), + } + return fd, unix.Connect(fd, &addr) +} + +func sendRequest(fd int, cmd string, arg string) (string, error) { + request := make([]byte, 0, 6+len(cmd)+len(arg)) + request = append(request, byte('1')) + request = append(request, byte(0)) + + request = append(request, []byte(cmd)...) + request = append(request, byte(0)) + + request = append(request, []byte(arg)...) + request = append(request, []byte{0, 0, 0}...) + + _, err := unix.Write(fd, request) + if err != nil { + return "", err + } + + response := make([]byte, 0) + + buf := make([]byte, 8192) + n, _ := unix.Read(fd, buf) + + for n != 0 { + response = append(response, buf...) + n, err = unix.Read(fd, buf) + } + + return string(response), err +} From 7425fd7dcba3c822b5cee194cd1f323865648fd8 Mon Sep 17 00:00:00 2001 From: Tolya Korniltsev Date: Fri, 2 Feb 2024 17:18:55 +0700 Subject: [PATCH 13/19] add compiler info --- .../discovery/process/analyze/analyze.go | 2 +- component/discovery/process/analyze/cpp.go | 21 +++++++++++++++++-- go.mod | 1 + go.sum | 2 ++ 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/component/discovery/process/analyze/analyze.go b/component/discovery/process/analyze/analyze.go index 52dc6e3e34e0..fdd97dfc96fd 100644 --- a/component/discovery/process/analyze/analyze.go +++ b/component/discovery/process/analyze/analyze.go @@ -31,8 +31,8 @@ func Analyze(logger log.Logger, input Input) *Results { Labels: make(map[string]string), } for _, a := range []analyzerFunc{ + analyzeBinary, analyzeGo, - analyzeCpp, analyzePython, analyzeDotNet, analyzeJava, diff --git a/component/discovery/process/analyze/cpp.go b/component/discovery/process/analyze/cpp.go index ad7d892841ac..e95e430c4fd9 100644 --- a/component/discovery/process/analyze/cpp.go +++ b/component/discovery/process/analyze/cpp.go @@ -2,13 +2,18 @@ package analyze import ( "strings" + + "github.com/xyproto/ainur" ) const ( - LabelCPP = "__meta_process_cpp__" + LabelCPP = "__meta_process_cpp__" + LabelCompiler = "__meta_process_binary_compiler__" + LabelStatic = "__meta_process_binary_static__" + LabelStripped = "__meta_process_binary_striped__" ) -func analyzeCpp(input Input, a *Results) error { +func analyzeBinary(input Input, a *Results) error { m := a.Labels libs, err := input.ElfFile.ImportedLibraries() if err != nil { @@ -22,5 +27,17 @@ func analyzeCpp(input Input, a *Results) error { } } + m[LabelCompiler] = ainur.Compiler(input.ElfFile) + if ainur.Static(input.ElfFile) { + m[LabelStatic] = "true" + } else { + m[LabelStatic] = "false" + } + if ainur.Stripped(input.ElfFile) { + m[LabelStripped] = "true" + } else { + m[LabelStripped] = "false" + } + return nil } diff --git a/go.mod b/go.mod index 305a0b6cc6f1..2104143f2db8 100644 --- a/go.mod +++ b/go.mod @@ -615,6 +615,7 @@ require ( github.com/open-telemetry/opentelemetry-collector-contrib/receiver/prometheusreceiver v0.87.0 github.com/open-telemetry/opentelemetry-collector-contrib/receiver/vcenterreceiver v0.87.0 github.com/pyroscope-io/dotnetdiag v0.0.0-00010101000000-000000000000 + github.com/xyproto/ainur v1.3.3 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.42.0 k8s.io/apimachinery v0.28.3 ) diff --git a/go.sum b/go.sum index f59c5ca4e563..9a3c3d6f01a3 100644 --- a/go.sum +++ b/go.sum @@ -2292,6 +2292,8 @@ github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtX github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xo/dburl v0.20.0 h1:v601OhM9J4Zh56R270ncM9HRgoxp39tf9+nt5ft9UD0= github.com/xo/dburl v0.20.0/go.mod h1:B7/G9FGungw6ighV8xJNwWYQPMfn3gsi2sn5SE8Bzco= +github.com/xyproto/ainur v1.3.3 h1:DjbkZg7iNblH1abwfIQG2siI0z3LOyVuWEmCq2S9GKc= +github.com/xyproto/ainur v1.3.3/go.mod h1:Sn5x2wSx2Q9RoZHIqJr927vVeM0oKwBl4lCMG+My/rk= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= From 6ae61a3f4714ff3d03aba49a8aba738fdf0f699e Mon Sep 17 00:00:00 2001 From: Christian Simon Date: Thu, 1 Feb 2024 09:16:02 +0000 Subject: [PATCH 14/19] Not a go executable is not an error we should show --- component/discovery/process/analyze/go.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/component/discovery/process/analyze/go.go b/component/discovery/process/analyze/go.go index d9153ed834e8..f8aaae9438f1 100644 --- a/component/discovery/process/analyze/go.go +++ b/component/discovery/process/analyze/go.go @@ -26,6 +26,9 @@ func analyzeGo(input Input, a *Results) error { m := a.Labels info, err := buildinfo.Read(input.File) // it reads elf second time if err != nil { + if err.Error() == "not a Go executable" { + return nil + } return err } From 03b6a8b1e577371dc48b39648ac5d3638c0951ee Mon Sep 17 00:00:00 2001 From: Aleksandar Petrov <8142643+aleks-p@users.noreply.github.com> Date: Fri, 9 Feb 2024 12:10:24 -0400 Subject: [PATCH 15/19] Fix build on MacOS, remove commented out code --- .../process/analyze/cache/buildid.go | 20 ------------------- .../discovery/process/analyze/cache/cache.go | 2 ++ .../process/analyze/cache/cache_test.go | 2 ++ .../discovery/process/list-processes/main.go | 2 ++ 4 files changed, 6 insertions(+), 20 deletions(-) diff --git a/component/discovery/process/analyze/cache/buildid.go b/component/discovery/process/analyze/cache/buildid.go index 56a561a04df1..508ea81e5ef1 100644 --- a/component/discovery/process/analyze/cache/buildid.go +++ b/component/discovery/process/analyze/cache/buildid.go @@ -10,26 +10,6 @@ import ( // copypaste from https://github.com/grafana/pyroscope/blob/8a7fe2b80c219bfda9be685ff27ca1dee4218a42/ebpf/symtab/elf/buildid.go#L31 -//type BuildID struct { -// ID string -// Typ string -//} -// -//func GNUBuildID(s string) BuildID { -// return BuildID{ID: s, Typ: "gnu"} -//} -//func GoBuildID(s string) BuildID { -// return BuildID{ID: s, Typ: "go"} -//} -// -//func (b *BuildID) Empty() bool { -// return b.ID == "" || b.Typ == "" -//} -// -//func (b *BuildID) GNU() bool { -// return b.Typ == "gnu" -//} - var ( ErrNoBuildIDSection = fmt.Errorf("build ID section not found") ) diff --git a/component/discovery/process/analyze/cache/cache.go b/component/discovery/process/analyze/cache/cache.go index 1b91bad51835..dc8a3ecfc76c 100644 --- a/component/discovery/process/analyze/cache/cache.go +++ b/component/discovery/process/analyze/cache/cache.go @@ -1,3 +1,5 @@ +//go:build linux + package cache import ( diff --git a/component/discovery/process/analyze/cache/cache_test.go b/component/discovery/process/analyze/cache/cache_test.go index 893bd24e296c..c2a7dd1d0e52 100644 --- a/component/discovery/process/analyze/cache/cache_test.go +++ b/component/discovery/process/analyze/cache/cache_test.go @@ -1,3 +1,5 @@ +//go:build linux + package cache import ( diff --git a/component/discovery/process/list-processes/main.go b/component/discovery/process/list-processes/main.go index fd939e0d81d8..6cd12bb8ea28 100644 --- a/component/discovery/process/list-processes/main.go +++ b/component/discovery/process/list-processes/main.go @@ -1,3 +1,5 @@ +//go:build linux + package main import ( From 444faa307b712b616256a8fe6db080a0ed17aa34 Mon Sep 17 00:00:00 2001 From: Aleksandar Petrov <8142643+aleks-p@users.noreply.github.com> Date: Fri, 9 Feb 2024 15:11:55 -0400 Subject: [PATCH 16/19] Fix failing test --- component/discovery/process/discover.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/component/discovery/process/discover.go b/component/discovery/process/discover.go index 5de4e938e767..94d43d7939b5 100644 --- a/component/discovery/process/discover.go +++ b/component/discovery/process/discover.go @@ -73,8 +73,10 @@ func convertProcess(p Process) discovery.Target { if p.uid != "" { t[labelProcessUID] = p.uid } - for k, v := range p.Analysis.Labels { - t[k] = v + if p.Analysis != nil { + for k, v := range p.Analysis.Labels { + t[k] = v + } } return t From 2c80d78e7c602e6beb34aa5b6144fd771fc0f476 Mon Sep 17 00:00:00 2001 From: Aleksandar Petrov <8142643+aleks-p@users.noreply.github.com> Date: Fri, 9 Feb 2024 16:18:03 -0400 Subject: [PATCH 17/19] Fix lint errors --- .../discovery/process/analyze/analyze.go | 5 ++++ .../discovery/process/analyze/cache/cache.go | 1 - .../process/analyze/cache/cache_test.go | 1 - component/discovery/process/analyze/cpp.go | 10 +++---- component/discovery/process/analyze/dotnet.go | 2 +- component/discovery/process/analyze/go.go | 6 ++-- component/discovery/process/analyze/java.go | 29 ++++++++++++------- component/discovery/process/analyze/python.go | 2 +- .../discovery/process/list-processes/main.go | 1 - 9 files changed, 34 insertions(+), 23 deletions(-) diff --git a/component/discovery/process/analyze/analyze.go b/component/discovery/process/analyze/analyze.go index fdd97dfc96fd..b94e565fb2ae 100644 --- a/component/discovery/process/analyze/analyze.go +++ b/component/discovery/process/analyze/analyze.go @@ -8,6 +8,11 @@ import ( "github.com/go-kit/log/level" ) +const ( + labelValueTrue = "true" + labelValueFalse = "false" +) + type Results struct { Labels map[string]string } diff --git a/component/discovery/process/analyze/cache/cache.go b/component/discovery/process/analyze/cache/cache.go index dc8a3ecfc76c..d6550466b7ca 100644 --- a/component/discovery/process/analyze/cache/cache.go +++ b/component/discovery/process/analyze/cache/cache.go @@ -64,7 +64,6 @@ func (c *Cache) AnalyzePID(pid string) (*analyze.Results, error) { return c.AnalyzePIDPath(uint32(ipid), pid, exePath) } func (c *Cache) AnalyzePIDPath(pid uint32, pidS string, exePath string) (*analyze.Results, error) { - e := c.GetPID(pid) if e != nil { return e.Results, nil diff --git a/component/discovery/process/analyze/cache/cache_test.go b/component/discovery/process/analyze/cache/cache_test.go index c2a7dd1d0e52..e2b8423c3b80 100644 --- a/component/discovery/process/analyze/cache/cache_test.go +++ b/component/discovery/process/analyze/cache/cache_test.go @@ -91,5 +91,4 @@ func TestCache(t *testing.T) { require.Equal(t, 0, len(c.pids)) require.Equal(t, 0, len(c.stats)) require.Equal(t, 0, len(c.buildIDs)) - } diff --git a/component/discovery/process/analyze/cpp.go b/component/discovery/process/analyze/cpp.go index e95e430c4fd9..6ff54e84e593 100644 --- a/component/discovery/process/analyze/cpp.go +++ b/component/discovery/process/analyze/cpp.go @@ -22,21 +22,21 @@ func analyzeBinary(input Input, a *Results) error { for _, lib := range libs { if strings.Contains(lib, "libc++") || strings.Contains(lib, "libstdc++") { - m[LabelCPP] = "true" + m[LabelCPP] = labelValueTrue break } } m[LabelCompiler] = ainur.Compiler(input.ElfFile) if ainur.Static(input.ElfFile) { - m[LabelStatic] = "true" + m[LabelStatic] = labelValueTrue } else { - m[LabelStatic] = "false" + m[LabelStatic] = labelValueFalse } if ainur.Stripped(input.ElfFile) { - m[LabelStripped] = "true" + m[LabelStripped] = labelValueTrue } else { - m[LabelStripped] = "false" + m[LabelStripped] = labelValueFalse } return nil diff --git a/component/discovery/process/analyze/dotnet.go b/component/discovery/process/analyze/dotnet.go index 5f6749162dbf..b0246662994d 100644 --- a/component/discovery/process/analyze/dotnet.go +++ b/component/discovery/process/analyze/dotnet.go @@ -72,7 +72,7 @@ func analyzeDotNet(input Input, a *Results) error { return err } - m[LabelDotNet] = "true" + m[LabelDotNet] = labelValueTrue m[LabelDotNetCommandLine] = info.CommandLine m[LabelDotNetOS] = info.OS m[LabelDotNetArch] = info.Arch diff --git a/component/discovery/process/analyze/go.go b/component/discovery/process/analyze/go.go index f8aaae9438f1..e3896b7510bd 100644 --- a/component/discovery/process/analyze/go.go +++ b/component/discovery/process/analyze/go.go @@ -32,7 +32,7 @@ func analyzeGo(input Input, a *Results) error { return err } - m[LabelGo] = "true" + m[LabelGo] = labelValueTrue if info.GoVersion != "" { m[LabelGoVersion] = info.GoVersion @@ -52,10 +52,10 @@ func analyzeGo(input Input, a *Results) error { for _, dep := range info.Deps { switch dep.Path { case goSdkModule: - m[LabelGoSdk] = "true" + m[LabelGoSdk] = labelValueTrue m[LabelGoSdkVersion] = dep.Version case godeltaprofModule: - m[LabelGoDeltaProf] = "true" + m[LabelGoDeltaProf] = labelValueTrue m[LabelGoDeltaProfVersion] = dep.Version default: //todo should we optionally/configurable include all deps? diff --git a/component/discovery/process/analyze/java.go b/component/discovery/process/analyze/java.go index a17731fbc1ac..1ce3a079b334 100644 --- a/component/discovery/process/analyze/java.go +++ b/component/discovery/process/analyze/java.go @@ -4,7 +4,6 @@ import ( "bufio" "errors" "fmt" - "golang.org/x/sys/unix" "os" "os/signal" "strconv" @@ -13,6 +12,7 @@ import ( "time" "github.com/prometheus/procfs" + "golang.org/x/sys/unix" ) const ( @@ -68,7 +68,7 @@ func analyzeJava(input Input, a *Results) error { return nil } - m[labelJava] = "true" + m[labelJava] = labelValueTrue jInfo, err := getInfoFromJcmd(int(input.PID)) if err != nil { return nil @@ -141,12 +141,21 @@ func attachAndRunJcmdCommand(pid int, cmd string) (string, error) { if err != nil { return "", err } - enterNS(pid, "net") - enterNS(pid, "ipc") - enterNS(pid, "mnt") - if (agentGid != targetGid && syscall.Setegid(int(targetGid)) != nil) || - (agentUid != targetUid && syscall.Seteuid(int(targetUid)) != nil) { + err = enterNS(pid, "net") + if err != nil { + return "", err + } + err = enterNS(pid, "ipc") + if err != nil { + return "", err + } + err = enterNS(pid, "mnt") + if err != nil { + return "", err + } + + if (agentGid != targetGid && syscall.Setegid(int(targetGid)) != nil) || (agentUid != targetUid && syscall.Seteuid(int(targetUid)) != nil) { return "", errors.New("failed to change credentials to match the target process") } @@ -212,7 +221,7 @@ func getProcessInfo(pid int) (uid, gid uint32, nspid int, err error) { return uid, gid, nspid, nil } -func enterNS(pid int, nsType string) bool { +func enterNS(pid int, nsType string) error { path := fmt.Sprintf("/proc/%d/ns/%s", pid, nsType) selfPath := fmt.Sprintf("/proc/self/ns/%s", nsType) @@ -223,12 +232,12 @@ func enterNS(pid int, nsType string) bool { newNS, err := syscall.Open(path, syscall.O_RDONLY, 0) _ = syscall.Close(newNS) if err != nil { - return false + return err } } } } - return true + return nil } func getTmpPath(pid int) (path string, err error) { diff --git a/component/discovery/process/analyze/python.go b/component/discovery/process/analyze/python.go index 1be439f23a33..663c65f61a59 100644 --- a/component/discovery/process/analyze/python.go +++ b/component/discovery/process/analyze/python.go @@ -34,7 +34,7 @@ func analyzePython(input Input, a *Results) error { if pythonVersion == "" { return nil } - m[LabelPython] = "true" + m[LabelPython] = labelValueTrue m[LabelPythonVersion] = pythonVersion return nil diff --git a/component/discovery/process/list-processes/main.go b/component/discovery/process/list-processes/main.go index 6cd12bb8ea28..59259b0553f3 100644 --- a/component/discovery/process/list-processes/main.go +++ b/component/discovery/process/list-processes/main.go @@ -28,7 +28,6 @@ func run() error { ) for _, p := range processes { - attributes = attributes[:4] attributes[0] = "msg" attributes[1] = "found process" From 15036e24ddf835d7218f72ae8757891c8bcfc394 Mon Sep 17 00:00:00 2001 From: Aleksandar Petrov <8142643+aleks-p@users.noreply.github.com> Date: Tue, 20 Feb 2024 16:35:15 -0400 Subject: [PATCH 18/19] Add analyze_executable to the discover_config block --- component/discovery/process/args.go | 22 ++++++++++++---------- component/discovery/process/discover.go | 14 +++++++++----- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/component/discovery/process/args.go b/component/discovery/process/args.go index 636f6231867d..f17e35898bb1 100644 --- a/component/discovery/process/args.go +++ b/component/discovery/process/args.go @@ -13,22 +13,24 @@ type Arguments struct { } type DiscoverConfig struct { - Cwd bool `river:"cwd,attr,optional"` - Exe bool `river:"exe,attr,optional"` - Commandline bool `river:"commandline,attr,optional"` - Username bool `river:"username,attr,optional"` - UID bool `river:"uid,attr,optional"` - ContainerID bool `river:"container_id,attr,optional"` + Cwd bool `river:"cwd,attr,optional"` + Exe bool `river:"exe,attr,optional"` + Commandline bool `river:"commandline,attr,optional"` + Username bool `river:"username,attr,optional"` + UID bool `river:"uid,attr,optional"` + ContainerID bool `river:"container_id,attr,optional"` + AnalyzeExecutable bool `river:"analyze_executable,attr,optional"` } var DefaultConfig = Arguments{ Join: nil, RefreshInterval: 60 * time.Second, DiscoverConfig: DiscoverConfig{ - Cwd: true, - Exe: true, - Commandline: true, - ContainerID: true, + Cwd: true, + Exe: true, + Commandline: true, + ContainerID: true, + AnalyzeExecutable: false, }, } diff --git a/component/discovery/process/discover.go b/component/discovery/process/discover.go index 94d43d7939b5..e298054943c2 100644 --- a/component/discovery/process/discover.go +++ b/component/discovery/process/discover.go @@ -149,11 +149,15 @@ func Discover(l log.Logger, cfg *DiscoverConfig, cache *analCache.Cache) ([]Proc continue } } - m, err := cache.AnalyzePID(spid) - if err != nil { - level.Error(l).Log("msg", "error analyzing process", "pid", spid, "err", err) - continue + var ar *analyze.Results + if cfg.AnalyzeExecutable { + ar, err = cache.AnalyzePID(spid) + if err != nil { + level.Error(l).Log("msg", "error analyzing process", "pid", spid, "err", err) + continue + } } + res = append(res, Process{ PID: spid, exe: exe, @@ -162,7 +166,7 @@ func Discover(l log.Logger, cfg *DiscoverConfig, cache *analCache.Cache) ([]Proc containerID: containerID, username: username, uid: uid, - Analysis: m, + Analysis: ar, }) active[uint32(p.Pid)] = struct{}{} } From 9d287ff7c5ded9c057fe9db9f7e36d8c6ecbace2 Mon Sep 17 00:00:00 2001 From: Aleksandar Petrov <8142643+aleks-p@users.noreply.github.com> Date: Tue, 20 Feb 2024 16:36:01 -0400 Subject: [PATCH 19/19] Add docs for the enhanced process discovery --- .../reference/components/discovery.process.md | 87 +++++++++++++++---- 1 file changed, 72 insertions(+), 15 deletions(-) diff --git a/docs/sources/flow/reference/components/discovery.process.md b/docs/sources/flow/reference/components/discovery.process.md index 839948d3d65b..e1497500c3fd 100644 --- a/docs/sources/flow/reference/components/discovery.process.md +++ b/docs/sources/flow/reference/components/discovery.process.md @@ -31,10 +31,10 @@ discovery.process "LABEL" { The following arguments are supported: -| Name | Type | Description | Default | Required | -|--------------------|---------------------|-----------------------------------------------------------------------------------------|---------|----------| +| Name | Type | Description | Default | Required | +|--------------------|---------------------|------------------------------------------------------------------------------------------|---------|----------| | `join` | `list(map(string))` | Join external targets to discovered processes targets based on `__container_id__` label. | | no | -| `refresh_interval` | `duration` | How often to sync targets. | "60s" | no | +| `refresh_interval` | `duration` | How often to sync targets. | "60s" | no | ### Targets joining @@ -42,6 +42,15 @@ If `join` is specified, `discovery.process` will join the discovered processes b For example, if `join` is specified as follows: +```river +discovery.process "all" { + join = discovery.kubernetes.