diff --git a/.github/workflows/kind.yml b/.github/workflows/kind.yml index c4f01fa6e..e20d9985c 100644 --- a/.github/workflows/kind.yml +++ b/.github/workflows/kind.yml @@ -22,6 +22,8 @@ jobs: uses: actions/setup-go@v4 with: go-version: "1.20" + - name: Setup BATS + uses: mig4/setup-bats@v1 - name: Build auto-instrumentation run: | IMG=otel-go-instrumentation:latest make docker-build @@ -68,9 +70,8 @@ jobs: kubectl wait --for=condition=Complete --timeout=60s job/sample-job - name: copy telemetry trace output run: | - kubectl cp -c filecp default/test-opentelemetry-collector-0:tmp/trace.json ./test/e2e/${{ matrix.library }}/traces.json.tmp - jq 'del(.resourceSpans[].scopeSpans[].spans[].endTimeUnixNano, .resourceSpans[].scopeSpans[].spans[].startTimeUnixNano) | .resourceSpans[].scopeSpans[].spans[].spanId|= (if . != "" then "xxxxx" else . end) | .resourceSpans[].scopeSpans[].spans[].traceId|= (if . != "" then "xxxxx" else . end) | .resourceSpans[].scopeSpans|=sort_by(.scope.name)' ./test/e2e/${{ matrix.library }}/traces.json.tmp | jq --sort-keys . > ./test/e2e/${{ matrix.library }}/traces.json - rm ./test/e2e/${{ matrix.library }}/traces.json.tmp - - name: verify output + kubectl cp -c filecp default/test-opentelemetry-collector-0:tmp/trace.json ./test/e2e/${{ matrix.library }}/traces-orig.json + rm -f ./test/e2e/${{ matrix.library }}/traces.json + - name: verify output and redact to traces.json run: | - make check-clean-work-tree + bats ./test/e2e/${{ matrix.library }}/verify.bats diff --git a/Makefile b/Makefile index f405190a7..ed058af7b 100644 --- a/Makefile +++ b/Makefile @@ -136,9 +136,9 @@ fixtures/%: kubectl wait --for=condition=Ready --timeout=60s pod/test-opentelemetry-collector-0 kubectl -n default create -f .github/workflows/e2e/k8s/sample-job.yml kubectl wait --for=condition=Complete --timeout=60s job/sample-job - kubectl cp -c filecp default/test-opentelemetry-collector-0:tmp/trace.json ./test/e2e/$(LIBRARY)/traces.json.tmp - jq 'del(.resourceSpans[].scopeSpans[].spans[].endTimeUnixNano, .resourceSpans[].scopeSpans[].spans[].startTimeUnixNano) | .resourceSpans[].scopeSpans[].spans[].spanId|= (if . != "" then "xxxxx" else . end) | .resourceSpans[].scopeSpans[].spans[].traceId|= (if . != "" then "xxxxx" else . end) | .resourceSpans[].scopeSpans|=sort_by(.scope.name)' ./test/e2e/$(LIBRARY)/traces.json.tmp | jq --sort-keys . > ./test/e2e/$(LIBRARY)/traces.json - rm ./test/e2e/$(LIBRARY)/traces.json.tmp + kubectl cp -c filecp default/test-opentelemetry-collector-0:tmp/trace.json ./test/e2e/$(LIBRARY)/traces-orig.json + rm -f ./test/e2e/$(LIBRARY)/traces.json + bats ./test/e2e/$(LIBRARY)/verify.bats kind delete cluster .PHONY: prerelease diff --git a/test/e2e/gin/verify.bats b/test/e2e/gin/verify.bats new file mode 100644 index 000000000..198590c16 --- /dev/null +++ b/test/e2e/gin/verify.bats @@ -0,0 +1,40 @@ +#!/usr/bin/env bats + +load ../../test_helpers/utilities + +LIBRARY_NAME="github.com/gin-gonic/gin" + +@test "go-auto :: includes service.name in resource attributes" { + result=$(resource_attributes_received | jq "select(.key == \"service.name\").value.stringValue") + assert_equal "$result" '"sample-app"' +} + +@test "${LIBRARY_NAME} :: emits a span name '{http.method} {http.target}' (per semconv)" { + result=$(span_names_for ${LIBRARY_NAME}) + assert_equal "$result" '"GET /hello-gin"' +} + +@test "${LIBRARY_NAME} :: includes http.method attribute" { + result=$(span_attributes_for ${LIBRARY_NAME} | jq "select(.key == \"http.method\").value.stringValue") + assert_equal "$result" '"GET"' +} + +@test "${LIBRARY_NAME} :: includes http.target attribute" { + result=$(span_attributes_for ${LIBRARY_NAME} | jq "select(.key == \"http.target\").value.stringValue") + assert_equal "$result" '"/hello-gin"' +} + +@test "${LIBRARY_NAME} :: trace ID present and valid in all spans" { + trace_id=$(spans_from_scope_named ${LIBRARY_NAME} | jq ".traceId") + assert_regex "$trace_id" ${MATCH_A_TRACE_ID} +} + +@test "${LIBRARY_NAME} :: span ID present and valid in all spans" { + span_id=$(spans_from_scope_named ${LIBRARY_NAME} | jq ".spanId") + assert_regex "$span_id" ${MATCH_A_SPAN_ID} +} + +@test "${LIBRARY_NAME} :: expected (redacted) trace output" { + redact_json + assert_equal "$(git --no-pager diff ${BATS_TEST_DIRNAME}/traces.json)" "" +} diff --git a/test/e2e/gorillamux/verify.bats b/test/e2e/gorillamux/verify.bats new file mode 100644 index 000000000..711ac3b7a --- /dev/null +++ b/test/e2e/gorillamux/verify.bats @@ -0,0 +1,40 @@ +#!/usr/bin/env bats + +load ../../test_helpers/utilities + +LIBRARY_NAME="github.com/gorilla/mux" + +@test "go-auto :: includes service.name in resource attributes" { + result=$(resource_attributes_received | jq "select(.key == \"service.name\").value.stringValue") + assert_equal "$result" '"sample-app"' +} + +@test "${LIBRARY_NAME} :: emits a span name '{http.method} {http.target}' (per semconv)" { + result=$(span_names_for ${LIBRARY_NAME}) + assert_equal "$result" '"GET /users/foo"' +} + +@test "${LIBRARY_NAME} :: includes http.method attribute" { + result=$(span_attributes_for ${LIBRARY_NAME} | jq "select(.key == \"http.method\").value.stringValue") + assert_equal "$result" '"GET"' +} + +@test "${LIBRARY_NAME} :: includes http.target attribute" { + result=$(span_attributes_for ${LIBRARY_NAME} | jq "select(.key == \"http.target\").value.stringValue") + assert_equal "$result" '"/users/foo"' +} + +@test "${LIBRARY_NAME} :: trace ID present and valid in all spans" { + trace_id=$(spans_from_scope_named ${LIBRARY_NAME} | jq ".traceId") + assert_regex "$trace_id" ${MATCH_A_TRACE_ID} +} + +@test "${LIBRARY_NAME} :: span ID present and valid in all spans" { + span_id=$(spans_from_scope_named ${LIBRARY_NAME} | jq ".spanId") + assert_regex "$span_id" ${MATCH_A_SPAN_ID} +} + +@test "${LIBRARY_NAME} :: expected (redacted) trace output" { + redact_json + assert_equal "$(git --no-pager diff ${BATS_TEST_DIRNAME}/traces.json)" "" +} diff --git a/test/e2e/nethttp/verify.bats b/test/e2e/nethttp/verify.bats new file mode 100644 index 000000000..ff9f6b3ee --- /dev/null +++ b/test/e2e/nethttp/verify.bats @@ -0,0 +1,40 @@ +#!/usr/bin/env bats + +load ../../test_helpers/utilities + +LIBRARY_NAME="net/http" + +@test "go-auto :: includes service.name in resource attributes" { + result=$(resource_attributes_received | jq "select(.key == \"service.name\").value.stringValue") + assert_equal "$result" '"sample-app"' +} + +@test "${LIBRARY_NAME} :: emits a span name '{http.method} {http.target}' (per semconv)" { + result=$(span_names_for ${LIBRARY_NAME}) + assert_equal "$result" '"GET /hello"' +} + +@test "${LIBRARY_NAME} :: includes http.method attribute" { + result=$(span_attributes_for ${LIBRARY_NAME} | jq "select(.key == \"http.method\").value.stringValue") + assert_equal "$result" '"GET"' +} + +@test "${LIBRARY_NAME} :: includes http.target attribute" { + result=$(span_attributes_for ${LIBRARY_NAME} | jq "select(.key == \"http.target\").value.stringValue") + assert_equal "$result" '"/hello"' +} + +@test "${LIBRARY_NAME} :: trace ID present and valid in all spans" { + trace_id=$(spans_from_scope_named ${LIBRARY_NAME} | jq ".traceId") + assert_regex "$trace_id" ${MATCH_A_TRACE_ID} +} + +@test "${LIBRARY_NAME} :: span ID present and valid in all spans" { + span_id=$(spans_from_scope_named ${LIBRARY_NAME} | jq ".spanId") + assert_regex "$span_id" ${MATCH_A_SPAN_ID} +} + +@test "${LIBRARY_NAME} :: expected (redacted) trace output" { + redact_json + assert_equal "$(git --no-pager diff ${BATS_TEST_DIRNAME}/traces.json)" "" +} diff --git a/test/test_helpers/utilities.bash b/test/test_helpers/utilities.bash new file mode 100644 index 000000000..d0cf8aa3f --- /dev/null +++ b/test/test_helpers/utilities.bash @@ -0,0 +1,109 @@ +# DATA RETRIEVERS + +# Returns a list of span names emitted by a given library/scope + # $1 - library/scope name +span_names_for() { + spans_from_scope_named $1 | jq '.name' +} + +# Returns a list of attributes emitted by a given library/scope +span_attributes_for() { + # $1 - library/scope name + + spans_from_scope_named $1 | \ + jq ".attributes[]" +} + +# Returns a list of all resource attributes +resource_attributes_received() { + spans_received | jq ".resource.attributes[]?" +} + +# Returns an array of all spans emitted by a given library/scope + # $1 - library/scope name +spans_from_scope_named() { + spans_received | jq ".scopeSpans[] | select(.scope.name == \"$1\").spans[]" +} + +# Returns an array of all spans received +spans_received() { + json_output | jq ".resourceSpans[]?" +} + +# Returns the content of the log file produced by a collector +# and located in the same directory as the BATS test file +# loading this helper script. +json_output() { + cat "${BATS_TEST_DIRNAME}/traces-orig.json" +} + +redact_json() { + json_output | \ + jq --sort-keys ' + del( + .resourceSpans[].scopeSpans[].spans[].startTimeUnixNano, + .resourceSpans[].scopeSpans[].spans[].endTimeUnixNano + ) + | .resourceSpans[].scopeSpans[].spans[].traceId|= (if + . // "" | test("^[A-Fa-f0-9]{32}$") then "xxxxx" else (. + "<-INVALID") + end) + | .resourceSpans[].scopeSpans[].spans[].spanId|= (if + . // "" | test("^[A-Fa-f0-9]{16}$") then "xxxxx" else (. + "<-INVALID") + end) + | .resourceSpans[].scopeSpans|=sort_by(.scope.name) + ' > ${BATS_TEST_DIRNAME}/traces.json +} + +# ASSERTION HELPERS + +# expect a 32-digit hexadecimal string (in quotes) +MATCH_A_TRACE_ID=^"\"[A-Fa-f0-9]{32}\"$" + +# expect a 16-digit hexadecimal string (in quotes) +MATCH_A_SPAN_ID=^"\"[A-Fa-f0-9]{16}\"$" + +# Fail and display details if the expected and actual values do not +# equal. Details include both values. +# +# Inspired by bats-assert * bats-support, but dramatically simplified +assert_equal() { + if [[ $1 != "$2" ]]; then + { + echo + echo "-- 💥 values are not equal 💥 --" + echo "expected : $2" + echo "actual : $1" + echo "--" + echo + } >&2 # output error to STDERR + return 1 + fi +} + +assert_regex() { + if ! [[ $1 =~ $2 ]]; then + { + echo + echo "-- 💥 value does not match regular expression 💥 --" + echo "value : $1" + echo "pattern : $2" + echo "--" + echo + } >&2 # output error to STDERR + return 1 + fi +} + +assert_not_empty() { + EMPTY=(\"\") + if [[ "$1" == "${EMPTY}" ]]; then + { + echo + echo "-- 💥 value is empty 💥 --" + echo "value : $1" + echo "--" + echo + } >&2 # output error to STDERR + return 1 + fi +}