From 085996ed9bf05cd93ba691e9e39b0bc58e5ffe2e Mon Sep 17 00:00:00 2001 From: Oren Ben-Meir <46640034+obenkenobi@users.noreply.github.com> Date: Mon, 4 Mar 2024 16:36:08 -0500 Subject: [PATCH] Support Zio 2 (#1778) * add zio 2 api and instrumentation --- .github/workflows/GHA-Functional-Tests.yaml | 2 +- .../workflows/GHA-Scala-Functional-Tests.yaml | 8 +- .github/workflows/GHA-Unit-Tests.yaml | 12 +- .github/workflows/publish_main_snapshot.yml | 4 +- .github/workflows/publish_release.yml | 4 +- .../newrelic-scala-zio2-api/README.md | 3 + .../newrelic-scala-zio2-api/build.gradle | 21 ++ .../scala/com/newrelic/zio2/api/Util.scala | 24 ++ .../api/ZIOTraceOps_Instrumentation.scala | 11 + instrumentation/zio-2/build.gradle | 25 ++ .../main/scala/zio/TokenAwareRunnable.java | 42 +++ .../zio-2/src/main/scala/zio/Utils.java | 60 +++++ .../zio/ZIOExecutor_Instrumentation.java | 21 ++ .../zio/ZIOScheduler_Instrumentation.java | 26 ++ instrumentation/zio/build.gradle | 2 +- .../java/zio/internal/TokenAwareRunnable.java | 6 +- .../zio/src/main/java/zio/internal/Utils.java | 1 + newrelic-scala-zio2-api/build.gradle.kts | 92 +++++++ .../com/newrelic/zio2/api/TraceOps.scala | 161 ++++++++++++ .../newrelic/zio2/api/ZIO2TraceOpsTests.scala | 242 ++++++++++++++++++ settings.gradle | 3 + 21 files changed, 750 insertions(+), 20 deletions(-) create mode 100644 instrumentation/newrelic-scala-zio2-api/README.md create mode 100644 instrumentation/newrelic-scala-zio2-api/build.gradle create mode 100644 instrumentation/newrelic-scala-zio2-api/src/main/scala/com/newrelic/zio2/api/Util.scala create mode 100644 instrumentation/newrelic-scala-zio2-api/src/main/scala/com/newrelic/zio2/api/ZIOTraceOps_Instrumentation.scala create mode 100644 instrumentation/zio-2/build.gradle create mode 100644 instrumentation/zio-2/src/main/scala/zio/TokenAwareRunnable.java create mode 100644 instrumentation/zio-2/src/main/scala/zio/Utils.java create mode 100644 instrumentation/zio-2/src/main/scala/zio/ZIOExecutor_Instrumentation.java create mode 100644 instrumentation/zio-2/src/main/scala/zio/ZIOScheduler_Instrumentation.java create mode 100644 newrelic-scala-zio2-api/build.gradle.kts create mode 100644 newrelic-scala-zio2-api/src/main/scala/com/newrelic/zio2/api/TraceOps.scala create mode 100644 newrelic-scala-zio2-api/src/test/scala/com/newrelic/zio2/api/ZIO2TraceOpsTests.scala diff --git a/.github/workflows/GHA-Functional-Tests.yaml b/.github/workflows/GHA-Functional-Tests.yaml index c8f786c123..06a0afecc8 100644 --- a/.github/workflows/GHA-Functional-Tests.yaml +++ b/.github/workflows/GHA-Functional-Tests.yaml @@ -73,7 +73,7 @@ jobs: with: name: functional-tests-results-java-${{ matrix.java-version }} # The regex for the path below will capture functional test HTML reports generated by gradle for all - # related modules: (functional_test, newrelic-scala-api, newrelic-scala-cats-api, :newrelic-cats-effect3-api, newrelic-scala-zio-api). + # related modules: (functional_test, newrelic-scala-api, newrelic-scala-cats-api, :newrelic-cats-effect3-api, newrelic-scala-zio-api, newrelic-scala-zio2-api). # However, it's critical that the previous build step does a ./gradlew clean or the regex will capture test reports # that were leftover in unrelated modules for unit and instrumentation tests. path: | diff --git a/.github/workflows/GHA-Scala-Functional-Tests.yaml b/.github/workflows/GHA-Scala-Functional-Tests.yaml index 6f8740efe5..025fc653ef 100644 --- a/.github/workflows/GHA-Scala-Functional-Tests.yaml +++ b/.github/workflows/GHA-Scala-Functional-Tests.yaml @@ -52,7 +52,7 @@ jobs: timeout-minutes: 25 run: | # Removed ":newrelic-cats-effect3-api:test" temporarily - ./gradlew $GRADLE_OPTIONS :newrelic-scala3-api:test :newrelic-scala-api:test :newrelic-scala-cats-api:test :newrelic-scala-zio-api:test :newrelic-scala-monix-api:test -PincludeScala -Ptest${{ matrix.java-version }} --continue + ./gradlew $GRADLE_OPTIONS :newrelic-scala3-api:test :newrelic-scala-api:test :newrelic-scala-cats-api:test :newrelic-scala-zio-api:test :newrelic-scala-zio2-api:test :newrelic-scala-monix-api:test -PincludeScala -Ptest${{ matrix.java-version }} --continue - name: Run functional tests against version defined in ${{ matrix.java-version }} (attempt 2) id: run_tests_2 @@ -61,14 +61,14 @@ jobs: timeout-minutes: 25 run: | # Removed ":newrelic-cats-effect3-api:test" temporarily - ./gradlew $GRADLE_OPTIONS :newrelic-scala3-api:test :newrelic-scala-api:test :newrelic-scala-cats-api:test :newrelic-scala-zio-api:test :newrelic-scala-monix-api:test -PincludeScala -Ptest${{ matrix.java-version }} --continue + ./gradlew $GRADLE_OPTIONS :newrelic-scala3-api:test :newrelic-scala-api:test :newrelic-scala-cats-api:test :newrelic-scala-zio-api:test :newrelic-scala-zio2-api:test :newrelic-scala-monix-api:test -PincludeScala -Ptest${{ matrix.java-version }} --continue - name: Run functional tests against version defined in ${{ matrix.java-version }} (attempt 3) if: steps.run_tests_2.outcome == 'failure' timeout-minutes: 25 run: | # Removed ":newrelic-cats-effect3-api:test" temporarily - ./gradlew $GRADLE_OPTIONS :newrelic-scala3-api:test :newrelic-scala-api:test :newrelic-scala-cats-api:test :newrelic-scala-zio-api:test :newrelic-scala-monix-api:test -PincludeScala -Ptest${{ matrix.java-version }} --continue + ./gradlew $GRADLE_OPTIONS :newrelic-scala3-api:test :newrelic-scala-api:test :newrelic-scala-cats-api:test :newrelic-scala-zio-api:test :newrelic-scala-zio2-api:test :newrelic-scala-monix-api:test -PincludeScala -Ptest${{ matrix.java-version }} --continue - name: Capture Jacoco reports if: matrix.java-version == '11' @@ -85,7 +85,7 @@ jobs: with: name: functional-tests-results-java-${{ matrix.java-version }} # The regex for the path below will capture functional test HTML reports generated by gradle for all - # related modules: (functional_test, newrelic-scala-api, newrelic-scala-cats-api, :newrelic-cats-effect3-api, newrelic-scala-zio-api). + # related modules: (functional_test, newrelic-scala-api, newrelic-scala-cats-api, :newrelic-cats-effect3-api, newrelic-scala-zio-api, newrelic-scala-zio2-api). # However, it's critical that the previous build step does a ./gradlew clean or the regex will capture test reports # that were leftover in unrelated modules for unit and instrumentation tests. path: | diff --git a/.github/workflows/GHA-Unit-Tests.yaml b/.github/workflows/GHA-Unit-Tests.yaml index e3663c9c52..4572952bed 100644 --- a/.github/workflows/GHA-Unit-Tests.yaml +++ b/.github/workflows/GHA-Unit-Tests.yaml @@ -47,19 +47,19 @@ jobs: id: run_tests_1 continue-on-error: true timeout-minutes: 20 - run: ./gradlew $GRADLE_OPTIONS test -x :functional_test:test -x :newrelic-scala3-api:test -x :newrelic-scala-api:test -x :newrelic-scala-cats-api:test -x :newrelic-cats-effect3-api:test -x :newrelic-scala-monix-api:test -x :newrelic-scala-zio-api:test -Ptest8 -PnoInstrumentation -PnonForkedTests --continue + run: ./gradlew $GRADLE_OPTIONS test -x :functional_test:test -x :newrelic-scala3-api:test -x :newrelic-scala-api:test -x :newrelic-scala-cats-api:test -x :newrelic-cats-effect3-api:test -x :newrelic-scala-monix-api:test -x :newrelic-scala-zio-api:test -x :newrelic-scala-zio2-api:test -Ptest8 -PnoInstrumentation -PnonForkedTests --continue - name: Run unit tests that do not require a forked JVM (attempt 2) id: run_tests_2 continue-on-error: true timeout-minutes: 20 if: steps.run_tests_1.outcome == 'failure' - run: ./gradlew $GRADLE_OPTIONS test -x :functional_test:test -x :newrelic-scala3-api:test -x :newrelic-scala-api:test -x :newrelic-scala-cats-api:test -x :newrelic-cats-effect3-api:test -x :newrelic-scala-monix-api:test -x :newrelic-scala-zio-api:test -Ptest8 -PnoInstrumentation -PnonForkedTests --continue + run: ./gradlew $GRADLE_OPTIONS test -x :functional_test:test -x :newrelic-scala3-api:test -x :newrelic-scala-api:test -x :newrelic-scala-cats-api:test -x :newrelic-cats-effect3-api:test -x :newrelic-scala-monix-api:test -x :newrelic-scala-zio-api:test -x :newrelic-scala-zio2-api:test -Ptest8 -PnoInstrumentation -PnonForkedTests --continue - name: Run unit tests that do not require a forked JVM (attempt 3) timeout-minutes: 20 if: steps.run_tests_2.outcome == 'failure' - run: ./gradlew $GRADLE_OPTIONS test -x :functional_test:test -x :newrelic-scala3-api:test -x :newrelic-scala-api:test -x :newrelic-scala-cats-api:test -x :newrelic-cats-effect3-api:test -x :newrelic-scala-monix-api:test -x :newrelic-scala-zio-api:test -Ptest8 -PnoInstrumentation -PnonForkedTests --continue + run: ./gradlew $GRADLE_OPTIONS test -x :functional_test:test -x :newrelic-scala3-api:test -x :newrelic-scala-api:test -x :newrelic-scala-cats-api:test -x :newrelic-cats-effect3-api:test -x :newrelic-scala-monix-api:test -x :newrelic-scala-zio-api:test -x :newrelic-scala-zio2-api:test -Ptest8 -PnoInstrumentation -PnonForkedTests --continue - name: Upload coverage to Codecov if: matrix.java-version == '17' @@ -94,20 +94,20 @@ jobs: id: run_forked_tests_1 continue-on-error: true timeout-minutes: 20 - run: ./gradlew $GRADLE_OPTIONS test -x :functional_test:test -x :newrelic-scala3-api:test -x :newrelic-scala-api:test -x :newrelic-scala-cats-api:test -x :newrelic-cats-effect3-api:test -x :newrelic-scala-monix-api:test -x :newrelic-scala-zio-api:test -Ptest8 -PnoInstrumentation -PforkedTests --continue + run: ./gradlew $GRADLE_OPTIONS test -x :functional_test:test -x :newrelic-scala3-api:test -x :newrelic-scala-api:test -x :newrelic-scala-cats-api:test -x :newrelic-cats-effect3-api:test -x :newrelic-scala-monix-api:test -x :newrelic-scala-zio-api:test -x :newrelic-scala-zio2-api:test -Ptest8 -PnoInstrumentation -PforkedTests --continue - name: Run unit tests that require a forked JVM (attempt 2) id: run_forked_tests_2 continue-on-error: true timeout-minutes: 20 if: steps.run_forked_tests_1.outcome == 'failure' - run: ./gradlew $GRADLE_OPTIONS test -x :functional_test:test -x :newrelic-scala3-api:test -x :newrelic-scala-api:test -x :newrelic-scala-cats-api:test -x :newrelic-cats-effect3-api:test -x :newrelic-scala-monix-api:test -x :newrelic-scala-zio-api:test -Ptest8 -PnoInstrumentation -PforkedTests --continue + run: ./gradlew $GRADLE_OPTIONS test -x :functional_test:test -x :newrelic-scala3-api:test -x :newrelic-scala-api:test -x :newrelic-scala-cats-api:test -x :newrelic-cats-effect3-api:test -x :newrelic-scala-monix-api:test -x :newrelic-scala-zio-api:test -x :newrelic-scala-zio2-api:test -Ptest8 -PnoInstrumentation -PforkedTests --continue - name: Run unit tests that require a forked JVM (attempt 3) id: run_forked_tests_3 timeout-minutes: 20 if: steps.run_forked_tests_2.outcome == 'failure' - run: ./gradlew $GRADLE_OPTIONS test -x :functional_test:test -x :newrelic-scala3-api:test -x :newrelic-scala-api:test -x :newrelic-scala-cats-api:test -x :newrelic-cats-effect3-api:test -x :newrelic-scala-monix-api:test -x :newrelic-scala-zio-api:test -Ptest8 -PnoInstrumentation -PforkedTests --continue + run: ./gradlew $GRADLE_OPTIONS test -x :functional_test:test -x :newrelic-scala3-api:test -x :newrelic-scala-api:test -x :newrelic-scala-cats-api:test -x :newrelic-cats-effect3-api:test -x :newrelic-scala-monix-api:test -x :newrelic-scala-zio-api:test -x :newrelic-scala-zio2-api:test -Ptest8 -PnoInstrumentation -PforkedTests --continue - name: Upload coverage to Codecov if: matrix.java-version == '17' diff --git a/.github/workflows/publish_main_snapshot.yml b/.github/workflows/publish_main_snapshot.yml index 5774c6ea70..395b0ae4f0 100644 --- a/.github/workflows/publish_main_snapshot.yml +++ b/.github/workflows/publish_main_snapshot.yml @@ -27,7 +27,7 @@ jobs: ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SIGNING_KEY }} ORG_GRADLE_PROJECT_signingKeyId: ${{ secrets.SIGNING_KEY_ID }} ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SIGNING_PASSWORD }} - run: ./gradlew $GRADLE_OPTIONS publish -x :newrelic-scala3-api:publish -x :newrelic-scala-api:publish -x :newrelic-scala-cats-api:publish -x :newrelic-cats-effect3-api:publish -x :newrelic-scala-zio-api:publish -x :agent-bridge:publish -x :agent-bridge-datastore:publish -x :newrelic-weaver:publish -x :newrelic-weaver-api:publish -x :newrelic-weaver-scala:publish -x :newrelic-weaver-scala-api:publish -x :newrelic-opentelemetry-agent-extension:publish + run: ./gradlew $GRADLE_OPTIONS publish -x :newrelic-scala3-api:publish -x :newrelic-scala-api:publish -x :newrelic-scala-cats-api:publish -x :newrelic-cats-effect3-api:publish -x :newrelic-scala-zio-api:publish -x :newrelic-scala-zio2-api:publish -x :agent-bridge:publish -x :agent-bridge-datastore:publish -x :newrelic-weaver:publish -x :newrelic-weaver-api:publish -x :newrelic-weaver-scala:publish -x :newrelic-weaver-scala-api:publish -x :newrelic-opentelemetry-agent-extension:publish - name: Publish snapshot scala apis env: SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} @@ -35,7 +35,7 @@ jobs: ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SIGNING_KEY }} ORG_GRADLE_PROJECT_signingKeyId: ${{ secrets.SIGNING_KEY_ID }} ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SIGNING_PASSWORD }} - run: ./gradlew $GRADLE_OPTIONS :newrelic-scala3-api:publish :newrelic-scala-api:publish :newrelic-scala-cats-api:publish :newrelic-cats-effect3-api:publish :newrelic-scala-zio-api:publish + run: ./gradlew $GRADLE_OPTIONS :newrelic-scala3-api:publish :newrelic-scala-api:publish :newrelic-scala-cats-api:publish :newrelic-cats-effect3-api:publish :newrelic-scala-zio-api:publish :newrelic-scala-zio2-api:publish - name: Publish snapshot apis for Security agent env: SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml index 943cd30828..7015384ecd 100644 --- a/.github/workflows/publish_release.yml +++ b/.github/workflows/publish_release.yml @@ -28,7 +28,7 @@ jobs: ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SIGNING_KEY }} ORG_GRADLE_PROJECT_signingKeyId: ${{ secrets.SIGNING_KEY_ID }} ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SIGNING_PASSWORD }} - run: ./gradlew $GRADLE_OPTIONS publish -x :newrelic-scala3-api:publish -x :newrelic-scala-api:publish -x :newrelic-scala-cats-api:publish -x :newrelic-cats-effect3-api:publish -x :newrelic-scala-zio-api:publish -x :agent-bridge:publish -x :agent-bridge-datastore:publish -x :newrelic-weaver:publish -x :newrelic-weaver-api:publish -x :newrelic-weaver-scala:publish -x :newrelic-weaver-scala-api:publish -x :newrelic-opentelemetry-agent-extension:publish -Prelease=true + run: ./gradlew $GRADLE_OPTIONS publish -x :newrelic-scala3-api:publish -x :newrelic-scala-api:publish -x :newrelic-scala-cats-api:publish -x :newrelic-cats-effect3-api:publish -x :newrelic-scala-zio-api:publish -x :newrelic-scala-zio2-api:publish -x :agent-bridge:publish -x :agent-bridge-datastore:publish -x :newrelic-weaver:publish -x :newrelic-weaver-api:publish -x :newrelic-weaver-scala:publish -x :newrelic-weaver-scala-api:publish -x :newrelic-opentelemetry-agent-extension:publish -Prelease=true - name: Publish release scala apis env: SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} @@ -36,7 +36,7 @@ jobs: ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SIGNING_KEY }} ORG_GRADLE_PROJECT_signingKeyId: ${{ secrets.SIGNING_KEY_ID }} ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SIGNING_PASSWORD }} - run: ./gradlew $GRADLE_OPTIONS :newrelic-scala3-api:publish :newrelic-scala-api:publish :newrelic-scala-cats-api:publish :newrelic-cats-effect3-api:publish :newrelic-scala-zio-api:publish -Prelease=true + run: ./gradlew $GRADLE_OPTIONS :newrelic-scala3-api:publish :newrelic-scala-api:publish :newrelic-scala-cats-api:publish :newrelic-cats-effect3-api:publish :newrelic-scala-zio-api:publish :newrelic-scala-zio2-api:publish -Prelease=true - name: Publish apis for Security agent env: SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} diff --git a/instrumentation/newrelic-scala-zio2-api/README.md b/instrumentation/newrelic-scala-zio2-api/README.md new file mode 100644 index 0000000000..53aa3ad898 --- /dev/null +++ b/instrumentation/newrelic-scala-zio2-api/README.md @@ -0,0 +1,3 @@ +This module instruments `TraceOps#txn` call in the newrelic zio 2 scala api. +This is used instead of the `@Trace` annotation which eagerly starts the Transcaction. This instrumentation takes +care of the lazy ZIO structure created in ZIO \ No newline at end of file diff --git a/instrumentation/newrelic-scala-zio2-api/build.gradle b/instrumentation/newrelic-scala-zio2-api/build.gradle new file mode 100644 index 0000000000..1ecaffc314 --- /dev/null +++ b/instrumentation/newrelic-scala-zio2-api/build.gradle @@ -0,0 +1,21 @@ +apply plugin: 'scala' + +isScalaProjectEnabled(project, "scala-2.13") + +dependencies { + implementation(project(":agent-bridge")) + implementation(project(":newrelic-weaver-api")) + implementation(project(":newrelic-weaver-scala-api")) + implementation(project(":newrelic-scala-zio2-api")) + implementation("org.scala-lang:scala-library:2.13.3") + implementation("dev.zio:zio_2.13:2.0.0") +} + +jar { + manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.newrelic-scala-zio2-api', + 'Implementation-Title-Alias': 'newrelic-scala-zio2-api_instrumentation' } +} + +verifyInstrumentation { + verifyClasspath = false //can't verify, newrelic-scala-zio2-api is a sub project +} diff --git a/instrumentation/newrelic-scala-zio2-api/src/main/scala/com/newrelic/zio2/api/Util.scala b/instrumentation/newrelic-scala-zio2-api/src/main/scala/com/newrelic/zio2/api/Util.scala new file mode 100644 index 0000000000..c202e0b21a --- /dev/null +++ b/instrumentation/newrelic-scala-zio2-api/src/main/scala/com/newrelic/zio2/api/Util.scala @@ -0,0 +1,24 @@ +package com.newrelic.zio2.api + +import com.newrelic.agent.bridge.AgentBridge +import zio._ + +object Util { + def wrapTrace[R, E, A](body: ZIO[R, E, A]): ZIO[R, E, A] = { + ZIO.attempt(AgentBridge.instrumentation.createScalaTxnTracer) + .foldZIO(_ => body, + tracer => if (tracer == null) { + body + } else { + body.mapBoth( + error => { + tracer.finish(new Throwable("ZIO txn body fail")) + error + }, + success => { + tracer.finish(172, null) + success + }) + }) + } +} diff --git a/instrumentation/newrelic-scala-zio2-api/src/main/scala/com/newrelic/zio2/api/ZIOTraceOps_Instrumentation.scala b/instrumentation/newrelic-scala-zio2-api/src/main/scala/com/newrelic/zio2/api/ZIOTraceOps_Instrumentation.scala new file mode 100644 index 0000000000..bb46b6188a --- /dev/null +++ b/instrumentation/newrelic-scala-zio2-api/src/main/scala/com/newrelic/zio2/api/ZIOTraceOps_Instrumentation.scala @@ -0,0 +1,11 @@ +package com.newrelic.zio2.api + +import com.newrelic.api.agent.weaver.Weaver +import com.newrelic.api.agent.weaver.scala.{ScalaMatchType, ScalaWeave} +import zio.ZIO + + +@ScalaWeave(`type` = ScalaMatchType.Object, `originalName` = "com.newrelic.zio2.api.TraceOps") +class ZIOTraceOps_Instrumentation { + def txn[R, E, A](body: ZIO[R, E, A]): ZIO[R, E, A] = Util.wrapTrace(Weaver.callOriginal) +} diff --git a/instrumentation/zio-2/build.gradle b/instrumentation/zio-2/build.gradle new file mode 100644 index 0000000000..5e169a0e47 --- /dev/null +++ b/instrumentation/zio-2/build.gradle @@ -0,0 +1,25 @@ +apply plugin: 'scala' + +isScalaProjectEnabled(project, "scala-2.13") + +dependencies { + implementation("dev.zio:zio_2.13:2.0.13") + implementation(project(":agent-bridge")) + implementation(project(":newrelic-weaver-scala-api")) + implementation(project(":newrelic-weaver-api")) + implementation('org.scala-lang:scala-library:2.13.10') +} + +jar { + manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.zio', + 'Implementation-Title-Alias': 'zio_instrumentation' } +} + +verifyInstrumentation { + passes 'dev.zio:zio_2.13:[2.0.0,)' +} + +site { + title 'Scala' + type 'Other' +} \ No newline at end of file diff --git a/instrumentation/zio-2/src/main/scala/zio/TokenAwareRunnable.java b/instrumentation/zio-2/src/main/scala/zio/TokenAwareRunnable.java new file mode 100644 index 0000000000..f55e49c98c --- /dev/null +++ b/instrumentation/zio-2/src/main/scala/zio/TokenAwareRunnable.java @@ -0,0 +1,42 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package zio; + +import com.newrelic.agent.bridge.AgentBridge; + +import static zio.Utils.getThreadTokenAndRefCount; +import static zio.Utils.clearThreadTokenAndRefCountAndTxn; +import static zio.Utils.setThreadTokenAndRefCount; +import static zio.Utils.logTokenInfo; + + +public final class TokenAwareRunnable implements Runnable { + private final Runnable delegate; + private AgentBridge.TokenAndRefCount tokenAndRefCount; + + public TokenAwareRunnable(Runnable delegate) { + this.delegate = delegate; + //get token state from calling Thread + this.tokenAndRefCount = getThreadTokenAndRefCount(); + logTokenInfo( tokenAndRefCount, "TokenAwareRunnable token info set"); + } + + @Override + public void run() { + try { + if (delegate != null) { + logTokenInfo(tokenAndRefCount, "Token info set in thread"); + setThreadTokenAndRefCount(tokenAndRefCount); + delegate.run(); + } + } finally { + logTokenInfo(tokenAndRefCount, "Clearing token info from thread "); + clearThreadTokenAndRefCountAndTxn(tokenAndRefCount); + } + } +} diff --git a/instrumentation/zio-2/src/main/scala/zio/Utils.java b/instrumentation/zio-2/src/main/scala/zio/Utils.java new file mode 100644 index 0000000000..50171a1b94 --- /dev/null +++ b/instrumentation/zio-2/src/main/scala/zio/Utils.java @@ -0,0 +1,60 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package zio; + +import java.text.MessageFormat; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; + +import com.newrelic.agent.bridge.AgentBridge; +import com.newrelic.agent.bridge.Transaction; + +public class Utils { + + public static AgentBridge.TokenAndRefCount getThreadTokenAndRefCount() { + AgentBridge.TokenAndRefCount tokenAndRefCount = AgentBridge.activeToken.get(); + // Used to be that if the tokenAndRefCount is not null, we increment the counter and then return the tokenAndRefCount + if (tokenAndRefCount == null) { + Transaction tx = AgentBridge.getAgent().getTransaction(false); + if (tx != null) { + tokenAndRefCount = new AgentBridge.TokenAndRefCount(tx.getToken(), + AgentBridge.getAgent().getTracedMethod(), new AtomicInteger(1)); + } + } else { + tokenAndRefCount.refCount.incrementAndGet(); + } + + return tokenAndRefCount; + } + + public static void setThreadTokenAndRefCount(AgentBridge.TokenAndRefCount tokenAndRefCount) { + if (tokenAndRefCount != null) { + AgentBridge.activeToken.set(tokenAndRefCount); + tokenAndRefCount.token.link(); + } + } + + public static void clearThreadTokenAndRefCountAndTxn(AgentBridge.TokenAndRefCount tokenAndRefCount) { + AgentBridge.activeToken.remove(); + if (tokenAndRefCount != null && tokenAndRefCount.refCount.decrementAndGet() <= 0) { + tokenAndRefCount.token.expire(); + tokenAndRefCount.token = null; + } + } + + public static void logTokenInfo(AgentBridge.TokenAndRefCount tokenAndRefCount, String msg) { + if (AgentBridge.getAgent().getLogger().isLoggable(Level.FINEST)) { + String tokenMsg = (tokenAndRefCount != null && tokenAndRefCount.token != null) + ? String.format("[%s:%s:%d]", tokenAndRefCount.token, tokenAndRefCount.token.getTransaction(), + tokenAndRefCount.refCount.get()) + : "[Empty token]"; + AgentBridge.getAgent().getLogger().log(Level.FINEST, MessageFormat.format("{0}: token info {1}", tokenMsg, msg)); + } + } + +} \ No newline at end of file diff --git a/instrumentation/zio-2/src/main/scala/zio/ZIOExecutor_Instrumentation.java b/instrumentation/zio-2/src/main/scala/zio/ZIOExecutor_Instrumentation.java new file mode 100644 index 0000000000..d47fc33313 --- /dev/null +++ b/instrumentation/zio-2/src/main/scala/zio/ZIOExecutor_Instrumentation.java @@ -0,0 +1,21 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package zio; + +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; + +@Weave(originalName = "zio.Executor", type = MatchType.BaseClass) +public class ZIOExecutor_Instrumentation { + + public boolean submit(Runnable runnable, Unsafe unsafe) { + runnable = new TokenAwareRunnable(runnable); + return Weaver.callOriginal(); + } +} diff --git a/instrumentation/zio-2/src/main/scala/zio/ZIOScheduler_Instrumentation.java b/instrumentation/zio-2/src/main/scala/zio/ZIOScheduler_Instrumentation.java new file mode 100644 index 0000000000..7d149092de --- /dev/null +++ b/instrumentation/zio-2/src/main/scala/zio/ZIOScheduler_Instrumentation.java @@ -0,0 +1,26 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package zio; + +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import scala.Boolean; +import scala.Function0; +import zio.TokenAwareRunnable; + +import java.time.Duration; + +@Weave(originalName = "zio.Scheduler", type = MatchType.BaseClass) +public class ZIOScheduler_Instrumentation { + + public Function0 schedule(Runnable task, Duration duration, Unsafe unsafe) { + task = new TokenAwareRunnable(task); + return Weaver.callOriginal(); + } +} diff --git a/instrumentation/zio/build.gradle b/instrumentation/zio/build.gradle index 4a5a147166..f8a1b58828 100644 --- a/instrumentation/zio/build.gradle +++ b/instrumentation/zio/build.gradle @@ -10,7 +10,7 @@ dependencies { } jar { - manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.zio', + manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.zio-2', 'Implementation-Title-Alias': 'zio_instrumentation' } } diff --git a/instrumentation/zio/src/main/java/zio/internal/TokenAwareRunnable.java b/instrumentation/zio/src/main/java/zio/internal/TokenAwareRunnable.java index 6536bea815..3a256a3288 100644 --- a/instrumentation/zio/src/main/java/zio/internal/TokenAwareRunnable.java +++ b/instrumentation/zio/src/main/java/zio/internal/TokenAwareRunnable.java @@ -2,11 +2,9 @@ import com.newrelic.agent.bridge.AgentBridge; -import static zio.internal.Utils.getThreadTokenAndRefCount; import static zio.internal.Utils.clearThreadTokenAndRefCountAndTxn; -import static zio.internal.Utils.setThreadTokenAndRefCount; import static zio.internal.Utils.logTokenInfo; - +import static zio.internal.Utils.setThreadTokenAndRefCount; public final class TokenAwareRunnable implements Runnable { private final Runnable delegate; @@ -15,7 +13,7 @@ public final class TokenAwareRunnable implements Runnable { public TokenAwareRunnable(Runnable delegate) { this.delegate = delegate; //get token state from calling Thread - this.tokenAndRefCount = getThreadTokenAndRefCount(); + this.tokenAndRefCount = Utils.getThreadTokenAndRefCount(); logTokenInfo( tokenAndRefCount, "TokenAwareRunnable token info set"); } diff --git a/instrumentation/zio/src/main/java/zio/internal/Utils.java b/instrumentation/zio/src/main/java/zio/internal/Utils.java index 6766095d34..a02582781b 100644 --- a/instrumentation/zio/src/main/java/zio/internal/Utils.java +++ b/instrumentation/zio/src/main/java/zio/internal/Utils.java @@ -11,6 +11,7 @@ public class Utils { public static AgentBridge.TokenAndRefCount getThreadTokenAndRefCount() { AgentBridge.TokenAndRefCount tokenAndRefCount = AgentBridge.activeToken.get(); + // Used to be that if the tokenAndRefCount is not null, we increment the counter and then return the tokenAndRefCount Transaction tx = AgentBridge.getAgent().getTransaction(false); if (tx != null) { tokenAndRefCount = new AgentBridge.TokenAndRefCount(tx.getToken(), diff --git a/newrelic-scala-zio2-api/build.gradle.kts b/newrelic-scala-zio2-api/build.gradle.kts new file mode 100644 index 0000000000..0cb0605664 --- /dev/null +++ b/newrelic-scala-zio2-api/build.gradle.kts @@ -0,0 +1,92 @@ +import com.nr.builder.publish.PublishConfig + +plugins { + `maven-publish` + `signing` + id("com.github.prokod.gradle-crossbuild-scala" ) +} + +evaluationDependsOn(":newrelic-api") + +crossBuild { + scalaVersionsCatalog = mapOf("2.12" to "2.12.13", "2.13" to "2.13.10") + builds { + register("scala") { + scalaVersions = setOf("2.12", "2.13") + } + } +} + +java { + withSourcesJar() + withJavadocJar() +} + +dependencies { + zinc("org.scala-sbt:zinc_2.13:1.7.1") + implementation("org.scala-lang:scala-library:2.13.10") + implementation("dev.zio:zio_2.13:2.0.13") + implementation(project(":newrelic-api")) + testImplementation(project(":instrumentation-test")) + testImplementation(project(path = ":newrelic-agent", configuration = "tests")) +} + +val crossBuildScala_212Jar by tasks.getting +val crossBuildScala_213Jar by tasks.getting + +val javadocJar by tasks.getting +val sourcesJar by tasks.getting + +mapOf( + "2.12" to crossBuildScala_212Jar, + "2.13" to crossBuildScala_213Jar +).forEach { (scalaVersion, versionedClassJar) -> + PublishConfig.config( + "crossBuildScala_${scalaVersion.replace(".", "")}", + project, + "New Relic Java agent Scala $scalaVersion API", + "The public Scala $scalaVersion API of the Java agent, and no-op implementations for safe usage without the agent." + ) { + artifact(sourcesJar) + artifact(javadocJar) + artifact(versionedClassJar) + } +} + +tasks { + //functional test setup here until scala 2.13 able to be used in functional test project + test { + dependsOn("jar") + setForkEvery(1) + maxParallelForks = Runtime.getRuntime().availableProcessors() + minHeapSize = "256m" + maxHeapSize = "768m" + val functionalTestArgs = listOf( + "-javaagent:${com.nr.builder.JarUtil.getNewRelicJar(project(":newrelic-agent")).absolutePath}", + "-Dnewrelic.config.file=${project(":newrelic-agent").projectDir}/src/test/resources/com/newrelic/agent/config/newrelic.yml", + "-Dnewrelic.unittest=true", + "-Dnewrelic.debug=true", + "-Dnewrelic.sync_startup=true", + "-Dnewrelic.send_data_on_exit=true", + "-Dnewrelic.send_data_on_exit_threshold=0", + "-Dnewrelic.config.log_level=finest", + "-Dnewrelic.config.startup_log_level=warn" + ) + jvmArgs(functionalTestArgs + "-Dnewrelic.config.extensions.dir=${projectDir}/src/test/resources/xml_files") + } + + //no scaladoc jar task, instead this work around makes scaladoc destination folder javadoc to ensure included in jar + javadoc { + dependsOn("scaladoc") + } + scaladoc { + val javadocDir = ( + destinationDir.absolutePath + .split("/") + .dropLast(1) + .plus("javadoc") + ).joinToString("/") + destinationDir = File(javadocDir) + } +} + diff --git a/newrelic-scala-zio2-api/src/main/scala/com/newrelic/zio2/api/TraceOps.scala b/newrelic-scala-zio2-api/src/main/scala/com/newrelic/zio2/api/TraceOps.scala new file mode 100644 index 0000000000..2bd76dcd15 --- /dev/null +++ b/newrelic-scala-zio2-api/src/main/scala/com/newrelic/zio2/api/TraceOps.scala @@ -0,0 +1,161 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.newrelic.zio2.api + +import com.newrelic.api.agent.{NewRelic, Segment} +import zio._ + +object TraceOps { + + /** + * Creates a segment to capture metrics for a given block of code, this will call {@link com.newrelic.api.agent.Transaction# startSegment ( String )}, + * execute the code block, then call {@link com.newrelic.api.agent.Segment# end ( )}. This {@link Segment} will show up in the Transaction Breakdown + * table, as well as the Transaction Trace page. This {@link Segment} will be reported in the "Custom/" metric + * e.g. The code below will produce 1 segment trace segment name + *
+    * trace("trace segment name") {
+    * val i = 1
+    * val j = 2
+    * i + j
+    * }
+    * 
+ * + * @param segmentName Name of the { @link Segment} segment in APM. + * This name will show up in the Transaction Breakdown table, as well as the Transaction Trace page. + *

+ * if null or an empty String, the agent will report "Unnamed Segment". + * @param block Code block segment is to capture metrics for + * @tparam S Type returned from executed code block + * @return Value returned by executed code block + */ + def trace[S](segmentName: String)(block: => S): S = { + val txn = NewRelic.getAgent.getTransaction() + val segment = txn.startSegment(segmentName) + try { + block + } finally { + segment.end() + } + } + + /** + * Creates a segment to capture metrics for value block : ZIO[R, E, A] + * When run the returned ZIO[R, E, A] calls {@link com.newrelic.api.agent.Transaction# startSegment ( + * String )}, executes the input code block, then calls {@link com.newrelic.api.agent.Segment# end ( )} + * This {@link Segment} will show up in the Transaction Breakdown table, as well as the Transaction Trace page. This {@link Segment} will be reported in the "Custom/" metric + * e.g. The code below will produce 2 segments trace segment 1 and trace segment 2 + *

+    * for {
+    * i <- asyncTrace("trace segment 1")(UIO(1))
+    * j <- asyncTrace("trace segment 2")(UIO(i + 1))
+    * } yield j
+    * 
+ * + * @param segmentName Name of the { @link Segment} segment in APM. + * This name will show up in the Transaction Breakdown table, as well as the Transaction Trace page. + *

+ * if null or an empty String, the agent will report "Unnamed Segment". + * @param block ZIO[R, E, A] value the segment is to capture metrics for. + * The block should return a ZIO[R, E, A] + * @return Value returned from completed asynchronous code block + */ + def asyncTrace[R, E, A](segmentName: String)(block: ZIO[R, E, A]): ZIO[R, E, A] = + for { + segment <- startSegment(segmentName) + b <- endSegmentOnError(block, segment) + _ <- ZIO.succeed(segment.end()) + } yield b + + /** + * Creates a segment to capture metrics for a given function, this will call {@link com.newrelic.api.agent.Transaction# startSegment ( String )}, + * execute the function, then call {@link com.newrelic.api.agent.Segment# end ( )}. This {@link Segment} will show up in the Transaction Breakdown + * table, as well as the Transaction Trace page. This {@link Segment} will be reported in the "Custom/" metric + * e.g. the code below will produce a segment trace map segment + *

+    * UIO(1).map(traceFun("trace map segment")(i => i + 1))
+    * 
+ * + * @param segmentName Name of the { @link Segment} segment in APM. + * This name will show up in the Transaction Breakdown table, as well as the Transaction Trace page. + *

+ * if null or an empty String, the agent will report "Unnamed Segment". + * @param f Function segment is to capture metrics for. + * @tparam T Input type for function segment is to capture metrics for. + * @tparam S Type returned from executed function + * @return Value returned from executed function + */ + def traceFun[T, S](segmentName: String)(f: T => S): T => S = { + t: T => + val txn = NewRelic.getAgent.getTransaction() + val segment = txn.startSegment(segmentName) + try { + f(t) + } finally { + segment.end() + } + } + + /** + * Creates a segment to capture metrics for given asynchronous function of return type : ZIO[R, E, A] + * When run the returned ZIO[R, E, A] calls {@link com.newrelic.api.agent.Transaction# startSegment ( String )}, + * executes the input function, + * then calls {@link com.newrelic.api.agent.Segment# end ( )} + * This {@link Segment} will show up in the Transaction Breakdown table, as well as the Transaction Trace page. This {@link Segment} will be reported in the "Custom/" metric + * e.g. The code below will produce 1 segment trace flatMap segment + *

+    * UIO(1).flatMap(asyncTraceFun("trace flatMap segment")(i => UIO(i + 1)))
+    * 
+ * + * @param segmentName Name of the { @link Segment} segment in APM. + * This name will show up in the Transaction Breakdown table, as well as the Transaction Trace page. + *

+ * if null or an empty String, the agent will report "Unnamed Segment". + * @param f Asynchronous function segment is to capture metrics for. + * @tparam T Input type for function segment is to capture metrics for. + * @tparam S Type returned from completed asynchronous function + * @return Value returned from completed asynchronous function + */ + def asyncTraceFun[R, E, A, T](segmentName: String)(f: T => ZIO[R, E, A]): T => ZIO[R, E, A] = { + t: T => + for { + segment <- startSegment(segmentName) + b <- endSegmentOnError(f(t), segment) + _ <- ZIO.succeed(segment.end()) + } yield b + } + + /** + * Wraps a given block of code so that a {@link com.newrelic.api.agent.Transaction} will be started and completed + * before and after the code is run. + * When this method is invoked within the context of an existing transaction this has no effect. + * The newly created {@link com.newrelic.api.agent.Transaction} will complete once the code block has been executed + * e.g. the code below will create a Transaction and with a segment trace map UIO + *

+    * txn {
+    * UIO(1).map(traceFun("trace map UIO")(i => i + 1))
+    * }
+    * 
+ * + * @param block Code block to be executed inside a transaction + * @tparam S Type returned by code block + * @return Value returned by code block + */ + def txn[R, E, A](body: ZIO[R, E, A]): ZIO[R, E, A] = body + + private def startSegment(segmentName: String): UIO[Segment] = ZIO.succeed { + val txn = NewRelic.getAgent.getTransaction() + txn.startSegment(segmentName) + } + + private def endSegmentOnError[R, E, A](zio: ZIO[R, E, A], segment: Segment): ZIO[R, E, A] = { + zio.catchAll(e => { + segment.end() + ZIO.fail(e) + }) + } +} diff --git a/newrelic-scala-zio2-api/src/test/scala/com/newrelic/zio2/api/ZIO2TraceOpsTests.scala b/newrelic-scala-zio2-api/src/test/scala/com/newrelic/zio2/api/ZIO2TraceOpsTests.scala new file mode 100644 index 0000000000..3867cd1780 --- /dev/null +++ b/newrelic-scala-zio2-api/src/test/scala/com/newrelic/zio2/api/ZIO2TraceOpsTests.scala @@ -0,0 +1,242 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.newrelic.zio2.api + +import com.newrelic.agent.introspec._ +import com.newrelic.api.agent.Trace +import com.newrelic.zio2.api.TraceOps._ +import org.junit._ +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import zio._ + +import java.util.concurrent.Executors +import scala.concurrent.ExecutionContext +import scala.jdk.CollectionConverters._ + +@RunWith(classOf[InstrumentationTestRunner]) +@InstrumentationTestConfig(includePrefixes = Array("none")) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class ZIO2TraceOpsTests { + + val introspector: Introspector = InstrumentationTestRunner.getIntrospector + + def executorService(nThreads: Int) = { + ExecutionContext.fromExecutor(Executors.newFixedThreadPool(nThreads)) + } + + val threadPoolOne: ExecutionContext = executorService(3) + val threadPoolTwo: ExecutionContext = executorService(3) + val threadPoolThree: ExecutionContext = executorService(3) + + @Before + def setup() = { + com.newrelic.agent.Transaction.clearTransaction + introspector.clear() + } + @After + def resetTxn() = { + com.newrelic.agent.Transaction.clearTransaction + introspector.clear() + } + + @Test + def asyncTraceProducesOneSegment(): Unit = { + //When + val txnBlock: ZIO[Any, Nothing,Int] = txn { + asyncTrace("getNumber")(ZIO.succeed(1)) + } + val result = Unsafe.unsafe(implicit u => { + Runtime.default.unsafe.run(txnBlock) + }).getOrElse(c => c) + val txnCount = introspector.getFinishedTransactionCount() + val traces = getTraces(introspector) + val segments = getSegments(traces) + + //Then + Assert.assertEquals("result correct", 1, result) + Assert.assertEquals("transaction finished", 1, txnCount) + Assert.assertEquals("trace present", 1, traces.size) + Assert.assertTrue("getFirstNumber segment exists", + segments.exists(_.getName == s"Custom/getNumber") + ) + } + + @Test + def chainedSyncAndAsyncTraceSegmentsCaptured(): Unit = { + //When + val txnBlock: ZIO[Any, Nothing,Int] = txn( + asyncTrace("getNumber")(ZIO.succeed(1)) + .map(traceFun("incrementNumber")(_ + 1)) + .flatMap(asyncTraceFun("flatMapIncrementNumber")(res => ZIO.succeed(res + 1))) + ) + val result = Unsafe.unsafe(implicit u => { + Runtime.default.unsafe.run(txnBlock) + }).getOrElse(c => c) + val txnCount = introspector.getFinishedTransactionCount() + val traces = getTraces(introspector) + val segments = getSegments(traces) + + //Then + Assert.assertEquals("Result correct", 3, result) + Assert.assertEquals("Transaction finished", 1, txnCount) + Assert.assertEquals("Trace present", 1, traces.size) + Assert.assertTrue("getNumber segment exists", segments.exists(_.getName == s"Custom/getNumber")) + Assert.assertTrue("incrementNumber segment exists", segments.exists(_.getName == s"Custom/incrementNumber")) + Assert.assertTrue("flatMapIncrementNumber segment exists", segments.exists(_.getName == s"Custom/flatMapIncrementNumber")) + } + + @Test + def asyncForComprehensionSegments: Unit = { + + //When + val txnBlock: ZIO[Any, Nothing,Int] = txn { + for { + oneFibre <- asyncTrace("one"){ + ZIO.succeed(1).fork + } + twoFibre <- asyncTrace("two")( + ZIO.succeed(2).fork) + three <- ZIO.succeed(getThree) + two <- twoFibre.join + one <- oneFibre.join + } yield one + two + three + } + val result = Unsafe.unsafe(implicit u => { + Runtime.default + .unsafe + .run(txnBlock) + }).getOrElse(c => c) + + val txnCount = introspector.getFinishedTransactionCount() + val traces = getTraces(introspector) + val segments = getSegments(traces) + + Assert.assertEquals("Result correct", 6, result) + Assert.assertEquals("Transaction finished", 1, txnCount) + Assert.assertEquals("Trace present", 1, traces.size) + Assert.assertTrue("one segment exists", segments.exists(_.getName == s"Custom/one")) + Assert.assertTrue("two segment exists", segments.exists(_.getName == s"Custom/two")) + Assert.assertTrue("getThree segment exists", segments.exists(_.getName.endsWith("getThree"))) + } + + + @Test + def sequentialAsyncTraceSegmentTimeCaptured(): Unit = { + val delayMillis = 1500 + + //When + val txnBlock = txn( + for { + one <- asyncTrace("one")(ZIO.succeed(1)) + _ <- asyncTrace("sleep")(ZIO.sleep(delayMillis.millis)) + two <- asyncTrace("two")(ZIO.succeed(one + 1)) + } yield two + ) + val result = Unsafe.unsafe(implicit u => { + Runtime.default.unsafe.run(txnBlock) + }).getOrElse(c => c) + val txnCount = introspector.getFinishedTransactionCount() + val traces = getTraces(introspector) + val segments = getSegments(traces) + + //Then + Assert.assertEquals("Result correct", 2, result) + Assert.assertEquals("Transaction finished", 1, txnCount) + Assert.assertEquals("Trace present", 1, traces.size) + + Assert.assertTrue("one segment exists", segments.exists(_.getName == s"Custom/one")) + Assert.assertTrue("two segment exists", segments.exists(_.getName == s"Custom/two")) + assertSegmentExistsAndTimeInRange("sleep", segments, delayMillis) + + } + + @Test + def segmentCompletedIfIOErrors(): Unit = { + + //When + val txnBlock: Task[Int] = txn( + for { + one <- asyncTrace("one")(ZIO.fromTry(scala.util.Success(1))) + _ <- asyncTrace("boom")(ZIO.fromTry(scala.util.Failure(new Throwable("Boom!")))) + } yield one + ) + + val result = Unsafe.unsafe(implicit u => { + Runtime.default.unsafe.run(txnBlock) + }) + + val txnCount = introspector.getFinishedTransactionCount() + val traces = getTraces(introspector) + val segments = getSegments(traces) + + //Then + result.fold( + failed => Assert.assertTrue("Result correct", failed.isInstanceOf[Throwable]), + _ => Assert.fail("Incorrect success result in test") + ) + Assert.assertEquals("Transaction finished", 1, txnCount) + Assert.assertEquals("Trace present", 1, traces.size) + Assert.assertTrue("one segment exists", segments.exists(_.getName == s"Custom/one")) + Assert.assertTrue("two segment exists", segments.exists(_.getName == s"Custom/boom")) + } + + + @Test + def parallelTraverse(): Unit = { + + //When + val txnBlock: ZIO[Any, Nothing,Int] = txn( + for { + one <- ZIO.succeed(trace("segment 1")(1)) + rest <- ZIO.foreachPar(List(2, 3, 4, 5))(i => asyncTrace(s"segment $i")(ZIO.succeed(i))) + sum <- asyncTrace("sum segments")(ZIO.succeed(rest.sum + one)) + } yield sum + ) + val result = Unsafe.unsafe(implicit u => { + Runtime.default.unsafe.run(txnBlock) + }).getOrElse(c => c) + val txnCount = introspector.getFinishedTransactionCount() + val traces = getTraces(introspector) + val segments = getSegments(traces) + + //Then + Assert.assertEquals("Result correct", 15, result) + Assert.assertEquals("Transaction finished", 1, txnCount) + Assert.assertEquals("Trace present", 1, traces.size) + List(1, 2, 3, 4, 5).foreach(i => + Assert.assertTrue(s"$i segment exists", segments.exists(_.getName == s"Custom/segment $i")) + ) + Assert.assertTrue(s"sum segments exists", segments.exists(_.getName == s"Custom/sum segments")) + } + + @Trace(async = true) + private def getThree = 3 + + private def getTraces(introspector: Introspector): Iterable[TransactionTrace] = + introspector.getTransactionNames.asScala.flatMap(transactionName => introspector.getTransactionTracesForTransaction(transactionName).asScala) + + private def getSegments(traces: Iterable[TransactionTrace]): Iterable[TraceSegment] = + traces.flatMap(trace => this.getSegments(trace.getInitialTraceSegment)) + + private def getSegments(segment: TraceSegment): List[TraceSegment] = { + val childSegments = segment.getChildren.asScala.flatMap(childSegment => getSegments(childSegment)).toList + segment :: childSegments + } + + private def assertSegmentExistsAndTimeInRange(segmentName: String, segments: Iterable[TraceSegment], minTime: Long, optMaxTime: Option[Long] = None) = { + val optDelayedSegment: Option[TraceSegment] = segments.find(_.getName == s"Custom/$segmentName") + Assert.assertTrue(s"scheduled $segmentName segment exists", optDelayedSegment.isDefined) + optDelayedSegment.foreach(delayedSegmentTime => { + Assert.assertTrue(s"delayedSegmentTime $segmentName less than minimum expected time", delayedSegmentTime.getRelativeEndTime >= minTime) + optMaxTime.foreach(maxTime => + Assert.assertTrue(s"delayedSegmentTime $segmentName greater than maximum expected time", delayedSegmentTime.getRelativeEndTime <= maxTime) + ) + }) + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 1250c5e486..25c83ddc99 100644 --- a/settings.gradle +++ b/settings.gradle @@ -29,6 +29,7 @@ include 'newrelic-scala-cats-api' include 'newrelic-cats-effect3-api' include 'newrelic-scala-monix-api' include 'newrelic-scala-zio-api' +include 'newrelic-scala-zio2-api' include 'agent-model' include 'agent-interfaces' include 'discovery' @@ -308,6 +309,7 @@ include 'instrumentation:newrelic-scala-monix-api' include 'instrumentation:newrelic-scala-cats-api' include 'instrumentation:newrelic-cats-effect3-api' include 'instrumentation:newrelic-scala-zio-api' +include 'instrumentation:newrelic-scala-zio2-api' include 'instrumentation:servlet-2.4' include 'instrumentation:servlet-5.0' include 'instrumentation:servlet-6.0' @@ -400,4 +402,5 @@ include 'instrumentation:wildfly-8-PORT' include 'instrumentation:wildfly-27' include 'instrumentation:wildfly-jmx-14' include 'instrumentation:zio' +include 'instrumentation:zio-2'