diff --git a/build.gradle.kts b/build.gradle.kts index ca63a9633..fbd8ee939 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,6 +3,7 @@ plugins { id("com.diffplug.spotless") version "5.2.0" apply false id("org.hypertrace.publish-plugin") version "0.3.3" apply false id("org.hypertrace.ci-utils-plugin") version "0.1.4" + id("org.gradle.test-retry") version "1.2.0" apply false } allprojects { diff --git a/gradle/java.gradle b/gradle/java.gradle new file mode 100644 index 000000000..53b15c8a9 --- /dev/null +++ b/gradle/java.gradle @@ -0,0 +1,209 @@ +import java.time.Duration + +apply plugin: 'java-library' +apply plugin: 'groovy' +apply plugin: 'org.gradle.test-retry' + +// Version to use to compile code and run tests. +def DEFAULT_JAVA_VERSION = 11 + +jar { + /* + Make Jar build fail on duplicate files + + By default Gradle Jar task can put multiple files with the same name + into a Jar. This may lead to confusion. For example if auto-service + annotation processing creates files with same name in `scala` and + `java` directory this would result in Jar having two files with the + same name in it. Which in turn would result in only one of those + files being actually considered when that Jar is used leading to very + confusing failures. + + Instead we should 'fail early' and avoid building such Jars. + */ + duplicatesStrategy = 'fail' +} + +repositories { + mavenLocal() + mavenCentral() + jcenter() + maven { + url "https://repo.typesafe.com/typesafe/releases" + } + // this is only needed for the working against unreleased otel-java snapshots + maven { + url "https://oss.jfrog.org/artifactory/oss-snapshot-local" + content { + includeGroup "io.opentelemetry" + } + } +} + +ext { + deps = [ + spock : [ + dependencies.create("org.spockframework:spock-core:1.3-groovy-2.5", { + exclude group: 'org.codehaus.groovy', module: 'groovy-all' + }), + // Used by Spock for mocking: + dependencies.create(group: 'org.objenesis', name: 'objenesis', version: '3.1') + ], + groovy : "org.codehaus.groovy:groovy-all:2.5.11", + testLogging: [ + dependencies.create(group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.3'), + dependencies.create(group: 'org.slf4j', name: 'log4j-over-slf4j', version: '1.7.30'), + dependencies.create(group: 'org.slf4j', name: 'jcl-over-slf4j', version: '1.7.30'), + dependencies.create(group: 'org.slf4j', name: 'jul-to-slf4j', version: '1.7.30'), + ] + ] +} + +dependencies { + compileOnly group: 'org.checkerframework', name: 'checker-qual', version: '3.6.1' + + testImplementation enforcedPlatform(group: 'org.junit', name: 'junit-bom', version: '5.7.0-M1') + testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api' + testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-params' + testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine' + testRuntimeOnly group: 'org.junit.vintage', name: 'junit-vintage-engine' + + testImplementation deps.spock + testImplementation deps.groovy + testImplementation deps.testLogging + testImplementation group: 'info.solidsoft.spock', name: 'spock-global-unroll', version: '0.5.1' + testImplementation group: 'com.github.stefanbirkner', name: 'system-rules', version: '1.19.0' +} + +jar { + manifest { + attributes( + "Implementation-Title": project.name, + "Implementation-Version": project.version, + "Implementation-Vendor": "Hypertace", + "Implementation-URL": "https://github.com/hypertrace/hypertrace", + ) + } +} + +normalization { + runtimeClasspath { + metaInf { + ignoreAttribute("Implementation-Version") + } + } +} + +javadoc { + options.addStringOption('Xdoclint:none', '-quiet') + + doFirst { + if (project.ext.has("apiLinks")) { + options.links(*project.apiLinks) + } + } + source = sourceSets.main.allJava + classpath = configurations.compileClasspath + + options { + encoding = "utf-8" + docEncoding = "utf-8" + charSet = "utf-8" + + setMemberLevel JavadocMemberLevel.PUBLIC + setAuthor true + + links "https://docs.oracle.com/javase/8/docs/api/" + source = 8 + } +} + +project.afterEvaluate { + if (project.plugins.hasPlugin('org.unbroken-dome.test-sets') && configurations.hasProperty("latestDepTestRuntime")) { + tasks.withType(Test).configureEach { + doFirst { + def testArtifacts = configurations.testRuntimeClasspath.resolvedConfiguration.resolvedArtifacts + def latestTestArtifacts = configurations.latestDepTestRuntimeClasspath.resolvedConfiguration.resolvedArtifacts + assert testArtifacts != latestTestArtifacts: "latestDepTest dependencies are identical to test" + } + } + } +} + +def isJavaVersionAllowed(JavaVersion version) { + if (project.hasProperty('minJavaVersionForTests') && project.getProperty('minJavaVersionForTests').compareTo(version) > 0) { + return false + } + if (project.hasProperty('maxJavaVersionForTests') && project.getProperty('maxJavaVersionForTests').compareTo(version) < 0) { + return false + } + return true +} + +def testJavaVersion = rootProject.findProperty('testJavaVersion') +if (testJavaVersion != null) { + def requestedJavaVersion = JavaVersion.toVersion(testJavaVersion) + tasks.withType(Test).all { + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(requestedJavaVersion.majorVersion) + } + enabled = isJavaVersionAllowed(requestedJavaVersion) + } +} else { + // We default to testing with Java 11 for most tests, but some tests don't support it, where we change + // the default test task's version so commands like `./gradlew check` can test all projects regardless + // of Java version. + if (!isJavaVersionAllowed(JavaVersion.toVersion(DEFAULT_JAVA_VERSION))) { + tasks.withType(Test) { + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(project.getProperty('maxJavaVersionForTests').majorVersion) + } + } + } +} + +tasks.withType(Test).configureEach { + useJUnitPlatform() + + // All tests must complete within 30 minutes. + // This value is quite big because with lower values (3 mins) we were experiencing large number of false positives + timeout = Duration.ofMinutes(30) + + retry { + // You can see tests that were retried by this mechanism in the collected test reports and build scans. + maxRetries = System.getenv("CI") != null ? 5 : 0 + } + + reports { + junitXml.outputPerTestCase = true + } + + testLogging { + exceptionFormat = 'full' + } +} + +tasks.withType(AbstractArchiveTask) { + preserveFileTimestamps = false + reproducibleFileOrder = true +} + +plugins.withId('net.ltgt.errorprone') { + dependencies { + annotationProcessor group: "com.uber.nullaway", name: "nullaway", version: versions.nullaway + errorprone group: "com.google.errorprone", name: "error_prone_core", version: versions.errorprone + } + + tasks.withType(JavaCompile) { + if (!name.toLowerCase().contains("test")) { + options.errorprone { + error("NullAway") + + // Doesn't work well with Java 8 + disable("FutureReturnValueIgnored") + + option("NullAway:AnnotatedPackages", "io.opentelemetry,com.linecorp.armeria,com.google.common") + } + } + } +} diff --git a/smoke-tests/build.gradle.kts b/smoke-tests/build.gradle.kts index 6a3bde2f1..edb9090ee 100644 --- a/smoke-tests/build.gradle.kts +++ b/smoke-tests/build.gradle.kts @@ -1,10 +1,16 @@ plugins { + groovy `java-library` } +apply { + from("$rootDir/gradle/java.gradle") +} + val versions: Map by extra dependencies{ + testImplementation(project(":testing-common")) testImplementation(project(":javaagent-core")) testImplementation("org.testcontainers:testcontainers:1.15.2") testImplementation("com.squareup.okhttp3:okhttp:4.9.0") @@ -12,6 +18,10 @@ dependencies{ testImplementation("io.opentelemetry:opentelemetry-proto:${versions["opentelemetry"]}") testImplementation("io.opentelemetry:opentelemetry-sdk:${versions["opentelemetry"]}") testImplementation("com.google.protobuf:protobuf-java-util:3.13.0") + testImplementation("org.spockframework:spock-core:1.3-groovy-2.5") + testImplementation("info.solidsoft.spock:spock-global-unroll:0.5.1") + testImplementation("com.fasterxml.jackson.core:jackson-databind:2.11.2") + testImplementation("org.codehaus.groovy:groovy-all:2.5.11") } tasks.test { @@ -20,10 +30,11 @@ tasks.test { junitXml.isOutputPerTestCase = true } + maxParallelForks = 2 val shadowTask : Jar = project(":javaagent").tasks.named("shadowJar").get() inputs.files(layout.files(shadowTask)) doFirst { jvmArgs("-Dsmoketest.javaagent.path=${shadowTask.archiveFile.get()}") } -} +} \ No newline at end of file diff --git a/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/AppServerTest.groovy b/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/AppServerTest.groovy new file mode 100644 index 000000000..a7929d209 --- /dev/null +++ b/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/AppServerTest.groovy @@ -0,0 +1,322 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hypertrace.agent.smoketest + +import static org.junit.Assume.assumeTrue + +import io.opentelemetry.proto.trace.v1.Span +import java.util.jar.Attributes +import java.util.jar.JarFile +import okhttp3.Request +import org.junit.runner.RunWith +import spock.lang.Shared +import spock.lang.Unroll + +@RunWith(AppServerTestRunner) +abstract class AppServerTest extends SmokeTest { + @Shared + String jdk + @Shared + String serverVersion + + def setupSpec() { + def appServer = AppServerTestRunner.currentAppServer(this.getClass()) + serverVersion = appServer.version() + jdk = appServer.jdk() + startTarget(jdk, serverVersion) + } + + def cleanupSpec() { + stopTarget() + } + + boolean testSmoke() { + true + } + + boolean testAsyncSmoke() { + true + } + + boolean testException() { + true + } + + boolean testRequestWebInfWebXml() { + true + } + + //TODO add assert that server spans were created by servers, not by servlets + @Unroll + def "#appServer smoke test on JDK #jdk"(String appServer, String jdk) { + assumeTrue(testSmoke()) + + String url = "http://localhost:${target.getMappedPort(8080)}/app/greeting" + def request = new Request.Builder().url(url).get().build() + //def currentAgentVersion = new JarFile(agentPath).getManifest().getMainAttributes().get(Attributes.Name + //.IMPLEMENTATION_VERSION) + + when: + def response = CLIENT.newCall(request).execute() + TraceInspector traces = new TraceInspector(waitForTraces()) + Set traceIds = traces.traceIds + String responseBody = response.body().string() + + println traces.getSpanStream().forEach({ span -> "************** " + span.toString() + " *******************************" }); + + then: "There is one trace" + traceIds.size() == 1 + + and: "trace id is present in the HTTP headers as reported by the called endpoint" + responseBody.contains(traceIds.find()) + + and: "Server spans in the distributed trace" + traces.countSpansByKind(Span.SpanKind.SPAN_KIND_SERVER) == 2 + + and: "Expected span names" + traces.countSpansByName(getSpanName('/app/greeting')) == 1 + traces.countSpansByName(getSpanName('/app/headers')) == 1 + + and: "The span for the initial web request" + traces.countFilteredAttributes("http.url", url) == 1 + + and: "Client and server spans for the remote call" + traces.countFilteredAttributes("http.url", "http://localhost:8080/app/headers") == 2 + + cleanup: + response?.close() + + where: + [appServer, jdk] << getTestParams() + } + + @Unroll + def "#appServer test static file found on JDK #jdk"(String appServer, String jdk) { + String url = "http://localhost:${target.getMappedPort(8080)}/app/hello.txt" + def request = new Request.Builder().url(url).get().build() + //def currentAgentVersion = new JarFile(agentPath).getManifest().getMainAttributes().get(Attributes.Name + //.IMPLEMENTATION_VERSION) + + when: + def response = CLIENT.newCall(request).execute() + TraceInspector traces = new TraceInspector(waitForTraces()) + Set traceIds = traces.traceIds + String responseBody = response.body().string() + + then: "There is one trace" + traceIds.size() == 1 + + and: "Response contains Hello" + responseBody.contains("Hello") + + and: "There is one server span" + traces.countSpansByKind(Span.SpanKind.SPAN_KIND_SERVER) == 1 + + and: "Expected span names" + traces.countSpansByName(getSpanName('/app/hello.txt')) == 1 + + and: "The span for the initial web request" + traces.countFilteredAttributes("http.url", url) == 1 + + cleanup: + response?.close() + + where: + [appServer, jdk] << getTestParams() + } + + @Unroll + def "#appServer test static file not found on JDK #jdk"(String appServer, String jdk) { + String url = "http://localhost:${target.getMappedPort(8080)}/app/file-that-does-not-exist" + def request = new Request.Builder().url(url).get().build() + //def currentAgentVersion = new JarFile(agentPath).getManifest().getMainAttributes().get(Attributes.Name + //.IMPLEMENTATION_VERSION) + + when: + def response = CLIENT.newCall(request).execute() + TraceInspector traces = new TraceInspector(waitForTraces()) + Set traceIds = traces.traceIds + + then: "There is one trace" + traceIds.size() == 1 + + and: "Response code is 404" + response.code() == 404 + + and: "There is one server span" + traces.countSpansByKind(Span.SpanKind.SPAN_KIND_SERVER) == 1 + + and: "Expected span names" + traces.countSpansByName(getSpanName('/app/file-that-does-not-exist')) == 1 + + and: "The span for the initial web request" + traces.countFilteredAttributes("http.url", url) == 1 + + cleanup: + response?.close() + + where: + [appServer, jdk] << getTestParams() + } + + @Unroll + def "#appServer test request for WEB-INF/web.xml on JDK #jdk"(String appServer, String jdk) { + assumeTrue(testRequestWebInfWebXml()) + + String url = "http://localhost:${target.getMappedPort(8080)}/app/WEB-INF/web.xml" + def request = new Request.Builder().url(url).get().build() + //def currentAgentVersion = new JarFile(agentPath).getManifest().getMainAttributes().get(Attributes.Name + //.IMPLEMENTATION_VERSION) + + when: + def response = CLIENT.newCall(request).execute() + TraceInspector traces = new TraceInspector(waitForTraces()) + Set traceIds = traces.traceIds + + then: "There is one trace" + traceIds.size() == 1 + + and: "Response code is 404" + response.code() == 404 + + and: "There is one server span" + traces.countSpansByKind(Span.SpanKind.SPAN_KIND_SERVER) == 1 + + and: "Expected span names" + traces.countSpansByName(getSpanName('/app/WEB-INF/web.xml')) == 1 + + and: "The span for the initial web request" + traces.countFilteredAttributes("http.url", url) == 1 + + cleanup: + response?.close() + + where: + [appServer, jdk] << getTestParams() + } + + @Unroll + def "#appServer test request with error JDK #jdk"(String appServer, String jdk) { + assumeTrue(testException()) + + String url = "http://localhost:${target.getMappedPort(8080)}/app/exception" + def request = new Request.Builder().url(url).get().build() + //def currentAgentVersion = new JarFile(agentPath).getManifest().getMainAttributes().get(Attributes.Name + //.IMPLEMENTATION_VERSION) + + when: + def response = CLIENT.newCall(request).execute() + TraceInspector traces = new TraceInspector(waitForTraces()) + Set traceIds = traces.traceIds + + then: "There is one trace" + traceIds.size() == 1 + + and: "Response code is 500" + response.code() == 500 + + and: "There is one server span" + traces.countSpansByKind(Span.SpanKind.SPAN_KIND_SERVER) == 1 + + and: "Expected span names" + traces.countSpansByName(getSpanName('/app/exception')) == 1 + + and: "There is one exception" + traces.countFilteredEventAttributes('exception.message', 'This is expected') == 1 + + and: "The span for the initial web request" + traces.countFilteredAttributes("http.url", url) == 1 + + cleanup: + response?.close() + + where: + [appServer, jdk] << getTestParams() + } + + @Unroll + def "#appServer test request outside deployed application JDK #jdk"(String appServer, String jdk) { + String url = "http://localhost:${target.getMappedPort(8080)}/this-is-definitely-not-there-but-there-should-be-a-trace-nevertheless" + def request = new Request.Builder().url(url).get().build() + //def currentAgentVersion = new JarFile(agentPath).getManifest().getMainAttributes().get(Attributes.Name + //.IMPLEMENTATION_VERSION) + + when: + def response = CLIENT.newCall(request).execute() + TraceInspector traces = new TraceInspector(waitForTraces()) + Set traceIds = traces.traceIds + + then: "There is one trace" + traceIds.size() == 1 + + and: "Response code is 404" + response.code() == 404 + + and: "There is one server span" + traces.countSpansByKind(Span.SpanKind.SPAN_KIND_SERVER) == 1 + + and: "Expected span names" + traces.countSpansByName(getSpanName('/this-is-definitely-not-there-but-there-should-be-a-trace-nevertheless')) == 1 + + and: "The span for the initial web request" + traces.countFilteredAttributes("http.url", url) == 1 + + cleanup: + response?.close() + + where: + [appServer, jdk] << getTestParams() + } + + @Unroll + def "#appServer async smoke test on JDK #jdk"(String appServer, String jdk) { + assumeTrue(testAsyncSmoke()) + + String url = "http://localhost:${target.getMappedPort(8080)}/app/asyncgreeting" + def request = new Request.Builder().url(url).get().build() + //def currentAgentVersion = new JarFile(agentPath).getManifest().getMainAttributes().get(Attributes.Name + //.IMPLEMENTATION_VERSION) + + when: + def response = CLIENT.newCall(request).execute() + TraceInspector traces = new TraceInspector(waitForTraces()) + Set traceIds = traces.traceIds + String responseBody = response.body().string() + + then: "There is one trace" + traceIds.size() == 1 + + and: "trace id is present in the HTTP headers as reported by the called endpoint" + responseBody.contains(traceIds.find()) + + and: "Server spans in the distributed trace" + traces.countSpansByKind(Span.SpanKind.SPAN_KIND_SERVER) == 2 + + and: "Expected span names" + traces.countSpansByName(getSpanName('/app/asyncgreeting')) == 1 + traces.countSpansByName(getSpanName('/app/headers')) == 1 + + and: "The span for the initial web request" + traces.countFilteredAttributes("http.url", url) == 1 + + and: "Client and server spans for the remote call" + traces.countFilteredAttributes("http.url", "http://localhost:8080/app/headers") == 2 + + cleanup: + response?.close() + + where: + [appServer, jdk] << getTestParams() + } + + protected abstract String getSpanName(String path); + + protected List> getTestParams() { + return [ + [serverVersion, jdk] + ] + } +} \ No newline at end of file diff --git a/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/GrpcSmokeTest.groovy b/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/GrpcSmokeTest.groovy new file mode 100644 index 000000000..afe0dee9d --- /dev/null +++ b/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/GrpcSmokeTest.groovy @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hypertrace.agent.smoketest + +import io.grpc.ManagedChannelBuilder +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest +import io.opentelemetry.proto.collector.trace.v1.TraceServiceGrpc +import spock.lang.Unroll + +class GrpcSmokeTest extends SmokeTest { + + protected String getTargetImage(String jdk, String serverVersion) { + "ghcr.io/open-telemetry/java-test-containers:smoke-grpc-jdk$jdk-20210129.520311770" + } + + @Unroll + def "grpc smoke test on JDK #jdk"(int jdk) { + setup: + startTarget(jdk) + + def channel = ManagedChannelBuilder.forAddress("localhost", target.getMappedPort(8080)) + .usePlaintext() + .build() + def stub = TraceServiceGrpc.newBlockingStub(channel) + + when: + stub.export(ExportTraceServiceRequest.getDefaultInstance()) + Collection traces = waitForTraces() + + then: + countSpansByName(traces, 'opentelemetry.proto.collector.trace.v1.TraceService/Export') == 1 + countSpansByName(traces, 'TestService.withSpan') == 1 + + cleanup: + stopTarget() + channel.shutdown() + + where: + jdk << [8, 11, 15] + } +} diff --git a/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/JaegerExporterSmokeTest.groovy b/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/JaegerExporterSmokeTest.groovy new file mode 100644 index 000000000..4a2946acc --- /dev/null +++ b/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/JaegerExporterSmokeTest.groovy @@ -0,0 +1,51 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hypertrace.agent.smoketest + +import static java.util.stream.Collectors.toSet + +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest +import java.util.jar.Attributes +import java.util.jar.JarFile +import okhttp3.Request + +class JaegerExporterSmokeTest extends SmokeTest { + + protected String getTargetImage(String jdk, String serverVersion) { + "ghcr.io/open-telemetry/java-test-containers:smoke-springboot-jdk$jdk-20210129.520311771" + } + + @Override + protected Map getExtraEnv() { + return [ + "OTEL_TRACES_EXPORTER" : "jaeger", + "OTEL_EXPORTER_JAEGER_ENDPOINT" : "collector:14250" + ] + } + + def "spring boot smoke test with jaeger grpc"() { + setup: + startTarget(11) + + String url = "http://localhost:${target.getMappedPort(8080)}/greeting" + def request = new Request.Builder().url(url).get().build() + + when: + def response = CLIENT.newCall(request).execute() + Collection traces = waitForTraces() + + then: + response.body().string() == "Hi!" + countSpansByName(traces, '/greeting') == 1 + countSpansByName(traces, 'WebController.greeting') == 1 + countSpansByName(traces, 'WebController.withSpan') == 1 + + cleanup: + stopTarget() + + } + +} diff --git a/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/JettySmokeTest.groovy b/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/JettySmokeTest.groovy new file mode 100644 index 000000000..e2ea5062c --- /dev/null +++ b/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/JettySmokeTest.groovy @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hypertrace.agent.smoketest + +@AppServer(version = "9.4.35", jdk = "8") +@AppServer(version = "9.4.35", jdk = "11") +@AppServer(version = "10.0.0", jdk = "11") +@AppServer(version = "10.0.0", jdk = "15") +class JettySmokeTest extends AppServerTest { + + protected String getTargetImage(String jdk, String serverVersion) { + "ghcr.io/open-telemetry/java-test-containers:jetty-${serverVersion}-jdk$jdk-20201215.422527843" + } + + def getJettySpanName() { + return serverVersion.startsWith("10.") ? "HandlerList.handle" : "HandlerCollection.handle" + } + + @Override + protected String getSpanName(String path) { + switch (path) { + case "/app/WEB-INF/web.xml": + case "/this-is-definitely-not-there-but-there-should-be-a-trace-nevertheless": + return getJettySpanName() + } + return path + } +} diff --git a/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/LibertyServletOnlySmokeTest.groovy b/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/LibertyServletOnlySmokeTest.groovy new file mode 100644 index 000000000..e2d69bbca --- /dev/null +++ b/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/LibertyServletOnlySmokeTest.groovy @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hypertrace.agent.smoketest + +import org.testcontainers.containers.BindMode +import org.testcontainers.containers.GenericContainer + +@AppServer(version = "20.0.0.12", jdk = "8") +class LibertyServletOnlySmokeTest extends LibertySmokeTest { + + protected void customizeContainer(GenericContainer container) { + container.withClasspathResourceMapping("liberty-servlet.xml", "/config/server.xml", BindMode.READ_ONLY) + } +} diff --git a/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/LibertySmokeTest.groovy b/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/LibertySmokeTest.groovy new file mode 100644 index 000000000..2fe241bf0 --- /dev/null +++ b/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/LibertySmokeTest.groovy @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hypertrace.agent.smoketest + +import java.time.Duration +import org.testcontainers.containers.wait.strategy.Wait +import org.testcontainers.containers.wait.strategy.WaitStrategy + +@AppServer(version = "20.0.0.12", jdk = "8") +@AppServer(version = "20.0.0.12", jdk = "11") +@AppServer(version = "20.0.0.12", jdk = "8-jdk-openj9") +@AppServer(version = "20.0.0.12", jdk = "11-jdk-openj9") +class LibertySmokeTest extends AppServerTest { + + protected String getTargetImage(String jdk, String serverVersion) { + "ghcr.io/open-telemetry/java-test-containers:liberty-${serverVersion}-jdk$jdk-20201215.422527843" + } + + @Override + protected WaitStrategy getWaitStrategy() { + return Wait + .forLogMessage(".*server is ready to run a smarter planet.*", 1) + .withStartupTimeout(Duration.ofMinutes(3)) + } + + @Override + protected String getSpanName(String path) { + switch (path) { + case "/app/greeting": + case "/app/headers": + case "/app/exception": + case "/app/asyncgreeting": + return path + } + return 'HTTP GET' + } +} diff --git a/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/PlaySmokeTest.groovy b/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/PlaySmokeTest.groovy new file mode 100644 index 000000000..2c9d66920 --- /dev/null +++ b/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/PlaySmokeTest.groovy @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hypertrace.agent.smoketest + +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest +import okhttp3.Request + +class PlaySmokeTest extends SmokeTest { + + protected String getTargetImage(String jdk, String serverVersion) { + "ghcr.io/open-telemetry/java-test-containers:smoke-play-jdk$jdk-20201128.1734635" + } + + def "play smoke test on JDK #jdk"(int jdk) { + setup: + startTarget(jdk) + String url = "http://localhost:${target.getMappedPort(8080)}/welcome?id=1" + def request = new Request.Builder().url(url).get().build() + + when: + def response = CLIENT.newCall(request).execute() + Collection traces = waitForTraces() + + then: + response.body().string() == "Welcome 1." + //Both play and akka-http support produce spans with the same name. + //One internal, one SERVER + countSpansByName(traces, '/welcome') == 2 + + cleanup: + stopTarget() + + where: + jdk << [8, 11, 15] + } + +} diff --git a/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/PropagationTest.groovy b/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/PropagationTest.groovy new file mode 100644 index 000000000..fc2af7e1b --- /dev/null +++ b/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/PropagationTest.groovy @@ -0,0 +1,121 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hypertrace.agent.smoketest + +import static java.util.stream.Collectors.toSet + +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest +import okhttp3.Request + +abstract class PropagationTest extends SmokeTest { + + @Override + protected String getTargetImage(String jdk, String serverVersion) { + "ghcr.io/open-telemetry/java-test-containers:smoke-springboot-jdk$jdk-20210129.520311771" + } + + def "Should propagate test"() { + setup: + startTarget(11) + String url = "http://localhost:${target.getMappedPort(8080)}/front" + def request = new Request.Builder().url(url).get().build() + + when: + def response = CLIENT.newCall(request).execute() + Collection traces = waitForTraces() + def traceIds = getSpanStream(traces) + .map({ bytesToHex(it.getTraceId().toByteArray()) }) + .collect(toSet()) + + then: + traceIds.size() == 1 + + def traceId = traceIds.first() + + response.body().string() == "${traceId};${traceId}" + + cleanup: + stopTarget() + + } + +} + +class DefaultPropagationTest extends PropagationTest { +} + +class W3CPropagationTest extends PropagationTest { + @Override + protected Map getExtraEnv() { + return ["otel.propagators": "tracecontext"] + } +} + +class B3PropagationTest extends PropagationTest { + @Override + protected Map getExtraEnv() { + return ["otel.propagators": "b3"] + } +} + +class B3MultiPropagationTest extends PropagationTest { + @Override + protected Map getExtraEnv() { + return ["otel.propagators": "b3multi"] + } +} + +class JaegerPropagationTest extends PropagationTest { + @Override + protected Map getExtraEnv() { + return ["otel.propagators": "jaeger"] + } +} + +class OtTracerPropagationTest extends SmokeTest { + @Override + protected String getTargetImage(String jdk, String serverVersion) { + "ghcr.io/open-telemetry/java-test-containers:smoke-springboot-jdk$jdk-20210129.520311771" + } + + // OtTracer only propagates lower half of trace ID so we have to mangle the trace IDs similar to + // the Lightstep backend. + def "Should propagate test"() { + setup: + startTarget(11) + String url = "http://localhost:${target.getMappedPort(8080)}/front" + def request = new Request.Builder().url(url).get().build() + + when: + def response = CLIENT.newCall(request).execute() + Collection traces = waitForTraces() + def traceIds = getSpanStream(traces) + .map({ bytesToHex(it.getTraceId().toByteArray()).substring(16) }) + .collect(toSet()) + + then: + traceIds.size() == 1 + + def traceId = traceIds.first() + + response.body().string().matches(/[0-9a-f]{16}${traceId};[0]{16}${traceId}/) + + cleanup: + stopTarget() + } + + @Override + protected Map getExtraEnv() { + return ["otel.propagators": "ottracer"] + } +} + +class XRayPropagationTest extends PropagationTest { + @Override + protected Map getExtraEnv() { + return ["otel.propagators": "xray"] + } +} diff --git a/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/SmokeTest.groovy b/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/SmokeTest.groovy new file mode 100644 index 000000000..ca02908d6 --- /dev/null +++ b/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/SmokeTest.groovy @@ -0,0 +1,252 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hypertrace.agent.smoketest + +import org.hypertrace.agent.testing.OkHttpUtils + +import static java.util.stream.Collectors.toSet + +import com.fasterxml.jackson.databind.ObjectMapper +import com.google.protobuf.util.JsonFormat +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest +import io.opentelemetry.proto.common.v1.AnyValue +import io.opentelemetry.proto.trace.v1.Span +import java.util.concurrent.TimeUnit +import java.util.regex.Pattern +import java.util.stream.Stream +import okhttp3.OkHttpClient +import okhttp3.Request +import org.slf4j.LoggerFactory +import org.testcontainers.containers.GenericContainer +import org.testcontainers.containers.Network +import org.testcontainers.containers.output.Slf4jLogConsumer +import org.testcontainers.containers.output.ToStringConsumer +import org.testcontainers.containers.wait.strategy.Wait +import org.testcontainers.containers.wait.strategy.WaitStrategy +import org.testcontainers.images.PullPolicy +import org.testcontainers.utility.MountableFile +import spock.lang.Shared +import spock.lang.Specification + +abstract class SmokeTest extends Specification { + private static final Pattern TRACE_ID_PATTERN = Pattern.compile(".*traceId=(?[a-zA-Z0-9]+).*") + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() + + protected static final OkHttpClient CLIENT = OkHttpUtils.client() + + @Shared + private Network network = Network.newNetwork() + @Shared + protected String agentPath = System.getProperty("smoketest.javaagent.path") + + @Shared + protected GenericContainer target + + protected abstract String getTargetImage(String jdk, String serverVersion) + + /** + * Subclasses can override this method to customise target application's environment + */ + protected Map getExtraEnv() { + return Collections.emptyMap() + } + + /** + * Subclasses can override this method to customise target application's environment + */ + protected void customizeContainer(GenericContainer container) { + } + + @Shared + private GenericContainer backend + + @Shared + private GenericContainer collector + + def setupSpec() { + backend = new GenericContainer<>("ghcr.io/open-telemetry/java-test-containers:smoke-fake-backend-20201128.1734635") + .withExposedPorts(8080) + .waitingFor(Wait.forHttp("/health").forPort(8080)) + .withNetwork(network) + .withNetworkAliases("backend") + .withImagePullPolicy(PullPolicy.alwaysPull()) + .withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger("smoke.tests.backend"))) + backend.start() + + collector = new GenericContainer<>("otel/opentelemetry-collector-dev:latest") + .dependsOn(backend) + .withNetwork(network) + .withNetworkAliases("collector") + .withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger("smoke.tests.collector"))) + .withImagePullPolicy(PullPolicy.alwaysPull()) + .withCopyFileToContainer(MountableFile.forClasspathResource("/otel.yaml"), "/etc/otel.yaml") + .withCommand("--config /etc/otel.yaml") + collector.start() + } + + def startTarget(int jdk, String serverVersion = null) { + startTarget(String.valueOf(jdk), serverVersion) + } + + def startTarget(String jdk, String serverVersion = null) { + def output = new ToStringConsumer() + target = new GenericContainer<>(getTargetImage(jdk, serverVersion)) + .withExposedPorts(8080) + .withNetwork(network) + .withLogConsumer(output) + .withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger("smoke.tests.target"))) + //.withCopyFileToContainer(MountableFile.forHostPath("/Users/samarth/Downloads/hypertrace-agent-all.jar"""), +//"""/opentelemetry-javaagent-all.jar") + .withCopyFileToContainer(MountableFile.forHostPath(agentPath), "/hypertrace-agent-all.jar") + .withEnv("JAVA_TOOL_OPTIONS", "-javaagent:/hypertrace-agent-all.jar -Dorg.hypertrace.agent.slf4j.simpleLogger.log.muzzleMatcher=true") + .withEnv("OTEL_BSP_MAX_EXPORT_BATCH_SIZE", "1") + .withEnv("OTEL_BSP_SCHEDULE_DELAY_MILLIS", "10") + .withEnv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://collector:55680") + .withEnv("HT_SERVICE_NAME", "CIService") + .withEnv("HT_REPORTING_ENDPOINT", "http://collector:9411/api/v2/spans") + .withEnv("OTEL_TRACE_EXPORTER", "otlp") + .withImagePullPolicy(PullPolicy.alwaysPull()) + .withEnv(extraEnv) + customizeContainer(target) + + WaitStrategy waitStrategy = getWaitStrategy() + if (waitStrategy != null) { + target = target.waitingFor(waitStrategy) + } + + target.start() + output + } + + protected WaitStrategy getWaitStrategy() { + return null + } + + def cleanup() { + CLIENT.newCall(new Request.Builder() + .url("http://localhost:${backend.getMappedPort(8080)}/clear-requests") + .build()) + .execute() + .close() + } + + def stopTarget() { + target.stop() + } + + def cleanupSpec() { + backend.stop() + collector.stop() + network.close() + } + + protected static Stream findResourceAttribute(Collection traces, + String attributeKey) { + return traces.stream() + .flatMap { it.getResourceSpansList().stream() } + .flatMap { it.getResource().getAttributesList().stream() } + .filter { it.key == attributeKey } + .map { it.value } + } + + protected static int countSpansByName(Collection traces, String spanName) { + return getSpanStream(traces).filter { it.name == spanName }.count() + } + + protected static Stream getSpanStream(Collection traces) { + return traces.stream() + .flatMap { it.getResourceSpansList().stream() } + .flatMap { it.getInstrumentationLibrarySpansList().stream() } + .flatMap { it.getSpansList().stream() } + } + + protected Collection waitForTraces() { + def content = waitForContent() + + return OBJECT_MAPPER.readTree(content).collect { + def builder = ExportTraceServiceRequest.newBuilder() + // TODO(anuraaga): Register parser into object mapper to avoid de -> re -> deserialize. + JsonFormat.parser().merge(OBJECT_MAPPER.writeValueAsString(it), builder) + return builder.build() + } + } + + private String waitForContent() { + long previousSize = 0 + long deadline = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(30) + String content = "[]" + while (System.currentTimeMillis() < deadline) { + def body = content = CLIENT.newCall(new Request.Builder() + .url("http://localhost:${backend.getMappedPort(8080)}/get-requests") + .build()) + .execute() + .body() + try { + content = body.string() + } finally { + body.close() + } + if (content.length() > 2 && content.length() == previousSize) { + break + } + previousSize = content.length() + println "Curent content size $previousSize" + TimeUnit.MILLISECONDS.sleep(500) + } + + return content + } + + protected static Set getLoggedTraceIds(ToStringConsumer output) { + output.toUtf8String().lines() + .flatMap(SmokeTest.&findTraceId) + .collect(toSet()) + } + + private static Stream findTraceId(String log) { + def m = TRACE_ID_PATTERN.matcher(log) + m.matches() ? Stream.of(m.group("traceId")) : Stream.empty() as Stream + } + + protected static boolean isVersionLogged(ToStringConsumer output, String version) { + output.toUtf8String().lines() + .filter({ it.contains("opentelemetry-javaagent - version: " + version) }) + .findFirst() + .isPresent() + } + + // TODO(anuraaga): Delete after https://github.com/open-telemetry/opentelemetry-java/pull/2750 + static String bytesToHex(byte[] bytes) { + char[] dest = new char[bytes.length * 2] + bytesToBase16(bytes, dest) + return new String(dest) + } + + private static void bytesToBase16(byte[] bytes, char[] dest) { + for (int i = 0; i < bytes.length; i++) { + byteToBase16(bytes[i], dest, i * 2) + } + } + + private static void byteToBase16(byte value, char[] dest, int destOffset) { + int b = value & 0xFF + dest[destOffset] = ENCODING[b] + dest[destOffset + 1] = ENCODING[b | 0x100] + } + + private static final String ALPHABET = "0123456789abcdef" + private static final char[] ENCODING = buildEncodingArray() + + private static char[] buildEncodingArray() { + char[] encoding = new char[512] + for (int i = 0; i < 256; ++i) { + encoding[i] = ALPHABET.charAt(i >>> 4) + encoding[i | 0x100] = ALPHABET.charAt(i & 0xF) + } + return encoding + } +} diff --git a/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/TomcatSmokeTest.groovy b/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/TomcatSmokeTest.groovy new file mode 100644 index 000000000..fa310b586 --- /dev/null +++ b/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/TomcatSmokeTest.groovy @@ -0,0 +1,28 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hypertrace.agent.smoketest + +@AppServer(version = "7.0.107", jdk = "8") +@AppServer(version = "8.5.60", jdk = "8") +@AppServer(version = "8.5.60", jdk = "11") +@AppServer(version = "9.0.40", jdk = "8") +@AppServer(version = "9.0.40", jdk = "11") +class TomcatSmokeTest extends AppServerTest { + + protected String getTargetImage(String jdk, String serverVersion) { + "ghcr.io/open-telemetry/java-test-containers:tomcat-${serverVersion}-jdk$jdk-20201215.422527843" + } + + @Override + protected String getSpanName(String path) { + switch (path) { + case "/app/WEB-INF/web.xml": + case "/this-is-definitely-not-there-but-there-should-be-a-trace-nevertheless": + return "CoyoteAdapter.service" + } + return path + } +} diff --git a/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/TomeeSmokeTest.groovy b/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/TomeeSmokeTest.groovy new file mode 100644 index 000000000..ce5719a27 --- /dev/null +++ b/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/TomeeSmokeTest.groovy @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hypertrace.agent.smoketest + +import java.time.Duration +import org.testcontainers.containers.wait.strategy.Wait +import org.testcontainers.containers.wait.strategy.WaitStrategy + +@AppServer(version = "7.0.0", jdk = "8") +@AppServer(version = "8.0.6", jdk = "8") +@AppServer(version = "8.0.6", jdk = "11") +class TomeeSmokeTest extends AppServerTest { + + protected String getTargetImage(String jdk, String serverVersion) { + "ghcr.io/open-telemetry/java-test-containers:tomee-${serverVersion}-jdk$jdk-20210202.531569197" + } + + @Override + protected WaitStrategy getWaitStrategy() { + return Wait + .forLogMessage(".*Server startup in.*", 1) + .withStartupTimeout(Duration.ofMinutes(3)) + } + + @Override + protected String getSpanName(String path) { + switch (path) { + case "/app/WEB-INF/web.xml": + return "CoyoteAdapter.service" + } + return path + } +} diff --git a/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/TraceInspector.java b/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/TraceInspector.java new file mode 100644 index 000000000..e876dc20d --- /dev/null +++ b/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/TraceInspector.java @@ -0,0 +1,105 @@ +/* + * Copyright The Hypertrace Authors + * + * 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 org.hypertrace.agent.smoketest; + +import com.google.protobuf.ByteString; +import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest; +import io.opentelemetry.proto.common.v1.AnyValue; +import io.opentelemetry.proto.common.v1.KeyValue; +import io.opentelemetry.proto.trace.v1.ResourceSpans; +import io.opentelemetry.proto.trace.v1.Span; +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class TraceInspector { + final Collection traces; + + public TraceInspector(Collection traces) { + this.traces = traces; + } + + public Stream getSpanStream() { + return traces.stream() + .flatMap(it -> it.getResourceSpansList().stream()) + .flatMap(it -> it.getInstrumentationLibrarySpansList().stream()) + .flatMap(it -> it.getSpansList().stream()); + } + + public Stream findResourceAttribute(String attributeKey) { + return traces.stream() + .flatMap(it -> it.getResourceSpansList().stream()) + .flatMap(it -> it.getResource().getAttributesList().stream()) + .filter(it -> it.getKey().equals(attributeKey)) + .map(KeyValue::getValue); + } + + public long countFilteredResourceAttributes(String attributeName, Object attributeValue) { + return traces.stream() + .flatMap(it -> it.getResourceSpansList().stream()) + .map(ResourceSpans::getResource) + .flatMap(it -> it.getAttributesList().stream()) + .filter(a -> a.getKey().equals(attributeName)) + .map(a -> a.getValue().getStringValue()) + .filter(s -> s.equals(attributeValue)) + .count(); + } + + public long countFilteredAttributes(String attributeName, Object attributeValue) { + return getSpanStream() + .flatMap(s -> s.getAttributesList().stream()) + .filter(a -> a.getKey().equals(attributeName)) + .map(a -> a.getValue().getStringValue()) + .filter(s -> s.equals(attributeValue)) + .count(); + } + + public long countFilteredEventAttributes(String attributeName, Object attributeValue) { + return getSpanStream() + .flatMap(s -> s.getEventsList().stream()) + .flatMap(e -> e.getAttributesList().stream()) + .filter(a -> a.getKey().equals(attributeName)) + .map(a -> a.getValue().getStringValue()) + .filter(s -> s.equals(attributeValue)) + .count(); + } + + protected int countSpansByName(String spanName) { + return (int) getSpanStream().filter(it -> it.getName().equals(spanName)).count(); + } + + protected int countSpansByKind(Span.SpanKind spanKind) { + return (int) getSpanStream().filter(it -> it.getKind().equals(spanKind)).count(); + } + + protected int countSpans() { + return (int) getSpanStream().count(); + } + + public int size() { + return traces.size(); + } + + public Set getTraceIds() { + return getSpanStream() + .map(Span::getTraceId) + .map(ByteString::toByteArray) + .map(SmokeTest::bytesToHex) + .collect(Collectors.toSet()); + } +} diff --git a/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/WildflySmokeTest.groovy b/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/WildflySmokeTest.groovy new file mode 100644 index 000000000..8e576d5d0 --- /dev/null +++ b/smoke-tests/src/test/groovy/org/hypertrace/agent/smoketest/WildflySmokeTest.groovy @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hypertrace.agent.smoketest + +import io.opentelemetry.proto.trace.v1.Span +import okhttp3.Request +import spock.lang.Unroll + +@AppServer(version = "13.0.0.Final", jdk = "8") +@AppServer(version = "17.0.1.Final", jdk = "11") +@AppServer(version = "21.0.0.Final", jdk = "11") +class WildflySmokeTest extends AppServerTest { + + protected String getTargetImage(String jdk, String serverVersion) { + "ghcr.io/open-telemetry/java-test-containers:wildfly-${serverVersion}-jdk$jdk-20201215.422527843" + } + + @Override + protected String getSpanName(String path) { + switch (path) { + case "/app/WEB-INF/web.xml": + case "/this-is-definitely-not-there-but-there-should-be-a-trace-nevertheless": + return "DisallowedMethodsHandler.handleRequest" + } + return path + } + + @Unroll + def "JSP smoke test on WildFly"() { + String url = "http://localhost:${target.getMappedPort(8080)}/app/jsp" + def request = new Request.Builder().url(url).get().build() + + when: + def response = CLIENT.newCall(request).execute() + TraceInspector traces = new TraceInspector(waitForTraces()) + String responseBody = response.body().string() + + then: + response.successful + responseBody.contains("Successful JSP test") + + traces.countSpansByKind(Span.SpanKind.SPAN_KIND_SERVER) == 1 + + traces.countSpansByName('/app/jsp') == 1 + + where: + [appServer, jdk] << getTestParams() + } +} diff --git a/smoke-tests/src/test/java/org/hypertrace/agent/smoketest/AppServer.java b/smoke-tests/src/test/java/org/hypertrace/agent/smoketest/AppServer.java new file mode 100644 index 000000000..45bbc6c74 --- /dev/null +++ b/smoke-tests/src/test/java/org/hypertrace/agent/smoketest/AppServer.java @@ -0,0 +1,34 @@ +/* + * Copyright The Hypertrace Authors + * + * 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 org.hypertrace.agent.smoketest; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Repeatable(org.hypertrace.agent.smoketest.AppServers.class) +@Inherited +public @interface AppServer { + String version(); + + String jdk(); +} diff --git a/smoke-tests/src/test/java/org/hypertrace/agent/smoketest/AppServerTestRunner.java b/smoke-tests/src/test/java/org/hypertrace/agent/smoketest/AppServerTestRunner.java new file mode 100644 index 000000000..3110e77e9 --- /dev/null +++ b/smoke-tests/src/test/java/org/hypertrace/agent/smoketest/AppServerTestRunner.java @@ -0,0 +1,70 @@ +/* + * Copyright The Hypertrace Authors + * + * 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 org.hypertrace.agent.smoketest; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.junit.runner.notification.RunNotifier; +import org.junit.runners.model.InitializationError; +import org.spockframework.runtime.Sputnik; + +/** + * Customized spock test runner that runs tests on multiple app server versions based on {@link + * AppServer} annotations. This runner selects first server based on {@link AppServer} annotation + * calls setupSpec, all test method and cleanupSpec, selects next {@link AppServer} and calls the + * same methods. This process is repeated until tests have run for all {@link AppServer} + * annotations. Tests should start server in setupSpec and stop it in cleanupSpec. + */ +public class AppServerTestRunner extends Sputnik { + private static final Map, AppServer> runningAppServer = + Collections.synchronizedMap(new HashMap<>()); + private final Class testClass; + private final AppServer[] appServers; + + public AppServerTestRunner(Class clazz) throws InitializationError { + super(clazz); + testClass = clazz; + appServers = clazz.getAnnotationsByType(AppServer.class); + if (appServers.length == 0) { + throw new IllegalStateException("Add AppServer or AppServers annotation to test class"); + } + } + + @Override + public void run(RunNotifier notifier) { + // run tests for all app servers + try { + for (AppServer appServer : appServers) { + runningAppServer.put(testClass, appServer); + super.run(notifier); + } + } finally { + runningAppServer.remove(testClass); + } + } + + // expose currently running app server + // used to get current server and jvm version inside the test class + public static AppServer currentAppServer(Class testClass) { + AppServer appServer = runningAppServer.get(testClass); + if (appServer == null) { + throw new IllegalStateException("Test not running for " + testClass); + } + return appServer; + } +} diff --git a/smoke-tests/src/test/java/org/hypertrace/agent/smoketest/AppServers.java b/smoke-tests/src/test/java/org/hypertrace/agent/smoketest/AppServers.java new file mode 100644 index 000000000..788c333b8 --- /dev/null +++ b/smoke-tests/src/test/java/org/hypertrace/agent/smoketest/AppServers.java @@ -0,0 +1,30 @@ +/* + * Copyright The Hypertrace Authors + * + * 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 org.hypertrace.agent.smoketest; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +public @interface AppServers { + AppServer[] value(); +} diff --git a/smoke-tests/src/test/resources/liberty-servlet.xml b/smoke-tests/src/test/resources/liberty-servlet.xml new file mode 100644 index 000000000..8157a50ba --- /dev/null +++ b/smoke-tests/src/test/resources/liberty-servlet.xml @@ -0,0 +1,12 @@ + + + + + servlet-4.0 + + + + + + + \ No newline at end of file diff --git a/smoke-tests/src/test/resources/otel.yaml b/smoke-tests/src/test/resources/otel.yaml new file mode 100644 index 000000000..8d38db49a --- /dev/null +++ b/smoke-tests/src/test/resources/otel.yaml @@ -0,0 +1,34 @@ +extensions: + health_check: + pprof: + endpoint: 0.0.0.0:1777 + zpages: + endpoint: 0.0.0.0:55679 + +receivers: + otlp: + protocols: + grpc: + zipkin: + jaeger: + protocols: + grpc: + +processors: + batch: + +exporters: + logging: + loglevel: debug + otlp: + endpoint: backend:8080 + insecure: true + +service: + pipelines: + traces: + receivers: [otlp, zipkin, jaeger] + processors: [batch] + exporters: [logging, otlp] + + extensions: [health_check, pprof, zpages] diff --git a/testing-common/build.gradle.kts b/testing-common/build.gradle.kts index 4bc82931a..afcde5ad7 100644 --- a/testing-common/build.gradle.kts +++ b/testing-common/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { api("io.opentelemetry:opentelemetry-sdk:${versions["opentelemetry"]}") compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:${versions["opentelemetry"]}-alpha") api("com.squareup.okhttp3:okhttp:4.9.0") + api("com.squareup.okhttp3:logging-interceptor:4.9.0") implementation("io.opentelemetry.javaagent:opentelemetry-javaagent-tooling:${versions["opentelemetry_java_agent"]}") implementation("io.opentelemetry.javaagent:opentelemetry-javaagent-spi:${versions["opentelemetry_java_agent"]}") implementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api:${versions["opentelemetry_java_agent"]}") diff --git a/testing-common/src/main/java/org/hypertrace/agent/testing/OkHttpUtils.java b/testing-common/src/main/java/org/hypertrace/agent/testing/OkHttpUtils.java new file mode 100644 index 000000000..ba0fcf18a --- /dev/null +++ b/testing-common/src/main/java/org/hypertrace/agent/testing/OkHttpUtils.java @@ -0,0 +1,67 @@ +/* + * Copyright The Hypertrace Authors + * + * 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 org.hypertrace.agent.testing; + +import java.util.concurrent.TimeUnit; +import okhttp3.OkHttpClient; +import okhttp3.logging.HttpLoggingInterceptor; +import okhttp3.logging.HttpLoggingInterceptor.Level; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class was moved from groovy to java because groovy kept trying to introspect on the + * OkHttpClient class which contains java 8 only classes, which caused the build to fail for java 7. + */ +public class OkHttpUtils { + + private static final Logger CLIENT_LOGGER = LoggerFactory.getLogger("http-client"); + + static { + ((ch.qos.logback.classic.Logger) CLIENT_LOGGER).setLevel(ch.qos.logback.classic.Level.DEBUG); + } + + private static final HttpLoggingInterceptor LOGGING_INTERCEPTOR = + new HttpLoggingInterceptor( + new HttpLoggingInterceptor.Logger() { + @Override + public void log(String message) { + CLIENT_LOGGER.debug(message); + } + }); + + static { + LOGGING_INTERCEPTOR.setLevel(Level.BASIC); + } + + static OkHttpClient.Builder clientBuilder() { + TimeUnit unit = TimeUnit.MINUTES; + return new OkHttpClient.Builder() + .addInterceptor(LOGGING_INTERCEPTOR) + .connectTimeout(1, unit) + .writeTimeout(1, unit) + .readTimeout(1, unit); + } + + public static OkHttpClient client() { + return client(false); + } + + public static OkHttpClient client(boolean followRedirects) { + return clientBuilder().followRedirects(followRedirects).build(); + } +}