From 819481ed7a252673cfb07178ef2548c077b61ab7 Mon Sep 17 00:00:00 2001 From: Stephen Lang Date: Fri, 10 Oct 2025 18:04:31 +0100 Subject: [PATCH 01/11] test: integration test drift and sync --- scripts/check-obi-drift.sh | 215 ++++++++++++++++++++++++++++++++++++ scripts/replace-function.go | 122 ++++++++++++++++++++ 2 files changed, 337 insertions(+) create mode 100755 scripts/check-obi-drift.sh create mode 100644 scripts/replace-function.go diff --git a/scripts/check-obi-drift.sh b/scripts/check-obi-drift.sh new file mode 100755 index 000000000..2b2e1a375 --- /dev/null +++ b/scripts/check-obi-drift.sh @@ -0,0 +1,215 @@ +#!/usr/bin/env bash +# Detect and optionally sync test functions that have drifted between Beyla and OBI +# +# Usage: +# ./scripts/check-obi-drift.sh # Check for drift (exit 1 if found) +# ./scripts/check-obi-drift.sh --sync # Apply OBI changes to Beyla files +# ./scripts/check-obi-drift.sh --help # Show help + +set -euo pipefail + +OBI_DIR=".obi-src/test/integration" +BEYLA_DIR="test/integration" +SYNC_MODE=false + +# Find test functions that exist in both Beyla and OBI +find_common_functions() { + # Get function names from both directories + local beyla_funcs=$(grep -rh "^func test[a-zA-Z0-9_]*(" "$BEYLA_DIR"/*.go 2>/dev/null | \ + sed 's/^func \([a-zA-Z0-9_]*\).*/\1/' | sort -u) + + local obi_funcs=$(grep -rh "^func test[a-zA-Z0-9_]*(" "$OBI_DIR"/*.go 2>/dev/null | \ + sed 's/^func \([a-zA-Z0-9_]*\).*/\1/' | sort -u) + + # Find common functions + comm -12 <(echo "$beyla_funcs") <(echo "$obi_funcs") +} + +# Extract function body from a file +extract_function() { + local func="$1" + local file="$2" + + # Extract function from start to closing brace at column 0 + # Use \( to match the opening parenthesis and avoid matching function name prefixes + sed -n "/^func $func(/,/^}/p" "$file" +} + +# Find which file contains a function in a directory +find_function_file() { + local func="$1" + local dir="$2" + + grep -l "^func $func(" "$dir"/*.go 2>/dev/null | head -1 +} + +# Sync function from OBI to Beyla +sync_function() { + local func="$1" + local beyla_file="$2" + local obi_file="$3" + + echo " Syncing $func from OBI..." + + # Create temp file with transformed function + local temp_func=$(mktemp) + + # Extract the OBI function body and transform for Beyla + extract_function "$func" "$obi_file" | \ + sed 's|go.opentelemetry.io/obi|github.com/grafana/beyla|g' | \ + sed 's|obi_|beyla_|g' | \ + sed 's|service_name="opentelemetry-ebpf-instrumentation"|service_name="beyla"|g' | \ + sed 's|telemetry.sdk.name", Type: "string", Value: "opentelemetry-ebpf-instrumentation"|telemetry.sdk.name", Type: "string", Value: "beyla"|g' \ + > "$temp_func" + + # Use go run with flags (avoids *_test.go file path parsing issues) + local script_dir="$(dirname "$0")" + go run "$script_dir/replace-function.go" -file "$beyla_file" -func "$func" -new "$temp_func" + + # Clean up + rm -f "$temp_func" + + echo " ✓ Synced $func in ${beyla_file##*/}" +} + +# Compare function implementations +check_drift() { + local func="$1" + + # Find files containing this function + local beyla_file=$(find_function_file "$func" "$BEYLA_DIR") + local obi_file=$(find_function_file "$func" "$OBI_DIR") + + if [[ -z "$beyla_file" ]] || [[ -z "$obi_file" ]]; then + return 0 # Skip if not found in both + fi + + # Extract function bodies + local beyla_body=$(extract_function "$func" "$beyla_file") + local obi_body=$(extract_function "$func" "$obi_file") + + # Normalize to ignore copyright, import path, and metric name changes + local beyla_normalized=$(echo "$beyla_body" | \ + grep -v "Copyright The OpenTelemetry Authors" | \ + grep -v "SPDX-License-Identifier: Apache-2.0" | \ + sed 's|github.com/grafana/beyla|go.opentelemetry.io/obi|g' | \ + sed 's|beyla_|obi_|g' | \ + sed 's|service_name="beyla"|service_name="opentelemetry-ebpf-instrumentation"|g' | \ + sed 's|telemetry.sdk.name", Type: "string", Value: "beyla"|telemetry.sdk.name", Type: "string", Value: "opentelemetry-ebpf-instrumentation"|g') + local obi_normalized=$(echo "$obi_body" | \ + grep -v "Copyright The OpenTelemetry Authors" | \ + grep -v "SPDX-License-Identifier: Apache-2.0") + + if [[ "$beyla_normalized" != "$obi_normalized" ]]; then + echo "$func" + echo " Beyla: ${beyla_file##*/}" + echo " OBI: ${obi_file##*/}" + echo "" + + if [[ "$SYNC_MODE" == "true" ]]; then + sync_function "$func" "$beyla_file" "$obi_file" + else + # Show color-coded diff using normalized versions + diff -u \ + <(echo "$beyla_normalized") \ + <(echo "$obi_normalized") 2>/dev/null | \ + sed 's/^-/\x1b[31m-/; s/^+/\x1b[32m+/; s/^@/\x1b[36m@/; s/$/\x1b[0m/' || true + fi + + echo "" + return 1 + fi + + return 0 +} + +# Show help +show_help() { + cat << EOF +Usage: $0 [OPTIONS] + +Check for drift between Beyla and OBI test functions, and optionally sync them. + +Options: + --sync Apply OBI changes to Beyla files + --help Show this help message + +Examples: + # Check for drift (exit 1 if any found) + $0 + + # Apply OBI changes to Beyla files + $0 --sync + +When --sync is used: + - Replaces drifted Beyla functions with OBI versions + - Creates a git-ready changeset + - You can review changes with 'git diff' and create a PR + +EOF +} + +# Main +main() { + if [[ "$SYNC_MODE" == "true" ]]; then + echo "Syncing drifted test functions from OBI to Beyla..." + else + echo "Checking for drift between Beyla and OBI test functions..." + fi + echo "" + + local common_funcs=$(find_common_functions) + local total=0 + local drifted=0 + + for func in $common_funcs; do + ((total++)) + if ! check_drift "$func"; then + ((drifted++)) + fi + done + + echo "Summary: $drifted/$total test functions have drifted from OBI" + + if [[ $drifted -gt 0 ]]; then + echo "" + if [[ "$SYNC_MODE" == "true" ]]; then + echo "✓ Synced $drifted functions from OBI" + echo "" + echo "Next steps:" + echo " 1. Review changes: git diff test/integration/" + echo " 2. Run tests: go test -tags=integration ./test/integration/..." + echo " 3. Create PR: git add test/integration/ && git commit -m 'Sync tests from OBI'" + else + echo "To sync these changes automatically:" + echo " $0 --sync" + echo "" + echo "To see full diff for a function:" + echo " diff -u test/integration/traces_test.go .obi-src/test/integration/traces_test.go" + exit 1 + fi + fi +} + +# Parse arguments +case "${1:-}" in + --sync) + SYNC_MODE=true + ;; + --help) + show_help + exit 0 + ;; + "") + # Default: check mode + ;; + *) + echo "Error: Unknown option: $1" + echo "" + show_help + exit 1 + ;; +esac + +main + diff --git a/scripts/replace-function.go b/scripts/replace-function.go new file mode 100644 index 000000000..efb9adc25 --- /dev/null +++ b/scripts/replace-function.go @@ -0,0 +1,122 @@ +package main + +import ( + "bytes" + "flag" + "fmt" + "os" + "regexp" +) + +// findFunction finds the start and end byte positions of a function in source code +func findFunction(content []byte, funcName string) (start, end int, err error) { + // Find function declaration + funcPattern := fmt.Sprintf(`(?m)^func %s\(`, regexp.QuoteMeta(funcName)) + re := regexp.MustCompile(funcPattern) + + loc := re.FindIndex(content) + if loc == nil { + return 0, 0, fmt.Errorf("function %s not found", funcName) + } + + start = loc[0] + + // Find the opening brace + openBrace := -1 + for i := loc[1]; i < len(content); i++ { + if content[i] == '{' { + openBrace = i + break + } + } + + if openBrace == -1 { + return 0, 0, fmt.Errorf("opening brace not found for function %s", funcName) + } + + // Count braces to find matching closing brace + braceCount := 1 + for i := openBrace + 1; i < len(content); i++ { + switch content[i] { + case '{': + braceCount++ + case '}': + braceCount-- + if braceCount == 0 { + // Found the matching closing brace + // Include the closing brace and newline if present + end = i + 1 + if end < len(content) && content[end] == '\n' { + end++ + } + return start, end, nil + } + } + } + + return 0, 0, fmt.Errorf("matching closing brace not found for function %s", funcName) +} + +// replaceFunction replaces a function in a Go file with new content, preserving exact formatting +func replaceFunction(filepath, funcName, newFuncContent string) error { + // Read the file + content, err := os.ReadFile(filepath) + if err != nil { + return fmt.Errorf("failed to read file: %w", err) + } + + // Find the function to replace + start, end, err := findFunction(content, funcName) + if err != nil { + return err + } + + // Build new content: before + new function + after + var buf bytes.Buffer + buf.Write(content[:start]) + buf.WriteString(newFuncContent) + if end < len(content) { + buf.Write(content[end:]) + } + + // Write back to file + return os.WriteFile(filepath, buf.Bytes(), 0644) +} + +func main() { + var ( + filepath string + funcName string + newFuncFile string + ) + + flag := flag.NewFlagSet(os.Args[0], flag.ExitOnError) + flag.StringVar(&filepath, "file", "", "Go source file to modify") + flag.StringVar(&funcName, "func", "", "Function name to replace") + flag.StringVar(&newFuncFile, "new", "", "File containing new function content") + + if err := flag.Parse(os.Args[1:]); err != nil { + fmt.Fprintf(os.Stderr, "Error parsing flags: %v\n", err) + os.Exit(1) + } + + if filepath == "" || funcName == "" || newFuncFile == "" { + fmt.Fprintf(os.Stderr, "Usage: %s -file -func -new \n", os.Args[0]) + flag.PrintDefaults() + os.Exit(1) + } + + // Read new function content + newFuncContent, err := os.ReadFile(newFuncFile) + if err != nil { + fmt.Fprintf(os.Stderr, "Error reading new function file: %v\n", err) + os.Exit(1) + } + + if err := replaceFunction(filepath, funcName, string(newFuncContent)); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + + fmt.Printf("✓ Replaced %s in %s\n", funcName, filepath) +} From 8b6988a2441b336d6edaf8bd763a0d3f33e4a48a Mon Sep 17 00:00:00 2001 From: Stephen Lang Date: Fri, 10 Oct 2025 19:23:24 +0100 Subject: [PATCH 02/11] test: sync obi integration tests --- test/integration/discovery_test.go | 41 +++++------ test/integration/http_go_otel_test.go | 6 +- test/integration/multiprocess_test.go | 6 +- test/integration/red_test.go | 9 +-- test/integration/red_test_client.go | 2 +- test/integration/red_test_dotnet.go | 3 +- test/integration/red_test_nodeclient.go | 8 +-- test/integration/red_test_nodejs.go | 2 +- test/integration/red_test_ruby.go | 1 - test/integration/red_test_rust.go | 16 ++--- test/integration/test_utils.go | 2 +- test/integration/traces_test.go | 95 +++++++++++++++---------- test/tools/debug.go | 19 +++++ 13 files changed, 126 insertions(+), 84 deletions(-) create mode 100644 test/tools/debug.go diff --git a/test/integration/discovery_test.go b/test/integration/discovery_test.go index 4f2f52156..ee78772a9 100644 --- a/test/integration/discovery_test.go +++ b/test/integration/discovery_test.go @@ -19,26 +19,8 @@ import ( ) func testSelectiveExports(t *testing.T) { - waitForTestComponents(t, "http://localhost:5000") - waitForTestComponents(t, "http://localhost:5002") waitForTestComponents(t, "http://localhost:5003") - // give enough time for the NodeJS injector to finish - // TODO: once we implement the instrumentation status query API, replace - // this with a proper check to see if the target process has finished - // being instrumented - time.Sleep(60 * time.Second) - - // Run couple of requests to make sure we flush out any transactions that might be - // stuck because of our tracking of full request times - for i := 0; i < 10; i++ { - doHTTPGet(t, "http://localhost:5000/a", 200) - doHTTPGet(t, "http://localhost:5001/b", 200) - } - - // wait a bit for the transactions to flush - time.Sleep(20 * time.Second) - getTraces := func(service string, path string) []jaeger.Trace { query := "http://localhost:16686/api/traces?service=" + service resp, err := http.Get(query) @@ -58,6 +40,23 @@ func testSelectiveExports(t *testing.T) { return traces } + // give enough time for the NodeJS injector to finish + // TODO: once we implement the instrumentation status query API, replace + // this with a proper check to see if the target process has finished + // being instrumented + test.Eventually(t, 3*time.Minute, func(t require.TestingT) { + doHTTPGet(t, "http://localhost:5001/b", 200) + bTraces := getTraces("service-b", "/b") + require.NotNil(t, bTraces) + }) + + // Run couple of requests to make sure we flush out any transactions that might be + // stuck because of our tracking of full request times + for i := 0; i < 10; i++ { + doHTTPGet(t, "http://localhost:5000/a", 200) + doHTTPGet(t, "http://localhost:5001/b", 200) + } + test.Eventually(t, testTimeout, func(t require.TestingT) { aTraces := getTraces("service-a", "/a") bTraces := getTraces("service-b", "/b") @@ -81,12 +80,14 @@ func testSelectiveExports(t *testing.T) { return results } - aMetrics := getMetrics("/a") + test.Eventually(t, 10*time.Second, func(t require.TestingT) { + require.NotEmpty(t, getMetrics("/a")) + }) + bMetrics := getMetrics("/b") cMetrics := getMetrics("/c") dMetrics := getMetrics("/d") - require.NotEmpty(t, aMetrics) require.NotEmpty(t, bMetrics) require.Empty(t, cMetrics) require.NotEmpty(t, dMetrics) diff --git a/test/integration/http_go_otel_test.go b/test/integration/http_go_otel_test.go index 10314b6e8..3124a70d7 100644 --- a/test/integration/http_go_otel_test.go +++ b/test/integration/http_go_otel_test.go @@ -63,7 +63,7 @@ func testForHTTPGoOTelLibrary(t *testing.T, route, svcNs string) { var tq jaeger.TracesQuery require.NoError(t, json.NewDecoder(resp.Body).Decode(&tq)) traces := tq.FindBySpan(jaeger.Tag{Key: "url.path", Type: "string", Value: "/" + slug}) - assert.LessOrEqual(t, 1, len(traces)) + require.NotEmpty(t, traces) trace = traces[0] require.Len(t, trace.Spans, 3) // parent - in queue - processing }, test.Interval(100*time.Millisecond)) @@ -107,7 +107,7 @@ func testInstrumentationMissing(t *testing.T, route, svcNs string) { `http_route="` + route + `",` + `url_path="` + route + `"}`) require.NoError(t, err) - require.Equal(t, len(results), 0) + require.Empty(t, results) }) slug := route[1:] @@ -122,7 +122,7 @@ func testInstrumentationMissing(t *testing.T, route, svcNs string) { var tq jaeger.TracesQuery require.NoError(t, json.NewDecoder(resp.Body).Decode(&tq)) traces := tq.FindBySpan(jaeger.Tag{Key: "url.path", Type: "string", Value: "/" + slug}) - require.Equal(t, len(traces), 0) + require.Empty(t, traces) }, test.Interval(100*time.Millisecond)) } diff --git a/test/integration/multiprocess_test.go b/test/integration/multiprocess_test.go index 75da5b251..a735c7cf0 100644 --- a/test/integration/multiprocess_test.go +++ b/test/integration/multiprocess_test.go @@ -90,7 +90,7 @@ func TestMultiProcess(t *testing.T) { if kprobeTracesEnabled() { t.Run("Nested traces with kprobes: rust -> java -> node -> go -> python -> rails", func(t *testing.T) { - testNestedHTTPTracesKProbes(t, true) + testNestedHTTPTracesKProbes(t) }) t.Run("Nested traces with kprobes: SSL node python rails", func(t *testing.T) { @@ -114,7 +114,7 @@ func TestMultiProcessAppCP(t *testing.T) { if kprobeTracesEnabled() { t.Run("Nested traces with kprobes: rust -> java -> node -> go -> python -> rails", func(t *testing.T) { - testNestedHTTPTracesKProbes(t, true) + testNestedHTTPTracesKProbes(t) }) } require.NoError(t, compose.Close()) @@ -130,7 +130,7 @@ func TestMultiProcessAppCPNoIP(t *testing.T) { if kprobeTracesEnabled() { t.Run("Nested traces with kprobes: rust -> java -> node -> go -> python -> rails", func(t *testing.T) { - testNestedHTTPTracesKProbes(t, true) + testNestedHTTPTracesKProbes(t) }) } require.NoError(t, compose.Close()) diff --git a/test/integration/red_test.go b/test/integration/red_test.go index 0934f3106..1d4bf0408 100644 --- a/test/integration/red_test.go +++ b/test/integration/red_test.go @@ -28,6 +28,7 @@ const ( instrumentedServiceGorillaMidURL = "http://localhost:8083" instrumentedServiceGorillaMid2URL = "http://localhost:8087" instrumentedServiceStdTLSURL = "https://localhost:8383" + instrumentedServiceJSONRPCURL = "http://localhost:8088" prometheusHostPort = "localhost:9090" jaegerQueryURL = "http://localhost:16686/api/traces" @@ -96,12 +97,12 @@ func testREDMetricsShortHTTP(t *testing.T) { func testExemplarsExist(t *testing.T) { url := "http://" + prometheusHostPort + "/api/v1/query_exemplars?query=http_server_request_duration_seconds_bucket" - var qtr = &http.Transport{ + qtr := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } - var qClient = &http.Client{Transport: qtr} + qClient := &http.Client{Transport: qtr} - req, err := http.NewRequest("GET", url, nil) + req, err := http.NewRequest(http.MethodGet, url, nil) require.NoError(t, err) r, err := qClient.Do(req) require.NoError(t, err) @@ -794,7 +795,7 @@ func testREDMetricsForHTTPLibraryNoRoute(t *testing.T, url, svcName string) { // Check that we never recorded any /metrics calls results, err = pq.Query(`http_server_request_duration_seconds_count{http_route="/metrics"}`) require.NoError(t, err) - require.Equal(t, len(results), 0) + require.Empty(t, results) } func testREDMetricsHTTPNoRoute(t *testing.T) { diff --git a/test/integration/red_test_client.go b/test/integration/red_test_client.go index 3d7ac5d7c..472e1137d 100644 --- a/test/integration/red_test_client.go +++ b/test/integration/red_test_client.go @@ -73,7 +73,7 @@ func testClientWithMethodAndStatusCode(t *testing.T, method string, statusCode i addr, ok = jaeger.FindIn(parent.Tags, "server.port") assert.True(t, ok) - assert.Equal(t, float64(443), addr.Value) + assert.EqualValues(t, 443, addr.Value) } func testREDMetricsForClientHTTPLibrary(t *testing.T) { diff --git a/test/integration/red_test_dotnet.go b/test/integration/red_test_dotnet.go index 0652d8e5a..8906d0c62 100644 --- a/test/integration/red_test_dotnet.go +++ b/test/integration/red_test_dotnet.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/beyla/v2/test/integration/components/prom" + "github.com/grafana/beyla/v2/test/tools" ) func testREDMetricsForNetHTTPLibrary(t *testing.T, url string, comm string) { @@ -36,7 +37,7 @@ func testREDMetricsForNetHTTPLibrary(t *testing.T, url string, comm string) { require.NoError(t, err) enoughPromResults(t, results) val := totalPromCount(t, results) - assert.LessOrEqual(t, 3, val) + assert.LessOrEqual(t, 2, val, "received:", tools.ToJSON(val)) if len(results) > 0 { res := results[0] addr := res.Metric["client_address"] diff --git a/test/integration/red_test_nodeclient.go b/test/integration/red_test_nodeclient.go index 8b6289489..a86d37f12 100644 --- a/test/integration/red_test_nodeclient.go +++ b/test/integration/red_test_nodeclient.go @@ -97,9 +97,7 @@ func testNodeClientWithMethodAndStatusCode(t *testing.T, method string, statusCo We check that the traceID has that 16 character 0 suffix and then we use the first 16 characters for looking up by Parent span. */ - if kprobeTracesEnabled() { - assert.NotEmpty(t, span.TraceID) - assert.Truef(t, strings.HasSuffix(span.TraceID, traceIDLookup), - "string %q should have suffix %q", span.TraceID, traceIDLookup) - } + assert.NotEmpty(t, span.TraceID) + assert.Truef(t, strings.HasSuffix(span.TraceID, traceIDLookup), + "string %q should have suffix %q", span.TraceID, traceIDLookup) } diff --git a/test/integration/red_test_nodejs.go b/test/integration/red_test_nodejs.go index 683260eb5..5e7b73481 100644 --- a/test/integration/red_test_nodejs.go +++ b/test/integration/red_test_nodejs.go @@ -16,7 +16,7 @@ import ( func testREDMetricsForNodeHTTPLibrary(t *testing.T, url, urlPath, comm, namespace string) { jsonBody, err := os.ReadFile(path.Join(pathRoot, "test", "integration", "components", "rusttestserver", "mid_data.json")) - assert.NoError(t, err) + require.NoError(t, err) assert.GreaterOrEqual(t, len(jsonBody), 100) // Call 3 times the instrumented service, forcing it to: diff --git a/test/integration/red_test_ruby.go b/test/integration/red_test_ruby.go index 28893c9a4..10834e217 100644 --- a/test/integration/red_test_ruby.go +++ b/test/integration/red_test_ruby.go @@ -160,7 +160,6 @@ func testHTTPTracesNestedNginx(t *testing.T) { require.NotEmpty(t, client.TraceID) require.Equal(t, server.TraceID, client.TraceID) require.NotEmpty(t, client.SpanID) - }, test.Interval(100*time.Millisecond)) } } diff --git a/test/integration/red_test_rust.go b/test/integration/red_test_rust.go index b1bb0c2b9..73d79ba2c 100644 --- a/test/integration/red_test_rust.go +++ b/test/integration/red_test_rust.go @@ -20,7 +20,7 @@ import ( func testREDMetricsForRustHTTPLibrary(t *testing.T, url, comm, namespace string, port int, notraces bool) { jsonBody, err := os.ReadFile(path.Join(pathRoot, "test", "integration", "components", "rusttestserver", "mid_data.json")) - assert.NoError(t, err) + require.NoError(t, err) assert.GreaterOrEqual(t, len(jsonBody), 100) urlPath := "/greeting" @@ -84,12 +84,12 @@ func testREDMetricsForRustHTTPLibrary(t *testing.T, url, comm, namespace string, require.Len(t, res, 1) parent := res[0] require.NotEmpty(t, parent.TraceID) - if kprobeTracesEnabled() { - require.Equal(t, traceID, parent.TraceID) - // Validate that "parent" is a CHILD_OF the traceparent's "parent-id" - childOfPID := trace.ChildrenOf(parentID) - require.Len(t, childOfPID, 1) - } + require.Equal(t, traceID, parent.TraceID) + + // Validate that "parent" is a CHILD_OF the traceparent's "parent-id" + childOfPID := trace.ChildrenOf(parentID) + require.Len(t, childOfPID, 1) + require.NotEmpty(t, parent.SpanID) // check duration is at least 2us assert.Less(t, (2 * time.Microsecond).Microseconds(), parent.Duration) @@ -219,7 +219,7 @@ func checkReportedRustEvents(t *testing.T, comm, namespace string, numEvents int func testREDMetricsForRustHTTP2Library(t *testing.T, url, comm, namespace string) { jsonBody, err := os.ReadFile(path.Join(pathRoot, "test", "integration", "components", "rusttestserver", "mid_data.json")) - assert.NoError(t, err) + require.NoError(t, err) assert.GreaterOrEqual(t, len(jsonBody), 100) urlPath := "/greeting" diff --git a/test/integration/test_utils.go b/test/integration/test_utils.go index cb04c50bd..6f760ad07 100644 --- a/test/integration/test_utils.go +++ b/test/integration/test_utils.go @@ -44,7 +44,7 @@ func doHTTPPost(t *testing.T, path string, status int, jsonBody []byte) { require.Equal(t, status, r.StatusCode) } -func doHTTPGet(t *testing.T, path string, status int) { +func doHTTPGet(t require.TestingT, path string, status int) { // Random fake body to cause the request to have some size (38 bytes) jsonBody := []byte(`{"productId": 123456, "quantity": 100}`) diff --git a/test/integration/traces_test.go b/test/integration/traces_test.go index f864ad469..cdf11a665 100644 --- a/test/integration/traces_test.go +++ b/test/integration/traces_test.go @@ -150,7 +150,6 @@ func testHTTPTracesCommon(t *testing.T, doTraceID bool, httpCode int) { {Key: "telemetry.sdk.language", Type: "string", Value: "go"}, {Key: "telemetry.sdk.name", Type: "string", Value: "beyla"}, {Key: "service.namespace", Type: "string", Value: "integration-test"}, - {Key: "telemetry.sdk.version", Type: "string", Value: "^test-"}, serviceInstance, }, process.Tags) assert.Empty(t, sd, sd.String()) @@ -331,12 +330,12 @@ func testHTTPTracesKProbes(t *testing.T) { require.Len(t, res, 1) parent := res[0] require.NotEmpty(t, parent.TraceID) - if kprobeTracesEnabled() { - require.Equal(t, traceID, parent.TraceID) - // Validate that "parent" is a CHILD_OF the traceparent's "parent-id" - childOfPID := trace.ChildrenOf(parentID) - require.Len(t, childOfPID, 1) - } + require.Equal(t, traceID, parent.TraceID) + + // Validate that "parent" is a CHILD_OF the traceparent's "parent-id" + childOfPID := trace.ChildrenOf(parentID) + require.Len(t, childOfPID, 1) + require.NotEmpty(t, parent.SpanID) // check duration is at least 2us assert.Less(t, (2 * time.Microsecond).Microseconds(), parent.Duration) @@ -648,7 +647,7 @@ func testHTTP2GRPCTracesNestedCallsWithContextPropagation(t *testing.T) { testHTTP2GRPCTracesNestedCalls(t, true) } -func testNestedHTTPTracesKProbes(t *testing.T, extended bool) { +func testNestedHTTPTracesKProbes(t *testing.T) { var traceID string waitForTestComponents(t, "http://localhost:3031") // nodejs @@ -657,6 +656,7 @@ func testNestedHTTPTracesKProbes(t *testing.T, extended bool) { waitForRubyTestComponents(t, "http://localhost:3041") // ruby waitForTestComponentsSub(t, "http://localhost:8086", "/greeting") // java waitForTestComponents(t, "http://localhost:8091") // rust + waitForTestComponents(t, instrumentedServiceJSONRPCURL) // go jsonrpc // Add and check for specific trace ID // Run couple of requests to make sure we flush out any transactions that might be @@ -764,6 +764,30 @@ func testNestedHTTPTracesKProbes(t *testing.T, extended bool) { ) assert.Empty(t, sd, sd.String()) + /* FIXME flaky + // Check the information of the go jsonrpc parent span + test.Eventually(t, testTimeout, func(t require.TestingT) { + res := trace.FindByOperationName("Arith.T /jsonrpc", "server") + require.Len(t, res, 1) + parent := res[0] + require.NotEmpty(t, parent.TraceID) + require.Equal(t, traceID, parent.TraceID) + require.NotEmpty(t, parent.SpanID) + // check duration is at least 2us + assert.Less(t, (2 * time.Microsecond).Microseconds(), parent.Duration) + // check span attributes + sd := parent.Diff( + jaeger.Tag{Key: "http.request.method", Type: "string", Value: "Arith.T"}, + jaeger.Tag{Key: "http.response.status_code", Type: "int64", Value: float64(200)}, + jaeger.Tag{Key: "url.path", Type: "string", Value: "/jsonrpc"}, + jaeger.Tag{Key: "server.port", Type: "int64", Value: float64(8088)}, + jaeger.Tag{Key: "http.route", Type: "string", Value: "/jsonrpc"}, + jaeger.Tag{Key: "span.kind", Type: "string", Value: "server"}, + ) + assert.Empty(t, sd, sd.String()) + }, test.Interval(100*time.Millisecond)) + */ + // Check the information of the python parent span res = trace.FindByOperationName("GET /tracemetoo", "server") require.Len(t, res, 1) @@ -806,16 +830,14 @@ func testNestedHTTPTracesKProbes(t *testing.T, extended bool) { } }) - if extended { - // test now with a different version of Java thread pool - for i := 0; i < 10; i++ { - doHTTPGet(t, "http://localhost:8086/jtraceA", 200) - } - - t.Run("Traces RestClient client /jtraceA", func(t *testing.T) { - ensureTracesMatch(t, "jtraceA") - }) + // test now with a different version of Java thread pool + for i := 0; i < 10; i++ { + doHTTPGet(t, "http://localhost:8086/jtraceA", 200) } + + t.Run("Traces RestClient client /jtraceA", func(t *testing.T) { + ensureTracesMatch(t, "jtraceA") + }) } func ensureTracesMatch(t *testing.T, urlPath string) { @@ -928,25 +950,26 @@ func testNestedHTTPSTracesKProbes(t *testing.T) { ) assert.Empty(t, sd, sd.String()) - // Check the information of the rails parent span - res = trace.FindByOperationName("GET /users", "server") - require.Len(t, res, 1) - parent = res[0] - require.NotEmpty(t, parent.TraceID) - require.Equal(t, traceID, parent.TraceID) - require.NotEmpty(t, parent.SpanID) - // check duration is at least 2us - assert.Less(t, (2 * time.Microsecond).Microseconds(), parent.Duration) - // check span attributes - sd = parent.Diff( - jaeger.Tag{Key: "http.request.method", Type: "string", Value: "GET"}, - jaeger.Tag{Key: "http.response.status_code", Type: "int64", Value: float64(403)}, // something config missing in rails, but 403 is OK :) - jaeger.Tag{Key: "url.path", Type: "string", Value: "/users"}, - jaeger.Tag{Key: "server.port", Type: "int64", Value: float64(3043)}, - jaeger.Tag{Key: "http.route", Type: "string", Value: "/users"}, - jaeger.Tag{Key: "span.kind", Type: "string", Value: "server"}, - ) - assert.Empty(t, sd, sd.String()) + // Disabled until we add PUMA reactor support, otherwise the test is flaky + // // Check the information of the rails parent span + // res = trace.FindByOperationName("GET /users", "server") + // require.Len(t, res, 1) + // parent = res[0] + // require.NotEmpty(t, parent.TraceID) + // require.Equal(t, traceID, parent.TraceID) + // require.NotEmpty(t, parent.SpanID) + // // check duration is at least 2us + // assert.Less(t, (2 * time.Microsecond).Microseconds(), parent.Duration) + // // check span attributes + // sd = parent.Diff( + // jaeger.Tag{Key: "http.request.method", Type: "string", Value: "GET"}, + // jaeger.Tag{Key: "http.response.status_code", Type: "int64", Value: float64(403)}, // something config missing in rails, but 403 is OK :) + // jaeger.Tag{Key: "url.path", Type: "string", Value: "/users"}, + // jaeger.Tag{Key: "server.port", Type: "int64", Value: float64(3043)}, + // jaeger.Tag{Key: "http.route", Type: "string", Value: "/users"}, + // jaeger.Tag{Key: "span.kind", Type: "string", Value: "server"}, + // ) + // assert.Empty(t, sd, sd.String()) // check client call (and ensure server port is correct/not swapped) res = trace.FindByOperationName("GET /users", "client") diff --git a/test/tools/debug.go b/test/tools/debug.go new file mode 100644 index 000000000..36f6852fe --- /dev/null +++ b/test/tools/debug.go @@ -0,0 +1,19 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package tools + +import ( + "encoding/json" + "fmt" +) + +// ToJSON is just a one-line convenience method to provide some debug data +// in tests, without having to deal with error handling nor byte-to-string transformation +func ToJSON(v any) string { + b, err := json.Marshal(v) + if err != nil { + panic(fmt.Sprintf("converting %+v to JSON: %s", v, err.Error())) + } + return string(b) +} From 17a2d33714defbd699f0b18e5febf67e2e82e222 Mon Sep 17 00:00:00 2001 From: Stephen Lang Date: Fri, 10 Oct 2025 20:30:47 +0100 Subject: [PATCH 03/11] test: more jsonrpc changes needed --- .../testserver/jsonrpc/body/formated.json | 11 ++ .../components/testserver/jsonrpc/jsonrpc.go | 85 +++++++++++++ .../components/testserver/std/std.go | 112 ++++++++++++++---- .../components/testserver/testserver.go | 7 ++ .../docker-compose-multiexec-host.yml | 1 + test/integration/docker-compose-multiexec.yml | 1 + 6 files changed, 197 insertions(+), 20 deletions(-) create mode 100644 test/integration/components/testserver/jsonrpc/body/formated.json create mode 100644 test/integration/components/testserver/jsonrpc/jsonrpc.go diff --git a/test/integration/components/testserver/jsonrpc/body/formated.json b/test/integration/components/testserver/jsonrpc/body/formated.json new file mode 100644 index 000000000..575e293bd --- /dev/null +++ b/test/integration/components/testserver/jsonrpc/body/formated.json @@ -0,0 +1,11 @@ +{ + "jsonrpc": "2.0", + "method": "Arith.Multiply", + "params": [ + { + "A": 7, + "B": 8 + } + ], + "id": 1 +} \ No newline at end of file diff --git a/test/integration/components/testserver/jsonrpc/jsonrpc.go b/test/integration/components/testserver/jsonrpc/jsonrpc.go new file mode 100644 index 000000000..342ecbf29 --- /dev/null +++ b/test/integration/components/testserver/jsonrpc/jsonrpc.go @@ -0,0 +1,85 @@ +package jsonrpc + +import ( + "fmt" + "io" + "log/slog" + "net/http" + "net/rpc" + "net/rpc/jsonrpc" +) + +// Args defines the arguments for the RPC methods. +type Args struct { + A, B int +} + +// Arith provides methods for arithmetic operations. +type Arith struct { + Logger *slog.Logger +} + +// Multiply multiplies two numbers and returns the result. +func (t *Arith) Multiply(args *Args, reply *int) error { + t.Logger.Debug("calling", "method", "Arith.Multiply") + *reply = args.A * args.B + return nil +} + +func (t *Arith) Traceme(args *Args, reply *int) error { + t.Logger.Debug("calling", "method", "Arith.Traceme") + requestURL := "http://pytestserver:7773/tracemetoo" + + t.Logger.Debug("calling", "url", requestURL) + + res, err := http.Get(requestURL) + if err != nil { + slog.Error("error making http request", "error", err) + return err + } + + defer res.Body.Close() + return t.Multiply(args, reply) +} + +// ReadWriteCloserWrapper wraps an io.Reader and io.Writer to implement io.ReadWriteCloser. +type ReadWriteCloserWrapper struct { + io.Reader + io.Writer +} + +// Close is a no-op to satisfy the io.ReadWriteCloser interface. +func (w *ReadWriteCloserWrapper) Close() error { + return nil +} + +func Setup(port int) { + log := slog.With("component", "jsonrpc.Arith") + arith := &Arith{ + Logger: log, + } + _ = rpc.Register(arith) + + address := fmt.Sprintf(":%d", port) + log.Info("starting JSON-RPC server", "address", address) + err := http.ListenAndServe(address, HTTPHandler(log)) + log.Error("JSON-RPC server has unexpectedly stopped", "error", err) +} + +func HTTPHandler(log *slog.Logger) http.HandlerFunc { + return func(rw http.ResponseWriter, req *http.Request) { + log.Debug("received request", "url", req.RequestURI) + if req.RequestURI == "/jsonrpc" { + if req.Method != http.MethodPost { + http.Error(rw, "Only POST method is allowed", http.StatusMethodNotAllowed) + return + } + // Wrap the request body and response writer in a ReadWriteCloser. + conn := &ReadWriteCloserWrapper{Reader: req.Body, Writer: rw} + // Serve the request using JSON-RPC codec. + rpc.ServeCodec(jsonrpc.NewServerCodec(conn)) + } else { + rw.WriteHeader(http.StatusOK) + } + } +} diff --git a/test/integration/components/testserver/std/std.go b/test/integration/components/testserver/std/std.go index a3a735f87..2ba789d14 100644 --- a/test/integration/components/testserver/std/std.go +++ b/test/integration/components/testserver/std/std.go @@ -1,8 +1,10 @@ package std import ( + "bytes" "context" "crypto/tls" + "errors" "fmt" "log/slog" "net" @@ -11,6 +13,11 @@ import ( "strconv" "time" + "go.opentelemetry.io/auto/sdk" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" @@ -18,9 +25,13 @@ import ( pb "github.com/grafana/beyla/v2/test/integration/components/testserver/grpc/routeguide" ) +var y2k = time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC) + +var tracer = otel.Tracer("trace-example") + func HTTPHandler(log *slog.Logger, echoPort int) http.HandlerFunc { return func(rw http.ResponseWriter, req *http.Request) { - log.Debug("received request", "url", req.RequestURI) + log.Info("received request", "url", req.RequestURI) if req.RequestURI == "/echo" { echoAsync(rw, echoPort) @@ -42,6 +53,11 @@ func HTTPHandler(log *slog.Logger, echoPort int) http.HandlerFunc { return } + if req.RequestURI == "/manual" { + manual(rw) + return + } + status := arg.DefaultStatus for k, v := range req.URL.Query() { if len(v) == 0 { @@ -68,17 +84,16 @@ func HTTPHandler(log *slog.Logger, echoPort int) http.HandlerFunc { func echoAsync(rw http.ResponseWriter, port int) { duration, err := time.ParseDuration("10s") - if err != nil { slog.Error("can't parse duration", "error", err) - rw.WriteHeader(500) + rw.WriteHeader(http.StatusInternalServerError) return } ctx, cancel := context.WithTimeout(context.Background(), duration) defer cancel() - results := make(chan interface{}) + results := make(chan any) go func() { echo(rw, port) @@ -91,7 +106,7 @@ func echoAsync(rw http.ResponseWriter, port int) { return case <-ctx.Done(): slog.Warn("timeout while waiting for test to complete") - rw.WriteHeader(500) + rw.WriteHeader(http.StatusInternalServerError) return } } @@ -105,7 +120,7 @@ func echo(rw http.ResponseWriter, port int) { res, err := http.Get(requestURL) if err != nil { slog.Error("error making http request", "error", err) - rw.WriteHeader(500) + rw.WriteHeader(http.StatusInternalServerError) return } @@ -113,17 +128,74 @@ func echo(rw http.ResponseWriter, port int) { rw.WriteHeader(res.StatusCode) } -var addrLowPort = net.TCPAddr{Port: 7000} -var transport = &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - DialContext: (&net.Dialer{ - LocalAddr: &addrLowPort, - }).DialContext, +func inner(id int) { + ctx := context.Background() + ts := y2k.Add(10 * time.Microsecond) + + t := tracer + + opts := []trace.SpanStartOption{ + trace.WithAttributes( + attribute.String("user", "user"+strconv.Itoa(id)), + attribute.Bool("admin", true), + ), + trace.WithTimestamp(y2k.Add(500 * time.Microsecond)), + trace.WithSpanKind(trace.SpanKindServer), + } + + _, span := t.Start(ctx, fmt.Sprintf("sig_inner %d", id), opts...) + defer span.End(trace.WithTimestamp(ts.Add(100 * time.Microsecond))) + + if id == 2 { + span.SetName("changed name") + span.SetAttributes( + attribute.String("test", "append"), + ) + } } + +func manual(rw http.ResponseWriter) { + slog.Debug("manual spans") + + ctx := context.Background() + ts := y2k.Add(10 * time.Microsecond) + + provider := sdk.TracerProvider() + t := provider.Tracer( + "main", + trace.WithInstrumentationVersion("v0.0.1"), + trace.WithSchemaURL("https://some_schema"), + ) + + _, span := t.Start(ctx, "sig", trace.WithTimestamp(ts)) + defer span.End(trace.WithTimestamp(ts.Add(100 * time.Microsecond))) + + inner(1) + inner(2) + + span.SetStatus(codes.Error, "application error") + span.RecordError( + errors.New("some unknown error"), + trace.WithTimestamp(y2k.Add(2*time.Second)), + trace.WithStackTrace(true), + trace.WithAttributes(attribute.Int("impact", 11)), + ) + + rw.WriteHeader(http.StatusOK) +} + +var ( + addrLowPort = net.TCPAddr{Port: 7000} + transport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + DialContext: (&net.Dialer{ + LocalAddr: &addrLowPort, + }).DialContext, + } +) var httpClient = &http.Client{Transport: transport} func echoLowPort(rw http.ResponseWriter) { - requestURL := os.Getenv("TARGET_URL") slog.Debug("calling", "url", requestURL) @@ -131,7 +203,7 @@ func echoLowPort(rw http.ResponseWriter) { res, err := httpClient.Get(requestURL) if err != nil { slog.Error("error making http request", "error", err) - rw.WriteHeader(500) + rw.WriteHeader(http.StatusInternalServerError) return } @@ -140,14 +212,14 @@ func echoLowPort(rw http.ResponseWriter) { } func echoDist(rw http.ResponseWriter) { - requestURL := "http://pytestserver:7773/tracemetoo" + requestURL := "http://testserver:8088/jsonrpc" slog.Debug("calling", "url", requestURL) - res, err := http.Get(requestURL) + res, err := http.Post(requestURL, "application/json", bytes.NewReader([]byte(`{"jsonrpc":"2.0","method":"Arith.Traceme","params":[{"A":1,"B":2}],"id":1}`))) if err != nil { slog.Error("error making http request", "error", err) - rw.WriteHeader(500) + rw.WriteHeader(http.StatusInternalServerError) return } @@ -160,7 +232,7 @@ func echoCall(rw http.ResponseWriter) { conn, err := grpc.NewClient("localhost:5051", opts...) if err != nil { slog.Error("fail to dial", "error", err) - rw.WriteHeader(500) + rw.WriteHeader(http.StatusInternalServerError) return } defer conn.Close() @@ -174,10 +246,10 @@ func echoCall(rw http.ResponseWriter) { _, err = client.GetFeature(ctx, point) if err != nil { slog.Error("client.GetFeature failed", "error", err) - rw.WriteHeader(500) + rw.WriteHeader(http.StatusInternalServerError) return } - rw.WriteHeader(204) + rw.WriteHeader(http.StatusNoContent) } func Setup(port int) { diff --git a/test/integration/components/testserver/testserver.go b/test/integration/components/testserver/testserver.go index 776b91b4f..8324165c2 100644 --- a/test/integration/components/testserver/testserver.go +++ b/test/integration/components/testserver/testserver.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/beyla/v2/test/integration/components/testserver/gorillamid" "github.com/grafana/beyla/v2/test/integration/components/testserver/gorillamid2" grpctest "github.com/grafana/beyla/v2/test/integration/components/testserver/grpc/server" + "github.com/grafana/beyla/v2/test/integration/components/testserver/jsonrpc" "github.com/grafana/beyla/v2/test/integration/components/testserver/std" ) @@ -35,6 +36,7 @@ type config struct { GorillaMid2Port int `env:"GORILLA_MID2_PORT" envDefault:"8087"` GRPCPort int `env:"GRPC_PORT" envDefault:"5051"` GRPCTLSPort int `env:"GRPC_TLS_PORT" envDefault:"50051"` + JSONRPCPort int `env:"JSRONRPC_PORT" envDefault:"8088"` LogLevel string `env:"LOG_LEVEL" envDefault:"INFO"` } @@ -88,6 +90,11 @@ func main() { close(wait) }() + go func() { + jsonrpc.Setup(cfg.JSONRPCPort) + close(wait) + }() + // wait indefinitely unless any server crashes <-wait slog.Warn("stopping process") diff --git a/test/integration/docker-compose-multiexec-host.yml b/test/integration/docker-compose-multiexec-host.yml index 09204f3da..c5c3280e4 100644 --- a/test/integration/docker-compose-multiexec-host.yml +++ b/test/integration/docker-compose-multiexec-host.yml @@ -8,6 +8,7 @@ services: image: hatest-testserver ports: - "8080:8080" + - "8088:8088" environment: LOG_LEVEL: DEBUG depends_on: diff --git a/test/integration/docker-compose-multiexec.yml b/test/integration/docker-compose-multiexec.yml index 084e6b2b0..5702af8a3 100644 --- a/test/integration/docker-compose-multiexec.yml +++ b/test/integration/docker-compose-multiexec.yml @@ -8,6 +8,7 @@ services: image: hatest-testserver ports: - "8080:8080" + - "8088:8088" environment: LOG_LEVEL: DEBUG depends_on: From 3e138bee1cac4d6b227d216934b1cdc307ee36e6 Mon Sep 17 00:00:00 2001 From: Stephen Lang Date: Mon, 20 Oct 2025 16:39:36 +0100 Subject: [PATCH 04/11] fix: Add missing JSRONRPC_PORT to duplicate_testserver.sh --- test/integration/components/testserver/duplicate_testserver.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/components/testserver/duplicate_testserver.sh b/test/integration/components/testserver/duplicate_testserver.sh index 0a7a7ac9f..47baf2af2 100644 --- a/test/integration/components/testserver/duplicate_testserver.sh +++ b/test/integration/components/testserver/duplicate_testserver.sh @@ -4,7 +4,7 @@ run_testserver() { # prefix for start ports. E.g. 808 sp=$1 - STD_PORT=${1}0 GIN_PORT=${1}1 GORILLA_PORT=${1}2 GORILLA_MID_PORT=${1}3 GRPC_PORT=${1}4 GRPC_TLS_PORT=${1}5 GORILLA_MID2_PORT=${1}7 STD_TLS_PORT=${1}8 ./duped_service + STD_PORT=${1}0 GIN_PORT=${1}1 GORILLA_PORT=${1}2 GORILLA_MID_PORT=${1}3 GRPC_PORT=${1}4 GRPC_TLS_PORT=${1}5 GORILLA_MID2_PORT=${1}7 STD_TLS_PORT=${1}8 JSRONRPC_PORT=${1}9 ./duped_service } # runs testserver twice From 1538dd48f147378a1694a6beae03b82b9848cd86 Mon Sep 17 00:00:00 2001 From: Stephen Lang Date: Mon, 20 Oct 2025 17:28:26 +0100 Subject: [PATCH 05/11] chore: backport 779 https://github.com/open-telemetry/opentelemetry-ebpf-instrumentation/pull/779 --- test/integration/components/beyla/Dockerfile | 13 +++++++++---- .../configs/instrumenter-config-elixir.yml | 4 +++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/test/integration/components/beyla/Dockerfile b/test/integration/components/beyla/Dockerfile index e9c47c819..a3be8e289 100644 --- a/test/integration/components/beyla/Dockerfile +++ b/test/integration/components/beyla/Dockerfile @@ -17,14 +17,19 @@ COPY vendor/ vendor/ COPY go.mod go.mod COPY go.sum go.sum COPY Makefile Makefile +COPY test/integration/components/beyla/beyla_wrapper.sh /beyla_wrapper.sh # Build RUN make compile-for-coverage # Create final image from minimal + built binary -FROM scratch +FROM ubuntu -WORKDIR / -COPY --from=builder /src/bin/beyla . +COPY --from=builder /src/bin/beyla /beyla +# Wrap the executable in a script that waits for the processes to end +# in order to avoid issues when tearing down the container on kernels 6.14+. +# https://github.com/open-telemetry/opentelemetry-ebpf-instrumentation/issues/781 +COPY --from=builder /beyla_wrapper.sh /beyla_wrapper.sh +RUN chmod +x /beyla_wrapper.sh -ENTRYPOINT [ "/beyla" ] +ENTRYPOINT [ "/beyla_wrapper.sh" ] diff --git a/test/integration/configs/instrumenter-config-elixir.yml b/test/integration/configs/instrumenter-config-elixir.yml index aeb9cbbc5..1ef4c295d 100644 --- a/test/integration/configs/instrumenter-config-elixir.yml +++ b/test/integration/configs/instrumenter-config-elixir.yml @@ -12,4 +12,6 @@ discovery: instrument: - open_ports: 4000 namespace: integration-test - containers_only: true \ No newline at end of file + # TODO: elixir fails to be recognized as running in a container on kernels 6.14+. + # https://github.com/open-telemetry/opentelemetry-ebpf-instrumentation/issues/780 + # containers_only: true From 840685727466667f998a545545d64854967bc928 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Mon, 20 Oct 2025 15:50:12 -0400 Subject: [PATCH 06/11] add missing port --- .../05-uninstrumented-few-services.yml | 71 ++++++++++--------- 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/test/integration/k8s/manifests/05-uninstrumented-few-services.yml b/test/integration/k8s/manifests/05-uninstrumented-few-services.yml index 5c0fb0753..5c5fa84bb 100644 --- a/test/integration/k8s/manifests/05-uninstrumented-few-services.yml +++ b/test/integration/k8s/manifests/05-uninstrumented-few-services.yml @@ -10,6 +10,9 @@ spec: - port: 8080 name: http0 targetPort: http0 + - port: 8088 + name: http1 + targetPort: http1 --- apiVersion: v1 kind: Service @@ -51,19 +54,19 @@ spec: name: pytestserver labels: app: pytestserver - deployment.environment: 'to-be-ignored-in-favor-of-annotation' + deployment.environment: "to-be-ignored-in-favor-of-annotation" annotations: - resource.opentelemetry.io/deployment.environment: 'integration-test' + resource.opentelemetry.io/deployment.environment: "integration-test" spec: affinity: - nodeAffinity: - requiredDuringSchedulingIgnoredDuringExecution: - nodeSelectorTerms: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: - matchExpressions: - - key: deployment/zone - operator: In - values: - - other-progs + - key: deployment/zone + operator: In + values: + - other-progs containers: - name: pytestserver image: pythontestserver:dev @@ -96,14 +99,14 @@ spec: app: testserver spec: affinity: - nodeAffinity: - requiredDuringSchedulingIgnoredDuringExecution: - nodeSelectorTerms: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: - matchExpressions: - - key: deployment/zone - operator: In - values: - - go-progs + - key: deployment/zone + operator: In + values: + - go-progs containers: - name: testserver image: testserver:dev @@ -112,6 +115,9 @@ spec: - containerPort: 8080 hostPort: 8080 name: http0 + - containerPort: 8088 + hostPort: 8088 + name: http1 env: - name: LOG_LEVEL value: "DEBUG" @@ -134,26 +140,27 @@ spec: # hostNetwork: true # dnsPolicy: ClusterFirstWithHostNet affinity: - nodeAffinity: - requiredDuringSchedulingIgnoredDuringExecution: - nodeSelectorTerms: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: - matchExpressions: - - key: deployment/zone - operator: In - values: - - other-progs + - key: deployment/zone + operator: In + values: + - other-progs containers: - name: utestserver - image: 'ghcr.io/open-telemetry/obi-testimg:rails-0.1.0' + image: "ghcr.io/open-telemetry/obi-testimg:rails-0.1.0" imagePullPolicy: Always - ports: - - containerPort: 3040 - hostPort: 3040 - name: http2 + ports: + - containerPort: 3040 + hostPort: 3040 + name: http2 volumeMounts: - - name: tmp-rails - mountPath: /tmp + - name: tmp-rails + mountPath: /tmp volumes: - - name: tmp-rails - emptyDir: {} + - name: tmp-rails + emptyDir: {} --- + From b3d06812aa956902ca7852434539b9b850140df8 Mon Sep 17 00:00:00 2001 From: Nikola Grcevski Date: Mon, 20 Oct 2025 18:35:14 -0400 Subject: [PATCH 07/11] fix wrapper --- test/integration/components/beyla/beyla_wrapper.sh | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/integration/components/beyla/beyla_wrapper.sh b/test/integration/components/beyla/beyla_wrapper.sh index 37a4fa016..9f871da3c 100755 --- a/test/integration/components/beyla/beyla_wrapper.sh +++ b/test/integration/components/beyla/beyla_wrapper.sh @@ -1,11 +1,5 @@ #!/bin/bash -# Mount debugfs, we should be running privileged, so should be doable -mount -t debugfs nodev /sys/kernel/debug - -# Start the trace pipe reader -cat /sys/kernel/debug/tracing/trace_pipe & - # Start the instrumenter ./beyla "$@" & From aabf5f04e92c83e27cf3b9db30e1b48771f4884b Mon Sep 17 00:00:00 2001 From: Stephen Lang Date: Tue, 21 Oct 2025 11:33:26 +0100 Subject: [PATCH 08/11] fix: trailing YAML separator causes duplicate manifest --- .../k8s/manifests/05-uninstrumented-few-services.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/integration/k8s/manifests/05-uninstrumented-few-services.yml b/test/integration/k8s/manifests/05-uninstrumented-few-services.yml index 5c5fa84bb..fab3dba50 100644 --- a/test/integration/k8s/manifests/05-uninstrumented-few-services.yml +++ b/test/integration/k8s/manifests/05-uninstrumented-few-services.yml @@ -162,5 +162,3 @@ spec: volumes: - name: tmp-rails emptyDir: {} ---- - From 69dbf1d728539f43fedefd341172fad39398ea31 Mon Sep 17 00:00:00 2001 From: Stephen Lang Date: Tue, 21 Oct 2025 14:15:58 +0100 Subject: [PATCH 09/11] fix: exclude beyla container from survey discovery --- test/integration/configs/instrumenter-config-promscrape.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/integration/configs/instrumenter-config-promscrape.yml b/test/integration/configs/instrumenter-config-promscrape.yml index d86aa5b10..5351b35ee 100644 --- a/test/integration/configs/instrumenter-config-promscrape.yml +++ b/test/integration/configs/instrumenter-config-promscrape.yml @@ -1,6 +1,8 @@ discovery: survey: - k8s_namespace: "*" + exclude_instrument: + - k8s_container_name: "beyla" routes: patterns: - /basic/:rnd From 54360a6bf3e600b9432081685e39f92a4f750b02 Mon Sep 17 00:00:00 2001 From: Stephen Lang Date: Tue, 21 Oct 2025 14:46:21 +0100 Subject: [PATCH 10/11] Revert "fix: exclude beyla container from survey discovery" This reverts commit 69dbf1d728539f43fedefd341172fad39398ea31. --- test/integration/configs/instrumenter-config-promscrape.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/integration/configs/instrumenter-config-promscrape.yml b/test/integration/configs/instrumenter-config-promscrape.yml index 5351b35ee..d86aa5b10 100644 --- a/test/integration/configs/instrumenter-config-promscrape.yml +++ b/test/integration/configs/instrumenter-config-promscrape.yml @@ -1,8 +1,6 @@ discovery: survey: - k8s_namespace: "*" - exclude_instrument: - - k8s_container_name: "beyla" routes: patterns: - /basic/:rnd From 3ed8e60e15c0652e073a8140cb306d889f55f804 Mon Sep 17 00:00:00 2001 From: Stephen Lang Date: Tue, 21 Oct 2025 14:53:35 +0100 Subject: [PATCH 11/11] feat: container name default exclusion --- pkg/beyla/config_test.go | 3 +++ pkg/services/criteria.go | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/pkg/beyla/config_test.go b/pkg/beyla/config_test.go index 2cc8e5257..ce127c0c3 100644 --- a/pkg/beyla/config_test.go +++ b/pkg/beyla/config_test.go @@ -273,6 +273,9 @@ network: services.GlobAttributes{ Metadata: map[string]*services.GlobAttr{"k8s_namespace": &servicesextra.K8sDefaultNamespacesGlob}, }, + services.GlobAttributes{ + Metadata: map[string]*services.GlobAttr{"k8s_container_name": &servicesextra.K8sDefaultExcludeContainerNamesGlob}, + }, }, DefaultOtlpGRPCPort: 4317, RouteHarvesterTimeout: 10 * time.Second, diff --git a/pkg/services/criteria.go b/pkg/services/criteria.go index facea32e9..ab4ec57b8 100644 --- a/pkg/services/criteria.go +++ b/pkg/services/criteria.go @@ -22,6 +22,7 @@ var K8sDefaultNamespacesGlob = services.NewGlob("{kube-system,kube-node-lease,lo var K8sDefaultNamespacesWithSurveyRegex = services.NewRegexp("^kube-system$|^kube-node-lease$|^local-path-storage$|^cert-manager$" + k8sGKEDefaultNamespacesRegex + k8sAKSDefaultNamespacesRegex) var K8sDefaultNamespacesWithSurveyGlob = services.NewGlob("{kube-system,kube-node-lease,local-path-storage,cert-manager" + k8sGKEDefaultNamespacesGlob + k8sAKSDefaultNamespacesGlob + "}") +var K8sDefaultExcludeContainerNamesGlob = services.NewGlob("{beyla,ebpf-instrument,alloy,prometheus-config-reloader,otelcol,otelcol-contrib}") var DefaultExcludeServices = services.RegexDefinitionCriteria{ services.RegexSelector{ @@ -47,6 +48,9 @@ var DefaultExcludeInstrument = services.GlobDefinitionCriteria{ services.GlobAttributes{ Metadata: map[string]*services.GlobAttr{"k8s_namespace": &K8sDefaultNamespacesGlob}, }, + services.GlobAttributes{ + Metadata: map[string]*services.GlobAttr{"k8s_container_name": &K8sDefaultExcludeContainerNamesGlob}, + }, } var DefaultExcludeInstrumentWithSurvey = services.GlobDefinitionCriteria{ services.GlobAttributes{ @@ -55,6 +59,9 @@ var DefaultExcludeInstrumentWithSurvey = services.GlobDefinitionCriteria{ services.GlobAttributes{ Metadata: map[string]*services.GlobAttr{"k8s_namespace": &K8sDefaultNamespacesWithSurveyGlob}, }, + services.GlobAttributes{ + Metadata: map[string]*services.GlobAttr{"k8s_container_name": &K8sDefaultExcludeContainerNamesGlob}, + }, } // DiscoveryConfig for the discover.ProcessFinder pipeline