Skip to content

Commit 754e788

Browse files
authored
Add programming language selector (#1326)
1 parent 944d50f commit 754e788

File tree

12 files changed

+380
-3
lines changed

12 files changed

+380
-3
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
log_config: yaml
2+
routes:
3+
patterns:
4+
- /basic/:rnd
5+
unmatched: path
6+
otel_metrics_export:
7+
endpoint: http://otelcol:4018
8+
otel_traces_export:
9+
endpoint: http://jaeger:4318
10+
discovery:
11+
instrument:
12+
- languages: "{rust,ruby}"
13+
exclude_instrument:
14+
- exe_path: "{obi,prometheus,otelcol*,all*,launcher}"
15+
attributes:
16+
kubernetes:
17+
enable: true
18+
cluster_name: my-kube
19+
select:
20+
http_server_request_duration_seconds_count:
21+
exclude: ["server_address"]
22+
"*":
23+
include: ["*"]

internal/test/integration/docker-compose-multiexec.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ services:
120120
context: ../../..
121121
dockerfile: ./internal/test/integration/components/obi/Dockerfile
122122
command:
123-
- --config=/configs/obi-config-multiexec.yml
123+
- --config=/configs/obi-config-multiexec${MULTI_TEST_MODE}.yml
124124
volumes:
125125
- ./configs/:/configs
126126
- ./system/sys/kernel/security:/sys/kernel/security

internal/test/integration/multiprocess_test.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package integration
55

66
import (
7+
"encoding/json"
78
"fmt"
89
"net/http"
910
"path"
@@ -15,7 +16,9 @@ import (
1516
"github.com/stretchr/testify/require"
1617

1718
"go.opentelemetry.io/obi/internal/test/integration/components/docker"
19+
"go.opentelemetry.io/obi/internal/test/integration/components/jaeger"
1820
"go.opentelemetry.io/obi/internal/test/integration/components/promtest"
21+
ti "go.opentelemetry.io/obi/pkg/test/integration"
1922
)
2023

2124
func TestMultiProcess(t *testing.T) {
@@ -226,3 +229,114 @@ func checkInstrumentedProcessesMetric(t *testing.T) {
226229
}
227230
}, testTimeout, 1000*time.Millisecond)
228231
}
232+
233+
// We are instrumenting only the Rust and Ruby services, all other server span queries should come empty
234+
func testPartialLanguageHTTPProbes(t *testing.T) {
235+
waitForTestComponentsSub(t, "http://localhost:8091", "/dist") // rust
236+
237+
for i := 0; i < 100; i++ {
238+
ti.DoHTTPGet(t, "http://localhost:8091/dist", 200)
239+
}
240+
241+
// check the rust service, it will not have any nested spans
242+
require.EventuallyWithT(t, func(ct *assert.CollectT) {
243+
resp, err := http.Get(jaegerQueryURL + "?service=greetings&operation=GET%20%2Fdist")
244+
require.NoError(ct, err)
245+
if resp == nil {
246+
return
247+
}
248+
require.Equal(ct, http.StatusOK, resp.StatusCode)
249+
var tq jaeger.TracesQuery
250+
require.NoError(ct, json.NewDecoder(resp.Body).Decode(&tq))
251+
traces := tq.FindBySpan(jaeger.Tag{Key: "url.path", Type: "string", Value: "/dist"})
252+
require.LessOrEqual(ct, 5, len(traces))
253+
for _, trace := range traces {
254+
// Check the information of the rust parent span
255+
res := trace.FindByOperationName("GET /dist", "server")
256+
require.Len(ct, res, 1)
257+
parent := res[0]
258+
require.NotEmpty(ct, parent.TraceID)
259+
require.NotEmpty(ct, parent.SpanID)
260+
// check duration is at least 2us
261+
assert.Less(ct, (2 * time.Microsecond).Microseconds(), parent.Duration)
262+
// check span attributes
263+
sd := parent.Diff(
264+
jaeger.Tag{Key: "http.request.method", Type: "string", Value: "GET"},
265+
jaeger.Tag{Key: "http.response.status_code", Type: "int64", Value: float64(200)},
266+
jaeger.Tag{Key: "url.path", Type: "string", Value: "/dist"},
267+
jaeger.Tag{Key: "server.port", Type: "int64", Value: float64(8090)},
268+
jaeger.Tag{Key: "http.route", Type: "string", Value: "/dist"},
269+
jaeger.Tag{Key: "span.kind", Type: "string", Value: "server"},
270+
)
271+
assert.Empty(ct, sd, sd.String())
272+
273+
// Check the information of the java parent span
274+
res = trace.FindByOperationName("GET /jtrace", "server")
275+
require.Empty(ct, res)
276+
277+
// Check the information of the nodejs parent span
278+
res = trace.FindByOperationName("GET /traceme", "server")
279+
require.Empty(ct, res)
280+
281+
// Check the information of the go parent span
282+
res = trace.FindByOperationName("GET /gotracemetoo", "server")
283+
require.Empty(ct, res)
284+
285+
// Check the information of the python parent span
286+
res = trace.FindByOperationName("GET /tracemetoo", "server")
287+
require.Empty(t, res)
288+
289+
// Check the information of the rails parent span
290+
res = trace.FindByOperationName("GET /users", "server")
291+
require.Empty(t, res)
292+
}
293+
}, testTimeout, 100*time.Millisecond)
294+
295+
require.EventuallyWithT(t, func(ct *assert.CollectT) {
296+
resp, err := http.Get(jaegerQueryURL + "?service=ruby&operation=GET%20%2Fusers")
297+
require.NoError(ct, err)
298+
if resp == nil {
299+
return
300+
}
301+
require.Equal(ct, http.StatusOK, resp.StatusCode)
302+
var tq jaeger.TracesQuery
303+
require.NoError(ct, json.NewDecoder(resp.Body).Decode(&tq))
304+
traces := tq.FindBySpan(jaeger.Tag{Key: "url.path", Type: "string", Value: "/users"})
305+
require.LessOrEqual(ct, 5, len(traces))
306+
for _, trace := range traces {
307+
// Check the information of the rust parent span
308+
res := trace.FindByOperationName("GET /users", "server")
309+
require.Len(ct, res, 1)
310+
}
311+
}, testTimeout, 100*time.Millisecond)
312+
313+
require.EventuallyWithT(t, func(ct *assert.CollectT) {
314+
resp, err := http.Get(jaegerQueryURL + "?service=testserver&operation=GET%20%2Fgotracemetoo")
315+
require.NoError(ct, err)
316+
if resp == nil {
317+
return
318+
}
319+
require.Equal(ct, http.StatusOK, resp.StatusCode)
320+
var tq jaeger.TracesQuery
321+
require.NoError(ct, json.NewDecoder(resp.Body).Decode(&tq))
322+
traces := tq.FindBySpan(jaeger.Tag{Key: "url.path", Type: "string", Value: "/gotracemetoo"})
323+
require.Empty(ct, traces)
324+
}, testTimeout, 100*time.Millisecond)
325+
}
326+
327+
func TestLanguageSelectors(t *testing.T) {
328+
compose, err := docker.ComposeSuite("docker-compose-multiexec.yml", path.Join(pathOutput, "test-suite-multiexec-lang.log"))
329+
require.NoError(t, err)
330+
331+
// we are going to setup discovery directly in the configuration file, choose the lang config file
332+
compose.Env = append(compose.Env, `OTEL_EBPF_EXECUTABLE_PATH=`, `OTEL_EBPF_OPEN_PORT=`, `MULTI_TEST_MODE=-lang`)
333+
require.NoError(t, compose.Up())
334+
335+
// We are testing with instrumenting only Ruby and Rust services, so from our call chain we should only see
336+
// traces for the two services written in the correct language
337+
t.Run("Partial traces: rust (OK) -> java (NO) -> node (NO) -> go (NO) -> python (NO) -> rails (OK)", func(t *testing.T) {
338+
testPartialLanguageHTTPProbes(t)
339+
})
340+
341+
require.NoError(t, compose.Close())
342+
}

pkg/appolly/discover/language_decorator.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ func (ld *languageDecorator) decorateEvent(ev *Event[ProcessAttrs]) {
7474
}
7575
t := _findProcLanguage(ev.Obj.pid)
7676
ev.Obj.detectedType = t
77+
ld.log.Debug("detected type", "pid", ev.Obj.pid, "type", t)
7778
ld.typeCache.Add(ino, t)
7879
}
7980
}

pkg/appolly/discover/language_decorator_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package discover
55

66
import (
7+
"log/slog"
78
"testing"
89

910
lru "github.com/hashicorp/golang-lru/v2"
@@ -17,6 +18,7 @@ func newTestDecorator(ignoredPaths []string) *languageDecorator {
1718
cache, _ := lru.New[uint64, svc.InstrumentableType](100)
1819
return &languageDecorator{
1920
typeCache: cache,
21+
log: slog.With("component", "LanguageDecorator"),
2022
ignoredPaths: ignoredPaths,
2123
}
2224
}

pkg/appolly/discover/matcher.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ func (m *Matcher) isExcluded(obj *ProcessAttrs, proc *services.ProcessInfo) bool
211211

212212
func (m *Matcher) matchProcess(obj *ProcessAttrs, p *services.ProcessInfo, a services.Selector) bool {
213213
log := m.Log.With("pid", p.Pid, "exe", p.ExePath)
214-
if !a.GetPath().IsSet() && a.GetOpenPorts().Len() == 0 && len(obj.metadata) == 0 {
214+
if !a.GetPath().IsSet() && !a.GetLanguages().IsSet() && a.GetOpenPorts().Len() == 0 && len(obj.metadata) == 0 {
215215
log.Debug("no Kube metadata, no local selection criteria. Ignoring")
216216
return false
217217
}
@@ -223,6 +223,10 @@ func (m *Matcher) matchProcess(obj *ProcessAttrs, p *services.ProcessInfo, a ser
223223
log.Debug("open ports do not match", "openPorts", a.GetOpenPorts(), "process ports", p.OpenPorts)
224224
return false
225225
}
226+
if a.GetLanguages().IsSet() && !m.matchByLanguage(obj, a) {
227+
log.Debug("executable language does not match", "languages", a.GetLanguages(), "type", obj.detectedType.String())
228+
return false
229+
}
226230
if a.IsContainersOnly() {
227231
ns, _ := namespaceFetcherFunc(p.Pid)
228232
if ns == m.Namespace && m.HasHostPidAccess {
@@ -253,6 +257,10 @@ func (m *Matcher) matchByExecutable(p *services.ProcessInfo, a services.Selector
253257
return a.GetPathRegexp().MatchString(p.ExePath)
254258
}
255259

260+
func (m *Matcher) matchByLanguage(actual *ProcessAttrs, a services.Selector) bool {
261+
return a.GetLanguages().MatchString(actual.detectedType.String())
262+
}
263+
256264
func (m *Matcher) matchByAttributes(actual *ProcessAttrs, required services.Selector) bool {
257265
if required == nil {
258266
return true

pkg/appolly/discover/matcher_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"gopkg.in/yaml.v3"
1212

1313
"go.opentelemetry.io/obi/pkg/appolly/app"
14+
"go.opentelemetry.io/obi/pkg/appolly/app/svc"
1415
"go.opentelemetry.io/obi/pkg/appolly/services"
1516
"go.opentelemetry.io/obi/pkg/internal/testutil"
1617
"go.opentelemetry.io/obi/pkg/obi"
@@ -76,6 +77,51 @@ func TestCriteriaMatcher(t *testing.T) {
7677
testMatch(t, matches[3], "exec-only", "", services.ProcessInfo{Pid: 6, ExePath: "/bin/clientweird99"})
7778
}
7879

80+
func TestCriteriaMatcherLanguage(t *testing.T) {
81+
pipeConfig := obi.Config{}
82+
require.NoError(t, yaml.Unmarshal([]byte(`discovery:
83+
services:
84+
- name: go-and-java
85+
namespace: foo
86+
languages: "go|java"
87+
- name: rust
88+
languages: rust
89+
`), &pipeConfig))
90+
91+
discoveredProcesses := msg.NewQueue[[]Event[ProcessAttrs]](msg.ChannelBufferLen(10))
92+
filteredProcessesQu := msg.NewQueue[[]Event[ProcessMatch]](msg.ChannelBufferLen(10))
93+
filteredProcesses := filteredProcessesQu.Subscribe()
94+
matcherFunc, err := criteriaMatcherProvider(&pipeConfig, discoveredProcesses, filteredProcessesQu)(t.Context())
95+
require.NoError(t, err)
96+
go matcherFunc(t.Context())
97+
defer filteredProcessesQu.Close()
98+
99+
// it will filter unmatching processes and return a ProcessMatch for these that match
100+
processInfo = func(pp ProcessAttrs) (*services.ProcessInfo, error) {
101+
exePath := map[app.PID]string{
102+
1: "/bin/weird33", 2: "/bin/weird33", 3: "server",
103+
4: "/bin/something", 5: "server", 6: "/bin/clientweird99",
104+
}[pp.pid]
105+
return &services.ProcessInfo{Pid: pp.pid, ExePath: exePath, OpenPorts: pp.openPorts}, nil
106+
}
107+
discoveredProcesses.Send([]Event[ProcessAttrs]{
108+
{Type: EventCreated, Obj: ProcessAttrs{pid: 1, openPorts: []uint32{1, 2, 3}, detectedType: svc.InstrumentableCPP}}, // filter
109+
{Type: EventDeleted, Obj: ProcessAttrs{pid: 2, openPorts: []uint32{4}, detectedType: svc.InstrumentableGeneric}}, // filter
110+
{Type: EventCreated, Obj: ProcessAttrs{pid: 3, openPorts: []uint32{8433}, detectedType: svc.InstrumentableJavaNative}}, // pass
111+
{Type: EventCreated, Obj: ProcessAttrs{pid: 4, openPorts: []uint32{8083}, detectedType: svc.InstrumentableJava}}, // pass
112+
{Type: EventCreated, Obj: ProcessAttrs{pid: 5, openPorts: []uint32{443}, detectedType: svc.InstrumentableGolang}}, // pass
113+
{Type: EventCreated, Obj: ProcessAttrs{pid: 6, detectedType: svc.InstrumentableRust}}, // pass
114+
})
115+
116+
matches := testutil.ReadChannel(t, filteredProcesses, testTimeout)
117+
require.Len(t, matches, 4)
118+
119+
testMatch(t, matches[0], "go-and-java", "foo", services.ProcessInfo{Pid: 3, ExePath: "server", OpenPorts: []uint32{8433}})
120+
testMatch(t, matches[1], "go-and-java", "foo", services.ProcessInfo{Pid: 4, ExePath: "/bin/something", OpenPorts: []uint32{8083}})
121+
testMatch(t, matches[2], "go-and-java", "foo", services.ProcessInfo{Pid: 5, ExePath: "server", OpenPorts: []uint32{443}})
122+
testMatch(t, matches[3], "rust", "", services.ProcessInfo{Pid: 6, ExePath: "/bin/clientweird99"})
123+
}
124+
79125
func TestCriteriaMatcher_Exclude(t *testing.T) {
80126
pipeConfig := obi.Config{}
81127
require.NoError(t, yaml.Unmarshal([]byte(`discovery:

pkg/appolly/discover/typer_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type dummyCriterion struct {
2828
func (d dummyCriterion) GetName() string { return d.name }
2929
func (d dummyCriterion) GetOpenPorts() *services.PortEnum { return nil }
3030
func (d dummyCriterion) GetPath() services.StringMatcher { return nil }
31+
func (d dummyCriterion) GetLanguages() services.StringMatcher { return nil }
3132
func (d dummyCriterion) RangeMetadata() iter.Seq2[string, services.StringMatcher] { return nil }
3233
func (d dummyCriterion) RangePodAnnotations() iter.Seq2[string, services.StringMatcher] { return nil }
3334
func (d dummyCriterion) RangePodLabels() iter.Seq2[string, services.StringMatcher] { return nil }

pkg/appolly/services/attr_glob.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ func (dc GlobDefinitionCriteria) Validate() error {
2323
for i := range dc {
2424
if dc[i].OpenPorts.Len() == 0 &&
2525
!dc[i].Path.IsSet() &&
26+
!dc[i].Languages.IsSet() &&
2627
len(dc[i].Metadata) == 0 &&
2728
len(dc[i].PodLabels) == 0 &&
2829
len(dc[i].PodAnnotations) == 0 {
@@ -61,6 +62,10 @@ type GlobAttributes struct {
6162
// list of port numbers (e.g. 80) and port ranges (e.g. 8080-8089)
6263
OpenPorts PortEnum `yaml:"open_ports"`
6364

65+
// Language allows defining services to instrument based on the
66+
// programming language they are written in. Use lowercase names, e.g. java,go
67+
Languages GlobAttr `yaml:"languages"`
68+
6469
// Path allows defining the regular expression matching the full executable path.
6570
Path GlobAttr `yaml:"exe_path"`
6671

@@ -150,6 +155,7 @@ func (p *GlobAttr) MatchString(input string) bool {
150155
func (ga *GlobAttributes) GetName() string { return ga.Name }
151156
func (ga *GlobAttributes) GetNamespace() string { return ga.Namespace }
152157
func (ga *GlobAttributes) GetPath() StringMatcher { return &ga.Path }
158+
func (ga *GlobAttributes) GetLanguages() StringMatcher { return &ga.Languages }
153159
func (ga *GlobAttributes) GetPathRegexp() StringMatcher { return nilMatcher{} }
154160
func (ga *GlobAttributes) GetOpenPorts() *PortEnum { return &ga.OpenPorts }
155161
func (ga *GlobAttributes) IsContainersOnly() bool { return ga.ContainersOnly }

pkg/appolly/services/attr_regex.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ func (dc RegexDefinitionCriteria) Validate() error {
2424
if dc[i].OpenPorts.Len() == 0 &&
2525
!dc[i].Path.IsSet() &&
2626
!dc[i].PathRegexp.IsSet() &&
27+
!dc[i].Languages.IsSet() &&
2728
len(dc[i].Metadata) == 0 &&
2829
len(dc[i].PodLabels) == 0 &&
2930
len(dc[i].PodAnnotations) == 0 {
@@ -66,6 +67,9 @@ type RegexSelector struct {
6667
OpenPorts PortEnum `yaml:"open_ports"`
6768
// Path allows defining the regular expression matching the full executable path.
6869
Path RegexpAttr `yaml:"exe_path"`
70+
// Language allows defining services to instrument based on the
71+
// programming language they are written in.
72+
Languages RegexpAttr `yaml:"languages"`
6973
// PathRegexp is deprecated but kept here for backwards compatibility with Beyla 1.0.x.
7074
// Deprecated. Please use Path (exe_path YAML attribute)
7175
PathRegexp RegexpAttr `yaml:"exe_path_regexp"`
@@ -155,6 +159,7 @@ func (p *RegexpAttr) MatchString(input string) bool {
155159
func (a *RegexSelector) GetName() string { return a.Name }
156160
func (a *RegexSelector) GetNamespace() string { return a.Namespace }
157161
func (a *RegexSelector) GetPath() StringMatcher { return &a.Path }
162+
func (a *RegexSelector) GetLanguages() StringMatcher { return &a.Languages }
158163
func (a *RegexSelector) GetPathRegexp() StringMatcher { return &a.PathRegexp }
159164
func (a *RegexSelector) GetOpenPorts() *PortEnum { return &a.OpenPorts }
160165
func (a *RegexSelector) IsContainersOnly() bool { return a.ContainersOnly }

0 commit comments

Comments
 (0)