/
typer.go
183 lines (163 loc) · 6.03 KB
/
typer.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
package discover
import (
"log/slog"
"strings"
"github.com/mariomac/pipes/pipe"
"github.com/grafana/beyla/pkg/beyla"
"github.com/grafana/beyla/pkg/internal/exec"
"github.com/grafana/beyla/pkg/internal/goexec"
"github.com/grafana/beyla/pkg/internal/imetrics"
"github.com/grafana/beyla/pkg/internal/svc"
)
type Instrumentable struct {
Type svc.InstrumentableType
InstrumentationError error
// in some runtimes, like python gunicorn, we need to allow
// tracing both the parent pid and all of its children pid
ChildPids []uint32
FileInfo *exec.FileInfo
Offsets *goexec.Offsets
}
// ExecTyperProvider classifies the discovered executables according to the
// executable type (Go, generic...), and filters these executables
// that are not instrumentable.
func ExecTyperProvider(cfg *beyla.Config, metrics imetrics.Reporter) pipe.MiddleProvider[[]Event[ProcessMatch], []Event[Instrumentable]] {
t := typer{
cfg: cfg,
metrics: metrics,
log: slog.With("component", "discover.ExecTyper"),
currentPids: map[int32]*exec.FileInfo{},
}
return func() (pipe.MiddleFunc[[]Event[ProcessMatch], []Event[Instrumentable]], error) {
// TODO: do it per executable
if !cfg.Discovery.SkipGoSpecificTracers {
t.loadAllGoFunctionNames()
}
return func(in <-chan []Event[ProcessMatch], out chan<- []Event[Instrumentable]) {
for i := range in {
out <- t.FilterClassify(i)
}
}, nil
}
}
type typer struct {
cfg *beyla.Config
metrics imetrics.Reporter
log *slog.Logger
currentPids map[int32]*exec.FileInfo
allGoFunctions []string
}
// FilterClassify returns the Instrumentable types for each received ProcessMatch,
// and filters out the processes that can't be instrumented (e.g. because of the lack
// of instrumentation points)
func (t *typer) FilterClassify(evs []Event[ProcessMatch]) []Event[Instrumentable] {
var out []Event[Instrumentable]
elfs := make([]*exec.FileInfo, 0, len(evs))
// Update first the PID map so we use only the parent processes
// in case of multiple matches
for i := range evs {
ev := &evs[i]
switch evs[i].Type {
case EventCreated:
svcID := svc.ID{Name: ev.Obj.Criteria.Name, Namespace: ev.Obj.Criteria.Namespace}
if elfFile, err := exec.FindExecELF(ev.Obj.Process, svcID); err != nil {
t.log.Warn("error finding process ELF. Ignoring", "error", err)
} else {
t.currentPids[ev.Obj.Process.Pid] = elfFile
elfs = append(elfs, elfFile)
}
case EventDeleted:
if fInfo, ok := t.currentPids[ev.Obj.Process.Pid]; ok {
delete(t.currentPids, ev.Obj.Process.Pid)
out = append(out, Event[Instrumentable]{
Type: EventDeleted,
Obj: Instrumentable{FileInfo: fInfo},
})
}
}
}
for i := range elfs {
inst := t.asInstrumentable(elfs[i])
t.log.Debug(
"found an instrumentable process",
"type", inst.Type.String(),
"exec", inst.FileInfo.CmdExePath, "pid", inst.FileInfo.Pid)
out = append(out, Event[Instrumentable]{Type: EventCreated, Obj: inst})
}
return out
}
// asInstrumentable classifies the type of executable (Go, generic...) and,
// in case of belonging to a forked process, returns its parent.
func (t *typer) asInstrumentable(execElf *exec.FileInfo) Instrumentable {
log := t.log.With("pid", execElf.Pid, "comm", execElf.CmdExePath)
log.Debug("getting instrumentable information")
// look for suitable Go application first
offsets, ok, err := t.inspectOffsets(execElf)
if ok {
// we found go offsets, let's see if this application is not a proxy
if !isGoProxy(offsets) {
log.Debug("identified as a Go service or client")
return Instrumentable{Type: svc.InstrumentableGolang, FileInfo: execElf, Offsets: offsets}
}
log.Debug("identified as a Go proxy")
} else {
log.Debug("identified as a generic, non-Go executable")
}
// select the parent (or grandparent) of the executable, if any
var child []uint32
parent, ok := t.currentPids[execElf.Ppid]
for ok && execElf.Ppid != execElf.Pid &&
// we will ignore parent processes that are not the same executable. For example,
// to avoid wrongly instrumenting process launcher such as systemd or containerd-shimd
// when they launch an instrumentable service
execElf.CmdExePath == parent.CmdExePath {
log.Debug("replacing executable by its parent", "ppid", execElf.Ppid)
child = append(child, uint32(execElf.Pid))
execElf = parent
parent, ok = t.currentPids[parent.Ppid]
}
detectedType := exec.FindProcLanguage(execElf.Pid, execElf.ELF)
log.Debug("instrumented", "comm", execElf.CmdExePath, "pid", execElf.Pid,
"child", child, "language", detectedType.String())
// Return the instrumentable without offsets, as it is identified as a generic
// (or non-instrumentable Go proxy) executable
return Instrumentable{Type: detectedType, FileInfo: execElf, ChildPids: child, InstrumentationError: err}
}
func (t *typer) inspectOffsets(execElf *exec.FileInfo) (*goexec.Offsets, bool, error) {
if !t.cfg.Discovery.SystemWide {
if t.cfg.Discovery.SkipGoSpecificTracers {
t.log.Debug("skipping inspection for Go functions", "pid", execElf.Pid, "comm", execElf.CmdExePath)
} else {
t.log.Debug("inspecting", "pid", execElf.Pid, "comm", execElf.CmdExePath)
offsets, err := goexec.InspectOffsets(execElf, t.allGoFunctions)
if err != nil {
t.log.Debug("couldn't find go specific tracers", "error", err)
return nil, false, err
}
return offsets, true, nil
}
}
return nil, false, nil
}
func isGoProxy(offsets *goexec.Offsets) bool {
for f := range offsets.Funcs {
// if we find anything of interest other than the Go runtime, we consider this a valid application
if !strings.HasPrefix(f, "runtime.") {
return false
}
}
return true
}
func (t *typer) loadAllGoFunctionNames() {
uniqueFunctions := map[string]struct{}{}
t.allGoFunctions = nil
for _, p := range newGoTracersGroup(t.cfg, t.metrics) {
for funcName := range p.GoProbes() {
// avoid duplicating function names
if _, ok := uniqueFunctions[funcName]; !ok {
uniqueFunctions[funcName] = struct{}{}
t.allGoFunctions = append(t.allGoFunctions, funcName)
}
}
}
}