diff --git a/.github/workflows/publish_snapshots.yml b/.github/workflows/publish_snapshots.yml index 81137529..a0d08297 100644 --- a/.github/workflows/publish_snapshots.yml +++ b/.github/workflows/publish_snapshots.yml @@ -36,7 +36,7 @@ jobs: env: OSSRH_USER: ${{ secrets.OSSRH_USER }} OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} - run: ./gradlew -Psigning.gnupg.keyName=CBEF2CF0 -Psigning.gnupg.executable=gpg -Psigning.gnupg.passphrase=${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }} publish + run: ./gradlew -Psigning.gnupg.keyName=${{ secrets.OSSRH_GPG_SECRET_KEY_NAME }} -Psigning.gnupg.executable=gpg -Psigning.gnupg.passphrase=${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }} publish - name: Plugin has SNAPSHOT version run: ./gradlew :radar-commons-gradle:properties | grep '^version:.*-SNAPSHOT$' @@ -45,4 +45,4 @@ jobs: env: OSSRH_USER: ${{ secrets.OSSRH_USER }} OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} - run: ./gradlew -Psigning.gnupg.keyName=CBEF2CF0 -Psigning.gnupg.executable=gpg -Psigning.gnupg.passphrase=${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }} :radar-commons-gradle:publish + run: ./gradlew -Psigning.gnupg.keyName=${{ secrets.OSSRH_GPG_SECRET_KEY_NAME }} -Psigning.gnupg.executable=gpg -Psigning.gnupg.passphrase=${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }} :radar-commons-gradle:publish diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index aa29b78a..f864aefa 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -1,6 +1,6 @@ @Suppress("ConstPropertyName", "MemberVisibilityCanBePrivate") object Versions { - const val project = "1.1.2" + const val project = "1.1.3" object Plugins { const val licenseReport = "2.5" @@ -17,7 +17,7 @@ object Versions { const val slf4j = "2.0.13" const val confluent = "7.6.0" const val kafka = "${confluent}-ce" - const val avro = "1.11.3" + const val avro = "1.12.0" const val jackson = "2.15.3" const val okhttp = "4.12.0" const val junit = "5.10.0" diff --git a/radar-commons-gradle/README.md b/radar-commons-gradle/README.md index a860f24b..c0f674c3 100644 --- a/radar-commons-gradle/README.md +++ b/radar-commons-gradle/README.md @@ -2,6 +2,17 @@ A Gradle plugin to do some common RADAR-base tasks. + +* [radar-commons-gradle](#radar-commons-gradle) + * [Usage](#usage) + * [radar-commons-gradle plugin](#radar-commons-gradle-plugin) + * [radarKotlin extension](#radarkotlin-extension) + * [Enable logging with log4j2](#enable-logging-with-log4j2) + * [Enable monitoring with Sentry](#enable-monitoring-with-sentry) + * [Enable Sentry source context](#enable-sentry-source-context) + * [Customizing Sentry configuration](#customizing-sentry-configuration) + + ## Usage Add the following block to `settings.gradle.kts` to get access to the RADAR-base plugins. @@ -51,6 +62,13 @@ subprojects { kotlinVersion.set(Versions.Plugins.kotlin) // already has a default value junitVersion.set(Versions.junit) // already has a default value ktlintVersion.set(Versions.ktlint) // already has a default value + slf4jVersion.set(Versions.slf4j) // already has a default value + // log4j2Version.set(Versions.log4j2) // setting this will enable log4j2 + // sentryVersion.set(Versions.sentry) // setting this will enable Sentry monitoring + // sentryEnabled.set(false) // setting this to true will enable Sentry monitoring + // sentrySourceContextToken.set("") // setting this will upload the source code context to Sentry + // sentryOrganization.set("radar-base") // already has a default value, only needed when setting 'sentrySourceContextToken' + // sentryProject.set("") // already has a default value, only needed when setting 'sentrySourceContextToken' } // Both values are required to be set to use radar-publishing. @@ -68,3 +86,103 @@ subprojects { } } ``` + +## radar-commons-gradle plugin + +This plugin provides the basics for RADAR-base projects. + +### radarKotlin extension + +#### Enable logging with log4j2 + +By default, no logging implementation is added to projects (only the slf4j interface is included). To enable log4j2, add +the following to your root project configurations: + +```gradle +... +subprojects { + ... + radarKotlin { + log4j2Version.set(Versions.log4j2) + } + ... +} +``` + +#### Enable monitoring with Sentry + +1. Activate log4j2 logging (see [above](#enable-logging-with-log4j2)) +2. Activate Sentry in the _radarKotlin_ extension: + +```gradle +... +subprojects { + ... + radarKotlin { + sentryEnabled.set(true) + } + ... +} +``` + +3. Add a Sentry log Appender to the `log4j2.xml` configuration file to `src/main/resources`. For example: + +```xml + + + + + + + + + + + + + + + + +``` + +4. Set the `SENTRY_DSN` environment variable at runtime to the DSN of your Sentry project. + +#### Enable Sentry source context + +Sentry can be configured to show the source code context of the error. For this to work, source code can be uploaded +to the target sentry organization and project during the build phase. To enable this, set the following values: + +```gradle +... +subprojects { + ... + radarKotlin { + sentrySourceContextToken.set("my-token") + sentryOrganization.set("my-organization") + sentryProject.set("my-project") + } + ... +} +``` + +NOTE: The organization and project must correspond to the values for the monitoring environment in Sentry. The +organization and project are encooded in Sentry OAuth token. + +#### Customizing Sentry configuration + +In a project that uses radar-commons-gradle, the Sentry configuration can be customized by using the _sentry_ extension +like for instance: + +build.gradle.kts + +```gradle +sentry { + debug = true + ... +} +... + +Additional information on config options: https://docs.sentry.io/platforms/java/guides/log4j2/gradle/ diff --git a/radar-commons-gradle/build.gradle.kts b/radar-commons-gradle/build.gradle.kts index d0a488a6..b919b348 100644 --- a/radar-commons-gradle/build.gradle.kts +++ b/radar-commons-gradle/build.gradle.kts @@ -29,7 +29,10 @@ dependencies { implementation("com.github.ben-manes:gradle-versions-plugin:${Versions.gradleVersionsPlugin}") implementation("io.github.gradle-nexus:publish-plugin:${Versions.Plugins.publishPlugin}") implementation("org.jlleitschuh.gradle:ktlint-gradle:${Versions.ktlint}") - implementation("com.github.jk1.dependency-license-report:com.github.jk1.dependency-license-report.gradle.plugin:${Versions.Plugins.licenseReport}") + implementation( + "com.github.jk1.dependency-license-report:com.github.jk1.dependency-license-report.gradle.plugin:${Versions.Plugins.licenseReport}", + ) + implementation("io.sentry.jvm.gradle:io.sentry.jvm.gradle.gradle.plugin:${Versions.sentry}") } gradlePlugin { @@ -69,7 +72,7 @@ tasks.withType { manifest { attributes( "Implementation-Title" to project.name, - "Implementation-Version" to project.version + "Implementation-Version" to project.version, ) } } @@ -135,13 +138,15 @@ publishing { } } -fun Project.propertyOrEnv(propertyName: String, envName: String): String? { - return if (hasProperty(propertyName)) { +fun Project.propertyOrEnv( + propertyName: String, + envName: String, +): String? = + if (hasProperty(propertyName)) { property(propertyName)?.toString() } else { System.getenv(envName) } -} nexusPublishing { this.repositories { @@ -176,7 +181,7 @@ tasks.withType { // They should be copied from the Versions.kt file directly to maintain consistency. @Suppress("ConstPropertyName", "MemberVisibilityCanBePrivate") object Versions { - const val project = "1.1.2" + const val project = "1.1.3" object Plugins { const val licenseReport = "2.5" @@ -192,8 +197,8 @@ object Versions { const val java = 17 const val slf4j = "2.0.13" const val confluent = "7.6.0" - const val kafka = "${confluent}-ce" - const val avro = "1.11.3" + const val kafka = "$confluent-ce" + const val avro = "1.12.0" const val jackson = "2.15.3" const val okhttp = "4.12.0" const val junit = "5.10.0" @@ -209,4 +214,5 @@ object Versions { const val guava = "32.1.1-jre" const val gradleVersionsPlugin = "0.50.0" const val ktlint = "12.0.3" + const val sentry = "4.10.0" } diff --git a/radar-commons-gradle/src/main/kotlin/org/radarbase/gradle/plugin/RadarKotlinPlugin.kt b/radar-commons-gradle/src/main/kotlin/org/radarbase/gradle/plugin/RadarKotlinPlugin.kt index b3c4f059..e6fc4b5d 100644 --- a/radar-commons-gradle/src/main/kotlin/org/radarbase/gradle/plugin/RadarKotlinPlugin.kt +++ b/radar-commons-gradle/src/main/kotlin/org/radarbase/gradle/plugin/RadarKotlinPlugin.kt @@ -1,6 +1,8 @@ package org.radarbase.gradle.plugin import com.github.jk1.license.LicenseReportPlugin +import io.sentry.android.gradle.extensions.SentryPluginExtension +import io.sentry.jvm.gradle.SentryJvmPlugin import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.plugins.ApplicationPlugin @@ -42,6 +44,10 @@ interface RadarKotlinExtension { val log4j2Version: Property val slf4jVersion: Property val ktlintVersion: Property + val sentryEnabled: Property + val sentryOrganization: Property + val sentryProject: Property + val sentrySourceContextToken: Property } class RadarKotlinPlugin : Plugin { @@ -52,12 +58,19 @@ class RadarKotlinPlugin : Plugin { kotlinApiVersion.convention("") junitVersion.convention(Versions.junit) ktlintVersion.convention(Versions.ktlint) - slf4jVersion.convention(Versions.ktlint) + slf4jVersion.convention(Versions.slf4j) + sentryEnabled.convention(false) + sentryOrganization.convention("radar-base") + sentryProject.convention(project.name) + sentrySourceContextToken.convention("") } apply(plugin = "kotlin") apply() + // SentryJvmPlugin will be removed in afterEvaluate when sentryEnabled == false. + apply() + repositories { mavenCentral { mavenContent { @@ -162,6 +175,20 @@ class RadarKotlinPlugin : Plugin { implementation("org.slf4j:slf4j-api:${extension.slf4jVersion.get()}") } } + if (extension.sentryEnabled.get()) { + val sentry = extensions.get("sentry") as SentryPluginExtension + sentry.org.set(extension.sentryOrganization) + sentry.projectName.set(extension.sentryProject) + if (extension.sentrySourceContextToken.isPresent && + extension.sentrySourceContextToken.get().isNotEmpty() + ) { + // Passing the source context token will activate upload of our source code to Sentry. + sentry.includeSourceContext.set(true) + sentry.authToken.set(extension.sentrySourceContextToken) + } + } else { + plugins.removeIf({ it is SentryJvmPlugin }) + } if (extension.log4j2Version.isPresent) { dependencies { val log4j2Version = extension.log4j2Version.get() @@ -171,6 +198,10 @@ class RadarKotlinPlugin : Plugin { runtimeOnly("org.apache.logging.log4j:log4j-slf4j2-impl:$log4j2Version") runtimeOnly("org.apache.logging.log4j:log4j-core:$log4j2Version") runtimeOnly("org.apache.logging.log4j:log4j-jul:$log4j2Version") + if (extension.sentryEnabled.get()) { + val annotationProcessor by configurations + annotationProcessor("org.apache.logging.log4j:log4j-core:$log4j2Version") + } } else { val testRuntimeOnly by configurations testRuntimeOnly("org.apache.logging.log4j:log4j-slf4j2-impl:$log4j2Version") diff --git a/radar-commons-gradle/src/main/kotlin/org/radarbase/gradle/plugin/Versions.kt b/radar-commons-gradle/src/main/kotlin/org/radarbase/gradle/plugin/Versions.kt index fb7e4bb2..40ae0207 100644 --- a/radar-commons-gradle/src/main/kotlin/org/radarbase/gradle/plugin/Versions.kt +++ b/radar-commons-gradle/src/main/kotlin/org/radarbase/gradle/plugin/Versions.kt @@ -6,4 +6,5 @@ object Versions { const val ktlint = "0.50.0" const val java = 11 const val junit = "5.10.0" + const val slf4j = "2.0.16" } diff --git a/radar-commons-kotlin/src/main/kotlin/org/radarbase/ktor/auth/ClientCredentialsConfig.kt b/radar-commons-kotlin/src/main/kotlin/org/radarbase/ktor/auth/ClientCredentialsConfig.kt index b1ba8a72..17797d92 100644 --- a/radar-commons-kotlin/src/main/kotlin/org/radarbase/ktor/auth/ClientCredentialsConfig.kt +++ b/radar-commons-kotlin/src/main/kotlin/org/radarbase/ktor/auth/ClientCredentialsConfig.kt @@ -6,6 +6,8 @@ data class ClientCredentialsConfig( val tokenUrl: String, val clientId: String? = null, val clientSecret: String? = null, + val scope: String? = null, + val audience: String? = null, ) { /** * Fill in the client ID and client secret from environment variables. The variables are diff --git a/radar-commons-kotlin/src/main/kotlin/org/radarbase/ktor/auth/OAuthClientProvider.kt b/radar-commons-kotlin/src/main/kotlin/org/radarbase/ktor/auth/OAuthClientProvider.kt index 9a2fd00b..18a8e8ad 100644 --- a/radar-commons-kotlin/src/main/kotlin/org/radarbase/ktor/auth/OAuthClientProvider.kt +++ b/radar-commons-kotlin/src/main/kotlin/org/radarbase/ktor/auth/OAuthClientProvider.kt @@ -56,6 +56,8 @@ fun Auth.clientCredentials( append("grant_type", "client_credentials") append("client_id", authConfig.clientId) append("client_secret", authConfig.clientSecret) + authConfig.scope?.let { append("scope", it) } + authConfig.audience?.let { append("audience", it) } }, ) { accept(ContentType.Application.Json) diff --git a/radar-commons/src/test/java/org/radarbase/data/AvroDatumEncoderTest.java b/radar-commons/src/test/java/org/radarbase/data/AvroDatumEncoderTest.java index 410ea225..8a110cc0 100644 --- a/radar-commons/src/test/java/org/radarbase/data/AvroDatumEncoderTest.java +++ b/radar-commons/src/test/java/org/radarbase/data/AvroDatumEncoderTest.java @@ -16,10 +16,6 @@ package org.radarbase.data; -import java.io.IOException; -import java.util.Arrays; -import java.util.UUID; -import java.util.concurrent.ThreadLocalRandom; import junit.framework.TestCase; import org.apache.avro.specific.SpecificData; import org.radarbase.topic.AvroTopic; @@ -29,9 +25,22 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; +import java.util.UUID; +import java.util.concurrent.ThreadLocalRandom; + +import static org.junit.Assert.assertArrayEquals; + public class AvroDatumEncoderTest extends TestCase { private static final Logger logger = LoggerFactory.getLogger(AvroDatumEncoderTest.class); + public static String byteArrayToHex(byte[] a) { + StringBuilder sb = new StringBuilder(a.length * 2); + for (byte b : a) + sb.append(String.format("%02x", b & 0xff)); + return sb.toString(); + } + public void testJson() throws IOException { AvroDatumEncoder encoder = new AvroDatumEncoder(SpecificData.get(), false); AvroTopic topic = new AvroTopic<>("keeeeys", ObservationKey.getClassSchema(), EmpaticaE4BloodVolumePulse.getClassSchema(), ObservationKey.class, EmpaticaE4BloodVolumePulse.class); @@ -58,20 +67,13 @@ public void testBinary() throws IOException { byte[] expectedKey = {2, 8, 116, 101, 115, 116, 2, 97, 2, 98}; System.out.println("key: 0x" + byteArrayToHex(key)); System.out.println("expected: 0x" + byteArrayToHex(expectedKey)); - assertTrue(Arrays.equals(expectedKey, key)); + assertArrayEquals(expectedKey, key); byte[] value = valueEncoder.encode(new EmpaticaE4BloodVolumePulse(0d, 0d, 0f)); // 8 bytes, 8 bytes, 4 bytes, all zero byte[] expectedValue = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; System.out.println("value: 0x" + byteArrayToHex(value)); System.out.println("expected: 0x" + byteArrayToHex(expectedValue)); - assertTrue(Arrays.equals(expectedValue, value)); - } - - public static String byteArrayToHex(byte[] a) { - StringBuilder sb = new StringBuilder(a.length * 2); - for(byte b: a) - sb.append(String.format("%02x", b & 0xff)); - return sb.toString(); + assertArrayEquals(expectedValue, value); } public void testSize() throws IOException {