diff --git a/agent/instrumentation/azure-functions/build.gradle.kts b/agent/instrumentation/azure-functions/build.gradle.kts index 0339c4ee49..76f39afea1 100644 --- a/agent/instrumentation/azure-functions/build.gradle.kts +++ b/agent/instrumentation/azure-functions/build.gradle.kts @@ -12,8 +12,10 @@ muzzle { val otelInstrumentationAlphaVersion: String by project dependencies { + compileOnly(project(":agent:agent-bootstrap")) compileOnly(project(":agent:instrumentation:azure-functions-worker-stub")) + testImplementation(project(":agent:agent-bootstrap")) testImplementation(project(":agent:instrumentation:azure-functions-worker-stub")) // TODO remove when start using io.opentelemetry.instrumentation.javaagent-instrumentation plugin diff --git a/agent/instrumentation/micrometer-1.0/build.gradle.kts b/agent/instrumentation/micrometer-1.0/build.gradle.kts index 1b61136bf9..1dc6fca9ef 100644 --- a/agent/instrumentation/micrometer-1.0/build.gradle.kts +++ b/agent/instrumentation/micrometer-1.0/build.gradle.kts @@ -23,10 +23,12 @@ muzzle { val otelInstrumentationAlphaVersion: String by project dependencies { + compileOnly(project(":agent:agent-bootstrap")) compileOnly("io.micrometer:micrometer-core:1.0.0") compileOnly("org.springframework.boot:spring-boot-actuator-autoconfigure:2.2.0.RELEASE") compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-bootstrap:$otelInstrumentationAlphaVersion") + testImplementation(project(":agent:agent-bootstrap")) testImplementation("com.microsoft.azure:azure-spring-boot-metrics-starter:2.2.3") { exclude("io.micrometer", "micrometer-core") } diff --git a/buildSrc/src/main/kotlin/ai.javaagent-instrumentation.gradle.kts b/buildSrc/src/main/kotlin/ai.javaagent-instrumentation.gradle.kts index 286d4cf0c9..6046929129 100644 --- a/buildSrc/src/main/kotlin/ai.javaagent-instrumentation.gradle.kts +++ b/buildSrc/src/main/kotlin/ai.javaagent-instrumentation.gradle.kts @@ -1,70 +1,5 @@ -import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar - plugins { - id("ai.java-conventions") - id("ai.shadow-conventions") - - id("io.opentelemetry.instrumentation.muzzle-generation") - id("io.opentelemetry.instrumentation.muzzle-check") -} - -val otelInstrumentationAlphaVersion: String by project - -val testInstrumentation by configurations.creating - -dependencies { - compileOnly("io.opentelemetry:opentelemetry-sdk") - compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") - compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi") - compileOnly("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api") - compileOnly("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-incubator") - compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api:$otelInstrumentationAlphaVersion") - annotationProcessor("com.google.auto.service:auto-service") - compileOnly("com.google.auto.service:auto-service") - compileOnly(project(":agent:agent-bootstrap")) - - testImplementation("io.opentelemetry.javaagent:opentelemetry-testing-common:$otelInstrumentationAlphaVersion") - // the bootstrap module is provided by the javaagent in the instrumentation test runtime, no need to include it - // (especially when it's not being shaded) - testCompileOnly(project(":agent:agent-bootstrap")) -} - -tasks.named("shadowJar").configure { - configurations = listOf(project.configurations.runtimeClasspath.get(), testInstrumentation) - - archiveFileName.set("agent-testing.jar") -} - -evaluationDependsOn(":agent:agent-for-testing") - -tasks.withType().configureEach { - val shadowJar = tasks.shadowJar.get() - val agentShadowJar = project(":agent:agent-for-testing").tasks.shadowJar.get() - - inputs.file(shadowJar.archiveFile) - - dependsOn(shadowJar) - dependsOn(agentShadowJar) - - jvmArgs("-Dotel.javaagent.debug=true") - jvmArgs("-javaagent:${agentShadowJar.archiveFile.get().asFile.absolutePath}") - jvmArgs("-Dotel.javaagent.experimental.initializer.jar=${shadowJar.archiveFile.get().asFile.absolutePath}") - jvmArgs("-Dotel.javaagent.testing.additional-library-ignores.enabled=false") - jvmArgs("-Dotel.javaagent.testing.fail-on-context-leak=true") - // prevent sporadic gradle deadlocks, see SafeLogger for more details - jvmArgs("-Dotel.javaagent.testing.transform-safe-logging.enabled=true") - // Reduce noise in assertion messages since we don't need to verify this in most tests. We check - // in smoke tests instead. - jvmArgs("-Dotel.javaagent.add-thread-details=false") - // needed for proper GlobalMeterProvider registration - jvmArgs("-Dotel.metrics.exporter=otlp") + id("ai.javaagent-testing") - // The sources are packaged into the testing jar so we need to make sure to exclude from the test - // classpath, which automatically inherits them, to ensure our shaded versions are used. - classpath = classpath.filter { - if (file("$buildDir/resources/main") == it || file("$buildDir/classes/java/main") == it) { - return@filter false - } - return@filter true - } + id("io.opentelemetry.instrumentation.javaagent-instrumentation") } diff --git a/buildSrc/src/main/kotlin/ai.javaagent-testing.gradle.kts b/buildSrc/src/main/kotlin/ai.javaagent-testing.gradle.kts new file mode 100644 index 0000000000..a220214c7f --- /dev/null +++ b/buildSrc/src/main/kotlin/ai.javaagent-testing.gradle.kts @@ -0,0 +1,23 @@ +// mostly copied from otel.javaagent-testing.gradle.kts + +plugins { + id("io.opentelemetry.instrumentation.javaagent-testing") + + id("ai.java-conventions") +} + +evaluationDependsOn(":agent:agent-for-testing") + +dependencies { + annotationProcessor("com.google.auto.service:auto-service") + compileOnly("com.google.auto.service:auto-service") + + testImplementation("org.testcontainers:testcontainers") +} + +configurations.configureEach { + if (name.endsWith("testruntimeclasspath", ignoreCase = true)) { + // Added by agent, don't let Gradle bring it in when running tests. + exclude(module = "javaagent-bootstrap") + } +} diff --git a/buildSrc/src/main/kotlin/io.opentelemetry.instrumentation.base.gradle.kts b/buildSrc/src/main/kotlin/io.opentelemetry.instrumentation.base.gradle.kts new file mode 100644 index 0000000000..64e5dddb6e --- /dev/null +++ b/buildSrc/src/main/kotlin/io.opentelemetry.instrumentation.base.gradle.kts @@ -0,0 +1,179 @@ +// mostly copied from upstream + +/** Common setup for manual instrumentation of libraries and javaagent instrumentation. */ + +plugins { + `java-library` +} + +/** + * We define three dependency configurations to use when adding dependencies to libraries being + * instrumented. + * + * - library: A dependency on the instrumented library. Results in the dependency being added to + * compileOnly and testImplementation. If the build is run with -PtestLatestDeps=true, the + * version when added to testImplementation will be overridden by `+`, the latest version + * possible. For simple libraries without different behavior between versions, it is possible + * to have a single dependency on library only. + * + * - testLibrary: A dependency on a library for testing. This will usually be used to either + * a) use a different version of the library for compilation and testing and b) to add a helper + * that is only required for tests (e.g., library-testing artifact). The dependency will be + * added to testImplementation and will have a version of `+` when testing latest deps as + * described above. + * + * - latestDepTestLibrary: A dependency on a library for testing when testing of latest dependency + * version is enabled. This dependency will be added as-is to testImplementation, but only if + * -PtestLatestDeps=true. The version will not be modified but it will be given highest + * precedence. Use this to restrict the latest version dependency from the default `+`, for + * example to restrict to just a major version by specifying `2.+`. + */ + +val testLatestDeps = gradle.startParameter.projectProperties["testLatestDeps"] == "true" +extra["testLatestDeps"] = testLatestDeps + +@CacheableRule +abstract class TestLatestDepsRule : ComponentMetadataRule { + override fun execute(context: ComponentMetadataContext) { + val version = context.details.id.version + if (version.contains("-alpha", true) + || version.contains("-beta", true) + || version.contains("-rc", true) + || version.contains(".rc", true) + || version.contains("-m", true) // e.g. spring milestones are published to grails repo + || version.contains(".m", true) // e.g. lettuce + || version.contains(".alpha", true) // e.g. netty + || version.contains(".beta", true) // e.g. hibernate + || version.contains(".cr", true) // e.g. hibernate + || version.endsWith("-nf-execution") // graphql + || GIT_SHA_PATTERN.matches(version) // graphql + || DATETIME_PATTERN.matches(version) // graphql + ) { + context.details.status = "milestone" + } + } + + companion object { + private val GIT_SHA_PATTERN = Regex("^.*-[0-9a-f]{7,}$") + private val DATETIME_PATTERN = Regex("^\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}.*$") + } +} + +configurations { + val library by creating { + isCanBeResolved = false + isCanBeConsumed = false + } + val testLibrary by creating { + isCanBeResolved = false + isCanBeConsumed = false + } + val latestDepTestLibrary by creating { + isCanBeResolved = false + isCanBeConsumed = false + } + + val testImplementation by getting + + listOf(library, testLibrary).forEach { configuration -> + // We use whenObjectAdded and copy into the real configurations instead of extension to allow + // mutating the version for latest dep tests. + configuration.dependencies.whenObjectAdded { + val dep = copy() + if (testLatestDeps) { + (dep as ExternalDependency).version { + require("latest.release") + } + } + testImplementation.dependencies.add(dep) + } + } + if (testLatestDeps) { + dependencies { + components { + all() + } + } + + latestDepTestLibrary.dependencies.whenObjectAdded { + val dep = copy() + val declaredVersion = dep.version + if (declaredVersion != null) { + (dep as ExternalDependency).version { + strictly(declaredVersion) + } + } + testImplementation.dependencies.add(dep) + } + } + named("compileOnly") { + extendsFrom(library) + } +} + +if (testLatestDeps) { + afterEvaluate { + tasks { + withType().configureEach { + with(options) { + // We may use methods that are deprecated in future versions, we check lint on the normal + // build and don't need this for testLatestDeps. + compilerArgs.add("-Xlint:-deprecation") + } + } + } + + if (tasks.names.contains("latestDepTest")) { + val latestDepTest by tasks.existing + tasks.named("test").configure { + dependsOn(latestDepTest) + } + } + } +} else { + afterEvaluate { + // Disable compiling latest dep tests for non latest dep builds in CI. This is needed to avoid + // breaking build because of a new library version which could force backporting latest dep + // fixes to release branches. + // This is only needed for modules where base version and latest dep tests use a different + // source directory. + var latestDepCompileTaskNames = arrayOf("compileLatestDepTestJava", "compileLatestDepTestGroovy", "compileLatestDepTestScala") + for (compileTaskName in latestDepCompileTaskNames) { + if (tasks.names.contains(compileTaskName)) { + tasks.named(compileTaskName).configure { + enabled = false + } + } + } + } +} + +tasks { + val generateInstrumentationVersionFile by registering { + val name = computeInstrumentationName() + val version = project.version as String + inputs.property("instrumentation.name", name) + inputs.property("instrumentation.version", version) + + val propertiesDir = layout.buildDirectory.dir("generated/instrumentationVersion/META-INF/io/opentelemetry/instrumentation/") + outputs.dir(propertiesDir) + + doLast { + File(propertiesDir.get().asFile, "$name.properties").writeText("version=$version") + } + } +} + +fun computeInstrumentationName(): String { + val name = when (projectDir.name) { + "javaagent", "library", "library-autoconfigure" -> projectDir.parentFile.name + else -> project.name + } + return "io.opentelemetry.$name" +} + +sourceSets { + main { + output.dir("build/generated/instrumentationVersion", "builtBy" to "generateInstrumentationVersionFile") + } +} diff --git a/buildSrc/src/main/kotlin/io.opentelemetry.instrumentation.javaagent-instrumentation.gradle.kts b/buildSrc/src/main/kotlin/io.opentelemetry.instrumentation.javaagent-instrumentation.gradle.kts new file mode 100644 index 0000000000..e701304634 --- /dev/null +++ b/buildSrc/src/main/kotlin/io.opentelemetry.instrumentation.javaagent-instrumentation.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("io.opentelemetry.instrumentation.javaagent-testing") + id("io.opentelemetry.instrumentation.muzzle-check") + id("io.opentelemetry.instrumentation.muzzle-generation") +} + +dependencies { + add("muzzleBootstrap", "io.opentelemetry.instrumentation:opentelemetry-instrumentation-api") + add("muzzleBootstrap", "io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-incubator") + add("muzzleBootstrap", "io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations-support") + add("muzzleTooling", "io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api") + add("muzzleTooling", "io.opentelemetry.javaagent:opentelemetry-javaagent-tooling") + + /* + Dependencies added to this configuration will be found by the muzzle gradle plugin during code + generation phase. These classes become part of the code that plugin inspects and traverses during + references collection phase. + */ + add("codegen", "io.opentelemetry.javaagent:opentelemetry-javaagent-tooling") +} diff --git a/buildSrc/src/main/kotlin/io.opentelemetry.instrumentation.javaagent-shadowing.gradle.kts b/buildSrc/src/main/kotlin/io.opentelemetry.instrumentation.javaagent-shadowing.gradle.kts new file mode 100644 index 0000000000..5c04a66377 --- /dev/null +++ b/buildSrc/src/main/kotlin/io.opentelemetry.instrumentation.javaagent-shadowing.gradle.kts @@ -0,0 +1,47 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + +plugins { + id("com.gradleup.shadow") +} + +// NOTE: any modifications below should also be made in +// io.opentelemetry.instrumentation.muzzle-check.gradle.kts +tasks.withType().configureEach { + mergeServiceFiles() + // Merge any AWS SDK service files that may be present (too bad they didn't just use normal + // service loader...) + mergeServiceFiles("software/amazon/awssdk/global/handlers") + + exclude("**/module-info.class") + + // rewrite dependencies calling Logger.getLogger + relocate("java.util.logging.Logger", "io.opentelemetry.javaagent.bootstrap.PatchLogger") + + if (project.findProperty("disableShadowRelocate") != "true") { + // prevents conflict with library instrumentation, since these classes live in the bootstrap class loader + relocate("io.opentelemetry.instrumentation", "io.opentelemetry.javaagent.shaded.instrumentation") { + // Exclude resource providers since they live in the agent class loader + exclude("io.opentelemetry.instrumentation.resources.*") + exclude("io.opentelemetry.instrumentation.spring.resources.*") + } + + // relocate(OpenTelemetry API) since these classes live in the bootstrap class loader + relocate("io.opentelemetry.api", "io.opentelemetry.javaagent.shaded.io.opentelemetry.api") + relocate("io.opentelemetry.semconv", "io.opentelemetry.javaagent.shaded.io.opentelemetry.semconv") + relocate("io.opentelemetry.context", "io.opentelemetry.javaagent.shaded.io.opentelemetry.context") + relocate("io.opentelemetry.common", "io.opentelemetry.javaagent.shaded.io.opentelemetry.common") + } + + // relocate(the OpenTelemetry extensions that are used by instrumentation modules) + // these extensions live in the AgentClassLoader, and are injected into the user's class loader + // by the instrumentation modules that use them + relocate("io.opentelemetry.contrib.awsxray", "io.opentelemetry.javaagent.shaded.io.opentelemetry.contrib.awsxray") + relocate("io.opentelemetry.extension.kotlin", "io.opentelemetry.javaagent.shaded.io.opentelemetry.extension.kotlin") + + // this is for instrumentation of opentelemetry-api and opentelemetry-instrumentation-api + relocate("application.io.opentelemetry", "io.opentelemetry") + relocate("application.io.opentelemetry.instrumentation.api", "io.opentelemetry.instrumentation.api") + + // this is for instrumentation on java.util.logging (since java.util.logging itself is shaded above) + relocate("application.java.util.logging", "java.util.logging") +} diff --git a/buildSrc/src/main/kotlin/io.opentelemetry.instrumentation.javaagent-testing.gradle.kts b/buildSrc/src/main/kotlin/io.opentelemetry.instrumentation.javaagent-testing.gradle.kts new file mode 100644 index 0000000000..b00652f7d5 --- /dev/null +++ b/buildSrc/src/main/kotlin/io.opentelemetry.instrumentation.javaagent-testing.gradle.kts @@ -0,0 +1,170 @@ +plugins { + `java-library` + + id("io.opentelemetry.instrumentation.base") + id("io.opentelemetry.instrumentation.muzzle-generation") + id("io.opentelemetry.instrumentation.javaagent-shadowing") +} + +dependencies { + /* + Dependencies added to this configuration will be found by the muzzle gradle plugin during code + generation phase. These classes become part of the code that plugin inspects and traverses during + references collection phase. + */ + add("codegen", "io.opentelemetry.javaagent:opentelemetry-javaagent-tooling") +} + +dependencies { + // Integration tests may need to define custom instrumentation modules so we include the standard + // instrumentation infrastructure for testing too. + compileOnly("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api") + compileOnly("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api-incubator") + compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-bootstrap") + // Apply common dependencies for instrumentation. + compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api") { + // OpenTelemetry SDK is not needed for compilation + exclude(group = "io.opentelemetry", module = "opentelemetry-sdk") + } + compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-tooling") { + // OpenTelemetry SDK is not needed for compilation + exclude(group = "io.opentelemetry", module = "opentelemetry-sdk") + } + + // Used by byte-buddy but not brought in as a transitive dependency + compileOnly("com.google.code.findbugs:annotations") +} + +testing { + suites.withType(JvmTestSuite::class).configureEach { + dependencies { + implementation("io.opentelemetry.javaagent:opentelemetry-testing-common") + } + } +} + +val testInstrumentation by configurations.creating { + isCanBeConsumed = false + isCanBeResolved = true +} + +tasks.shadowJar { + configurations = listOf(project.configurations.runtimeClasspath.get(), testInstrumentation) + + archiveFileName.set("agent-testing.jar") +} + +val agentForTesting by configurations.creating { + isCanBeConsumed = false + isCanBeResolved = true +} + +dependencies { + agentForTesting("io.opentelemetry.javaagent:opentelemetry-agent-for-testing") +} + +class JavaagentTestArgumentsProvider( + @InputFile + @PathSensitive(PathSensitivity.RELATIVE) + val agentShadowJar: File, + + @InputFile + @PathSensitive(PathSensitivity.RELATIVE) + val shadowJar: File, +) : CommandLineArgumentProvider { + override fun asArguments(): Iterable = listOf( + "-Dotel.javaagent.debug=true", + "-javaagent:${agentShadowJar.absolutePath}", + // make the path to the javaagent available to tests + "-Dotel.javaagent.testing.javaagent-jar-path=${agentShadowJar.absolutePath}", + "-Dotel.javaagent.experimental.initializer.jar=${shadowJar.absolutePath}", + "-Dotel.javaagent.testing.additional-library-ignores.enabled=false", + "-Dotel.javaagent.testing.fail-on-context-leak=${findProperty("failOnContextLeak") != false}", + // prevent sporadic gradle deadlocks, see SafeLogger for more details + "-Dotel.javaagent.testing.transform-safe-logging.enabled=true", + // Reduce noise in assertion messages since we don't need to verify this in most tests. We check + // in smoke tests instead. + "-Dotel.javaagent.add-thread-details=false", + "-Dotel.javaagent.experimental.indy=${findProperty("testIndy") == "true"}", + // suppress repeated logging of "No metric data to export - skipping export." + // since PeriodicMetricReader is configured with a short interval + "-Dio.opentelemetry.javaagent.slf4j.simpleLogger.log.io.opentelemetry.sdk.metrics.export.PeriodicMetricReader=INFO", + // suppress a couple of verbose ClassNotFoundException stack traces logged at debug level + "-Dio.opentelemetry.javaagent.slf4j.simpleLogger.log.io.grpc.internal.ServerImplBuilder=INFO", + "-Dio.opentelemetry.javaagent.slf4j.simpleLogger.log.io.grpc.internal.ManagedChannelImplBuilder=INFO", + "-Dio.opentelemetry.javaagent.slf4j.simpleLogger.log.io.perfmark.PerfMark=INFO", + "-Dio.opentelemetry.javaagent.slf4j.simpleLogger.log.io.grpc.Context=INFO", + "-Dotel.java.experimental.span-attributes.copy-from-baggage.include=test-baggage-key-1,test-baggage-key-2" + ) +} + +// need to run this after evaluate because testSets plugin adds new test tasks +afterEvaluate { + tasks.withType().configureEach { + val shadowJar = tasks.shadowJar.get() + val agentShadowJar = agentForTesting.resolve().first() + + dependsOn(shadowJar) + // TODO: Figure out why dependsOn override is still needed in otel.javaagent-testing despite + // this dependency. + dependsOn(agentForTesting.buildDependencies) + + jvmArgumentProviders.add( + JavaagentTestArgumentsProvider( + agentShadowJar, + shadowJar.archiveFile.get().asFile + ) + ) + + // We do fine-grained filtering of the classpath of this codebase's sources since Gradle's + // configurations will include transitive dependencies as well, which tests do often need. + classpath = classpath.filter { + if (file(layout.buildDirectory.dir("resources/main")).equals(it) || file( + layout.buildDirectory.dir( + "classes/java/main" + ) + ).equals(it) + ) { + // The sources are packaged into the testing jar, so we need to exclude them from the test + // classpath, which automatically inherits them, to ensure our shaded versions are used. + return@filter false + } + + // TODO: Better not to have this naming constraint, we can likely use plugin identification + // instead. + + val lib = it.absoluteFile + if (lib.name.startsWith("opentelemetry-javaagent-")) { + // These dependencies are packaged into the testing jar, so we need to exclude them from the test + // classpath, which automatically inherits them, to ensure our shaded versions are used. + return@filter false + } + if (lib.name.startsWith("opentelemetry-") && lib.name.contains("-autoconfigure-")) { + // These dependencies should not be on the test classpath, because they will auto-instrument + // the library and the tests could pass even if the javaagent instrumentation fails to apply + return@filter false + } + return@filter true + } + } +} + +// shadowJar is only used for creating a jar for testing, but the shadow plugin automatically adds +// it to a project's published Java component. Skip it if publishing is configured for this +// project. +plugins.withId("maven-publish") { + configure { + (components["java"] as AdhocComponentWithVariants).run { + withVariantsFromConfiguration(configurations["shadowRuntimeElements"]) { + skip() + } + } + } +} + +configurations.configureEach { + if (name.endsWith("testruntimeclasspath", ignoreCase = true)) { + // Added by agent, don't let Gradle bring it in when running tests. + exclude("io.opentelemetry.javaagent", "opentelemetry-javaagent-bootstrap") + } +} diff --git a/dependencyManagement/build.gradle.kts b/dependencyManagement/build.gradle.kts index 14dc2f9b24..e28241574d 100644 --- a/dependencyManagement/build.gradle.kts +++ b/dependencyManagement/build.gradle.kts @@ -69,6 +69,7 @@ val DEPENDENCIES = listOf( "io.opentelemetry.contrib:opentelemetry-jfr-connection:${otelContribVersion}-alpha", "io.opentelemetry.contrib:opentelemetry-runtime-attach-core:${otelContribVersion}-alpha", "com.google.code.findbugs:jsr305:3.0.2", + "com.google.code.findbugs:annotations:3.0.1", "com.github.spotbugs:spotbugs-annotations:4.9.3" )