From 821cb9d96fb394231a398c0e7e3b594442e42412 Mon Sep 17 00:00:00 2001 From: Jerry Duffy Date: Fri, 23 Feb 2024 10:46:08 -0500 Subject: [PATCH 1/3] new scala3 api project; updates GHA for new artifact --- .github/workflows/publish_main_snapshot.yml | 4 +- .github/workflows/publish_release.yml | 4 +- newrelic-scala3-api/build.gradle.kts | 65 ++++ .../com/newrelic/scala/api/TraceOps.scala | 164 +++++++++++ .../newrelic/scala/api/TraceOpsDSLTest.scala | 277 ++++++++++++++++++ settings.gradle | 1 + 6 files changed, 511 insertions(+), 4 deletions(-) create mode 100644 newrelic-scala3-api/build.gradle.kts create mode 100644 newrelic-scala3-api/src/main/scala/com/newrelic/scala/api/TraceOps.scala create mode 100644 newrelic-scala3-api/src/test/scala/com/newrelic/scala/api/TraceOpsDSLTest.scala diff --git a/.github/workflows/publish_main_snapshot.yml b/.github/workflows/publish_main_snapshot.yml index b2fbc59d31..5774c6ea70 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-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 :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-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 - 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 32353cc8a4..943cd30828 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-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 :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-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 -Prelease=true - name: Publish apis for Security agent env: SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} diff --git a/newrelic-scala3-api/build.gradle.kts b/newrelic-scala3-api/build.gradle.kts new file mode 100644 index 0000000000..3a3a4280c5 --- /dev/null +++ b/newrelic-scala3-api/build.gradle.kts @@ -0,0 +1,65 @@ +import com.nr.builder.publish.PublishConfig + +plugins { + scala + `maven-publish` + signing +} +evaluationDependsOn(":newrelic-api") + + +java { + withSourcesJar() + withJavadocJar() +} + +publishing { + publications { + create("maven") { + artifactId = "newrelic-scala-api_3" + + from(components["java"]) + } + } +} + +dependencies { + implementation("org.scala-lang:scala-library:2.13.10") + implementation("org.scala-lang:scala3-library_3:3.3.0") + implementation(project(":newrelic-api")) + testImplementation(project(":instrumentation-test")) + testImplementation(project(path = ":newrelic-agent", configuration = "tests")) +} + +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.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-scala3-api/src/main/scala/com/newrelic/scala/api/TraceOps.scala b/newrelic-scala3-api/src/main/scala/com/newrelic/scala/api/TraceOps.scala new file mode 100644 index 0000000000..594251c1ba --- /dev/null +++ b/newrelic-scala3-api/src/main/scala/com/newrelic/scala/api/TraceOps.scala @@ -0,0 +1,164 @@ +package com.newrelic.scala.api + + +import com.newrelic.api.agent.{NewRelic, Trace, Transaction} + +import scala.concurrent.{ExecutionContext, Future} + +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 a given asynchronous 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()} on the completion of the asynchronous code block. + * 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")(Future(1))
+   *    j <- asyncTrace("trace segment 2")(Future(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 Asynchronous code block segment is to capture metrics for. + * The block should return a { @link Future} + * @param ec The execution context on which the future is run + * @tparam S Type returned from completed asynchronous code block + * @return Value returned from completed asynchronous code block + */ + def asyncTrace[S](segmentName: String)(block: => Future[S])(implicit ec: ExecutionContext): Future[S] = { + val txn = NewRelic.getAgent.getTransaction() + val segment = txn.startSegment(segmentName) + val evaluatedBlock = block + evaluatedBlock.onComplete { + case _ => segment.end() + } + evaluatedBlock + } + + /** + * 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 2 segments trace map segment and trace filter segment + *

+   * Future(1)
+   *    .map(traceFun("trace map segment")(i => i + 1))
+   *    .filter(traceFun("trace filter segment")(i => i % 2 == 0))
+   * 
+ * + * @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 a given asynchronous function, this will call {@link com.newrelic.api.agent.Transaction#startSegment(String)}, + * execute the function, then call {@link com.newrelic.api.agent.Segment#end()} on the completion of the asynchronous function. + * 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 + *

+   * Future(1).flatMap(asyncTraceFun("trace flatMap segment")(i => Future(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. + * @param ec The execution context on which the future is run + * @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[T, S](segmentName: String)(f: T => Future[S])(implicit ec: ExecutionContext): T => Future[S] = { + (t: T) => + val txn = NewRelic.getAgent.getTransaction() + val segment = txn.startSegment(segmentName) + val evaluatedFunc = f(t) + evaluatedFunc.onComplete { + case _ => segment.end() + } + evaluatedFunc + + } + + /** + * Starts a {@link com.newrelic.api.agent.Transaction} for a given block of code. + * 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 2 segments trace option creation and trace map option + *

+   * txn {
+   *    val o1 = trace("trace option creation")(Some(1))
+   *    o1.map(traceFun("trace map option")(i => i + 1))
+   * }
+   * 
+ * e.g. the code below will create a Transaction and with 2 segments trace map future and trace filter future + *
+   * txn {
+   *    Future(1)
+   *        .map(traceFun("trace map future")(i => i + 1))
+   *        .filter(traceFun("trace filter future")(i => i % 2 == 0))
+   * }
+   * 
+ * + * @param block Code block to be executed inside a transaction + * @tparam S Type returned by code block + * @return Value returned by code block + */ + @Trace(dispatcher = true) + def txn[S](block: => S): S = block + +} + diff --git a/newrelic-scala3-api/src/test/scala/com/newrelic/scala/api/TraceOpsDSLTest.scala b/newrelic-scala3-api/src/test/scala/com/newrelic/scala/api/TraceOpsDSLTest.scala new file mode 100644 index 0000000000..51b41f3869 --- /dev/null +++ b/newrelic-scala3-api/src/test/scala/com/newrelic/scala/api/TraceOpsDSLTest.scala @@ -0,0 +1,277 @@ +package com.newrelic.scala.api + +import java.util.concurrent.Executors + +import com.newrelic.agent.introspec.{InstrumentationTestConfig, InstrumentationTestRunner, Introspector, TraceSegment, TransactionTrace} +import com.newrelic.scala.api.TraceOps._ +import org.junit.runner.RunWith +import org.junit.{After, Assert, Test} + +import scala.concurrent.duration.{DurationInt, FiniteDuration} +import scala.concurrent.{Await, ExecutionContext, ExecutionContextExecutor, Future} + +import scala.jdk.CollectionConverters._ + +@RunWith(classOf[InstrumentationTestRunner]) +@InstrumentationTestConfig(includePrefixes = Array("scala.concurrent.impl.")) +class TraceOpsDSLTest { + + val threadPoolThree: ExecutionContextExecutor = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(3)) + + @After + def resetTxn() = { + com.newrelic.agent.Transaction.clearTransaction + } + + @Test + def asyncTraceProducesOneSegment(): Unit = { + implicit val ec: ExecutionContext = threadPoolThree + + //Given + val introspector: Introspector = InstrumentationTestRunner.getIntrospector + + //When + val txnBlock: Future[Int] = txn { + asyncTrace("getNumber")(Future(1)) + } + val result = Await.result(txnBlock, 2.seconds) + 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 syncTraceProducesOneSegment(): Unit = { + //Given + val introspector: Introspector = InstrumentationTestRunner.getIntrospector + + //When + val txnBlock: Int = txn { + trace("getNumber")(1) + } + + val txnCount = introspector.getFinishedTransactionCount(2000) + val traces = getTraces(introspector) + val segments = getSegments(traces) + + //Then + Assert.assertEquals("result correct", 1, txnBlock) + 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 chainedSyncTraceSegmentsCaptured(): Unit = { + //Given + val introspector: Introspector = InstrumentationTestRunner.getIntrospector + //When + + val txnBlock: Option[Int] = txn( + Option(trace("getNumber")(1)) + .map(traceFun("incrementNumber")(_ + 1)) + .flatMap(traceFun("flatMapIncrementNumber")(res => Option(res + 1))) + .filter(traceFun("filterNumber")(_ % 2 == 1)) + ) + val txnCount = introspector.getFinishedTransactionCount(2000) + val traces = getTraces(introspector) + val segments = getSegments(traces) + + Assert.assertEquals("Result correct", Some(3), txnBlock) + 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")) + Assert.assertTrue("filterNumber segment exists", segments.exists(_.getName == s"Custom/filterNumber")) + } + + @Test + def chainedSyncAndAsyncTraceSegmentsCaptured(): Unit = { + //Given + val introspector: Introspector = InstrumentationTestRunner.getIntrospector + //When + implicit val ec: ExecutionContext = threadPoolThree + + val txnBlock: Future[Int] = txn( + asyncTrace("getNumber")(Future(1)) + .map(traceFun("incrementNumber")(_ + 1)) + .flatMap(asyncTraceFun("flatMapIncrementNumber")(res => Future(res + 1))) + .filter(traceFun("filterNumber")(_ % 2 == 1)) + ) + val result = Await.result(txnBlock, 2.seconds) + val txnCount = introspector.getFinishedTransactionCount() + val traces = getTraces(introspector) + val segments = getSegments(traces) + + 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")) + Assert.assertTrue("filterNumber segment exists", segments.exists(_.getName == s"Custom/filterNumber")) + } + + @Test + def syncForComprehensionSegments(): Unit = { + //Given + val introspector: Introspector = InstrumentationTestRunner.getIntrospector + val txnBlock: Option[Int] = txn( + for { + one <- Option(trace("one")(1)) + two <- Option(trace("two")(one + 1)) + three <- Option(trace("three")(two + 1)) + } yield three + ) + val txnCount = introspector.getFinishedTransactionCount(2000) + val traces = getTraces(introspector) + val segments = getSegments(traces) + + Assert.assertEquals("Result correct", Some(3), txnBlock) + 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("three segment exists", segments.exists(_.getName == s"Custom/three")) + } + + + @Test + def asyncForComprehensionSegments(): Unit = { + //Given + val introspector: Introspector = InstrumentationTestRunner.getIntrospector + implicit val ec: ExecutionContext = threadPoolThree + val txnBlock: Future[Int] = txn( + for { + one <- asyncTrace("one")(Future(1)) + two <- Future(trace("two")(one + 1)) + three <- asyncTrace("three")(Future(two + 1)) + } yield three + ) + + val result = Await.result(txnBlock, 2.seconds) + val txnCount = introspector.getFinishedTransactionCount() + val traces = getTraces(introspector) + val segments = getSegments(traces) + + Assert.assertEquals("Result correct", 3, 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("three segment exists", segments.exists(_.getName == s"Custom/three")) + } + + + @Test + def sequentialAsyncTraceSegmentTimeCaptured(): Unit = { + //Given + val introspector: Introspector = InstrumentationTestRunner.getIntrospector + + val delayMillis = 1500 + //When + implicit val ec: ExecutionContext = threadPoolThree + + //create 3 numbers in parallel then sum the result + val txnBlock: Future[Int] = txn( + asyncTrace("root")( + for { + one <- asyncTrace("one")(scheduleFuture(delayMillis.millis)(1)) + two <- asyncTrace("two")(scheduleFuture(delayMillis.millis)(one + 1)) + three <- asyncTrace("three")(scheduleFuture(delayMillis.millis)(two + 1)) + } yield three) + ) + val result = Await.result(txnBlock, 6.seconds) + val txnCount = introspector.getFinishedTransactionCount() + val traces = getTraces(introspector) + val segments = getSegments(traces) + + Assert.assertEquals("Result correct", 3, result) + Assert.assertEquals("Transaction finished", 1, txnCount) + Assert.assertEquals("Trace present", 1, traces.size) + + //ensure root segment exists and time take is at least the time taken for other 3 segments executed sequentially + assertSegmentExistsAndTimeInRange("root", segments, delayMillis * 3) + assertSegmentExistsAndTimeInRange("one", segments, delayMillis) + assertSegmentExistsAndTimeInRange("two", segments, delayMillis) + assertSegmentExistsAndTimeInRange("three", segments, delayMillis) + + } + + @Test + def parrallelAsyncTraceSegmentTimeCaptured(): Unit = { + //Given + val introspector: Introspector = InstrumentationTestRunner.getIntrospector + + val delayMillis = 1500 + //When + implicit val ec: ExecutionContext = threadPoolThree + + //create 3 numbers in parallel then sum the result + val txnBlock: Future[Int] = txn( + asyncTrace("root")(Future.sequence( + List( + asyncTrace("one")(scheduleFuture(delayMillis.millis)(1)), + asyncTrace("two")(scheduleFuture(delayMillis.millis)(2)), + asyncTrace("three")(scheduleFuture(delayMillis.millis)(3)), + ) + ).map(_.sum)) + ) + val result = Await.result(txnBlock, 2.seconds) + 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) + + //ensure root segment exists and time take is less than the 3 segments executed sequentially + assertSegmentExistsAndTimeInRange("root", segments, delayMillis, Some(delayMillis * 3 - 100)) + assertSegmentExistsAndTimeInRange("one", segments, delayMillis) + assertSegmentExistsAndTimeInRange("two", segments, delayMillis) + assertSegmentExistsAndTimeInRange("three", segments, delayMillis) + + } + + private def scheduleFuture[T](delay: FiniteDuration)(body: => T): Future[T] = { + implicit val ec: ExecutionContext = threadPoolThree + + Future { + Thread.sleep(delay.toMillis) + body + } + } + + 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) + ) + }) + } + +} + diff --git a/settings.gradle b/settings.gradle index bf1b1602bb..592cb560cb 100644 --- a/settings.gradle +++ b/settings.gradle @@ -24,6 +24,7 @@ buildCache { // Core Projects include 'newrelic-api' include 'newrelic-scala-api' +include 'newrelic-scala3-api' include 'newrelic-scala-cats-api' include 'newrelic-cats-effect3-api' include 'newrelic-scala-monix-api' From fe38609fb4fbd06fb020be61475330d2d23a37c2 Mon Sep 17 00:00:00 2001 From: Jerry Duffy Date: Wed, 28 Feb 2024 08:32:19 -0500 Subject: [PATCH 2/3] run scala3-api unit tests via GHA --- .github/workflows/GHA-Scala-Functional-Tests.yaml | 6 +++--- .github/workflows/GHA-Unit-Tests.yaml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/GHA-Scala-Functional-Tests.yaml b/.github/workflows/GHA-Scala-Functional-Tests.yaml index 94809f15db..6f8740efe5 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-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-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-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-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-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-monix-api:test -PincludeScala -Ptest${{ matrix.java-version }} --continue - name: Capture Jacoco reports if: matrix.java-version == '11' diff --git a/.github/workflows/GHA-Unit-Tests.yaml b/.github/workflows/GHA-Unit-Tests.yaml index 65b3c430d2..9956579842 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-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 -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-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 -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-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 -Ptest8 -PnoInstrumentation -PnonForkedTests --continue - name: Upload coverage to Codecov if: matrix.java-version == '17' From c9b9a87b91496ada4fae9be6db078f6a7ccd4f35 Mon Sep 17 00:00:00 2001 From: Jerry Duffy Date: Wed, 28 Feb 2024 12:30:05 -0500 Subject: [PATCH 3/3] temp disable zio api test z_multipleTransactionsWithThreadHopsDoNotBleed --- .../src/test/scala/com/newrelic/zio/api/TraceOpsDSLTest.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/newrelic-scala-zio-api/src/test/scala/com/newrelic/zio/api/TraceOpsDSLTest.scala b/newrelic-scala-zio-api/src/test/scala/com/newrelic/zio/api/TraceOpsDSLTest.scala index 3024b419bf..4f0ba0a7fd 100644 --- a/newrelic-scala-zio-api/src/test/scala/com/newrelic/zio/api/TraceOpsDSLTest.scala +++ b/newrelic-scala-zio-api/src/test/scala/com/newrelic/zio/api/TraceOpsDSLTest.scala @@ -5,7 +5,7 @@ import com.newrelic.api.agent.Trace import com.newrelic.zio.api.TraceOps._ import org.junit.runner.RunWith import org.junit.runners.MethodSorters -import org.junit.{After, Assert, Before, FixMethodOrder, Test} +import org.junit.{After, Assert, Before, FixMethodOrder, Ignore, Test} import zio.Exit.Success import zio.clock.Clock @@ -205,6 +205,7 @@ class ZIOTraceOpsTests { This test is named to be evaluated last alphabetically due to side effects from the ZIO Runtime environment. */ @Test + @Ignore def z_multipleTransactionsWithThreadHopsDoNotBleed(): Unit = { val delayMillis = 500