diff --git a/.github/workflows/lambda-test.yml b/.github/workflows/lambda-test.yml deleted file mode 100644 index 95a5c4e6..00000000 --- a/.github/workflows/lambda-test.yml +++ /dev/null @@ -1,108 +0,0 @@ -name: Lambda Release Test - -on: - workflow_dispatch: - workflow_run: - workflows: [Push] - types: - - completed - -permissions: - packages: write - contents: read - id-token: write - security-events: write - -env: - SW_APM_DEBUG_LEVEL: trace - AGENT_DOWNLOAD_URL: https://agent-binaries.global.st-ssp.solarwinds.com/apm/java/latest/solarwinds-apm-agent.jar - SW_APM_COLLECTOR: ${{ secrets.SW_APM_COLLECTOR }} - SW_APM_SERVICE_KEY_AO: ${{ secrets.SW_APM_SERVICE_KEY_AO }} - SW_APM_SERVICE_KEY: ${{ secrets.SW_APM_SERVICE_KEY }} - GITHUB_USERNAME: ${{ github.actor }} - SWO_LOGIN_URL: ${{ secrets.SWO_LOGIN_URL }} - SWO_HOST_URL: ${{ secrets.SWO_HOST_URL }} - SWO_EMAIL: ${{ secrets.SWO_EMAIL }} - SWO_PWORD: ${{ secrets.SWO_PWORD }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }} - CENTRAL_TOKEN: ${{ secrets.CENTRAL_TOKEN }} - -jobs: - lambda-release-test: - runs-on: ubuntu-latest - env: - LAMBDA: "true" - OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Free Disk Space before Build - run: | - echo "Disk space before pre-build cleanup:" - df -h - docker system prune -af - sudo rm -rf /usr/local/.ghcup - sudo rm -rf /opt/hostedtoolcache/CodeQL - sudo rm -rf /usr/local/lib/android/sdk/ndk - sudo rm -rf /usr/share/dotnet - sudo rm -rf /opt/ghc - sudo rm -rf /usr/local/share/boost - echo "Disk space after pre-build cleanup:" - df -h - - - name: Set up JDK 17 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 - with: - java-version: '17' - distribution: 'temurin' - - - name: Set agent version - id: set_version - uses: ./.github/actions/version - - - name: Set snapshot version - run: | - GIT_HASH=$(git rev-parse --short "$GITHUB_SHA") - echo "AGENT_VERSION=${{ steps.set_version.outputs.version }}.$GIT_HASH" >> $GITHUB_ENV - - - name: Build smoke-test - run: | - cd smoke-tests - ./gradlew build -x test - - - name: Build webmvc jar - run: | - cd smoke-tests - ./gradlew :spring-boot-webmvc:build - - - name: Build webmvc image - run: | - cd smoke-tests/spring-boot-webmvc - docker image build --tag smt:webmvc . - - - name: Docker login - run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $GITHUB_USERNAME --password-stdin - - - name: Execute smoke tests - run: | - cd smoke-tests - ./gradlew test --tests "com.solarwinds.LambdaTest" -Pno-reports - - - name: Free Disk Space After Build - run: | - echo "Disk space before post-build cleanup:" - df -h - sudo rm -rf /usr/local/.ghcup - sudo rm -rf /opt/hostedtoolcache/CodeQL - sudo rm -rf /usr/local/lib/android/sdk/ndk - sudo rm -rf /usr/share/dotnet - sudo rm -rf /opt/ghc - sudo rm -rf /usr/local/share/boost - sudo rm -rf smoke-tests/build/ - echo "Disk space after post-build cleanup:" - df -h - - - name: Docker logout - if: always() - run: docker logout \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 1d4507e3..5202c194 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -82,10 +82,6 @@ allprojects { } } } - - tasks.withType { - finalizedBy(tasks.named("cleanListedDependencies")) - } } } diff --git a/buildSrc/src/main/kotlin/solarwinds.instrumentation-conventions.gradle.kts b/buildSrc/src/main/kotlin/solarwinds.instrumentation-conventions.gradle.kts index 2b6922a7..f00cf114 100644 --- a/buildSrc/src/main/kotlin/solarwinds.instrumentation-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/solarwinds.instrumentation-conventions.gradle.kts @@ -147,6 +147,11 @@ class JavaagentProvider( "--add-opens=java.base/java.lang=ALL-UNNAMED", "--add-opens=java.base/java.lang.invoke=ALL-UNNAMED", "-XX:+IgnoreUnrecognizedVMOptions", - "-Dotel.javaagent.debug=true" + "-Dotel.javaagent.debug=true", + "-Dio.opentelemetry.context.enableStrictContext=true", + "-Dotel.java.experimental.span-stacktrace.min.duration=0ms", + "-Dsw.apm.profiler.enabled=true", + "-Dsw.apm.profiler.interval=10", + "-Dsw.apm.span.stacktrace.filters=thread.id,os.description,http.request.method" ) } diff --git a/instrumentation/spring-webmvc/spring-webmvc-3.1/javaagent/build.gradle.kts b/instrumentation/spring-webmvc/spring-webmvc-3.1/javaagent/build.gradle.kts index 7e8e1f48..cb226c7b 100644 --- a/instrumentation/spring-webmvc/spring-webmvc-3.1/javaagent/build.gradle.kts +++ b/instrumentation/spring-webmvc/spring-webmvc-3.1/javaagent/build.gradle.kts @@ -49,6 +49,10 @@ dependencies { compileOnly("org.springframework:spring-webmvc:3.1.0.RELEASE") compileOnly("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api") compileOnly("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-incubator") + + testImplementation("org.springframework:spring-webmvc:5.3.30") + testImplementation("org.springframework:spring-test:5.3.30") + testImplementation("javax.servlet:javax.servlet-api:3.1.0") } swoJava { diff --git a/instrumentation/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/java/com/solarwinds/opentelemetry/instrumentation/webmvc/v3_1/HandlerNameInstrumentationTest.java b/instrumentation/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/java/com/solarwinds/opentelemetry/instrumentation/webmvc/v3_1/HandlerNameInstrumentationTest.java new file mode 100644 index 00000000..dec0a364 --- /dev/null +++ b/instrumentation/spring-webmvc/spring-webmvc-3.1/javaagent/src/test/java/com/solarwinds/opentelemetry/instrumentation/webmvc/v3_1/HandlerNameInstrumentationTest.java @@ -0,0 +1,99 @@ +/* + * © SolarWinds Worldwide, LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.solarwinds.opentelemetry.instrumentation.webmvc.v3_1; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockServletConfig; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.servlet.DispatcherServlet; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +@ExtendWith(MockitoExtension.class) +class HandlerNameInstrumentationTest { + + @RegisterExtension + static final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Test + void verifyHandlerNameIsStampedOnServerSpan() { + testing.runWithSpan( + "server", + () -> { + try { + AnnotationConfigWebApplicationContext appContext = + new AnnotationConfigWebApplicationContext(); + appContext.register(TestConfig.class); + + DispatcherServlet dispatcherServlet = new DispatcherServlet(appContext); + dispatcherServlet.init(new MockServletConfig()); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/greet"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + try { + dispatcherServlet.service(request, response); + } finally { + dispatcherServlet.destroy(); + appContext.close(); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("server") + .hasKind(SpanKind.INTERNAL) + .hasAttributesSatisfying( + attributes -> + assertEquals( + "TestController.greet", + attributes.get(AttributeKey.stringKey("HandlerName")))))); + } + + @EnableWebMvc + @org.springframework.context.annotation.Configuration + static class TestConfig { + @org.springframework.context.annotation.Bean + public TestController testController() { + return new TestController(); + } + } + + @RestController + static class TestController { + @GetMapping("/greet") + public String greet() { + return "hello"; + } + } +} diff --git a/instrumentation/spring-webmvc/spring-webmvc-6/javaagent/build.gradle.kts b/instrumentation/spring-webmvc/spring-webmvc-6/javaagent/build.gradle.kts index 6c66c6bc..c6762a23 100644 --- a/instrumentation/spring-webmvc/spring-webmvc-6/javaagent/build.gradle.kts +++ b/instrumentation/spring-webmvc/spring-webmvc-6/javaagent/build.gradle.kts @@ -37,6 +37,11 @@ dependencies { compileOnly("org.springframework:spring-webmvc:6.0.0") compileOnly("jakarta.servlet:jakarta.servlet-api:5.0.0") compileOnly("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-incubator") + + testImplementation("org.springframework:spring-webmvc:6.0.0") + testImplementation("org.springframework:spring-test:6.0.0") + testImplementation("jakarta.servlet:jakarta.servlet-api:5.0.0") + testImplementation("org.apache.tomcat.embed:tomcat-embed-core:10.1.0") } swoJava { diff --git a/instrumentation/spring-webmvc/spring-webmvc-6/javaagent/src/test/java/com/solarwinds/opentelemetry/instrumentation/webmvc/v6_0/HandlerNameInstrumentationTest.java b/instrumentation/spring-webmvc/spring-webmvc-6/javaagent/src/test/java/com/solarwinds/opentelemetry/instrumentation/webmvc/v6_0/HandlerNameInstrumentationTest.java new file mode 100644 index 00000000..469a2622 --- /dev/null +++ b/instrumentation/spring-webmvc/spring-webmvc-6/javaagent/src/test/java/com/solarwinds/opentelemetry/instrumentation/webmvc/v6_0/HandlerNameInstrumentationTest.java @@ -0,0 +1,98 @@ +/* + * © SolarWinds Worldwide, LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.solarwinds.opentelemetry.instrumentation.webmvc.v6_0; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.servlet.DispatcherServlet; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +@ExtendWith(MockitoExtension.class) +class HandlerNameInstrumentationTest { + + @RegisterExtension + static final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Test + void verifyHandlerNameIsStampedOnServerSpan() { + testing.runWithSpan( + "server", + () -> { + try { + AnnotationConfigWebApplicationContext appContext = + new AnnotationConfigWebApplicationContext(); + appContext.register(TestConfig.class); + + DispatcherServlet dispatcherServlet = new DispatcherServlet(appContext); + dispatcherServlet.init(new org.springframework.mock.web.MockServletConfig()); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/greet"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + try { + dispatcherServlet.service(request, response); + } finally { + dispatcherServlet.destroy(); + appContext.close(); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("server") + .hasKind(SpanKind.INTERNAL) + .hasAttributesSatisfying( + attributes -> + assertEquals( + "TestController.greet", + attributes.get(AttributeKey.stringKey("HandlerName")))))); + } + + @EnableWebMvc + @org.springframework.context.annotation.Configuration + static class TestConfig { + @org.springframework.context.annotation.Bean + public TestController testController() { + return new TestController(); + } + } + + @RestController + static class TestController { + @GetMapping("/greet") + public String greet() { + return "hello"; + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index fe5b8038..85dc7d02 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -49,4 +49,5 @@ include("libs:core") include("libs:config") include("libs:logging") include("libs:sampling") +include("testing:feature-tests") diff --git a/smoke-tests/k6/basic.js b/smoke-tests/k6/basic.js index 6ab77dd3..9a157f39 100644 --- a/smoke-tests/k6/basic.js +++ b/smoke-tests/k6/basic.js @@ -21,10 +21,10 @@ import names from "./names.js"; const baseUri = `http://petclinic:9966/petclinic/api`; const webMvcUri = `http://webmvc:8080`; export const options = { - duration: "30m", - minIterationDuration: "5m", - vus: 10, - iterations: 200, + duration: "5m", + minIterationDuration: "1m", + vus: 5, + iterations: 25, }; function verify_that_trace_is_persisted() { @@ -70,9 +70,6 @@ function verify_that_trace_is_persisted() { "trace is returned": tdr => tdr.data.traceDetails.traceId.toLowerCase() === traceId.toLowerCase() }); - // check that db query is captured(JDBC check) - const {data: {traceDetails: {allQueries}}} = traceDetailResponse; - check(allQueries, {"JDBC is not broken": aq => aq.length > 0}) return } @@ -80,291 +77,6 @@ function verify_that_trace_is_persisted() { } -function verify_that_span_data_is_persisted() { - const newOwner = names.randomOwner(); - let retryCount = Number.parseInt(`${__ENV.SWO_RETRY_COUNT}`) || 1000; - for (; retryCount > 0; retryCount--) { - const newOwnerResponse = http.post(`${baseUri}/owners`, JSON.stringify(newOwner), - { - headers: { - 'Content-Type': 'application/json', - "x-trace-options": "trigger-trace;custom-info=chubi;sw-keys=lo:se,check-id:123" - } - }); - - const traceContext = newOwnerResponse.headers['X-Trace'] - const [_, traceId, __, flag] = traceContext.split("-") - console.log("Trace context -> ", traceContext) - if ((parseInt(flag, 16) & 1) === 0) continue; - - const spanRawDataPayload = { - "operationName": "getSubSpanRawData", - "variables": { - "traceId": traceId.toUpperCase() - }, - "query": "query getSubSpanRawData($traceId: ID!, $spanFilter: TraceArchiveSpanFilter) {\n traceArchive(\n traceId: $traceId\n spanFilter: $spanFilter\n ) {\n traceId\n traceSpans {\n edges {\n node {\n events {\n eventId\n properties {\n key\n value\n __typename\n }\n __typename\n }\n spanId\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n}\n" - } - - for (; retryCount > 0; retryCount--) { - let spanDataResponse = http.post(`${__ENV.SWO_HOST_URL}/common/graphql`, JSON.stringify(spanRawDataPayload), - { - headers: { - 'Content-Type': 'application/json', - 'Cookie': `${__ENV.SWO_COOKIE}`, - 'X-Csrf-Token': `${__ENV.SWO_XSR_TOKEN}` - } - }); - - spanDataResponse = JSON.parse(spanDataResponse.body) - if (spanDataResponse['errors']) { - console.log("Error -> Persisted trace response:", JSON.stringify(spanDataResponse)) - continue - } - - const {data: {traceArchive: {traceSpans: {edges}}}} = spanDataResponse - for (let i = 0; i < edges.length; i++) { - const edge = edges[i] - const {node: {events}} = edge - for (let j = 0; j < events.length; j++) { - const event = events[j] - const {properties} = event - let found = false - - for (let k = 0; k < properties.length; k++) { - const property = properties[k] - check(property, {"mvc handler name is added": prop => prop.key === "HandlerName"}) - check(property, {"trigger trace": prop => prop.key === "TriggeredTrace"}) - - if (property.key === "PDKeys") { - check(property, {"xtrace-options is added to root span": prop => prop.value === "{check-id=123, lo=se}"}) - found = true - } - - if (property.key === "custom-info") { - check(property, {"xtrace-options is added to root span": prop => prop.value === "chubi"}) - found = true - } - } - if (found) return; // assumes that all checked property must exist at the same node(root span). - } - } - } - - } - -} - -function verify_profile() { - const newOwner = names.randomOwner(); - let retryCount = Number.parseInt(`${__ENV.SWO_RETRY_COUNT}`) || 1000; - for (; retryCount > 0; retryCount--) { - const newOwnerResponse = http.post(`${baseUri}/owners`, JSON.stringify(newOwner), - { - headers: { - 'Content-Type': 'application/json', - "x-trace-options": "trigger-trace;custom-info=chubi;sw-keys=lo:se,check-id:123" - } - }); - - const traceContext = newOwnerResponse.headers['X-Trace'] - const [_, __, ___, flag] = traceContext.split("-") - console.log("Trace context -> ", traceContext) - if ((parseInt(flag, 16) & 1) === 0) continue; - - const endTime = Date.now(); - const startTime = endTime - 10 * 60 * 1000; - const profileSpanQueryPayload = { - "operationName": "getSearchedTraceRequests", - "variables": { - "searchQuery": "sw.profile.spans:1", - "orderBy": { - "direction": "DESC", - "sort": "TIME" - }, - "first": 15, - "timeFilter": { - "startTime": `${startTime}`, - "endTime": `${endTime}` - }, - "serviceNames": ["java-apm-smoke-test","java-apm-smoke-test-webmvc"] - }, - "query": "query getSearchedTraceRequests($first: Int, $serviceNames: [String!], $timeFilter: TimeRangeInput!, $orderBy: TraceRequestItemsOrderBy!, $searchQuery: String!) {\n trace {\n requests(\n context: {serviceNames: $serviceNames}\n search: {query: $searchQuery, timeRange: $timeFilter}\n paging: {first: $first}\n orderBy: $orderBy\n ) {\n totalCount\n pageInfo {\n startCursor\n endCursor\n hasPreviousPage\n hasNextPage\n __typename\n }\n edges {\n node {\n id\n traceId\n spanId\n time\n transaction\n httpMethod\n httpStatus\n service\n serviceEntityId\n websiteEntityName\n awsLambdaEntityName\n awsLambdaEntityId\n hostEntityName\n hostEntityId\n websiteId\n duration {\n value\n units\n __typename\n }\n __typename\n }\n cursor\n __typename\n }\n __typename\n }\n __typename\n }\n}\n" - } - - for (; retryCount > 0; retryCount--) { - let profileResponse = http.post(`${__ENV.SWO_HOST_URL}/common/graphql`, JSON.stringify(profileSpanQueryPayload), - { - headers: { - 'Content-Type': 'application/json', - 'Cookie': `${__ENV.SWO_COOKIE}`, - 'X-Csrf-Token': `${__ENV.SWO_XSR_TOKEN}` - } - }); - - profileResponse = JSON.parse(profileResponse.body) - if (profileResponse['errors']) { - console.log("Error -> Profile spans response:", JSON.stringify(profileResponse)) - continue - } - - const {data: {trace: {requests: {edges}}}} = profileResponse - if (check(edges, {"code profiling": prop => prop.length > 0})) { - return; - } - } - } -} - -function verify_that_span_data_is_persisted_0() { - let retryCount = Number.parseInt(`${__ENV.SWO_RETRY_COUNT}`) || 1000; - for (; retryCount > 0; retryCount--) { - const transactionName = "int-test" - const response = http.get(`${webMvcUri}/greet/${transactionName}`, { - headers: { - 'Content-Type': 'application/json', - "x-trace-options": "trigger-trace;custom-info=chubi;sw-keys=lo:se,check-id:123" - } - }); - - const traceContext = response.headers['X-Trace'] - const [_, traceId, __, flag] = traceContext.split("-") - console.log("Trace context -> ", traceContext) - if ((parseInt(flag, 16) & 1) === 0) continue; - - const spanRawDataPayload = { - "operationName": "getSubSpanRawData", - "variables": { - "traceId": traceId.toUpperCase() - }, - "query": "query getSubSpanRawData($traceId: ID!, $spanFilter: TraceArchiveSpanFilter) {\n traceArchive(\n traceId: $traceId\n spanFilter: $spanFilter\n ) {\n traceId\n traceSpans {\n edges {\n node {\n events {\n eventId\n properties {\n key\n value\n __typename\n }\n __typename\n }\n spanId\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n}\n" - } - - for (; retryCount > 0; retryCount--) { - let spanDataResponse = http.post(`${__ENV.SWO_HOST_URL}/common/graphql`, JSON.stringify(spanRawDataPayload), - { - headers: { - 'Content-Type': 'application/json', - 'Cookie': `${__ENV.SWO_COOKIE}`, - 'X-Csrf-Token': `${__ENV.SWO_XSR_TOKEN}` - } - }); - - spanDataResponse = JSON.parse(spanDataResponse.body) - if (spanDataResponse['errors']) { - console.log("Error -> Persisted trace response:", JSON.stringify(spanDataResponse)) - continue - } - - const {data: {traceArchive: {traceSpans: {edges}}}} = spanDataResponse - for (let i = 0; i < edges.length; i++) { - const edge = edges[i] - const {node: {events}} = edge - - for (let j = 0; j < events.length; j++) { - const event = events[j] - const {properties} = event - - for (let k = 0; k < properties.length; k++) { - const property = properties[k] - check(property, {"trigger trace": prop => prop.key === "TriggeredTrace"}) - - if (property.key === "sw.transaction") { - check(property, {"custom transaction name": prop => prop.value === transactionName}) - return; - } - } - } - } - } - } -} - -function verify_distributed_trace() { - let retryCount = Number.parseInt(`${__ENV.SWO_RETRY_COUNT}`) || 1000; - for (; retryCount > 0; retryCount--) { - const response = http.get(`${webMvcUri}/distributed`, { - headers: { - 'Content-Type': 'application/json' - } - }); - - const traceContext = response.headers['X-Trace'] - const [_, traceId, __, flag] = traceContext.split("-") - console.log("[Distributed]Trace context -> ", traceContext) - if ((parseInt(flag, 16) & 1) === 0) continue; - - const spanRawDataPayload = { - "operationName": "getSubSpanRawData", - "variables": { - "traceId": traceId.toUpperCase() - }, - "query": "query getSubSpanRawData($traceId: ID!, $spanFilter: TraceArchiveSpanFilter) {\n traceArchive(\n traceId: $traceId\n spanFilter: $spanFilter\n ) {\n traceId\n traceSpans {\n edges {\n node {\n events {\n eventId\n properties {\n key\n value\n __typename\n }\n __typename\n }\n spanId\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n}\n" - } - - for (; retryCount > 0; retryCount--) { - let spanDataResponse = http.post(`${__ENV.SWO_HOST_URL}/common/graphql`, JSON.stringify(spanRawDataPayload), - { - headers: { - 'Content-Type': 'application/json', - 'Cookie': `${__ENV.SWO_COOKIE}`, - 'X-Csrf-Token': `${__ENV.SWO_XSR_TOKEN}` - } - }); - - spanDataResponse = JSON.parse(spanDataResponse.body) - if (spanDataResponse['errors']) { - console.log("Error -> Distributed trace response:", JSON.stringify(spanDataResponse)) - continue - } - - const {data: {traceArchive: {traceSpans: {edges}}}} = spanDataResponse - console.log("Edges: ", edges) - - - let contextCheck = false - let sdkCheck = false - for (let i = 0; i < edges.length; i++) { - const edge = edges[i] - const {node: {events}} = edge - - for (let j = 0; j < events.length; j++) { - const event = events[j] - const {properties} = event - - for (let k = 0; k < properties.length; k++) { - const property = properties[k] - check(property, { - "check that remote service, java-apm-smoke-test, is path of the trace": prop => { - contextCheck = contextCheck || prop.value === "java-apm-smoke-test" - return contextCheck - } - }) - check(property, { - "sdk-trace": prop => { - sdkCheck = sdkCheck || prop.value === "SDK.trace.test" - return sdkCheck - } - }) - } - - if (contextCheck && sdkCheck) { - return - } - } - } - } - } -} - -function verify_that_specialty_path_is_not_sampled() { - const specialtiesUrl = `${baseUri}/specialties`; - const specialtiesResponse = http.get(specialtiesUrl); - const traceContext = specialtiesResponse.headers['X-Trace'] - - const [_, __, ___, flag] = traceContext.split("-") - check(flag, {"verify that transaction is filtered": f => (parseInt(f, 16) & 1) === 0}) -} - function verify_that_metrics_are_reported(metric, checkFn, service="lambda-e2e") { let retryCount = Number.parseInt(`${__ENV.SWO_RETRY_COUNT}`) || 1000; for (let i = 0; i < retryCount; i++) { @@ -457,85 +169,6 @@ function verify_that_metrics_are_reported(metric, checkFn, service="lambda-e2e") } } -function check_transaction_name(property) { - if (property.key === "sw.transaction") { - check(property, {"transaction-name": prop => prop.value === "lambda-test-txn"}) - return true; - } - return false -} - - -function check_code_stack_trace(property) { - if (property.key === "code.stacktrace") { - check(property, {"code.stacktrace": _ => true}) - return true; - } - return false -} - -function check_property(fn) { - let retryCount = Number.parseInt(`${__ENV.SWO_RETRY_COUNT}`) || 1000; - for (; retryCount > 0; retryCount--) { - const newOwner = names.randomOwner(); - const response = http.post(`${baseUri}/owners`, JSON.stringify(newOwner), - { - headers: { - 'Content-Type': 'application/json', - } - } - ); - - const traceContext = response.headers['X-Trace'] - const [_, traceId, __, flag] = traceContext.split("-") - console.log("Trace context -> ", traceContext) - if ((parseInt(flag, 16) & 1) === 0) continue; - - const spanRawDataPayload = { - "operationName": "getSubSpanRawData", - "variables": { - "traceId": traceId.toUpperCase() - }, - "query": "query getSubSpanRawData($traceId: ID!, $spanFilter: TraceArchiveSpanFilter) {\n traceArchive(\n traceId: $traceId\n spanFilter: $spanFilter\n ) {\n traceId\n traceSpans {\n edges {\n node {\n events {\n eventId\n properties {\n key\n value\n __typename\n }\n __typename\n }\n spanId\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n}\n" - } - - for (; retryCount > 0; retryCount--) { - let spanDataResponse = http.post(`${__ENV.SWO_HOST_URL}/common/graphql`, JSON.stringify(spanRawDataPayload), - { - headers: { - 'Content-Type': 'application/json', - 'Cookie': `${__ENV.SWO_COOKIE}`, - 'X-Csrf-Token': `${__ENV.SWO_XSR_TOKEN}` - } - }); - - spanDataResponse = JSON.parse(spanDataResponse.body) - if (spanDataResponse['errors']) { - console.log("Error -> Transaction name response:", JSON.stringify(spanDataResponse)) - continue - } - - const {data: {traceArchive: {traceSpans: {edges}}}} = spanDataResponse - for (let i = 0; i < edges.length; i++) { - const edge = edges[i] - const {node: {events}} = edge - - for (let j = 0; j < events.length; j++) { - const event = events[j] - const {properties} = event - - for (let k = 0; k < properties.length; k++) { - const property = properties[k] - if (fn(property)) { - return; - } - } - } - } - } - } -} - function getEntityId() { let retryCount = Number.parseInt(`${__ENV.SWO_RETRY_COUNT}`) || 1000; const entityQueryPayload = { @@ -644,104 +277,21 @@ function silence(fn) { } export default function () { - silence(verify_that_span_data_is_persisted_0) - - const request_count = (measurement) => check(measurement, {"request_count": mrs => mrs.value > 0}) - const tracecount = (measurement) => check(measurement, {"tracecount": mrs => mrs.value > 0}) - const samplecount = (measurement) => check(measurement, {"samplecount": mrs => mrs.value > 0}) - const response_time = (measurement) => check(measurement, {"response_time": mrs => mrs.value > 0}) - - if (`${__ENV.LAMBDA}` === "true") { - + const service = "java-apm-smoke-test" silence(function () { - verify_that_metrics_are_reported("trace.service.request_count", request_count) + verify_that_metrics_are_reported("jvm.memory.used", + (measurement) => check(measurement, {"otel-metrics": mrs => mrs.value > 0}), + service + ) }) silence(function () { - verify_that_metrics_are_reported("trace.service.tracecount", tracecount) + verify_that_metrics_are_reported("jvm.network.time", + (measurement) => check(measurement, {"otel-metrics-17": mrs => mrs.value > 0}), + service + ) }) - silence(function () { - verify_that_metrics_are_reported("trace.service.samplecount", samplecount) - }) - - silence(function () { - verify_that_metrics_are_reported("trace.service.response_time", response_time) - }) - - silence(function () { - check_property(check_transaction_name) - }) - - silence(function () { - check_property(check_code_stack_trace) - }) - - silence(function () { - verify_that_metrics_are_reported("trace.jvm.Runtime.Uptime", - (measurement) => check(measurement, {"trace.jvm-metrics": mrs => mrs.value === 0}) - ) - }) - - silence(function () { - verify_that_metrics_are_reported("trace.jvm.Threading.ThreadCount", - (measurement) => check(measurement, {"trace.jvm-metrics": mrs => mrs.value === 0}) - ) - }) - - } else { - const service = "java-apm-smoke-test" - silence(function () { - verify_that_metrics_are_reported("trace.service.request_count", request_count, service) - }) - - silence(function () { - verify_that_metrics_are_reported("trace.service.tracecount", tracecount, service) - }) - - silence(function () { - verify_that_metrics_are_reported("trace.service.samplecount", samplecount, service) - }) - - silence(function () { - verify_that_metrics_are_reported("trace.service.response_time", response_time, service) - }) - - silence(function () { - verify_that_metrics_are_reported("jvm.memory.used", - (measurement) => check(measurement, {"otel-metrics": mrs => mrs.value > 0}), - service - ) - }) - - silence(function () { - verify_that_metrics_are_reported("jvm.network.time", - (measurement) => check(measurement, {"otel-metrics-17": mrs => mrs.value > 0}), - service - ) - }) - - silence(function () { - verify_that_metrics_are_reported("trace.jvm.Runtime.Uptime", - (measurement) => check(measurement, {"trace.jvm-metrics": mrs => mrs.value === 0}), - service - ) - }) - - silence(function () { - verify_that_metrics_are_reported("trace.jvm.Threading.ThreadCount", - (measurement) => check(measurement, {"trace.jvm-metrics": mrs => mrs.value === 0}), - service - ) - }) silence(verify_logs_export) - silence(verify_that_specialty_path_is_not_sampled) - silence(function () { - check_property(check_code_stack_trace) - }) - silence(verify_that_span_data_is_persisted) silence(verify_that_trace_is_persisted) - silence(verify_distributed_trace) - silence(verify_profile) - } }; diff --git a/smoke-tests/src/test/java/com/solarwinds/LambdaTest.java b/smoke-tests/src/test/java/com/solarwinds/LambdaTest.java deleted file mode 100644 index 9aa31ad1..00000000 --- a/smoke-tests/src/test/java/com/solarwinds/LambdaTest.java +++ /dev/null @@ -1,175 +0,0 @@ -/* - * © SolarWinds Worldwide, LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.solarwinds; - -import com.jayway.jsonpath.PathNotFoundException; -import com.solarwinds.agents.Agent; -import com.solarwinds.agents.SwoLambdaAgentResolver; -import com.solarwinds.config.Configs; -import com.solarwinds.config.TestConfig; -import com.solarwinds.containers.K6Container; -import com.solarwinds.containers.PetClinicRestContainer; -import com.solarwinds.containers.PostgresContainer; -import com.solarwinds.containers.SpringBootWebMvcContainer; -import com.solarwinds.results.ResultsCollector; -import com.solarwinds.util.LogStreamAnalyzer; -import com.solarwinds.util.NamingConventions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import org.slf4j.LoggerFactory; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.containers.Network; -import org.testcontainers.containers.output.Slf4jLogConsumer; - -import java.io.IOException; -import java.nio.file.Files; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - -@EnabledIfEnvironmentVariable(named = "LAMBDA", matches = "true") -public class LambdaTest { - private static final Network NETWORK = Network.newNetwork(); - - private static final NamingConventions namingConventions = new NamingConventions(); - - private static final LogStreamAnalyzer logStreamAnalyzer = new LogStreamAnalyzer<>( - List.of( - "Got settings from file:", - "Applying instrumentation: sw-jdbc" - ) - , new Slf4jLogConsumer(LoggerFactory.getLogger("k6"))); - - - @BeforeAll - static void runTestConfig() { - TestConfig config = Configs.LAMBDA_E2E.config; - config - .agents() - .forEach( - agent -> { - try { - runAppOnce(agent); - } catch (Exception e) { - fail("Unhandled exception in " + config.name(), e); - } - }); - } - - static void runAppOnce(Agent agent) throws Exception { - GenericContainer postgres = new PostgresContainer(NETWORK).build(); - postgres.start(); - - GenericContainer webMvc = new SpringBootWebMvcContainer(new SwoLambdaAgentResolver(), NETWORK, agent).build(); - webMvc.start(); - - GenericContainer petClinic = new PetClinicRestContainer(new SwoLambdaAgentResolver(), NETWORK, agent).build(); - petClinic.start(); - petClinic.followOutput(logStreamAnalyzer); - - GenericContainer k6 = new K6Container(NETWORK, agent, namingConventions).build(); - k6.start(); - - petClinic.execInContainer("kill", "1"); - webMvc.execInContainer("kill", "1"); - postgres.stop(); - } - - @Test - void assertThatRequestCountMetricIsReported() throws IOException { - String resultJson = new String( - Files.readAllBytes(namingConventions.local.k6Results(Configs.LAMBDA_E2E.config.agents().get(0)))); - - double passes = ResultsCollector.read(resultJson, "$.root_group.checks.['request_count'].passes"); - assertTrue(passes > 1, "Expects a count > 1 "); - } - - @Test - void assertThatTraceCountMetricIsReported() throws IOException { - String resultJson = new String( - Files.readAllBytes(namingConventions.local.k6Results(Configs.LAMBDA_E2E.config.agents().get(0)))); - - double passes = ResultsCollector.read(resultJson, "$.root_group.checks.['tracecount'].passes"); - assertTrue(passes > 1, "Expects a count > 1 "); - } - - @Test - void assertThatSampleCountMetricIsReported() throws IOException { - String resultJson = new String( - Files.readAllBytes(namingConventions.local.k6Results(Configs.LAMBDA_E2E.config.agents().get(0)))); - - double passes = ResultsCollector.read(resultJson, "$.root_group.checks.['samplecount'].passes"); - assertTrue(passes > 1, "Expects a count > 1 "); - } - - @Test - void assertThatResponseTimeMetricIsReported() throws IOException { - String resultJson = new String( - Files.readAllBytes(namingConventions.local.k6Results(Configs.LAMBDA_E2E.config.agents().get(0)))); - - double passes = ResultsCollector.read(resultJson, "$.root_group.checks.['response_time'].passes"); - assertTrue(passes > 1, "Expects a count > 1 "); - } - - @Test - void assertThatCustomTransactionNameTakesEffect() throws IOException { - String resultJson = new String( - Files.readAllBytes(namingConventions.local.k6Results(Configs.LAMBDA_E2E.config.agents().get(0)))); - - double passes = ResultsCollector.read(resultJson, "$.root_group.checks.['transaction-name'].passes"); - assertTrue(passes > 1, "Environment based transaction naming is broken "); - } - - @Test - void assertThatSettingsAreReadFromFile() { - Boolean actual = logStreamAnalyzer.getAnswer().get("Got settings from file:"); - assertTrue(actual, "file based settings is not being used"); - } - - @Test - void assertThatJDBCInstrumentationIsApplied() { - Boolean actual = logStreamAnalyzer.getAnswer().get("Applying instrumentation: sw-jdbc"); - assertTrue(actual, "sw-jdbc instrumentation is not applied"); - } - - @Test - void assertSDKTransactionNaming() throws IOException { - String resultJson = new String(Files.readAllBytes(namingConventions.local.k6Results(Configs.LAMBDA_E2E.config.agents().get(0)))); - double passes = ResultsCollector.read(resultJson, "$.root_group.checks.['custom transaction name'].passes"); - assertTrue(passes > 1, "SDK transaction naming is broken"); - } - - @Test - void assertThatCodeStacktraceIsCaptured() throws IOException { - String resultJson = new String( - Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - - double passes = ResultsCollector.read(resultJson, "$.root_group.checks.['code.stacktrace'].passes"); - assertTrue(passes > 0, "Expects a count > 0"); - } - - @Test - void assertThatTraceJvmMetricsAreNotCollected() throws IOException { - String resultJson = new String( - Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - assertThrows(PathNotFoundException.class, () -> ResultsCollector.read(resultJson, "$.root_group.checks.['trace.jvm-metrics'].fails")); - } -} diff --git a/smoke-tests/src/test/java/com/solarwinds/SmokeTest.java b/smoke-tests/src/test/java/com/solarwinds/SmokeTest.java index a6c1876e..044baa0f 100644 --- a/smoke-tests/src/test/java/com/solarwinds/SmokeTest.java +++ b/smoke-tests/src/test/java/com/solarwinds/SmokeTest.java @@ -38,7 +38,6 @@ import org.slf4j.LoggerFactory; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; -import org.testcontainers.containers.output.OutputFrame; import org.testcontainers.containers.output.Slf4jLogConsumer; import java.io.IOException; @@ -122,13 +121,6 @@ void assertXTrace() throws IOException { assertEquals(0, fails, "Less than a 100 percent of the responses has X-Trace header"); } - @Test - void assertTransactionFiltering() throws IOException { - String resultJson = new String(Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - double fails = ResultsCollector.read(resultJson, "$.root_group.checks.['verify that transaction is filtered'].fails"); - assertEquals(0, fails, "transaction filtering doesn't work"); - } - @Test void assertTraceIngestion() throws IOException { String resultJson = new String(Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); @@ -136,55 +128,6 @@ void assertTraceIngestion() throws IOException { assertTrue(passes > 0, "trace ingestion is not working. There maybe network issues"); } - @Test - void assertJDBC() throws IOException { - String resultJson = new String(Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - double passes = ResultsCollector.read(resultJson, "$.root_group.checks.['JDBC is not broken'].passes"); - assertTrue(passes > 0, "JDBC instrumentation doesn't work"); - } - - @Test - void assertXTraceOptions() throws IOException { - String resultJson = new String(Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - double passes = ResultsCollector.read(resultJson, "$.root_group.checks.['xtrace-options is added to root span'].passes"); - assertTrue(passes > 1, "Xtrace options is not captured in root span"); - } - - @Test - void assertMvcInstrumentation() throws IOException { - String resultJson = new String(Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - double passes = ResultsCollector.read(resultJson, "$.root_group.checks.['mvc handler name is added'].passes"); - assertTrue(passes > 0, "MVC instrumentation is broken"); - } - - @Test - void assertTriggerTrace() throws IOException { - String resultJson = new String(Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - double passes = ResultsCollector.read(resultJson, "$.root_group.checks.['trigger trace'].passes"); - assertTrue(passes > 0, "trigger trace is broken"); - } - - @Test - void assertCodeProfiling() throws IOException { - String resultJson = new String(Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - double passes = ResultsCollector.read(resultJson, "$.root_group.checks.['code profiling'].passes"); - assertTrue(passes > 0, "code profiling is broken"); - } - - @Test - void assertContextPropagation() throws IOException { - String resultJson = new String(Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - double passes = ResultsCollector.read(resultJson, "$.root_group.checks.['check that remote service, java-apm-smoke-test, is path of the trace'].passes"); - assertTrue(passes > 0, "context propagation is broken"); - } - - @Test - void assertTransactionNaming() throws IOException { - String resultJson = new String(Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - double passes = ResultsCollector.read(resultJson, "$.root_group.checks.['custom transaction name'].passes"); - assertTrue(passes > 0, "transaction naming is broken"); - } - @Test void assertTraceContextInLog() { Boolean actual = logStreamAnalyzer.getAnswer().get("trace_id=[a-z0-9]+\\s+span_id=[a-z0-9]+\\s+trace_flags=[0-9a-f]{2}"); @@ -211,115 +154,42 @@ void assertAgentAzureMetadata() { assertTrue(actual, "Azure metadata is not retrieved"); } - @Test - void assertServiceNameIsSameAsOneInServiceKey() { - Boolean actual = logStreamAnalyzer.getAnswer().get("This log line is used for validation only: service.name: java-apm-smoke-test"); - assertTrue(actual, "service.name is not updated with name in service key"); - } - @Test void assertThatTransactionNameBufferIsCleared() { Boolean actual = logStreamAnalyzer.getAnswer().get("Clearing transaction name buffer. Unique transaction count: \\d+"); assertTrue(actual, "Transaction name buffer is not getting cleared on metric flush"); } - @Test - void assertThatJDBCInstrumentationIsApplied() { - Boolean actual = logStreamAnalyzer.getAnswer().get("Applying instrumentation: sw-jdbc"); - assertTrue(actual, "sw-jdbc instrumentation is not applied"); - } - - @Test - void assertThatProxyIsUsed() { - Boolean actual = logStreamAnalyzer.getAnswer().get("CONNECT otel.collector.na-01.st-ssp.solarwinds.com:443"); - assertTrue(actual, "not using proxy"); - } - - @Test - void assertThatLogsAreExported() throws IOException { - String resultJson = new String( - Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - double passes = ResultsCollector.read(resultJson, - "$.root_group.checks.['logs'].passes"); - assertTrue(passes > 0, "log export is broken"); - } - - @Test - void assertThatMetricsAreExported() throws IOException { - String resultJson = new String( - Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - double passes = ResultsCollector.read(resultJson, - "$.root_group.checks.['otel-metrics'].passes"); - assertTrue(passes > 0, "otel metric export is broken"); - } - - @Test - void assertThatRuntimeMetrics17AreExported() throws IOException { - String resultJson = new String( - Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - double passes = ResultsCollector.read(resultJson, - "$.root_group.checks.['otel-metrics-17'].passes"); - assertTrue(passes > 0, "JVM 17+ runtime metric export is broken"); - } - - @Test - void assertThatSdkTracingIsWorking() throws IOException { - String resultJson = new String( - Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - - double passes = ResultsCollector.read(resultJson, "$.root_group.checks.['sdk-trace'].passes"); - assertTrue(passes > 0, "SDK trace is not working, expected a count > 0"); - } - @Test - void assertThatRequestCountMetricIsReported() throws IOException { - String resultJson = new String( - Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - - double passes = ResultsCollector.read(resultJson, "$.root_group.checks.['request_count'].passes"); - assertTrue(passes > 0, "Expects a count > 0"); + void assertThatProxyIsUsed() { + Boolean actual = logStreamAnalyzer.getAnswer().get("CONNECT otel.collector.na-01.st-ssp.solarwinds.com:443"); + assertTrue(actual, "not using proxy"); } @Test - void assertThatTraceCountMetricIsReported() throws IOException { + void assertThatLogsAreExported() throws IOException { String resultJson = new String( Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - - double passes = ResultsCollector.read(resultJson, "$.root_group.checks.['tracecount'].passes"); - assertTrue(passes > 0, "Expects a count > 0"); - } - - @Test - void assertThatSampleCountMetricIsReported() throws IOException { - String resultJson = new String( - Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - - double passes = ResultsCollector.read(resultJson, "$.root_group.checks.['samplecount'].passes"); - assertTrue(passes > 0, "Expects a count > 0"); + double passes = ResultsCollector.read(resultJson, + "$.root_group.checks.['logs'].passes"); + assertTrue(passes > 0, "log export is broken"); } @Test - void assertThatResponseTimeMetricIsReported() throws IOException { + void assertThatMetricsAreExported() throws IOException { String resultJson = new String( Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - - double passes = ResultsCollector.read(resultJson, "$.root_group.checks.['response_time'].passes"); - assertTrue(passes > 0, "Expects a count > 0"); - } - - @Test - void assertThatCodeStacktraceIsCaptured() throws IOException { - String resultJson = new String( - Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - - double passes = ResultsCollector.read(resultJson, "$.root_group.checks.['code.stacktrace'].passes"); - assertTrue(passes > 0, "Expects a count > 0"); + double passes = ResultsCollector.read(resultJson, + "$.root_group.checks.['otel-metrics'].passes"); + assertTrue(passes > 0, "otel metric export is broken"); } @Test - void assertThatTraceJvmMetricsAreNotCollected() throws IOException { + void assertThatRuntimeMetrics17AreExported() throws IOException { String resultJson = new String( Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - assertThrows(PathNotFoundException.class, () -> ResultsCollector.read(resultJson, "$.root_group.checks.['trace.jvm-metrics'].fails")); + double passes = ResultsCollector.read(resultJson, + "$.root_group.checks.['otel-metrics-17'].passes"); + assertTrue(passes > 0, "JVM 17+ runtime metric export is broken"); } } diff --git a/smoke-tests/src/test/java/com/solarwinds/SmokeTestV2.java b/smoke-tests/src/test/java/com/solarwinds/SmokeTestV2.java index a9e504c7..9d8b6096 100644 --- a/smoke-tests/src/test/java/com/solarwinds/SmokeTestV2.java +++ b/smoke-tests/src/test/java/com/solarwinds/SmokeTestV2.java @@ -114,13 +114,6 @@ void assertXTrace() throws IOException { assertEquals(0, fails, "Less than a 100 percent of the responses has X-Trace header"); } - @Test - void assertTransactionFiltering() throws IOException { - String resultJson = new String(Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - double fails = ResultsCollector.read(resultJson, "$.root_group.checks.['verify that transaction is filtered'].fails"); - assertEquals(0, fails, "transaction filtering doesn't work"); - } - @Test void assertTraceIngestion() throws IOException { String resultJson = new String(Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); @@ -128,55 +121,6 @@ void assertTraceIngestion() throws IOException { assertTrue(passes > 0, "trace ingestion is not working. There maybe network issues"); } - @Test - void assertJDBC() throws IOException { - String resultJson = new String(Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - double passes = ResultsCollector.read(resultJson, "$.root_group.checks.['JDBC is not broken'].passes"); - assertTrue(passes > 0, "JDBC instrumentation doesn't work"); - } - - @Test - void assertXTraceOptions() throws IOException { - String resultJson = new String(Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - double passes = ResultsCollector.read(resultJson, "$.root_group.checks.['xtrace-options is added to root span'].passes"); - assertTrue(passes > 1, "Xtrace options is not captured in root span"); - } - - @Test - void assertMvcInstrumentation() throws IOException { - String resultJson = new String(Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - double passes = ResultsCollector.read(resultJson, "$.root_group.checks.['mvc handler name is added'].passes"); - assertTrue(passes > 0, "MVC instrumentation is broken"); - } - - @Test - void assertTriggerTrace() throws IOException { - String resultJson = new String(Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - double passes = ResultsCollector.read(resultJson, "$.root_group.checks.['trigger trace'].passes"); - assertTrue(passes > 0, "trigger trace is broken"); - } - - @Test - void assertCodeProfiling() throws IOException { - String resultJson = new String(Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - double passes = ResultsCollector.read(resultJson, "$.root_group.checks.['code profiling'].passes"); - assertTrue(passes > 0, "code profiling is broken"); - } - - @Test - void assertContextPropagation() throws IOException { - String resultJson = new String(Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - double passes = ResultsCollector.read(resultJson, "$.root_group.checks.['check that remote service, java-apm-smoke-test, is path of the trace'].passes"); - assertTrue(passes > 0, "context propagation is broken"); - } - - @Test - void assertTransactionNaming() throws IOException { - String resultJson = new String(Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - double passes = ResultsCollector.read(resultJson, "$.root_group.checks.['custom transaction name'].passes"); - assertTrue(passes > 0, "transaction naming is broken"); - } - @Test void assertTraceContextInLog() { Boolean actual = logStreamAnalyzer.getAnswer().get("trace_id=[a-z0-9]+\\s+span_id=[a-z0-9]+\\s+trace_flags=[0-9a-f]{2}"); @@ -209,12 +153,6 @@ void assertThatTransactionNameBufferIsCleared() { assertTrue(actual, "Transaction name buffer is not getting cleared on metric flush"); } - @Test - void assertThatJDBCInstrumentationIsApplied() { - Boolean actual = logStreamAnalyzer.getAnswer().get("Applying instrumentation: sw-jdbc"); - assertTrue(actual, "sw-jdbc instrumentation is not applied"); - } - @Test void assertThatLogsAreExported() throws IOException { String resultJson = new String( @@ -241,65 +179,4 @@ void assertThatRuntimeMetrics17AreExported() throws IOException { "$.root_group.checks.['otel-metrics-17'].passes"); assertTrue(passes > 0, "JVM 17+ runtime metric export is broken"); } - - @Test - void assertThatSdkTracingIsWorking() throws IOException { - String resultJson = new String( - Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - - double passes = ResultsCollector.read(resultJson, "$.root_group.checks.['sdk-trace'].passes"); - assertTrue(passes > 0, "SDK trace is not working, expected a count > 0"); - } - - @Test - void assertThatRequestCountMetricIsReported() throws IOException { - String resultJson = new String( - Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - - double passes = ResultsCollector.read(resultJson, "$.root_group.checks.['request_count'].passes"); - assertTrue(passes > 0, "Expects a count > 0"); - } - - @Test - void assertThatTraceCountMetricIsReported() throws IOException { - String resultJson = new String( - Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - - double passes = ResultsCollector.read(resultJson, "$.root_group.checks.['tracecount'].passes"); - assertTrue(passes > 0, "Expects a count > 0"); - } - - @Test - void assertThatSampleCountMetricIsReported() throws IOException { - String resultJson = new String( - Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - - double passes = ResultsCollector.read(resultJson, "$.root_group.checks.['samplecount'].passes"); - assertTrue(passes > 0, "Expects a count > 0"); - } - - @Test - void assertThatResponseTimeMetricIsReported() throws IOException { - String resultJson = new String( - Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - - double passes = ResultsCollector.read(resultJson, "$.root_group.checks.['response_time'].passes"); - assertTrue(passes > 0, "Expects a count > 0"); - } - - @Test - void assertThatCodeStacktraceIsCaptured() throws IOException { - String resultJson = new String( - Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - - double passes = ResultsCollector.read(resultJson, "$.root_group.checks.['code.stacktrace'].passes"); - assertTrue(passes > 0, "Expects a count > 0"); - } - - @Test - void assertThatTraceJvmMetricsAreNotCollected() throws IOException { - String resultJson = new String( - Files.readAllBytes(namingConventions.local.k6Results(Configs.E2E.config.agents().get(0)))); - assertThrows(PathNotFoundException.class, () -> ResultsCollector.read(resultJson, "$.root_group.checks.['trace.jvm-metrics'].fails")); - } } diff --git a/testing/agent-test-extension/build.gradle.kts b/testing/agent-test-extension/build.gradle.kts index bafd9665..0ec07db7 100644 --- a/testing/agent-test-extension/build.gradle.kts +++ b/testing/agent-test-extension/build.gradle.kts @@ -4,5 +4,9 @@ plugins { dependencies { compileOnly(project(":libs:shared")) + compileOnly(project(":libs:core")) + compileOnly(project(":libs:sampling")) + compileOnly(project(":bootstrap")) compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") + compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api") } diff --git a/testing/agent-test-extension/src/main/java/com/solarwinds/opentelemetry/extensions/TestAgentListener.java b/testing/agent-test-extension/src/main/java/com/solarwinds/opentelemetry/extensions/TestAgentListener.java new file mode 100644 index 00000000..42cc451f --- /dev/null +++ b/testing/agent-test-extension/src/main/java/com/solarwinds/opentelemetry/extensions/TestAgentListener.java @@ -0,0 +1,45 @@ +/* + * © SolarWinds Worldwide, LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.solarwinds.opentelemetry.extensions; + +import com.google.auto.service.AutoService; +import com.solarwinds.joboe.core.settings.TestSettingsReader; +import com.solarwinds.joboe.core.util.TestUtils; +import com.solarwinds.opentelemetry.core.AgentState; +import io.opentelemetry.javaagent.extension.AgentListener; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import java.util.concurrent.TimeUnit; + +@AutoService(AgentListener.class) +public class TestAgentListener implements AgentListener { + + @Override + public void afterAgent(AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk) { + AgentState.waitForReady(30, TimeUnit.SECONDS); + TestSettingsReader reader = TestUtils.initSettingsReader(); + reader.put( + new TestSettingsReader.SettingsMockupBuilder() + .withFlags(true, false, true, true, false) + .withSampleRate(1_000_000) + .build()); + } + + @Override + public int order() { + return Integer.MAX_VALUE; + } +} diff --git a/testing/agent-test-extension/src/main/java/com/solarwinds/opentelemetry/extensions/TestAutoConfigurationCustomizer.java b/testing/agent-test-extension/src/main/java/com/solarwinds/opentelemetry/extensions/TestAutoConfigurationCustomizer.java deleted file mode 100644 index ca443b91..00000000 --- a/testing/agent-test-extension/src/main/java/com/solarwinds/opentelemetry/extensions/TestAutoConfigurationCustomizer.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * © SolarWinds Worldwide, LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.solarwinds.opentelemetry.extensions; - -import com.google.auto.service.AutoService; -import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer; -import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; - -@AutoService(AutoConfigurationCustomizerProvider.class) -public class TestAutoConfigurationCustomizer implements AutoConfigurationCustomizerProvider { - - @Override - public void customize(AutoConfigurationCustomizer autoConfiguration) { - autoConfiguration.addTracerProviderCustomizer( - (tracerProviderBuilder, configProperties) -> - tracerProviderBuilder.setSampler(new TestSampler())); - } - - @Override - public int order() { - return Integer.MAX_VALUE; - } -} diff --git a/testing/agent-test-extension/src/main/java/com/solarwinds/opentelemetry/extensions/TestSampler.java b/testing/agent-test-extension/src/main/java/com/solarwinds/opentelemetry/extensions/TestSampler.java deleted file mode 100644 index d3ba5844..00000000 --- a/testing/agent-test-extension/src/main/java/com/solarwinds/opentelemetry/extensions/TestSampler.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * © SolarWinds Worldwide, LLC. All rights reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.solarwinds.opentelemetry.extensions; - -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.trace.SpanKind; -import io.opentelemetry.context.Context; -import io.opentelemetry.sdk.trace.data.LinkData; -import io.opentelemetry.sdk.trace.samplers.Sampler; -import io.opentelemetry.sdk.trace.samplers.SamplingResult; -import java.util.List; - -public class TestSampler extends SolarwindsSampler { - private final Sampler sampler = Sampler.alwaysOn(); - - @Override - public SamplingResult shouldSample( - Context parentContext, - String traceId, - String name, - SpanKind spanKind, - Attributes attributes, - List parentLinks) { - return sampler.shouldSample(parentContext, traceId, name, spanKind, attributes, parentLinks); - } -} diff --git a/testing/feature-tests/build.gradle.kts b/testing/feature-tests/build.gradle.kts new file mode 100644 index 00000000..d294d863 --- /dev/null +++ b/testing/feature-tests/build.gradle.kts @@ -0,0 +1,34 @@ +/* + * © SolarWinds Worldwide, LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("solarwinds.instrumentation-conventions") +} + +dependencies { + testImplementation(project(":solarwinds-otel-sdk")) + testImplementation(project(":libs:sampling")) + testImplementation(project(":libs:core")) + testImplementation("jakarta.servlet:jakarta.servlet-api:5.0.0") +} + +tasks.named("compileTestJava") { + dependsOn(project(":solarwinds-otel-sdk").tasks.named("shadowJar")) +} + +swoJava { + minJavaVersionSupported.set(JavaVersion.VERSION_17) +} diff --git a/testing/feature-tests/src/test/java/com/solarwinds/opentelemetry/instrumentation/feature/test/ContextPropagationTest.java b/testing/feature-tests/src/test/java/com/solarwinds/opentelemetry/instrumentation/feature/test/ContextPropagationTest.java new file mode 100644 index 00000000..9142f6a6 --- /dev/null +++ b/testing/feature-tests/src/test/java/com/solarwinds/opentelemetry/instrumentation/feature/test/ContextPropagationTest.java @@ -0,0 +1,52 @@ +/* + * © SolarWinds Worldwide, LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.solarwinds.opentelemetry.instrumentation.feature.test; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class ContextPropagationTest { + + @RegisterExtension + static final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Test + void verifyTraceContextPropagatesViaW3CHeaders() { + testing.runWithSpan( + "service-a", + () -> { + Map headers = new HashMap<>(); + GlobalOpenTelemetry.getPropagators() + .getTextMapPropagator() + .inject(Context.current(), headers, Map::put); + + assertThat(headers).containsKey("traceparent"); + assertThat(headers.get("traceparent")) + .matches("00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}"); + + assertThat(headers).containsKey("tracestate"); + assertThat(headers.get("tracestate")).contains("sw="); + }); + } +} diff --git a/testing/feature-tests/src/test/java/com/solarwinds/opentelemetry/instrumentation/feature/test/MetricsGenerationTest.java b/testing/feature-tests/src/test/java/com/solarwinds/opentelemetry/instrumentation/feature/test/MetricsGenerationTest.java new file mode 100644 index 00000000..1cea2218 --- /dev/null +++ b/testing/feature-tests/src/test/java/com/solarwinds/opentelemetry/instrumentation/feature/test/MetricsGenerationTest.java @@ -0,0 +1,71 @@ +/* + * © SolarWinds Worldwide, LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.solarwinds.opentelemetry.instrumentation.feature.test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.sdk.metrics.data.MetricData; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class MetricsGenerationTest { + + @RegisterExtension + static final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); + + static Stream metricNames() { + return Stream.of( + Arguments.of("trace.service.response_time"), + Arguments.of("trace.service.request_count"), + Arguments.of("trace.service.tokenbucket_exhaustion_count"), + Arguments.of("trace.service.tracecount"), + Arguments.of("trace.service.samplecount"), + Arguments.of("trace.service.through_trace_count"), + Arguments.of("trace.service.triggered_trace_count")); + } + + @ParameterizedTest(name = "{0} metric is reported") + @MethodSource("metricNames") + void verifyMetricIsReportedAfterServerSpan(String metricName) { + Tracer tracer = GlobalOpenTelemetry.get().getTracer("test"); + Span span = + tracer.spanBuilder("GET /petclinic/api/pettypes").setSpanKind(SpanKind.SERVER).startSpan(); + span.end(); + + testing.waitForTraces(1); + await() + .atMost(5, TimeUnit.SECONDS) + .untilAsserted( + () -> { + List metrics = testing.metrics(); + assertThat(metrics) + .as("%s metric should be reported", metricName) + .anyMatch(m -> m.getName().equals(metricName)); + }); + } +} diff --git a/testing/feature-tests/src/test/java/com/solarwinds/opentelemetry/instrumentation/feature/test/ProfilingSpanProcessorTest.java b/testing/feature-tests/src/test/java/com/solarwinds/opentelemetry/instrumentation/feature/test/ProfilingSpanProcessorTest.java new file mode 100644 index 00000000..1055a1f2 --- /dev/null +++ b/testing/feature-tests/src/test/java/com/solarwinds/opentelemetry/instrumentation/feature/test/ProfilingSpanProcessorTest.java @@ -0,0 +1,65 @@ +/* + * © SolarWinds Worldwide, LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.solarwinds.opentelemetry.instrumentation.feature.test; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class ProfilingSpanProcessorTest { + + @RegisterExtension + static final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Test + void verifyProfilingSpanProcessorIsRegistered() { + testing.runWithSpan("profiling-test", () -> {}); + + List> traces = testing.waitForTraces(1); + assertThat(traces).isNotEmpty(); + + SpanData span = traces.get(0).get(0); + Long profileSpans = span.getAttributes().get(AttributeKey.longKey("sw.profile.spans")); + assertThat(profileSpans) + .as("sw.profile.spans attribute should be set by SolarwindsProfilingSpanProcessor") + .isNotNull(); + } + + @Test + void verifyProfilesIsCollected() { + testing.runWithSpan( + "profiling-test", + () -> { + try { + Thread.sleep(2000); + } catch (InterruptedException ignore) { + } + }); + + List> traces = testing.waitForTraces(1); + assertThat(traces).isNotEmpty(); + + SpanData span = traces.get(0).get(0); + Long profileSpans = span.getAttributes().get(AttributeKey.longKey("sw.profile.spans")); + assertThat(profileSpans).as("sw.profile.spans attribute should be set 1").isEqualTo(1); + } +} diff --git a/testing/feature-tests/src/test/java/com/solarwinds/opentelemetry/instrumentation/feature/test/SdkTracingTest.java b/testing/feature-tests/src/test/java/com/solarwinds/opentelemetry/instrumentation/feature/test/SdkTracingTest.java new file mode 100644 index 00000000..aa20de03 --- /dev/null +++ b/testing/feature-tests/src/test/java/com/solarwinds/opentelemetry/instrumentation/feature/test/SdkTracingTest.java @@ -0,0 +1,103 @@ +/* + * © SolarWinds Worldwide, LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.solarwinds.opentelemetry.instrumentation.feature.test; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class SdkTracingTest { + + @RegisterExtension + static final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Test + void verifyManualSpanCreationWithSdkTracer() { + testing.runWithSpan( + "root", + () -> { + Tracer tracer = GlobalOpenTelemetry.get().getTracer("sdk.tracing"); + Span span = tracer.spanBuilder("greet-span").startSpan(); + span.setAttribute("sw.test.source", "SDK.trace.test"); + span.end(); + }); + + List> traces = testing.waitForTraces(1); + assertThat(traces).hasSize(1); + + List spans = traces.get(0); + assertThat(spans).hasSize(2); + + SpanData greetSpan = + spans.stream().filter(s -> "greet-span".equals(s.getName())).findFirst().orElseThrow(); + assertThat(greetSpan.getAttributes().get(AttributeKey.stringKey("sw.test.source"))) + .isEqualTo("SDK.trace.test"); + } + + @Test + void verifyCodeStacktraceIsCaptured() { + testing.runWithSpan( + "root", + () -> { + Tracer tracer = GlobalOpenTelemetry.get().getTracer("sdk.tracing"); + Span span = tracer.spanBuilder("code-stacktrace").startSpan(); + span.setAttribute("thread.id", "test-1"); + span.end(); + }); + + List> traces = testing.waitForTraces(1); + assertThat(traces).hasSize(1); + + List spans = traces.get(0); + assertThat(spans).hasSize(2); + + SpanData spanData = + spans.stream().filter(s -> "code-stacktrace".equals(s.getName())).findFirst().orElseThrow(); + assertThat(spanData.getAttributes().get(AttributeKey.stringKey("code.stacktrace"))).isNotNull(); + } + + @Test + void verifyResourceNameFromServiceKey() { + testing.runWithSpan( + "root", + () -> { + Tracer tracer = GlobalOpenTelemetry.get().getTracer("sdk.tracing"); + Span span = tracer.spanBuilder("resource-name").startSpan(); + span.setAttribute("thread.id", "test-1"); + span.end(); + }); + + List> traces = testing.waitForTraces(1); + assertThat(traces).hasSize(1); + + List spans = traces.get(0); + assertThat(spans).hasSize(2); + + SpanData spanData = + spans.stream().filter(s -> "resource-name".equals(s.getName())).findFirst().orElseThrow(); + assertThat(spanData.getResource().getAttributes().get(AttributeKey.stringKey("service.name"))) + .isEqualTo("test-app"); + } +} diff --git a/testing/feature-tests/src/test/java/com/solarwinds/opentelemetry/instrumentation/feature/test/TransactionFilteringTest.java b/testing/feature-tests/src/test/java/com/solarwinds/opentelemetry/instrumentation/feature/test/TransactionFilteringTest.java new file mode 100644 index 00000000..ae87848f --- /dev/null +++ b/testing/feature-tests/src/test/java/com/solarwinds/opentelemetry/instrumentation/feature/test/TransactionFilteringTest.java @@ -0,0 +1,118 @@ +/* + * © SolarWinds Worldwide, LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.solarwinds.opentelemetry.instrumentation.feature.test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import com.solarwinds.joboe.core.settings.SimpleSettingsFetcher; +import com.solarwinds.joboe.core.settings.TestSettingsReader; +import com.solarwinds.joboe.core.util.TestUtils; +import com.solarwinds.joboe.sampling.ResourceMatcher; +import com.solarwinds.joboe.sampling.SampleRateSource; +import com.solarwinds.joboe.sampling.SamplingConfiguration; +import com.solarwinds.joboe.sampling.SettingsManager; +import com.solarwinds.joboe.sampling.TraceConfig; +import com.solarwinds.joboe.sampling.TraceConfigs; +import com.solarwinds.joboe.sampling.TracingMode; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.semconv.UrlAttributes; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class TransactionFilteringTest { + + @RegisterExtension + static final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @BeforeEach + void setUp() { + Map urlConfigs = new LinkedHashMap<>(); + urlConfigs.put( + url -> url.contains("/specialties"), + new TraceConfig(0, SampleRateSource.FILE, TracingMode.DISABLED.toFlags())); + + TestSettingsReader reader = TestUtils.initSettingsReader(); + reader.put( + new TestSettingsReader.SettingsMockupBuilder() + .withFlags(true, false, true, true, false) + .withSampleRate(1_000_000) + .build()); + + SettingsManager.initialize( + new SimpleSettingsFetcher(reader), + SamplingConfiguration.builder() + .internalTransactionSettings(new TraceConfigs(urlConfigs)) + .build()); + } + + @AfterEach + void tearDown() { + TestSettingsReader reader = TestUtils.initSettingsReader(); + reader.put( + new TestSettingsReader.SettingsMockupBuilder() + .withFlags(true, false, true, true, false) + .withSampleRate(1_000_000) + .build()); + + SettingsManager.initialize( + new SimpleSettingsFetcher(reader), SamplingConfiguration.builder().build()); + } + + @Test + void verifyFilteredUrlIsNotSampled() { + Tracer tracer = GlobalOpenTelemetry.get().getTracer("test"); + Span span = + tracer + .spanBuilder("GET /specialties") + .setSpanKind(SpanKind.SERVER) + .setAttribute(UrlAttributes.URL_PATH, "/petclinic/api/specialties") + .startSpan(); + span.end(); + + await() + .during(1, TimeUnit.SECONDS) + .atMost(3, TimeUnit.SECONDS) + .untilAsserted(() -> assertThat(testing.waitForTraces(0)).isEmpty()); + } + + @Test + void verifyNonFilteredUrlIsSampled() { + Tracer tracer = GlobalOpenTelemetry.get().getTracer("test"); + Span span = + tracer + .spanBuilder("GET /owners") + .setSpanKind(SpanKind.SERVER) + .setAttribute(UrlAttributes.URL_PATH, "/petclinic/api/owners") + .startSpan(); + + span.end(); + List> traces = testing.waitForTraces(1); + assertThat(traces).hasSize(1); + } +} diff --git a/testing/feature-tests/src/test/java/com/solarwinds/opentelemetry/instrumentation/feature/test/TransactionNamingTest.java b/testing/feature-tests/src/test/java/com/solarwinds/opentelemetry/instrumentation/feature/test/TransactionNamingTest.java new file mode 100644 index 00000000..57cfa5c6 --- /dev/null +++ b/testing/feature-tests/src/test/java/com/solarwinds/opentelemetry/instrumentation/feature/test/TransactionNamingTest.java @@ -0,0 +1,61 @@ +/* + * © SolarWinds Worldwide, LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.solarwinds.opentelemetry.instrumentation.feature.test; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.solarwinds.api.ext.SolarwindsAgent; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class TransactionNamingTest { + + @RegisterExtension + static final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Test + void verifySdkSetTransactionName() { + testing.runWithSpan( + "root", + () -> { + SolarwindsAgent.setTransactionName("int-test"); + }); + + List> traces = testing.waitForTraces(1); + assertThat(traces).hasSize(1); + + SpanData rootSpan = traces.get(0).get(0); + assertThat(rootSpan.getAttributes().get(AttributeKey.stringKey("sw.transaction"))) + .isEqualTo("int-test"); + } + + @Test + void verifyTransactionNameFallsBackToSpanName() { + testing.runWithSpan("GET /petclinic/api/owners", () -> {}); + + List> traces = testing.waitForTraces(1); + assertThat(traces).hasSize(1); + + SpanData rootSpan = traces.get(0).get(0); + String transactionName = rootSpan.getAttributes().get(AttributeKey.stringKey("sw.transaction")); + assertThat(transactionName).isEqualTo("GET /petclinic/api/owners"); + } +} diff --git a/testing/feature-tests/src/test/java/com/solarwinds/opentelemetry/instrumentation/feature/test/TriggerTraceTest.java b/testing/feature-tests/src/test/java/com/solarwinds/opentelemetry/instrumentation/feature/test/TriggerTraceTest.java new file mode 100644 index 00000000..3d213f21 --- /dev/null +++ b/testing/feature-tests/src/test/java/com/solarwinds/opentelemetry/instrumentation/feature/test/TriggerTraceTest.java @@ -0,0 +1,112 @@ +/* + * © SolarWinds Worldwide, LLC. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.solarwinds.opentelemetry.instrumentation.feature.test; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class TriggerTraceTest { + + @RegisterExtension + static final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Test + void verifyTriggerTraceStampsAttributesOnRootSpan() { + Map headers = new HashMap<>(); + headers.put("X-Trace-Options", "trigger-trace;custom-info=chubi;sw-keys=lo:se,check-id:123"); + + Context extractedContext = + GlobalOpenTelemetry.getPropagators() + .getTextMapPropagator() + .extract(Context.root(), headers, MapTextMapGetter.INSTANCE); + + Tracer tracer = GlobalOpenTelemetry.get().getTracer("test"); + Span span = + tracer + .spanBuilder("server-request") + .setParent(extractedContext) + .setSpanKind(SpanKind.SERVER) + .startSpan(); + + span.end(); + List> traces = testing.waitForTraces(1); + + assertThat(traces).hasSize(1); + SpanData rootSpan = traces.get(0).get(0); + assertThat(rootSpan.getAttributes().get(AttributeKey.booleanKey("TriggeredTrace"))).isTrue(); + + assertThat(rootSpan.getAttributes().get(AttributeKey.stringKey("SWKeys"))) + .isEqualTo("lo:se,check-id:123"); + assertThat(rootSpan.getAttributes().get(AttributeKey.stringKey("custom-info"))) + .isEqualTo("chubi"); + } + + @Test + void verifyTriggerTraceWithoutCustomKeysOnlyStampsTriggeredTrace() { + Map headers = new HashMap<>(); + headers.put("X-Trace-Options", "trigger-trace"); + + Context extractedContext = + GlobalOpenTelemetry.getPropagators() + .getTextMapPropagator() + .extract(Context.root(), headers, MapTextMapGetter.INSTANCE); + + Tracer tracer = GlobalOpenTelemetry.get().getTracer("test"); + Span span = + tracer + .spanBuilder("simple-trigger") + .setParent(extractedContext) + .setSpanKind(SpanKind.SERVER) + .startSpan(); + span.end(); + + List> traces = testing.waitForTraces(1); + assertThat(traces).hasSize(1); + + SpanData rootSpan = traces.get(0).get(0); + assertThat(rootSpan.getAttributes().get(AttributeKey.booleanKey("TriggeredTrace"))).isTrue(); + assertThat(rootSpan.getAttributes().get(AttributeKey.stringKey("SWKeys"))).isNull(); + } + + private enum MapTextMapGetter implements TextMapGetter> { + INSTANCE; + + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Override + public String get(Map carrier, String key) { + return carrier == null ? null : carrier.get(key); + } + } +}