Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions .github/workflows/obi-patch-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: OBI Patch CI

on:
pull_request:
paths:
- "ebpf/patches/obi/**"
- "ebpf/Dockerfile"
- ".github/workflows/obi-patch-ci.yml"
push:
branches:
- main
paths:
- "ebpf/patches/obi/**"
- "ebpf/Dockerfile"
- ".github/workflows/obi-patch-ci.yml"

permissions:
contents: read

concurrency:
group: obi-patch-${{ github.ref }}
cancel-in-progress: true

jobs:
test-obi-patches:
runs-on: ubuntu-22.04
timeout-minutes: 45
steps:
- uses: actions/checkout@v4

- name: Read OBI build pins
run: |
{
echo "OBI_VERSION=$(grep -oP '^ARG OBI_VERSION=\K.*' ebpf/Dockerfile | head -1 | tr -d '\"')"
echo "OBI_REVISION=$(grep -oP '^ARG OBI_REVISION=\K.*' ebpf/Dockerfile | head -1 | tr -d '\"')"
echo "OBI_GENERATOR=$(grep -oP '^FROM \Kghcr.io/open-telemetry/obi-generator:[^ ]+' ebpf/Dockerfile | head -1)"
} >> "$GITHUB_ENV"

- name: Clone OBI and apply patches
run: |
git clone --depth 1 --branch "$OBI_VERSION" \
https://github.com/open-telemetry/opentelemetry-ebpf-instrumentation.git "$RUNNER_TEMP/obi"
test "$(git -C "$RUNNER_TEMP/obi" rev-parse HEAD)" = "$OBI_REVISION"
git -C "$RUNNER_TEMP/obi" apply "$GITHUB_WORKSPACE"/ebpf/patches/obi/*.patch

- name: Generate eBPF sources
run: |
docker run --rm \
-v "$RUNNER_TEMP/obi:/src" \
-w /src \
"$OBI_GENERATOR"

- uses: actions/setup-go@v5
with:
go-version-file: ${{ runner.temp }}/obi/go.mod
cache: false

- name: Download Go modules
working-directory: ${{ runner.temp }}/obi
run: go mod download

- name: Test patched packages
working-directory: ${{ runner.temp }}/obi
run: go test -race ./pkg/appolly/discover ./pkg/internal/goexec

- name: Build production OBI stage
run: docker build --target obi-builder --file ebpf/Dockerfile .
2 changes: 1 addition & 1 deletion collector/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ RUN mkdir -p /versions/0-default \
# Set environment variables
ENV BASE_URL=https://telemetry.betterstack.com
ENV CLUSTER_COLLECTOR=false
ENV COLLECTOR_VERSION=1.1.29
ENV COLLECTOR_VERSION=1.1.30
ENV VECTOR_VERSION=0.47.0
ENV OBI_VERSION=0.10.0
ENV CLUSTER_AGENT_VERSION=1.6.1
Expand Down
27 changes: 20 additions & 7 deletions ebpf/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,25 @@ FROM quay.io/prometheuscommunity/elasticsearch-exporter:v1.11.0 AS elasticsearch
# Add pgbouncer exporter to the image
FROM prometheuscommunity/pgbouncer-exporter:v0.12.0 AS pgbouncer-exporter

# Download OBI binary from GitHub release (Docker image otel/ebpf-instrument:v0.5.0 exists but is amd64-only)
FROM debian:13.5-slim AS obi-source
# Build OBI from source with patches
FROM ghcr.io/open-telemetry/obi-generator:0.2.15@sha256:9cbb1b567377d5779b04e6bcdb87431c77a19e797b4630eba30f5417de96ea33 AS obi-builder
RUN apk add --no-cache bash git make
ARG TARGETARCH
ARG CUSTOMIZATION_TAG="betterstack"
ARG OBI_VERSION="v0.10.0"
ADD https://github.com/open-telemetry/opentelemetry-ebpf-instrumentation/releases/download/${OBI_VERSION}/obi-${OBI_VERSION}-linux-${TARGETARCH}.tar.gz /tmp/obi.tar.gz
RUN tar -xzf /tmp/obi.tar.gz -C /tmp
ARG OBI_REVISION="4a4dc7ecdb2aeba6791d02fbebb5048c0f0c1261"
RUN git clone --depth 1 --branch ${OBI_VERSION} \
https://github.com/open-telemetry/opentelemetry-ebpf-instrumentation.git /tmp/obi && \
test "$(git -C /tmp/obi rev-parse HEAD)" = "${OBI_REVISION}"
WORKDIR /tmp/obi
RUN --mount=type=cache,target=/go/pkg/mod go mod download
COPY ebpf/patches/obi/*.patch /tmp/obi-patches/
RUN git apply /tmp/obi-patches/*.patch
ENV GOARCH=${TARGETARCH}
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
/generate.sh && \
make compile RELEASE_VERSION="${OBI_VERSION}-${CUSTOMIZATION_TAG}"

# Final stage - Debian slim for glibc compatibility with node-agent
FROM debian:13.5-slim
Expand All @@ -79,9 +92,9 @@ RUN curl -sS https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem \
# Create necessary directories
RUN mkdir -p /etc/supervisor/conf.d /var/log/supervisor /enrichment /bootstrap

# Copy OBI binary and license files from extracted tarball
COPY --from=obi-source --chmod=755 /tmp/obi /usr/local/bin/ebpf-instrument
COPY --from=obi-source /tmp/LICENSE /tmp/NOTICE /usr/share/doc/ebpf-instrument/
# Copy OBI binary and license files
COPY --from=obi-builder --chmod=755 /tmp/obi/bin/obi /usr/local/bin/ebpf-instrument
COPY --from=obi-builder /tmp/obi/LICENSE /tmp/obi/NOTICE /usr/share/doc/ebpf-instrument/

# Copy Node Agent
COPY --from=node-agent-builder --chmod=755 /usr/bin/coroot-node-agent /usr/local/bin/node-agent
Expand Down
67 changes: 67 additions & 0 deletions ebpf/patches/obi/001-respect-child-exclusions.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
diff --git a/pkg/appolly/discover/matcher.go b/pkg/appolly/discover/matcher.go
index 414feac82..79a4c8796 100644
--- a/pkg/appolly/discover/matcher.go
+++ b/pkg/appolly/discover/matcher.go
@@ -183,7 +183,7 @@ func (m *Matcher) filterCreated(obj ProcessAttrs) (Event[ProcessMatch], bool) {
}

// We didn't match the process, but let's see if the parent PID is tracked, it might be the child hasn't opened the port yet
- if procMatch, ok := m.ProcessHistory[proc.PPid]; ok {
+ if procMatch, ok := m.ProcessHistory[proc.PPid]; ok && !m.isExcluded(&obj, proc) {
m.Log.Debug("found process by matching the process parent id", "pid", proc.Pid, "ppid", proc.PPid, "comm", proc.ExePath, "metadata", obj.metadata)

procMatch.Process = proc
diff --git a/pkg/appolly/discover/matcher_test.go b/pkg/appolly/discover/matcher_test.go
index 10da151ae..c4b28eb99 100644
--- a/pkg/appolly/discover/matcher_test.go
+++ b/pkg/appolly/discover/matcher_test.go
@@ -448,6 +448,49 @@ func TestCriteriaMatcherMissingPort(t *testing.T) {
assert.Zero(t, matches[1].Obj.DynamicSelectorPID)
}

+func TestCriteriaMatcherExcludedChildDoesNotInheritParentMatch(t *testing.T) {
+ pipeConfig := obi.Config{}
+ require.NoError(t, yaml.Unmarshal([]byte(`discovery:
+ instrument:
+ - name: port-only
+ open_ports: 80
+ exclude_instrument:
+ - exe_path: /tmp/provjob*
+`), &pipeConfig))
+
+ discoveredProcesses := msg.NewQueue[[]Event[ProcessAttrs]](msg.ChannelBufferLen(10))
+ filteredProcessesQu := msg.NewQueue[[]Event[ProcessMatch]](msg.ChannelBufferLen(10))
+ filteredProcesses := filteredProcessesQu.Subscribe()
+ matcherFunc, err := criteriaMatcherProvider(&pipeConfig, discoveredProcesses, filteredProcessesQu, FindingCriteria(&pipeConfig), nil)(t.Context())
+ require.NoError(t, err)
+ go matcherFunc(t.Context())
+ defer filteredProcessesQu.Close()
+
+ processInfo = func(pp ProcessAttrs) (*services.ProcessInfo, error) {
+ proc := map[app.PID]struct {
+ Exe string
+ PPid app.PID
+ }{
+ 1: {Exe: "/bin/parent"},
+ 2: {Exe: "/bin/allowed", PPid: 1},
+ 3: {Exe: "/tmp/provjob123 (deleted)", PPid: 1},
+ }[pp.pid]
+ return &services.ProcessInfo{Pid: pp.pid, ExePath: proc.Exe, PPid: proc.PPid, OpenPorts: pp.openPorts}, nil
+ }
+
+ discoveredProcesses.Send([]Event[ProcessAttrs]{
+ {Type: EventCreated, Obj: ProcessAttrs{pid: 1, openPorts: []uint32{80}}},
+ {Type: EventCreated, Obj: ProcessAttrs{pid: 2}},
+ {Type: EventCreated, Obj: ProcessAttrs{pid: 3}},
+ })
+
+ matches := testutil.ReadChannel(t, filteredProcesses, testTimeout)
+ require.Len(t, matches, 2)
+ assert.Equal(t, app.PID(1), matches[0].Obj.Process.Pid)
+ assert.Equal(t, app.PID(2), matches[1].Obj.Process.Pid)
+ assert.NotContains(t, matches[1].Obj.Process.ExePath, "provjob")
+}
+
func TestDynamicMatcher_ChildInheritsDynamicSelectorPID(t *testing.T) {
dynamicSelector := NewDynamicPIDSelector()
dynamicSelector.AddPIDs(100)
71 changes: 71 additions & 0 deletions ebpf/patches/obi/002-reject-invalid-go-versions.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
diff --git a/pkg/internal/goexec/instructions.go b/pkg/internal/goexec/instructions.go
index 0c7d62447..e41f06e7c 100644
--- a/pkg/internal/goexec/instructions.go
+++ b/pkg/internal/goexec/instructions.go
@@ -304,6 +304,10 @@ func loadModuledataOffsets(elfF *elf.File) (moduledataOffsets, error) {
return moduledataOffsets{}, fmt.Errorf("getting Go version: %w", err)
}

+ if !supportedGoVersion(goVersion) {
+ return moduledataOffsets{}, fmt.Errorf("unsupported Go version: %v. Minimum supported version is %v", goVersion, minGoVersion)
+ }
+
goVersion = strings.ReplaceAll(goVersion, "go", "")

offs, err := offsets.Read(bytes.NewBufferString(prefetchedOffsets))
diff --git a/pkg/internal/goexec/moduledata_test.go b/pkg/internal/goexec/moduledata_test.go
index 72c25f576..e13d417ac 100644
--- a/pkg/internal/goexec/moduledata_test.go
+++ b/pkg/internal/goexec/moduledata_test.go
@@ -77,7 +77,7 @@ const (
//
// pclntableData is the value written into the runtime.moduledata pclntable.data field.
// All other moduledata fields are fixed (pcHeader = testGopclntabVMA, text = testTextVMA, …).
-func buildTestELF(t *testing.T, pclntableData uint64) *elf.File {
+func buildTestELF(t *testing.T, pclntableData uint64, goVersions ...string) *elf.File {
t.Helper()

const (
@@ -161,6 +161,17 @@ func buildTestELF(t *testing.T, pclntableData uint64) *elf.File {
put64(db+int(testMDText), testTextVMA) // text
put64(db+int(testMDEtext), testTextVMA+testTextSize) // etext

+ if len(goVersions) > 0 {
+ buildInfoOffset := db + 0x200
+ copy(buf[buildInfoOffset:], buildInfoMagic)
+ buf[buildInfoOffset+14] = 8
+ buf[buildInfoOffset+15] = 2
+ buildInfo := binary.AppendUvarint(nil, uint64(len(goVersions[0])))
+ buildInfo = append(buildInfo, goVersions[0]...)
+ buildInfo = append(buildInfo, 0)
+ copy(buf[buildInfoOffset+32:], buildInfo)
+ }
+
// ── .shstrtab ────────────────────────────────────────────────────────────────
shstrtab := "\x00.text\x00.gopclntab\x00.data\x00.shstrtab\x00"
copy(buf[shstrtabFileOff:], shstrtab)
@@ -208,6 +219,24 @@ func buildTestELF(t *testing.T, pclntableData uint64) *elf.File {
return f
}

+func TestLoadModuledataOffsets(t *testing.T) {
+ elfF := buildTestELF(t, testGopclntabVMA, "go1.26.4")
+
+ actual, err := loadModuledataOffsets(elfF)
+ require.NoError(t, err)
+ require.Equal(t, testMDOffsets, actual)
+}
+
+func TestLoadModuledataOffsetsRejectsInvalidGoVersion(t *testing.T) {
+ elfF := buildTestELF(t, testGopclntabVMA, "unknown")
+
+ var err error
+ require.NotPanics(t, func() {
+ _, err = loadModuledataOffsets(elfF)
+ })
+ require.ErrorContains(t, err, "unsupported Go version: unknown")
+}
+
// emptyRelocs returns a zero-populated relocationInfo (no RELA, no RELR).
func emptyRelocs() relocationInfo {
return relocationInfo{
Loading