From 90eccc23d1785eb887ee21d24e48af03597be829 Mon Sep 17 00:00:00 2001 From: Kate Anderson Date: Tue, 26 Mar 2024 16:31:37 -0700 Subject: [PATCH 1/5] initial commit for pekko http --- .../apache-pekko-http-2.13_1/build.gradle | 37 + .../http/Function0Wrapper.java | 28 + .../http/Function1Wrapper.java | 38 + .../org.apache.pekko/http/FutureWrapper.java | 156 ++ .../http/InboundWrapper.scala | 35 + .../http/OutboundWrapper.scala | 28 + .../http/PathMatcherScalaUtils.scala | 98 + .../http/PathMatcherUtils.java | 430 +++++ .../http/RequestWrapper.scala | 62 + .../http/ResponseWrapper.scala | 44 + .../marshalling/PekkoHttpMarshal.java | 29 + .../PekkoHttpMarshallerMapper.java | 48 + .../PekkoHttpToResponseMarshallable.java | 29 + .../server/Directive_Instrumentation.java | 27 + ...hMatcherConstruction_Instrumentation.scala | 20 + .../NewRelicRequestContextWrapper.scala | 117 ++ .../server/PathMatcher_Instrumentation.scala | 66 + .../server/PekkoHttpContextFunction.scala | 70 + .../server/PekkoHttpPathMatchers.java | 74 + .../server/PekkoHttpRequestContext.scala | 55 + .../directives/OnSuccessMagnetDirective.java | 26 + .../directives/PekkoExecutionDirectives.java | 32 + .../http/PekkoHttpRoutesTest.java | 1669 +++++++++++++++++ .../http/PekkoResponseWrapperTest.java | 64 + .../org.apache.pekko/http/AsyncApp.scala | 22 + .../org.apache.pekko/http/HttpServer.scala | 57 + .../http/HttpServerRule.scala | 26 + .../http/PekkoHttpTestRoutes.scala | 451 +++++ .../http/StatusCheckActor.scala | 43 + settings.gradle | 1 + 30 files changed, 3882 insertions(+) create mode 100644 instrumentation/apache-pekko-http-2.13_1/build.gradle create mode 100644 instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/Function0Wrapper.java create mode 100644 instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/Function1Wrapper.java create mode 100644 instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/FutureWrapper.java create mode 100644 instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/InboundWrapper.scala create mode 100644 instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/OutboundWrapper.scala create mode 100644 instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/PathMatcherScalaUtils.scala create mode 100644 instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/PathMatcherUtils.java create mode 100644 instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/RequestWrapper.scala create mode 100644 instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/ResponseWrapper.scala create mode 100644 instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/marshalling/PekkoHttpMarshal.java create mode 100644 instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/marshalling/PekkoHttpMarshallerMapper.java create mode 100644 instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/marshalling/PekkoHttpToResponseMarshallable.java create mode 100644 instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/Directive_Instrumentation.java create mode 100644 instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/ImplicitPathMatcherConstruction_Instrumentation.scala create mode 100644 instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/NewRelicRequestContextWrapper.scala create mode 100644 instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/PathMatcher_Instrumentation.scala create mode 100644 instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/PekkoHttpContextFunction.scala create mode 100644 instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/PekkoHttpPathMatchers.java create mode 100644 instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/PekkoHttpRequestContext.scala create mode 100644 instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/directives/OnSuccessMagnetDirective.java create mode 100644 instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/directives/PekkoExecutionDirectives.java create mode 100644 instrumentation/apache-pekko-http-2.13_1/src/test/java/com/agent/instrumentation/org.apache.pekko/http/PekkoHttpRoutesTest.java create mode 100644 instrumentation/apache-pekko-http-2.13_1/src/test/java/com/agent/instrumentation/org.apache.pekko/http/PekkoResponseWrapperTest.java create mode 100644 instrumentation/apache-pekko-http-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/AsyncApp.scala create mode 100644 instrumentation/apache-pekko-http-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/HttpServer.scala create mode 100644 instrumentation/apache-pekko-http-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/HttpServerRule.scala create mode 100644 instrumentation/apache-pekko-http-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/PekkoHttpTestRoutes.scala create mode 100644 instrumentation/apache-pekko-http-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/StatusCheckActor.scala diff --git a/instrumentation/apache-pekko-http-2.13_1/build.gradle b/instrumentation/apache-pekko-http-2.13_1/build.gradle new file mode 100644 index 0000000000..c0bd3216d1 --- /dev/null +++ b/instrumentation/apache-pekko-http-2.13_1/build.gradle @@ -0,0 +1,37 @@ +apply plugin: 'scala' + +isScalaProjectEnabled(project, "scala-2.13") + +sourceSets.test.scala.srcDir "src/test/java" +sourceSets.test.java.srcDirs = [] + +jar { + manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.apache-pekko-http-2.13_1' } +} + +dependencies { + implementation(project(":agent-bridge")) + implementation(project(":newrelic-weaver-api")) + implementation(project(":newrelic-weaver-scala-api")) + implementation("org.apache.pekko:pekko-http_2.13:1.0.1") + implementation("org.apache.pekko:pekko-stream_2.13:1.0.1") + implementation("org.apache.pekko:pekko-actor_2.13:1.0.2") + + testImplementation(project(":instrumentation:apache-pekko-1")) { transitive = false } + testImplementation(project(":instrumentation:scala-2.13.0")) { transitive = false } + testImplementation("com.jayway.restassured:rest-assured:2.7.0") + testImplementation("jakarta.xml.ws:jakarta.xml.ws-api:2.3.3") +} + +verifyInstrumentation { + passesOnly('org.apache.pekko:pekko-http_2.13:[1.0.0,)') { + implementation("org.apache.pekko:pekko-stream_2.13:1.0.0") + } + excludeRegex 'org.apache.pekko:pekko-http_2.13:.*(RC|M)[0-9]*$' + excludeRegex 'org.apache.pekko:pekko-http_2.13:.*-[0-9a-f]{8}$' +} + +site { + title 'Pekko Http' + type 'Framework' +} diff --git a/instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/Function0Wrapper.java b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/Function0Wrapper.java new file mode 100644 index 0000000000..072cd3c1ff --- /dev/null +++ b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/Function0Wrapper.java @@ -0,0 +1,28 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.agent.instrumentation.org.apache.pekko.http; + +import scala.Function0; +import scala.concurrent.Future; +import scala.runtime.AbstractFunction0; + +public class Function0Wrapper extends AbstractFunction0> { + + private final Function0> original; + + public Function0Wrapper(Function0> original) { + this.original = original; + } + + @Override + public Future apply() { + Future result = original.apply(); + return new FutureWrapper<>(result); + } + +} diff --git a/instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/Function1Wrapper.java b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/Function1Wrapper.java new file mode 100644 index 0000000000..346133edb2 --- /dev/null +++ b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/Function1Wrapper.java @@ -0,0 +1,38 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.agent.instrumentation.org.apache.pekko.http; + +import com.newrelic.agent.bridge.AgentBridge; +import com.newrelic.api.agent.Token; +import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.weaver.Weaver; +import scala.Function1; +import scala.runtime.AbstractFunction1; + +public class Function1Wrapper extends AbstractFunction1 { + + private final Function1 original; + private final Token token; + + public Function1Wrapper(Function1 original, Token token) { + this.original = original; + this.token = token; + } + + @Override + @Trace(async = true) + public U apply(T v1) { + try { + token.linkAndExpire(); + } catch (Throwable t) { + AgentBridge.instrumentation.noticeInstrumentationError(t, Weaver.getImplementationTitle()); + } + return original.apply(v1); + } + +} diff --git a/instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/FutureWrapper.java b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/FutureWrapper.java new file mode 100644 index 0000000000..b758e8a506 --- /dev/null +++ b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/FutureWrapper.java @@ -0,0 +1,156 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.agent.instrumentation.org.apache.pekko.http; + +import com.newrelic.agent.bridge.AgentBridge; +import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.weaver.Weaver; +import scala.$less$colon$less; +import scala.Function1; +import scala.Function2; +import scala.Option; +import scala.PartialFunction; +import scala.Tuple2; +import scala.concurrent.Awaitable; +import scala.concurrent.CanAwait; +import scala.concurrent.ExecutionContext; +import scala.concurrent.Future; +import scala.concurrent.duration.Duration; +import scala.reflect.ClassTag; +import scala.util.Try; + +import java.util.concurrent.TimeoutException; + +public class FutureWrapper implements Future { + + private final Future original; + + public FutureWrapper(Future original) { + this.original = original; + } + + @Override + public void onComplete(Function1, U> f, ExecutionContext executor) { + try { + f = new Function1Wrapper<>(f, NewRelic.getAgent().getTransaction().getToken()); + } catch (Throwable t) { + AgentBridge.instrumentation.noticeInstrumentationError(t, Weaver.getImplementationTitle()); + } + original.onComplete(f, executor); + } + + @Override + public boolean isCompleted() { + return original.isCompleted(); + } + + @Override + public Option> value() { + return original.value(); + } + + @Override + public Future failed() { + return original.failed(); + } + + @Override + public void foreach(Function1 f, ExecutionContext executor) { + original.foreach(f, executor); + } + + @Override + public Future transform(Function1 s, Function1 f, ExecutionContext executor) { + return original.transform(s, f, executor); + } + + @Override + public Future transform(Function1, Try> f, ExecutionContext executor) { + return original.transform(f, executor); + } + + @Override + public Future transformWith(Function1, Future> f, ExecutionContext executor) { + return original.transformWith(f, executor); + } + + @Override + public Future map(Function1 f, ExecutionContext executor) { + return original.map(f, executor); + } + + @Override + public Future flatMap(Function1> f, ExecutionContext executor) { + return original.flatMap(f, executor); + } + + @Override + public Future flatten($less$colon$less> ev) { + return original.flatten(ev); + } + + @Override + public Future filter(Function1 p, ExecutionContext executor) { + return original.filter(p, executor); + } + + @Override + public Future withFilter(Function1 p, ExecutionContext executor) { + return original.withFilter(p, executor); + } + + @Override + public Future collect(PartialFunction pf, ExecutionContext executor) { + return original.collect(pf, executor); + } + + @Override + public Future recover(PartialFunction pf, ExecutionContext executor) { + return original.recover(pf, executor); + } + + @Override + public Future recoverWith(PartialFunction> pf, ExecutionContext executor) { + return original.recoverWith(pf, executor); + } + + @Override + public Future> zip(Future that) { + return original.zip(that); + } + + @Override + public Future zipWith(Future that, Function2 f, ExecutionContext executor) { + return original.zipWith(that, f, executor); + } + + @Override + public Future fallbackTo(Future that) { + return original.fallbackTo(that); + } + + @Override + public Future mapTo(ClassTag tag) { + return original.mapTo(tag); + } + + @Override + public Future andThen(PartialFunction, U> pf, ExecutionContext executor) { + return original.andThen(pf, executor); + } + + @Override + public Awaitable ready(Duration atMost, CanAwait permit) throws InterruptedException, TimeoutException { + return original.ready(atMost, permit); + } + + @Override + public T result(Duration atMost, CanAwait permit) throws TimeoutException, InterruptedException { + return original.result(atMost, permit); + } +} diff --git a/instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/InboundWrapper.scala b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/InboundWrapper.scala new file mode 100644 index 0000000000..a4caed79bb --- /dev/null +++ b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/InboundWrapper.scala @@ -0,0 +1,35 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.agent.instrumentation.org.apache.pekko.http + +import org.apache.pekko.http.scaladsl.model.HttpRequest +import com.newrelic.api.agent.{ExtendedInboundHeaders, HeaderType} + +import java.util +import scala.jdk.javaapi.CollectionConverters + +class InboundWrapper(request: HttpRequest) extends ExtendedInboundHeaders { + + def getHeaderType: HeaderType = { + HeaderType.HTTP + } + + def getHeader(name: String): String = { + request.headers.find(header => header.is(name.toLowerCase)).map(header => header.value).orNull + } + + override def getHeaders(name: String): util.List[String] = { + val headers = request.headers.filter(header => header.is(name.toLowerCase)).map(header => header.value) + if (headers.isEmpty) { + return null + } + CollectionConverters.asJava(headers) + } + +} + diff --git a/instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/OutboundWrapper.scala b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/OutboundWrapper.scala new file mode 100644 index 0000000000..fc45cf160a --- /dev/null +++ b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/OutboundWrapper.scala @@ -0,0 +1,28 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.agent.instrumentation.org.apache.pekko.http + +import org.apache.pekko.http.scaladsl.model.HttpResponse +import org.apache.pekko.http.scaladsl.model.headers.RawHeader +import com.newrelic.api.agent.{HeaderType, OutboundHeaders} + +class OutboundWrapper(var response: HttpResponse) extends OutboundHeaders { + + def getHeaderType: HeaderType = { + HeaderType.HTTP + } + + def setHeader(name: String, value: String): Unit = { + response = response.addHeader(new RawHeader(name, value)) + } + + def getHeader(name: String): String = { + response.headers.find(header => header.is(name.toLowerCase)).map(header => header.value).orNull + } + +} diff --git a/instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/PathMatcherScalaUtils.scala b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/PathMatcherScalaUtils.scala new file mode 100644 index 0000000000..75010a3e0c --- /dev/null +++ b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/PathMatcherScalaUtils.scala @@ -0,0 +1,98 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.agent.instrumentation.org.apache.pekko.http + +import org.apache.pekko.http.scaladsl.model.Uri.Path +import org.apache.pekko.http.scaladsl.server.util.Tuple +import org.apache.pekko.http.scaladsl.server.{PathMatcher, PathMatcher0} +import com.newrelic.agent.bridge.AgentBridge +import com.newrelic.api.agent.weaver.Weaver + +import scala.util.matching.Regex + +object PathMatcherScalaUtils { + + def pathMatcher0Wrapper(runBefore: Path => Unit, original: PathMatcher0): PathMatcher0 = { + new PathMatcher[Unit] { + def apply(path: Path): PathMatcher.Matching[Unit] = { + try { + runBefore.apply(path) + } catch { + case t: Throwable => AgentBridge.instrumentation.noticeInstrumentationError(t, Weaver.getImplementationTitle) + } + original.apply(path) + } + } + } + + def pathMatcherWrapper[L](runBefore: Path => Unit, runAfter: (Path, PathMatcher.Matching[L]) => Unit, original: PathMatcher[L])(implicit ev: Tuple[L]): PathMatcher[L] = { + new PathMatcher[L] { + override def apply(path: Path): PathMatcher.Matching[L] = { + try { + runBefore.apply(path) + } catch { + case t: Throwable => AgentBridge.instrumentation.noticeInstrumentationError(t, Weaver.getImplementationTitle) + } + val result = original.apply(path) + try { + runAfter.apply(path, result) + } catch { + case t: Throwable => AgentBridge.instrumentation.noticeInstrumentationError(t, Weaver.getImplementationTitle) + } + result + } + } + } + + def appendNegation(): Path => Unit = { + (path: Path) => PathMatcherUtils.appendNegation() + } + + def appendOptional(): Path => Unit = { + (path: Path) => PathMatcherUtils.appendOptional() + } + + def appendPipe(): Path => Unit = { + (path: Path) => PathMatcherUtils.appendPipe(path) + } + + def appendRegex[L](regex: Regex): (Path, PathMatcher.Matching[L]) => Unit = { + + (path: Path, matching: PathMatcher.Matching[L]) => { + val nrRequestContext = PathMatcherUtils.nrRequestContext.get() + if (matching.isInstanceOf[PathMatcher.Matched[L]] && nrRequestContext != null && !nrRequestContext.regexHolder.contains(regex.toString())) { + PathMatcherUtils.appendRegex(path, regex.pattern.toString, matching) + nrRequestContext.regexHolder.add(regex.toString()) + } + } + } + + def startRepeat(): Path => Unit = { + (path: Path) => PathMatcherUtils.startRepeat(path) + } + + def endRepeat[L](): (Path, PathMatcher.Matching[L]) => Unit = { + (path: Path, matching: PathMatcher.Matching[L]) => { + PathMatcherUtils.endRepeat(path, matching) + } + } + + def appendStaticString[L](prefix: String): (Path, PathMatcher.Matching[L]) => Unit = { + (path: Path, matching: PathMatcher.Matching[L]) => { + PathMatcherUtils.appendStaticString(path, prefix, matching) + } + } + + def emptyFunction1(): Path => Unit = { + (path: Path) => () + } + + def emptyFunction2[L](): (Path, PathMatcher.Matching[L]) => Unit = { + (path: Path, pathMatcher: PathMatcher.Matching[L]) => () + } +} diff --git a/instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/PathMatcherUtils.java b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/PathMatcherUtils.java new file mode 100644 index 0000000000..721aeddbf3 --- /dev/null +++ b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/PathMatcherUtils.java @@ -0,0 +1,430 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.agent.instrumentation.org.apache.pekko.http; + +import org.apache.pekko.http.scaladsl.model.HttpRequest; +import org.apache.pekko.http.scaladsl.model.Uri; +import org.apache.pekko.http.scaladsl.server.Directive; +import org.apache.pekko.http.scaladsl.server.NewRelicRequestContextWrapper; +import org.apache.pekko.http.scaladsl.server.PathMatcher; +import org.apache.pekko.http.scaladsl.server.RequestContext; +import org.apache.pekko.http.scaladsl.server.RequestContextImpl; +import org.apache.pekko.http.scaladsl.server.RouteResult; +import org.apache.pekko.http.scaladsl.server.util.Tuple; +import com.newrelic.agent.bridge.AgentBridge; +import scala.Function1; +import scala.concurrent.Future; +import scala.runtime.AbstractFunction1; + +import java.util.Deque; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.logging.Level; + +public class PathMatcherUtils { + + /** + * The purpose of this initializer is to hook into a place that's only called once during initialization of the + * pekko-http library so we can work around an issue where our agent fails to transform the RequestContext class. + */ + static { + AgentBridge.getAgent().getLogger().log(Level.FINER, "Retransforming org.apache.pekko.http.scaladsl.server.RequestContextImpl"); + AgentBridge.instrumentation.retransformUninstrumentedClass(RequestContextImpl.class); + AgentBridge.getAgent().getLogger().log(Level.FINER, "Retransformed org.apache.pekko.http.scaladsl.server.RequestContextImpl"); + AgentBridge.getAgent().getLogger().log(Level.FINER, "Retransforming org.apache.pekko.http.scaladsl.server.NewRelicRequestContextWrapper"); + AgentBridge.instrumentation.retransformUninstrumentedClass(NewRelicRequestContextWrapper.class); + AgentBridge.getAgent().getLogger().log(Level.FINER, "Retransformed org.apache.pekko.http.scaladsl.server.NewRelicRequestContextWrapper"); + } + + public static final Class matchedClass = PathMatcher.Matched.class; + public static final Class unmatchedClass = PathMatcher.Unmatched$.class; + + // This allows us to get access to the request context (where the assembled path information is stored) + public static final ThreadLocal nrRequestContext = + new ThreadLocal() { + @Override + protected NewRelicRequestContextWrapper initialValue() { + return null; + } + }; + + /** + * When we have a match on a portion of the path, this method appends one of the numeric types to the path. This + * helps to prevent a metric explosion by replacing potentially dynamic values with static strings. + * + * @param numberMatchType the numeric path type ("IntNumber", "HexIntNumber", etc) + * @param path the current path match to append to + * @param matching the type of match (Matched or Unmatched) + */ + public static void appendNumberMatch(String numberMatchType, Uri.Path path, PathMatcher.Matching matching) { + if (matching.getClass().isAssignableFrom(matchedClass)) { + insertPathValue(numberMatchType); + } else if (matching.getClass().isAssignableFrom(unmatchedClass)) { + handleUnmatched(path, numberMatchType); + } + } + + /** + * Add any "Segment" matches to the path. + * + * @param path the current path match to append to + * @param matching the type of match (Matched or Unmatched) + */ + public static void appendSegment(Uri.Path path, PathMatcher.Matching matching) { + if (matching.getClass().isAssignableFrom(matchedClass)) { + insertPathValue("Segment"); + } else if (matching.getClass().isAssignableFrom(unmatchedClass)) { + handleUnmatched(path, "Segment"); + } + } + + /** + * Mark the start of a repeating pattern + */ + public static void startRepeat(Uri.Path path) { + NewRelicRequestContextWrapper ctx = nrRequestContext.get(); + if (ctx != null && !ctx.divertRepeat().get()) { + ctx.divertRepeat().set(true); + + Deque pathQueue = ctx.matchedPath(); + pathQueue.offer("("); + } + } + + /** + * Mark the end of the repeating pattern by copying the temporary "repeatHolder" pattern into the main + * matched path. A repeat pattern will look something like this: + *

+ * "(IntNumber/).repeat()" + * + * @param path the current path match to append to + * @param matching the type of match (Matched or Unmatched) + */ + public static void endRepeat(Uri.Path path, PathMatcher.Matching matching) { + final int MAX_APPENDED_SEGMENTS_TO_INCLUDE = 2; + + NewRelicRequestContextWrapper ctx = nrRequestContext.get(); + if (ctx == null) { + return; + } + ctx.divertRepeat().set(false); + + if (matching instanceof PathMatcher.Matched) { + PathMatcher.Matched> matched = (PathMatcher.Matched) matching; + Deque repeatPathQueue = ctx.repeatHolder(); + Deque pathQueue = ctx.matchedPath(); + for (int i = 0; i < MAX_APPENDED_SEGMENTS_TO_INCLUDE; i++) { + String queueResult = repeatPathQueue.pollFirst(); + if (queueResult != null) { + pathQueue.add(queueResult); + } + } + pathQueue.offer(").repeat()"); + } + + ctx.repeatHolder(new LinkedBlockingDeque()); + } + + /** + * Add any static string matches to the path. This will be something like "/foo" or "/bar" + * + * @param path the current path match to append to + * @param prefix the matched static prefix (hardcoded string) + * @param matching the type of match (Matched or Unmatched) + */ + public static void appendStaticString(Uri.Path path, String prefix, PathMatcher.Matching matching) { + if (matching.getClass().isAssignableFrom(matchedClass)) { + insertPathValue(prefix); + } else if (matching.getClass().isAssignableFrom(unmatchedClass)) { + handleUnmatched(path, prefix); + } + } + + /** + * Add any regex matches to the path + * + * @param path the current path match to append to + * @param regexPattern the matched regex mattern + * @param matching the type of match (Matched or Unmatched) + */ + public static void appendRegex(Uri.Path path, String regexPattern, PathMatcher.Matching matching) { + if (matching.getClass().isAssignableFrom(matchedClass)) { + insertPathValue(regexPattern); + } else if (matching.getClass().isAssignableFrom(unmatchedClass)) { + handleUnmatched(path, regexPattern); + } + } + + /** + * Adds a negation ("!") to the path + */ + public static void appendNegation() { + insertPathValue("!"); + } + + /** + * Adds a tilde ("~") to the path which represents a concatenation. There are a couple of special cases here for ignoring + * the tilde if it's the first part of the path, if the previous item added was a slash or if we are in the middle of a repeat. + * This helps clear up the final path value and get it as close to what the user entered as possible. + * + * @param path the current path match to append to + */ + public static void appendTilde(Uri.Path path) { + Deque pathQueue = getPathQueue(); + + NewRelicRequestContextWrapper ctx = nrRequestContext.get(); + if (pathQueue.isEmpty() && ctx != null && ctx.divertRepeat().get()) { + // We are in the middle of a repeating segment, we don't want these to show up as tildes (~) + return; + } + + if (pathQueue.isEmpty() || !pathQueue.peekLast().equals("/")) { + insertPathValue("~"); + } + } + + /** + * Adds a pipe ("|") to the path + * + * @param path the current path match to append to + */ + public static void appendPipe(Uri.Path path) { + insertPathValue("|"); + } + + /** + * Adds an optional ("?") to the path + */ + public static void appendOptional() { + insertPathValue("?"); + } + + /** + * Adds a Slash ("/") to the path + * + * @param path the current path match to append to + */ + public static void appendSlash(Uri.Path path, PathMatcher.Matching matching) { + if (matching.getClass().isAssignableFrom(matchedClass)) { + Deque pathQueue = getPathQueue(); + + // Special case to help clean up extra tildes + if (!pathQueue.isEmpty() && pathQueue.peekLast().equals("~")) { + pathQueue.removeLast(); + } + insertPathValue("/"); + } else if (!path.isEmpty() && matching.getClass().isAssignableFrom(unmatchedClass)) { + handleUnmatched(path, "/"); + } + } + + /** + * Adds a "Remaining" or "RemainingPath" to the end of the path to correspond to that matcher. + * + * @param type the type of "Remaining" match + * @param path the current path match to append to + * @param matched the type of match (Matched) + */ + public static void appendRemaining(String type, Uri.Path path, PathMatcher.Matched matched) { + insertPathValue(type); + } + + /** + * This is used when a specific path partially matches and we have more to check via a tilde concatenation. If the + * secondary match fails we need to handle the unmatch and clear out the path. + * + * @param matching the type of match. + */ + public static void andThen(PathMatcher.Matching matching, Uri.Path pathRest) { + if (matching != null && matching.getClass().isAssignableFrom(unmatchedClass)) { + handleUnmatched(pathRest, null); + } else { + NewRelicRequestContextWrapper ctx = nrRequestContext.get(); + if (ctx != null) { + // We had a successful Match, record the current queue length on the context + ctx.currentMatchedQueueLength().set(getPathQueue().size()); + } + } + } + + /** + * Finishes the current path by gathering up all of the stored values in the Deque and building up a string + * to use as a the transaction name. + * + * @param ctx the wrapped RequestContext holding the path information + * @return the transaction name for this path + */ + public static String finishPathAndGetTransactionName(NewRelicRequestContextWrapper ctx) { + Deque pathElements = ctx.matchedPath(); + + StringBuilder finalPath = new StringBuilder(); + if (pathElements == null) { + finalPath.append("Unknown Route"); + } else { + // First, do some cleanup + if (!pathElements.isEmpty() && pathElements.peekLast().equals("~")) { + pathElements.removeLast(); + } + if (!pathElements.isEmpty() && !pathElements.peekFirst().equals("/")) { + pathElements.addFirst("/"); + } + + for (String pathElement; (pathElement = pathElements.poll()) != null; ) { + // More cleanup + if (pathElement.equals("|")) { + continue; + } + + finalPath.append(pathElement); + } + + pathElements.clear(); + } + + String finalPathString = finalPath.toString(); + return finalPathString.isEmpty() ? "Unknown Route" : finalPathString; + } + + /** + * Handles inserting the new path value into the Deque as well as special-case logic for optionals + * + * @param pathValue the value to insert + */ + private static void insertPathValue(String pathValue) { + Deque pathQueue = getPathQueue(); + if (!pathQueue.isEmpty() && pathQueue.peekLast().equals("!")) { + // The result during a "negation" matched, which means this shouldn't match + pathQueue.clear(); + return; + } + + boolean previousOptional = !pathQueue.isEmpty() && pathQueue.peekLast().equals("?"); + if (previousOptional) { + pathQueue.removeLast(); + } + pathQueue.offer(pathValue); + if (previousOptional) { + pathQueue.offer(".?"); + } + } + + private static Deque getPathQueue() { + NewRelicRequestContextWrapper ctx = nrRequestContext.get(); + if (ctx != null) { + if (ctx.divertRepeat().get()) { + return ctx.repeatHolder(); + } + return ctx.matchedPath(); + } + return new LinkedBlockingDeque<>(); + } + + /** + * Since we came across an "unmatched" that means we'll need to erase our previous progress as it didn't match + * anything. The one special case here is if the previous operator was a negation ("!") and in that case we + * want to continue attempting to match. + * + * @param path the current path that failed to match + * @param prefix in the case of a negation this is the value following it (the value to be negated) + */ + private static void handleUnmatched(Uri.Path path, String prefix) { + NewRelicRequestContextWrapper ctx = nrRequestContext.get(); + if (ctx != null) { + Deque pathQueue = getPathQueue(); + if (!pathQueue.isEmpty()) { + if (pathQueue.peekLast().equals("!")) { + pathQueue.offer(prefix); // !{prefix} + } else if (pathQueue.peekLast().equals("|")) { + // Pipe ("|") here means that the first match failed, but the second might + // not so we want to remove the pipe marker and let the match continue + pathQueue.removeLast(); + } else if (pathQueue.peekLast().equals("?")) { + pathQueue.removeLast(); + pathQueue.offer(prefix); + pathQueue.offer(".?"); + } else if (pathQueue.peekLast().equals("~") && !path.isEmpty()) { + // If we got here, it means that we matched something and it is a concatenation so we may have paths + // to remove in order to get back to a last known "matching" state. + int expectedQueueSize = ctx.currentMatchedQueueLength().get() + 1; // Include the tilde (~) + + // If the expected queue size is the same as the current path, they it means we have paths to remove + // so we need to set the current matched queue length back to the previous known match. Otherwise, + // we can move forward by setting the previous size equal to the new current size + if (expectedQueueSize == getPathQueue().size()) { + ctx.currentMatchedQueueLength().set(ctx.previousMatchedQueueLength().get()); + } else { + ctx.previousMatchedQueueLength().set(ctx.currentMatchedQueueLength().get()); + } + + for (int i = getPathQueue().size(); i > expectedQueueSize - 1; i--) { + pathQueue.removeLast(); + } + + } else if (prefix == null) { + for (int i = getPathQueue().size(); i > ctx.previousMatchedQueueLength().get(); i--) { + pathQueue.removeLast(); + } + } else if (ctx.divertRepeat().get()) { + // If we are in the middle of a repeat match, an "unmatched" is not an issue + return; + } else { + int matchedQueueSize = ctx.currentMatchedQueueLength().get(); + int previousQueueSize = ctx.previousMatchedQueueLength().get(); + int expectedQueueSize = matchedQueueSize + 1; // Includes the trailing tilde or slash + if (matchedQueueSize > 0 && previousQueueSize == 0 && pathQueue.size() == expectedQueueSize) { + // This case is here to handle where we've matched a pathPrefix, a sub path has failed but we have more paths we want to check. + // If we just cleared out the queue here we would either get an UnknownRoute or an incorrect route. Setting the + // previousMatchedQueueLength here ensures that we keep the pathPrefix for additional checks + ctx.previousMatchedQueueLength().set(ctx.currentMatchedQueueLength().get()); + } else { + pathQueue.clear(); + ctx.regexHolder().clear(); + } + } + } else { + pathQueue.clear(); + ctx.regexHolder().clear(); + } + } + } + + public static void reset() { + nrRequestContext.remove(); + } + + public static void setHttpRequest(HttpRequest request) { + AgentBridge.getAgent().getTransaction().setWebRequest(new RequestWrapper(request)); + } + + public static class DirectiveWrapper extends Directive { + + private final Directive underlying; + + public DirectiveWrapper(Tuple ev, Directive underlying) { + super(ev); + this.underlying = underlying; + + // Remove the current request context since we may be switching threads in this directive + nrRequestContext.remove(); + } + + @Override + public Function1> tapply(Function1>> f) { + Function1> result = underlying.tapply(f); + return result.compose(new AbstractFunction1() { + @Override + public RequestContext apply(RequestContext requestContext) { + if (requestContext instanceof NewRelicRequestContextWrapper) { + // If we have a New Relic wrapped RequestContext we should set this back into the thread local so we can use it after this directive + nrRequestContext.set(((NewRelicRequestContextWrapper) requestContext)); + } + return requestContext; + } + }); + } + } +} diff --git a/instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/RequestWrapper.scala b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/RequestWrapper.scala new file mode 100644 index 0000000000..092067ae84 --- /dev/null +++ b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/RequestWrapper.scala @@ -0,0 +1,62 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.agent.instrumentation.org.apache.pekko.http + +import org.apache.pekko.http.scaladsl.model.HttpRequest +import com.newrelic.api.agent.{ExtendedRequest, HeaderType} + +import java.util +import scala.jdk.javaapi.CollectionConverters + +class RequestWrapper(request: HttpRequest) extends ExtendedRequest { + + def getMethod: String = { + request.method.name + } + + def getRequestURI: String = { + request.uri.path.toString() + } + + def getRemoteUser: String = { + null + } + + def getParameterNames: util.Enumeration[_] = { + CollectionConverters.asJavaEnumeration(request.uri.query().toMap.keysIterator) + } + + def getParameterValues(name: String): Array[String] = { + request.uri.query().getAll(name).toArray + } + + def getAttribute(name: String): AnyRef = { + null + } + + def getCookieValue(name: String): String = { + request.cookies.find(cookie => cookie.name.equalsIgnoreCase(name)).map(cookie => cookie.value).orNull + } + + def getHeaderType: HeaderType = { + HeaderType.HTTP + } + + def getHeader(name: String): String = { + request.headers.find(header => header.is(name.toLowerCase)).map(header => header.value).orNull + } + + override def getHeaders(name: String): util.List[String] = { + val headers = request.headers.filter(header => header.is(name.toLowerCase)).map(header => header.value) + if (headers.isEmpty) { + return null + } + CollectionConverters.asJava(headers) + } + +} diff --git a/instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/ResponseWrapper.scala b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/ResponseWrapper.scala new file mode 100644 index 0000000000..08180d1874 --- /dev/null +++ b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/com/agent/instrumentation/org.apache.pekko/http/ResponseWrapper.scala @@ -0,0 +1,44 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.agent.instrumentation.org.apache.pekko.http + +import org.apache.pekko.http.scaladsl.model.HttpResponse +import org.apache.pekko.http.scaladsl.model.headers.RawHeader +import com.newrelic.api.agent.{ExtendedResponse, HeaderType} + +class ResponseWrapper(var response: HttpResponse) extends ExtendedResponse { + + def getStatus: Int = { + response.status.intValue + } + + def getStatusMessage: String = { + response.status.reason + } + + def getContentType: String = { + response.entity.contentType.value + } + + def getHeaderType: HeaderType = { + HeaderType.HTTP + } + + def setHeader(name: String, value: String): Unit = { + response = response.addHeader(new RawHeader(name, value)) + } + + def getContentLength: Long = { + val contentLength = response.getHeader("Content-Length") + if (contentLength.isPresent) { + return contentLength.get().value().toLong + } + response.entity.getContentLengthOption().orElse(-1L) + } + +} diff --git a/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/marshalling/PekkoHttpMarshal.java b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/marshalling/PekkoHttpMarshal.java new file mode 100644 index 0000000000..83464dab55 --- /dev/null +++ b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/marshalling/PekkoHttpMarshal.java @@ -0,0 +1,29 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package org.apache.pekko.http.scaladsl.marshalling; + +import org.apache.pekko.http.scaladsl.model.HttpRequest; +import org.apache.pekko.http.scaladsl.model.HttpResponse; +import com.agent.instrumentation.org.apache.pekko.http.PathMatcherUtils; +import com.agent.instrumentation.org.apache.pekko.http.RequestWrapper; +import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import scala.concurrent.ExecutionContext; +import scala.concurrent.Future; + +@Weave(originalName = "org.apache.pekko.http.scaladsl.marshalling.Marshal") +public class PekkoHttpMarshal { + + public Future toResponseFor(HttpRequest request, Marshaller m, ExecutionContext ec) { + NewRelic.getAgent().getTransaction().setWebRequest(new RequestWrapper(request)); + PathMatcherUtils.reset(); + return Weaver.callOriginal(); + } + +} diff --git a/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/marshalling/PekkoHttpMarshallerMapper.java b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/marshalling/PekkoHttpMarshallerMapper.java new file mode 100644 index 0000000000..c00327caf2 --- /dev/null +++ b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/marshalling/PekkoHttpMarshallerMapper.java @@ -0,0 +1,48 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package org.apache.pekko.http.scaladsl.marshalling; + +import org.apache.pekko.http.scaladsl.model.HttpResponse; +import com.agent.instrumentation.org.apache.pekko.http.ResponseWrapper; +import com.newrelic.agent.bridge.AgentBridge; +import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.Token; +import com.newrelic.api.agent.Trace; +import com.newrelic.api.agent.Transaction; +import com.newrelic.api.agent.weaver.Weaver; +import scala.runtime.AbstractFunction1; + +public class PekkoHttpMarshallerMapper extends AbstractFunction1 { + + private final Token token; + + public PekkoHttpMarshallerMapper(Token token) { + this.token = token; + } + + @Override + @Trace(async = true) + public HttpResponse apply(HttpResponse httpResponse) { + try { + if (token != null) { + token.linkAndExpire(); + } + ResponseWrapper responseWrapper = new ResponseWrapper(httpResponse); + Transaction transaction = NewRelic.getAgent().getTransaction(); + transaction.setWebResponse(responseWrapper); + transaction.addOutboundResponseHeaders(); + transaction.markResponseSent(); + + return responseWrapper.response(); + } catch (Throwable t) { + AgentBridge.instrumentation.noticeInstrumentationError(t, Weaver.getImplementationTitle()); + return httpResponse; + } + } + +} diff --git a/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/marshalling/PekkoHttpToResponseMarshallable.java b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/marshalling/PekkoHttpToResponseMarshallable.java new file mode 100644 index 0000000000..f56f696c18 --- /dev/null +++ b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/marshalling/PekkoHttpToResponseMarshallable.java @@ -0,0 +1,29 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package org.apache.pekko.http.scaladsl.marshalling; + +import org.apache.pekko.http.scaladsl.model.HttpResponse; +import com.newrelic.api.agent.Token; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.NewField; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; + +@Weave(type = MatchType.Interface, originalName = "org.apache.pekko.http.scaladsl.marshalling.ToResponseMarshallable") +public abstract class PekkoHttpToResponseMarshallable { + + @NewField + public Token token; + + public Marshaller marshaller() { + Marshaller marshaller = Weaver.callOriginal(); + PekkoHttpMarshallerMapper pekkoHttpMarshallerMapper = new PekkoHttpMarshallerMapper(token); + return marshaller.map(pekkoHttpMarshallerMapper); + } + +} diff --git a/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/Directive_Instrumentation.java b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/Directive_Instrumentation.java new file mode 100644 index 0000000000..b8a40520a9 --- /dev/null +++ b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/Directive_Instrumentation.java @@ -0,0 +1,27 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package org.apache.pekko.http.scaladsl.server; + +import org.apache.pekko.http.scaladsl.server.util.Tuple; +import com.agent.instrumentation.org.apache.pekko.http.PathMatcherUtils; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import scala.Function1; +import scala.concurrent.Future; + +@Weave(type = MatchType.BaseClass, originalName = "org.apache.pekko.http.scaladsl.server.Directive$") +public abstract class Directive_Instrumentation { + + public Directive apply(Function1>>, + Function1>> function1, Tuple tuple) { + // Wrap any Directives that we see in order to propagate our request context properly + return new PathMatcherUtils.DirectiveWrapper<>(tuple, (Directive) Weaver.callOriginal()); + } + +} diff --git a/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/ImplicitPathMatcherConstruction_Instrumentation.scala b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/ImplicitPathMatcherConstruction_Instrumentation.scala new file mode 100644 index 0000000000..065e6d688a --- /dev/null +++ b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/ImplicitPathMatcherConstruction_Instrumentation.scala @@ -0,0 +1,20 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package org.apache.pekko.http.scaladsl.server + +import com.agent.instrumentation.org.apache.pekko.http.PathMatcherScalaUtils +import com.newrelic.api.agent.weaver.{MatchType, Weave, Weaver} + +import scala.util.matching.Regex + +@Weave(`type` = MatchType.BaseClass, originalName = "org.apache.pekko.http.scaladsl.server.ImplicitPathMatcherConstruction") +abstract class ImplicitPathMatcherConstruction_Instrumentation { + implicit def _regex2PathMatcher(regex: Regex): PathMatcher1[String] = { + PathMatcherScalaUtils.pathMatcherWrapper(PathMatcherScalaUtils.emptyFunction1(), PathMatcherScalaUtils.appendRegex(regex), Weaver.callOriginal()) + } +} diff --git a/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/NewRelicRequestContextWrapper.scala b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/NewRelicRequestContextWrapper.scala new file mode 100644 index 0000000000..687883cd0c --- /dev/null +++ b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/NewRelicRequestContextWrapper.scala @@ -0,0 +1,117 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package org.apache.pekko.http.scaladsl.server + +import org.apache.pekko.event.LoggingAdapter +import org.apache.pekko.http.scaladsl.marshalling.ToResponseMarshallable +import org.apache.pekko.http.scaladsl.model.{HttpRequest, Uri} +import org.apache.pekko.http.scaladsl.settings.{ParserSettings, RoutingSettings} +import org.apache.pekko.stream.Materializer +import com.agent.instrumentation.org.apache.pekko.http.PathMatcherUtils +import com.newrelic.agent.bridge.{AgentBridge, Token} +import com.newrelic.api.agent.{Trace, TransactionNamePriority} + +import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger} +import scala.collection.mutable +import scala.concurrent.{ExecutionContextExecutor, Future} + +class NewRelicRequestContextWrapper(originalRequestContext: Object, + underlyingRequestContext: RequestContextImpl, + var token: Token, + var matchedPath: java.util.Deque[String], + var divertRepeat: AtomicBoolean, + var currentMatchedQueueLength: AtomicInteger, + var previousMatchedQueueLength: AtomicInteger, + var repeatHolder: java.util.Deque[String], + var regexHolder: mutable.Set[String], + request: HttpRequest, + unmatchedPath: Uri.Path, + executionContext: ExecutionContextExecutor, + materializer: Materializer, + log: LoggingAdapter, + settings: RoutingSettings, + parserSettings: ParserSettings) + extends RequestContextImpl(request, unmatchedPath, executionContext, materializer, log, settings, parserSettings) { + PathMatcherUtils.nrRequestContext.set(this) + originalRequestContext match { + case wrapper: NewRelicRequestContextWrapper => + token(wrapper.token) + matchedPath(wrapper.matchedPath) + divertRepeat(wrapper.divertRepeat) + currentMatchedQueueLength(wrapper.currentMatchedQueueLength) + previousMatchedQueueLength(wrapper.previousMatchedQueueLength) + repeatHolder(wrapper.repeatHolder) + regexHolder(wrapper.regexHolder) + case _ => + } + + override def reconfigure(executionContext: ExecutionContextExecutor, materializer: Materializer, log: LoggingAdapter, + settings: RoutingSettings): RequestContext = { + underlyingRequestContext.reconfigure(executionContext, materializer, log, settings) + } + + //INCOMPLETE --- COME BACK LATER + + @Trace(async = true) + override def complete(trm: ToResponseMarshallable): Future[RouteResult] = { + try { + if (token != null) { + val transactionName = PathMatcherUtils.finishPathAndGetTransactionName(this) + token.getTransaction.setTransactionName(TransactionNamePriority.FRAMEWORK_HIGH, false, "PekkoHttp", transactionName) + token.link() + } + + underlyingRequestContext.complete(trm).map(result => { + completeResponse(token) + result + })(executionContext) + } catch { + case t: Throwable => AgentBridge.instrumentation.noticeInstrumentationError(t, "pekko-http-2.4.5") + underlyingRequestContext.complete(trm) + } + } + + @Trace(async = true) + def completeResponse(token: Token): Unit = { + try { + if (token != null) { + token.linkAndExpire() + } + } catch { + case t: Throwable => AgentBridge.instrumentation.noticeInstrumentationError(t, "pekko-http-2.4.5") + } + } + + def token(token: Token): Unit = { + this.token = token + } + + def matchedPath(matchedPath: java.util.Deque[String]): Unit = { + this.matchedPath = matchedPath + } + + def divertRepeat(divertRepeat: AtomicBoolean): Unit = { + this.divertRepeat = divertRepeat + } + + def currentMatchedQueueLength(currentMatchedQueueLength: AtomicInteger): Unit = { + this.currentMatchedQueueLength = currentMatchedQueueLength + } + + def previousMatchedQueueLength(previousMatchedQueueLength: AtomicInteger): Unit = { + this.previousMatchedQueueLength = previousMatchedQueueLength + } + + def repeatHolder(repeatHolder: java.util.Deque[String]): Unit = { + this.repeatHolder = repeatHolder + } + + def regexHolder(regexHolder: mutable.Set[String]): Unit = { + this.regexHolder = regexHolder + } +} diff --git a/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/PathMatcher_Instrumentation.scala b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/PathMatcher_Instrumentation.scala new file mode 100644 index 0000000000..e15ccf2071 --- /dev/null +++ b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/PathMatcher_Instrumentation.scala @@ -0,0 +1,66 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package org.apache.pekko.http.scaladsl.server + +import org.apache.pekko.http.scaladsl.model.Uri.Path +import org.apache.pekko.http.scaladsl.server.PathMatcher.Matching +import org.apache.pekko.http.scaladsl.server.util.Tuple +import com.agent.instrumentation.org.apache.pekko.http.{PathMatcherScalaUtils, PathMatcherUtils} +import com.newrelic.api.agent.weaver.{MatchType, Weave, Weaver} + +@Weave(`type` = MatchType.BaseClass, originalName = "org.apache.pekko.http.scaladsl.server.PathMatcher") +abstract class PathMatcher_Instrumentation[L] { + + def ev: Tuple[L] = Weaver.callOriginal() + + def |[R >: L : Tuple](other: PathMatcher[_ <: R]): PathMatcher[R] = { + PathMatcherScalaUtils.pathMatcherWrapper(PathMatcherScalaUtils.appendPipe(), PathMatcherScalaUtils.emptyFunction2(), Weaver.callOriginal()) + } + + def unary_!(): PathMatcher0 = { + val result: PathMatcher0 = Weaver.callOriginal() + PathMatcherScalaUtils.pathMatcher0Wrapper(PathMatcherScalaUtils.appendNegation(), result) + } + + def repeat(min: Int, max: Int, separator: PathMatcher0 = PathMatchers.Neutral)(implicit lift: PathMatcher.Lift[L, List]): PathMatcher[lift.Out] = { + PathMatcherScalaUtils.pathMatcherWrapper(PathMatcherScalaUtils.startRepeat(), PathMatcherScalaUtils.endRepeat(), + Weaver.callOriginal())(lift.OutIsTuple) + } +} + +@Weave(`type` = MatchType.ExactClass, originalName = "org.apache.pekko.http.scaladsl.server.PathMatcher$") +abstract class PathMatcherObject_Instrumentation { + def apply[L](prefix: Path, extractions: L, evidence: Tuple[L]): PathMatcher[L] = { + PathMatcherScalaUtils.pathMatcherWrapper(PathMatcherScalaUtils.emptyFunction1(), PathMatcherScalaUtils.appendStaticString(prefix.toString()), + Weaver.callOriginal())(evidence) + } +} + +@Weave(`type` = MatchType.ExactClass, originalName = "org.apache.pekko.http.scaladsl.server.PathMatcher$EnhancedPathMatcher") +abstract class EnhancedPathMatcher[L](underlying: PathMatcher_Instrumentation[L]) { + def ?(implicit lift: PathMatcher.Lift[L, Option]): PathMatcher[lift.Out] = { + val result: PathMatcher[lift.Out] = Weaver.callOriginal() + PathMatcherScalaUtils.pathMatcherWrapper(PathMatcherScalaUtils.appendOptional(), PathMatcherScalaUtils.emptyFunction2(), result)(lift.OutIsTuple) + } +} + +@Weave(`type` = MatchType.ExactClass, originalName = "org.apache.pekko.http.scaladsl.server.PathMatcher$Matched") +class Matched_Instrumentation[L: Tuple](path: Path, extractions: L) { + + def pathRest: Path = { + Weaver.callOriginal() + } + + def andThen[R: Tuple](f: (Path, L) ⇒ Matching[R]): Matching[R] = { + PathMatcherUtils.appendTilde(null) + val returnValue = Weaver.callOriginal.asInstanceOf[Matching[R]] + PathMatcherUtils.andThen(returnValue, pathRest) + returnValue + } + +} diff --git a/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/PekkoHttpContextFunction.scala b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/PekkoHttpContextFunction.scala new file mode 100644 index 0000000000..1f7d705917 --- /dev/null +++ b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/PekkoHttpContextFunction.scala @@ -0,0 +1,70 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package org.apache.pekko.http.scaladsl.server + +import com.agent.instrumentation.org.apache.pekko.http.PathMatcherUtils +import com.newrelic.agent.bridge.AgentBridge +import com.newrelic.api.agent.Trace + +import java.util.concurrent.LinkedBlockingDeque +import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger} +import java.util.logging.Level +import scala.collection.mutable +import scala.concurrent.Future +import scala.runtime.AbstractFunction1 + +object PekkoHttpContextFunction { + + final val retransformed = new AtomicBoolean(false) + + def contextWrapper(original: Function1[RequestContext, Future[RouteResult]]): Function1[RequestContext, Future[RouteResult]] = { + if (retransformed.compareAndSet(false, true)) { + AgentBridge.getAgent.getLogger.log(Level.FINER, "Retransforming org.apache.pekko.http.scaladsl.server.PekkoHttpContextFunction") + AgentBridge.instrumentation.retransformUninstrumentedClass(classOf[ContextWrapper]) + AgentBridge.getAgent.getLogger.log(Level.FINER, "Retransformed org.apache.pekko.http.scaladsl.server.PekkoHttpContextFunction") + } + + new ContextWrapper(original) + } + +} + +// REVISIT THIS AND UNPACK IT + +class ContextWrapper(original: Function1[RequestContext, Future[RouteResult]]) extends AbstractFunction1[RequestContext, Future[RouteResult]] { + + @Trace(dispatcher = true) + override def apply(ctx: RequestContext): Future[RouteResult] = { + try { + val tracedMethod = AgentBridge.getAgent.getTracedMethod + tracedMethod.setMetricName("PekkoHttp") + // Pekko-http 10.1.5 uses CallbackRunnable and we lose transaction context between Directives + AgentBridge.getAgent.getTracedMethod.setTrackCallbackRunnable(true); + val token = AgentBridge.getAgent.getTransaction(false).getToken + PathMatcherUtils.setHttpRequest(ctx.request) + // We use this method to wire up our RequestContext wrapper and start our transaction + val newCtx = new NewRelicRequestContextWrapper(ctx, ctx.asInstanceOf[RequestContextImpl], token, + new LinkedBlockingDeque[String], new AtomicBoolean(false), new AtomicInteger(0), new AtomicInteger(0), + new LinkedBlockingDeque[String], new mutable.HashSet[String], ctx.request, ctx.unmatchedPath, ctx.executionContext, ctx.materializer, + ctx.log, ctx.settings, ctx.parserSettings) + original.apply(newCtx) + } catch { + case t: Throwable => { + AgentBridge.instrumentation.noticeInstrumentationError(t, "pekko-http-2.4.5") + original.apply(ctx) + } + } + } + + override def compose[A](g: (A) => RequestContext): (A) => Future[RouteResult] = original.compose(g) + + override def andThen[A](g: (Future[RouteResult]) => A): (RequestContext) => A = original.andThen(g) + + override def toString(): String = original.toString() + +} diff --git a/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/PekkoHttpPathMatchers.java b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/PekkoHttpPathMatchers.java new file mode 100644 index 0000000000..869efae4d5 --- /dev/null +++ b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/PekkoHttpPathMatchers.java @@ -0,0 +1,74 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package org.apache.pekko.http.scaladsl.server; + +import org.apache.pekko.http.scaladsl.model.Uri; +import com.agent.instrumentation.org.apache.pekko.http.PathMatcherUtils; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import scala.Tuple1; +import scala.runtime.BoxedUnit; + +@Weave(type = MatchType.ExactClass, originalName = "org.apache.pekko.http.scaladsl.server.PathMatchers") +public class PekkoHttpPathMatchers { + + @Weave(type = MatchType.ExactClass, originalName = "org.apache.pekko.http.scaladsl.server.PathMatchers$Slash$") + public static class PekkoHttpSlash$ { + + public PathMatcher.Matching apply(final Uri.Path path) { + PathMatcher.Matching matching = Weaver.callOriginal(); + PathMatcherUtils.appendSlash(path, matching); + return matching; + } + + } + + @Weave(type = MatchType.ExactClass, originalName = "org.apache.pekko.http.scaladsl.server.PathMatchers$Remaining$") + public static class PekkoHttpRemaining$ { + + public PathMatcher.Matched> apply(final Uri.Path path) { + PathMatcher.Matched> matched = Weaver.callOriginal(); + PathMatcherUtils.appendRemaining("Remaining", path, matched); + return matched; + } + + } + + @Weave(type = MatchType.ExactClass, originalName = "org.apache.pekko.http.scaladsl.server.PathMatchers$RemainingPath$") + public static class PekkoHttpRemainingPath$ { + + public PathMatcher.Matched> apply(final Uri.Path path) { + PathMatcher.Matched> matched = Weaver.callOriginal(); + PathMatcherUtils.appendRemaining("RemainingPath", path, matched); + return matched; + } + + } + + @Weave(type = MatchType.BaseClass, originalName = "org.apache.pekko.http.scaladsl.server.PathMatchers$NumberMatcher") + public static class PekkoHttpNumberMatcher { + + public PathMatcher.Matching> apply(final Uri.Path path) { + PathMatcher.Matching> matching = Weaver.callOriginal(); + PathMatcherUtils.appendNumberMatch(getClass().getSimpleName().replaceAll("\\$", ""), path, matching); + return matching; + } + + } + + @Weave(type = MatchType.ExactClass, originalName = "org.apache.pekko.http.scaladsl.server.PathMatchers$Segment$") + public static class PekkoHttpSegment$ { + + public PathMatcher.Matching> apply(final Uri.Path path) { + PathMatcher.Matching> matching = Weaver.callOriginal(); + PathMatcherUtils.appendSegment(path, matching); + return matching; + } + } +} diff --git a/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/PekkoHttpRequestContext.scala b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/PekkoHttpRequestContext.scala new file mode 100644 index 0000000000..1b22184b60 --- /dev/null +++ b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/PekkoHttpRequestContext.scala @@ -0,0 +1,55 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package org.apache.pekko.http.scaladsl.server + +import org.apache.pekko.event.LoggingAdapter +import org.apache.pekko.http.scaladsl.marshalling.PekkoHttpToResponseMarshallable +import org.apache.pekko.http.scaladsl.model._ +import org.apache.pekko.http.scaladsl.settings.{ParserSettings, RoutingSettings} +import org.apache.pekko.stream.Materializer +import com.agent.instrumentation.org.apache.pekko.http.PathMatcherUtils +import com.newrelic.api.agent.weaver.{Weave, Weaver} + +import java.util.concurrent.LinkedBlockingDeque +import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger} +import scala.collection.mutable +import scala.concurrent.{ExecutionContextExecutor, Future} + +@Weave(originalName = "org.apache.pekko.http.scaladsl.server.RequestContextImpl") +abstract class PekkoHttpRequestContext(request: HttpRequest, + unmatchedPath: Uri.Path, + executionContext: ExecutionContextExecutor, + materializer: Materializer, + log: LoggingAdapter, + settings: RoutingSettings, + parserSettings: ParserSettings) { + + def complete(trm: PekkoHttpToResponseMarshallable): Future[RouteResult] = { + val contextWrapper = PathMatcherUtils.nrRequestContext.get() + if (trm != null && contextWrapper != null) { + trm.token = contextWrapper.token + } + Weaver.callOriginal() // This ends up calling complete on our NewRelicRequestContextWrapper + } + + def reconfigure(executionContext: ExecutionContextExecutor, materializer: Materializer, log: LoggingAdapter, settings: RoutingSettings): RequestContext = { + Weaver.callOriginal() + } + + private def copy(request: HttpRequest, + unmatchedPath: Uri.Path, + executionContext: ExecutionContextExecutor, + materializer: Materializer, + log: LoggingAdapter, + settings: RoutingSettings, + parserSettings: ParserSettings): RequestContextImpl = { + return new NewRelicRequestContextWrapper(this, Weaver.callOriginal(), null, new LinkedBlockingDeque[String](), + new AtomicBoolean(false), new AtomicInteger(0), new AtomicInteger(0), new LinkedBlockingDeque[String], new mutable.HashSet[String], request, + unmatchedPath, executionContext, materializer, log, settings, parserSettings) + } +} diff --git a/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/directives/OnSuccessMagnetDirective.java b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/directives/OnSuccessMagnetDirective.java new file mode 100644 index 0000000000..fd2aeaf23d --- /dev/null +++ b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/directives/OnSuccessMagnetDirective.java @@ -0,0 +1,26 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package org.apache.pekko.http.scaladsl.server.directives; + +import org.apache.pekko.http.scaladsl.server.util.Tupler; +import com.agent.instrumentation.org.apache.pekko.http.Function0Wrapper; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import scala.Function0; +import scala.concurrent.Future; + +@Weave(type = MatchType.BaseClass, originalName = "org.apache.pekko.http.scaladsl.server.directives.OnSuccessMagnet$") +public class OnSuccessMagnetDirective { + + public OnSuccessMagnet apply(Function0> f, Tupler tupler) { + f = new Function0Wrapper(f); + return Weaver.callOriginal(); + } + +} diff --git a/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/directives/PekkoExecutionDirectives.java b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/directives/PekkoExecutionDirectives.java new file mode 100644 index 0000000000..699264371f --- /dev/null +++ b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/directives/PekkoExecutionDirectives.java @@ -0,0 +1,32 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package org.apache.pekko.http.scaladsl.server.directives; + +import org.apache.pekko.http.scaladsl.server.PekkoHttpContextFunction; +import org.apache.pekko.http.scaladsl.server.ExceptionHandler; +import org.apache.pekko.http.scaladsl.server.RejectionHandler; +import org.apache.pekko.http.scaladsl.server.RequestContext; +import org.apache.pekko.http.scaladsl.server.RouteResult; +import org.apache.pekko.http.scaladsl.settings.ParserSettings; +import org.apache.pekko.http.scaladsl.settings.RoutingSettings; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import scala.Function1; +import scala.concurrent.Future; + +@Weave(originalName = "org.apache.pekko.http.scaladsl.server.Route$") +public class PekkoExecutionDirectives { + + public Function1> seal(Function1> f1, + RoutingSettings routingSettings, ParserSettings parserSettings, RejectionHandler rejectionHandler, + ExceptionHandler exceptionHandler) { + Function1> result = Weaver.callOriginal(); + return PekkoHttpContextFunction.contextWrapper(result); + } + +} diff --git a/instrumentation/apache-pekko-http-2.13_1/src/test/java/com/agent/instrumentation/org.apache.pekko/http/PekkoHttpRoutesTest.java b/instrumentation/apache-pekko-http-2.13_1/src/test/java/com/agent/instrumentation/org.apache.pekko/http/PekkoHttpRoutesTest.java new file mode 100644 index 0000000000..025062ddf4 --- /dev/null +++ b/instrumentation/apache-pekko-http-2.13_1/src/test/java/com/agent/instrumentation/org.apache.pekko/http/PekkoHttpRoutesTest.java @@ -0,0 +1,1669 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.agent.instrumentation.org.apache.pekko.http; + +import com.jayway.restassured.response.Headers; +import com.jayway.restassured.response.ValidatableResponse; +import com.newrelic.agent.HeadersUtil; +import com.newrelic.agent.introspec.InstrumentationTestConfig; +import com.newrelic.agent.introspec.InstrumentationTestRunner; +import com.newrelic.agent.introspec.Introspector; +import com.newrelic.agent.introspec.TransactionEvent; +import com.newrelic.agent.util.Obfuscator; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.UnsupportedEncodingException; +import java.util.Collection; +import java.util.UUID; + +import static com.jayway.restassured.RestAssured.given; +import static org.hamcrest.Matchers.containsString; + +@RunWith(InstrumentationTestRunner.class) +@InstrumentationTestConfig(includePrefixes = { "pekko", "scala" }) +public class PekkoHttpRoutesTest { + + private static final long TIMEOUT = 30000; + + @Rule + public HttpServerRule server = HttpServerRule$.MODULE$.apply(InstrumentationTestRunner.getIntrospector().getRandomPort(), + new PekkoHttpTestRoutes().routes()); + + @Test + public void testHostAndPort() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/hostandport") + .then() + .body(containsString("OK")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), introspector.getTransactionNames().contains(getTransactionPrefix() + "/hostandport")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testFutureErrorStatusCode() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/test-error") + .then() + .body(containsString("There was an internal server error.")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), introspector.getTransactionNames().contains(getTransactionPrefix() + "/test-error")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "500"); + } + + @Test + public void testFutureError2StatusCode() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/test-error-2") + .then() + .body(containsString("ErrorTest")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), introspector.getTransactionNames().contains(getTransactionPrefix() + "/test-error-2")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "500"); + } + + @Test + public void testPrefixFirst() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/prefix-first") + .then() + .body(containsString("prefix-first")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), introspector.getTransactionNames().contains(getTransactionPrefix() + "/prefix-first")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testPrefixFirstFuture() { + given().baseUri("http://localhost:" + server.getPort()).when() + .get("/prefix-first-future") + .then() + .body(containsString("prefix-first-future")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/prefix-first-future")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testPrefixFirstSecond() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/prefix-first-second") + .then() + .body(containsString("prefix-first-second")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/prefix-first-second")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testPrefixFirstSecondFuture() { + given() + .baseUri("http://localhost:" + server.getPort()).when() + .get("/prefix-first-second-future") + .then() + .body(containsString("prefix-first-second-future")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/prefix-first-second-future")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testSimpleRoute() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/simple/route") + .then() + .body(containsString("Simple Route")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/simple/route")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testSimpleRouteFuture() { + given() + .baseUri("http://localhost:" + server.getPort()).when() + .get("/simple/route/future") + .then() + .body(containsString("Simple Route Future")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/simple/route/future")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testSimpleRouteWithQueryParam() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/simple/route?query=value") + .then() + .body(containsString("Simple Route")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/simple/route")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testSimpleRouteFutureWithQueryParam() { + given() + .baseUri("http://localhost:" + server.getPort()).when() + .get("/simple/route/future?query=value") + .then() + .body(containsString("Simple Route Future")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/simple/route/future")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testSimpleRouteCAT() throws UnsupportedEncodingException { + String idHeader = Obfuscator.obfuscateNameUsingKey("1xyz234#1xyz3333", "cafebabedeadbeef8675309babecafe1beefdead"); + + ValidatableResponse response = given() + .header(HeadersUtil.NEWRELIC_ID_HEADER, idHeader) + .baseUri("http://localhost:" + server.getPort()).when() + .get("/simple/route?query=value") + .then() + .body(containsString("Simple Route")); + + Headers responseHeaders = response.extract().headers(); + Assert.assertTrue(responseHeaders.hasHeaderWithName(HeadersUtil.NEWRELIC_APP_DATA_HEADER)); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/simple/route")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testSimpleRouteFutureCAT() throws UnsupportedEncodingException { + String idHeader = Obfuscator.obfuscateNameUsingKey("1xyz234#1xyz3333", "cafebabedeadbeef8675309babecafe1beefdead"); + + ValidatableResponse response = given() + .header(HeadersUtil.NEWRELIC_ID_HEADER, idHeader) + .baseUri("http://localhost:" + server.getPort()).when() + .get("/simple/route/future?query=value") + .then() + .body(containsString("Simple Route Future")); + + Headers responseHeaders = response.extract().headers(); + Assert.assertTrue(responseHeaders.hasHeaderWithName(HeadersUtil.NEWRELIC_APP_DATA_HEADER)); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/simple/route/future")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testUUIDRoute() { + String uuidRegex = "[\\da-fA-F]{8}-[\\da-fA-F]{4}-[\\da-fA-F]{4}-[\\da-fA-F]{4}-[\\da-fA-F]{12}"; + UUID uuid = UUID.randomUUID(); + given() + .baseUri("http://localhost:" + server.getPort()).when() + .get("/uuid/" + uuid.toString()) + .then() + .body(containsString("UUID: " + uuid.toString())); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/uuid/" + uuidRegex)); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testUUIDFutureRoute() { + String uuidRegex = "[\\da-fA-F]{8}-[\\da-fA-F]{4}-[\\da-fA-F]{4}-[\\da-fA-F]{4}-[\\da-fA-F]{12}"; + UUID uuid = UUID.randomUUID(); + given() + .baseUri("http://localhost:" + server.getPort()).when().get("/uuid/future/" + uuid.toString()) + .then() + .body(containsString("UUID Future: " + uuid.toString())); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/uuid/future/" + uuidRegex)); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testRegexRoute() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/regex/5") + .then() + .body(containsString("Regex: 5")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/regex/\\d+")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testRegexFutureRoute() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/regex/future/5") + .then() + .body(containsString("Regex Future: 5")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/regex/future/\\d+")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testNoMatchRegexRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when() + .get("/regex/a") + .then() + .body(containsString("The requested resource could not be found.")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/Unknown Route")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "404"); + } + + @Test + public void testNoMatchFutureRegexRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when().get("/regex/future/a") + .then() + .body(containsString("The requested resource could not be found.")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/Unknown Route")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "404"); + } + + @Test + public void testMapRoute() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/map/red") + .then() + .body(containsString("Map: 1")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/map/red")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testMapFutureRoute() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/map/future/red") + .then() + .body(containsString("Map Future: 1")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/map/future/red")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testAlternateMapRoute() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/map/blue") + .then() + .body(containsString("Map: 3")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/map/blue")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testAlternateMapFutureRoute() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/map/future/blue") + .then() + .body(containsString("Map Future: 3")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/map/future/blue")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testSegmentRoute() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/segment/foobar") + .then() + .body(containsString("Segment: bar")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/segment/foo~Segment")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testSegmentFutureRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when() + .get("/segment/future/foobar") + .then() + .body(containsString("Segment Future: bar")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/segment/future/foo~Segment")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testAlternateSegmentRoute() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/segment/food100") + .then() + .body(containsString("Segment: d100")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/segment/foo~Segment")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testAlternateSegmentFutureRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when() + .get("/segment/future/food100") + .then() + .body(containsString("Segment Future: d100")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/segment/future/foo~Segment")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testUnmatchedSegmentRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when() + .get("/segment") + .then() + .body(containsString("The requested resource could not be found.")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/Unknown Route")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "404"); + } + + @Test + public void testUnmatchedSegmentFutureRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when().get("/segment/future") + .then() + .body(containsString("The requested resource could not be found.")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/Unknown Route")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "404"); + } + + @Test + public void testAlternateUnmatchedSegmentRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when() + .get("/segment/foo") + .then() + .body(containsString("The requested resource could not be found.")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/Unknown Route")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "404"); + } + + @Test + public void testAlternateUnmatchedSegmentFutureRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when().get("/segment/future/foo") + .then() + .body(containsString("The requested resource could not be found.")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/Unknown Route")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "404"); + } + + @Test + public void testSecondaryAlternateUnmatchedSegmentRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when().get("/segment/foobar/baz") + .then() + .body(containsString("The requested resource could not be found.")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/Unknown Route")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "404"); + } + + @Test + public void testSecondaryAlternateUnmatchedSegmentFutureRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when().get("/segment/future/foobar/baz") + .then() + .body(containsString("The requested resource could not be found.")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/Unknown Route")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "404"); + } + + @Test + public void testPathEndRoute() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/pathend") + .then() + .body(containsString("PathEnd")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/pathend")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testPathEndFutureRoute() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/pathendfuture") + .then() + .body(containsString("PathEndFuture")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/pathendfuture")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testNonPathEndRoute() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/pathend/extra") + .then() + .body(containsString("PathEnd: extra")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/pathend/Segment")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testNonPathEndFutureRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when() + .get("/pathendfuture/extra") + .then() + .body(containsString("PathEndFuture: extra")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/pathendfuture/Segment")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testRemainingPath() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/remaining") + .then() + .body(containsString("Remain: ing")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/remain~Remaining")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testRemainingFutureRoute() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/futureremaining") + .then() + .body(containsString("FutureRemain: ing")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/futureremain~Remaining")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testAlternateRemainingRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when() + .get("/remaining/this/is/the/remaining") + .then() + .body(containsString("Remain: ing/this/is/the/remaining")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/remain~Remaining")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testAlternateRestFutureRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when().get("/futureremaining/this/is/the/remaining") + .then() + .body(containsString("FutureRemain: ing/this/is/the/remaining")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/futureremain~Remaining")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testSecondaryAlternateRestRoute() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/remaining") + .then() + .body(containsString("Remain: ing")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/remain~Remaining")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testSecondaryAlternateRemainingFutureRoute() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/futureremaining") + .then() + .body(containsString("FutureRemain: ing")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/futureremain~Remaining")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testRemainingPathRoute() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/remainingpath/remaining") + .then() + .body(containsString("RemainingPath: remaining")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/remainingpath/RemainingPath")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testRemainingPathFutureRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when() + .get("/futureremainingpath/remaining") + .then() + .body(containsString("FutureRemainingPath: remaining")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/futureremainingpath/RemainingPath")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testIntNumberRoute() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/int/10") + .then() + .body(containsString("IntNumber: 10")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/int/IntNumber")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testIntNumberFutureRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when() + .get("/intfuture/10") + .then() + .body(containsString("IntNumberFuture: 10")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/intfuture/IntNumber")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testLongNumberRoute() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/long/1337") + .then() + .body(containsString("LongNumber: 1337")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/long/LongNumber")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testLongNumberFutureRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when() + .get("/longfuture/1337") + .then() + .body(containsString("LongNumberFuture: 1337")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/longfuture/LongNumber")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testHexIntNumberRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when() + .get("/hexint/CAFE") + .then() + .body(containsString("HexIntNumber: 51966")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/hexint/HexIntNumber")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testHexIntNumberFutureRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when() + .get("/hexintfuture/CAFE") + .then() + .body(containsString("HexIntNumberFuture: 51966")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/hexintfuture/HexIntNumber")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testHexLongNumberRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when() + .get("/hexlong/CAFE") + .then() + .body(containsString("HexLongNumber: 51966")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/hexlong/HexLongNumber")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testHexLongNumberFutureRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when() + .get("/hexlongfuture/CAFE") + .then() + .body(containsString("HexLongNumberFuture: 51966")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/hexlongfuture/HexLongNumber")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testDoubleNumberRoute() { + String doubleRegex = "[+-]?\\d*\\.?\\d*"; + given() + .baseUri("http://localhost:" + server.getPort()).when() + .get("/double/123.456") + .then() + .body(containsString("DoubleNumber: 123.456")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/double/" + doubleRegex)); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testDoubleNumberFutureRoute() { + String doubleRegex = "[+-]?\\d*\\.?\\d*"; + given() + .baseUri("http://localhost:" + server.getPort()).when() + .get("/double/future/123.456") + .then() + .body(containsString("DoubleNumberFuture: 123.456")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/double/future/" + doubleRegex)); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testSegmentsRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when() + .get("/segments/here/are/segments") + .then() + .body(containsString("Segments: here,are,segments")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/segments/(Segment/).repeat()")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testSegmentsFutureRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when().get("/futuresegments/here/are/segments") + .then() + .body(containsString("FutureSegments: here,are,segments")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), introspector.getTransactionNames().contains( + getTransactionPrefix() + "/futuresegments/(Segment/).repeat()")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testUnmatchedRepeatRouteTooFewItems() { + given() + .baseUri("http://localhost:" + server.getPort()).when().get("/repeat/52/complex") + .then() + .body(containsString("The requested resource could not be found.")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/Unknown Route")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "404"); + } + + @Test + public void testUnmatchedRepeatRouteTooManyItems() { + given() + .baseUri("http://localhost:" + server.getPort()).when().get("/repeat/52/53/54/55/complex") + .then() + .body(containsString("The requested resource could not be found.")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/Unknown Route")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "404"); + } + + @Test + public void testComplexRepeatRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when() + .get("/repeat/52/53/54/complex") + .then() + .body(containsString("Repeat: 52,53,54")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), introspector.getTransactionNames().contains( + getTransactionPrefix() + "/repeat/(IntNumber/).repeat()/complex")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testZeroRepeatRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when() + .get("/zerorepeat/") + .then() + .body(containsString("ZeroRepeat:")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), introspector.getTransactionNames().contains( + getTransactionPrefix() + "/zerorepeat/().repeat()")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testComplexRepeatFutureRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when() + .get("/futurerepeat/52/53/complex") + .then() + .body(containsString("FutureRepeat: 52,53")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), introspector.getTransactionNames().contains( + getTransactionPrefix() + "/futurerepeat/(IntNumber/).repeat()/complex")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testAlternativeComplexRepeatRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when() + .get("/repeat/52/53/54/complex") + .then() + .body(containsString("Repeat: 52,53,54")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), introspector.getTransactionNames().contains( + getTransactionPrefix() + "/repeat/(IntNumber/).repeat()/complex")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testAlternativeComplexRepeatFutureRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when() + .get("/futurerepeat/52/53/54/complex") + .then() + .body(containsString("FutureRepeat: 52,53,54")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), introspector.getTransactionNames().contains( + getTransactionPrefix() + "/futurerepeat/(IntNumber/).repeat()/complex")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testPipeRoute() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/pipe/i5") + .then() + .body(containsString("Pipe: 5")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/pipe/i~IntNumber")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testFuturePipeRoute() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/futurepipe/i5") + .then() + .body(containsString("FuturePipe: 5")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/futurepipe/i~IntNumber")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testAlternativePipeRoute() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/pipe/hCAFE") + .then() + .body(containsString("Pipe: 51966")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/pipe/h~HexIntNumber")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testAlternativePipeFutureRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when() + .get("/futurepipe/hCAFE") + .then() + .body(containsString("FuturePipe: 51966")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/futurepipe/h~HexIntNumber")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testPipeOptionalRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when() + .get("/pipe/optional/X/create") + .then() + .body(containsString("Pipe + Optional: null")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/pipe/optional/X~IntNumber.?/create")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testPipeOptionalFutureRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when().get("/futurepipe/optional/X/create") + .then() + .body(containsString("FuturePipe + Optional: null")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/futurepipe/optional/X~IntNumber.?/create")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testAlternativePipeOptionalRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when() + .get("/pipe/optional/X71/edit") + .then() + .body(containsString("Pipe + Optional: 71")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/pipe/optional/X~IntNumber.?/edit")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testAlternativePipeOptionalFutureRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when() + .get("/futurepipe/optional/X71/edit") + .then() + .body(containsString("FuturePipe + Optional: 71")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/futurepipe/optional/X~IntNumber.?/edit")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testNegationRoute() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/match") + .then() + .body(containsString("Negation")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/match~!nomatch")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testNegationFutureRoute() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/futurematch") + .then() + .body(containsString("FutureNegation")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/futurematch~!nomatch")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testAlternativeNegationRoute() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/matchno") + .then() + .body(containsString("Negation")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/match~!nomatch")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testAlternativeNegationFutureRoute() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/futurematchno") + .then() + .body(containsString("FutureNegation")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/futurematch~!nomatch")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testUnmatchedNegationRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when().get("/matchnomatch") + .then() + .body(containsString("The requested resource could not be found.")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/Unknown Route")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "404"); + } + + @Test + public void testUnmatchedNegationFutureRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when().get("/futurematchnomatch") + .then() + .body(containsString("The requested resource could not be found.")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/Unknown Route")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "404"); + } + + @Test + public void testSinglePrefix() { + given() + .baseUri("http://localhost:" + server.getPort()).when().get("/v1") + .then() + .body(containsString("The requested resource could not be found.")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/Unknown Route")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "404"); + } + + @Test + public void testDoublePrefixNoParam() { + given() + .baseUri("http://localhost:" + server.getPort()).when().get("/v1/containers") + .then() + .body(containsString("Request is missing required query parameter 'parameter'")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/Unknown Route")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "404"); + } + + @Test + public void testDoublePrefixWithParam() { + given() + .baseUri("http://localhost:" + server.getPort()).when().get("/v1/containers?parameter=12345") + .then() + .body(containsString("ContainersParam: 12345")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/v1/containers")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testDoublePrefixWithSegment() { + given() + .baseUri("http://localhost:" + server.getPort()).when().get("/v1/containers/DT12345") + .then() + .body(containsString("ContainersSegment: DT12345")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/v1/containers/Segment")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testDoublePrefixWithSegmentAndString() { + given() + .baseUri("http://localhost:" + server.getPort()).when().get("/v1/containers/DT12345/details") + .then() + .body(containsString("ContainersSegmentDetails: DT12345")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/v1/containers/Segment/details")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testDoublePrefixWithSegmentAndStrings() { + given() + .baseUri("http://localhost:" + server.getPort()).when().get("/v1/containers/DT12345/details/test") + .then() + .body(containsString("ContainersSegmentDetailsTest: DT12345")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/v1/containers/Segment/details/test")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testCompleteWithFuture() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/future/2000") + .then() + .body(containsString("OK")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/future/IntNumber")); + Collection transactionEvents = introspector.getTransactionEvents(getTransactionPrefix() + "/future/IntNumber"); + TransactionEvent transactionEvent = (TransactionEvent) transactionEvents.toArray()[0]; + Assert.assertNotNull(transactionEvent); + Assert.assertTrue(transactionEvent.getDurationInSec() >= 2.0); + + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testCustomAsyncDirectiveNoResult() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/custom-directive/v2/docs?parameter=test") + .then() + .body(containsString("CustomDirectiveDocsParam: test")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/custom-directive/v2/docs")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + + } + + @Test + public void testCustomAsyncDirectiveWithResult() { + for (int i = 0; i < 100; i++) { + given() + .baseUri("http://localhost:" + server.getPort()) + .header("X-NewRelic-Directive", "booyah!") + .when() + .get("/custom-directive/v2/docs/booyah") + .then() + .body(containsString("CustomDirectiveDocsSegment: booyah")); + } + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(100, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertEquals(1, introspector.getTransactionNames().size()); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/custom-directive/v2/docs/Segment")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 100, "200"); + + } + + @Test + public void testPathEndRoute2() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/path-end") + .then() + .body(containsString("Get path end!")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/path-end")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testPathEndRemainingRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when() + .get("/path-prefix-end") + .then() + .body(containsString("Get path end!")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/path-prefix-end")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testPathEndRemainingRoute2() { + given() + .baseUri("http://localhost:" + server.getPort()).when() + .get("/path-prefix-end/first-case") + .then() + .body(containsString("First case")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/path-prefix-end/first-case")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testPathEndRemainingRoute3() { + given() + .baseUri("http://localhost:" + server.getPort()).when() + .get("/path-prefix-end/whatever") + .then() + .body(containsString("Remaining: whatever")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/path-prefix-end/Remaining")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + } + + @Test + public void testAsyncDirectiveRoute() { + given() + .baseUri("http://localhost:" + server.getPort()).when() + .get("/callid/102") + .then() + .body(containsString("Pong_OK")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + Assert.assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + Assert.assertTrue(introspector.getTransactionNames().toString(), + introspector.getTransactionNames().contains(getTransactionPrefix() + "/callid/Segment")); + + Collection transactionEvents = introspector.getTransactionEvents(introspector.getTransactionNames().toArray()[0].toString()); + assertResponseCodeOnTxEvents(transactionEvents, 1, "200"); + assertCustomAttributeOnTxEvents(transactionEvents, "attr1"); + assertCustomAttributeOnTxEvents(transactionEvents, "attr2"); + } + + private void assertResponseCodeOnTxEvents(Collection transactionEvents, int expectedSize, String expectedResponseCode) { + Assert.assertNotNull(transactionEvents); + Assert.assertEquals(expectedSize, transactionEvents.size()); + for (TransactionEvent transactionEvent : transactionEvents) { + String httpResponseCode = String.valueOf(transactionEvent.getAttributes().get("httpResponseCode")); + Assert.assertNotNull(httpResponseCode); + Assert.assertEquals(expectedResponseCode, httpResponseCode); + int statusCode = (Integer) transactionEvent.getAttributes().get("http.statusCode"); + Assert.assertEquals(Integer.parseInt(expectedResponseCode), statusCode); + } + } + + private void assertCustomAttributeOnTxEvents(Collection transactionEvents, String expectedAttributeKey) { + Assert.assertNotNull(transactionEvents); + for (TransactionEvent transactionEvent : transactionEvents) { + String attributeValue = String.valueOf(transactionEvent.getAttributes().get(expectedAttributeKey)); + Assert.assertNotNull(attributeValue); + } + } + + private String getTransactionPrefix() { + return "WebTransaction/PekkoHttp"; + } + +} diff --git a/instrumentation/apache-pekko-http-2.13_1/src/test/java/com/agent/instrumentation/org.apache.pekko/http/PekkoResponseWrapperTest.java b/instrumentation/apache-pekko-http-2.13_1/src/test/java/com/agent/instrumentation/org.apache.pekko/http/PekkoResponseWrapperTest.java new file mode 100644 index 0000000000..5e5d9eb7be --- /dev/null +++ b/instrumentation/apache-pekko-http-2.13_1/src/test/java/com/agent/instrumentation/org.apache.pekko/http/PekkoResponseWrapperTest.java @@ -0,0 +1,64 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.agent.instrumentation.org.apache.pekko.http; + +import com.newrelic.agent.introspec.InstrumentationTestConfig; +import com.newrelic.agent.introspec.InstrumentationTestRunner; +import com.newrelic.agent.introspec.Introspector; +import com.newrelic.agent.introspec.TransactionEvent; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Collection; + +import static com.jayway.restassured.RestAssured.given; +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@RunWith(InstrumentationTestRunner.class) +@InstrumentationTestConfig(includePrefixes = { "pekko", "scala" }) +public class PekkoResponseWrapperTest { + + private static final long TIMEOUT = 30000; + + @Rule + public HttpServerRule server = HttpServerRule$.MODULE$.apply(InstrumentationTestRunner.getIntrospector().getRandomPort(), + new PekkoHttpTestRoutes().routes()); + + @Test + public void testAttributesContainsContentType() { + given() + .baseUri("http://localhost:" + server.getPort()) + .when() + .get("/prefix-first") + .then() + .body(containsString("prefix-first")); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + assertEquals(1, introspector.getFinishedTransactionCount(TIMEOUT)); + assertTrue(introspector.getTransactionNames().toString(), introspector.getTransactionNames().contains(getTransactionPrefix() + "/prefix-first")); + + String txName = introspector.getTransactionNames().toArray()[0].toString(); + assertNotNull(txName); + Collection transactionEvents = introspector.getTransactionEvents(txName); + assertFalse(transactionEvents.isEmpty()); + TransactionEvent transactionEvent = (TransactionEvent) transactionEvents.toArray()[0]; + assertNotNull(transactionEvent); + assertTrue(transactionEvent.getAttributes().containsKey("response.headers.contentType")); + Object o = transactionEvent.getAttributes().get("response.headers.contentType"); + assertNotNull(o); + } + + private String getTransactionPrefix() { + return "WebTransaction/PekkoHttp"; + } +} diff --git a/instrumentation/apache-pekko-http-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/AsyncApp.scala b/instrumentation/apache-pekko-http-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/AsyncApp.scala new file mode 100644 index 0000000000..4a2f44815e --- /dev/null +++ b/instrumentation/apache-pekko-http-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/AsyncApp.scala @@ -0,0 +1,22 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.agent.instrumentation.org.apache.pekko.http + +import java.util.concurrent.Executors +import scala.concurrent.{ExecutionContext, ExecutionContextExecutor, Future} + +object AsyncApp { + + implicit val executor: ExecutionContextExecutor = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(2)) + + def asyncTest(header: String): Future[Option[String]] = { + Future { + Some(header) + } + } +} diff --git a/instrumentation/apache-pekko-http-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/HttpServer.scala b/instrumentation/apache-pekko-http-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/HttpServer.scala new file mode 100644 index 0000000000..121cda754a --- /dev/null +++ b/instrumentation/apache-pekko-http-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/HttpServer.scala @@ -0,0 +1,57 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.agent.instrumentation.org.apache.pekko.http + +import org.apache.pekko.actor.ActorSystem +import org.apache.pekko.event.Logging +import org.apache.pekko.http.scaladsl.Http +import org.apache.pekko.http.scaladsl.Http.ServerBinding +import org.apache.pekko.http.scaladsl.server.Directives._ +import org.apache.pekko.http.scaladsl.server.{RequestContext, Route} +import org.apache.pekko.stream.ActorMaterializer +import org.apache.pekko.util.Timeout +import com.typesafe.config.ConfigFactory + +import scala.concurrent.duration._ +import scala.concurrent.{Await, Future} +import scala.language.postfixOps + +class HttpServer(val routes: Route = RouteService.defaultRoute) { + implicit val system = ActorSystem() + implicit val executor = system.dispatcher + implicit val materializer = ActorMaterializer() + implicit val timeout: Timeout = 60 seconds + + val config = ConfigFactory.load() + val logger = Logging(system, getClass) + + var handle: Future[ServerBinding] = _ + + def start(port: Int) = { + Await.ready({ + handle = Http().bindAndHandle(routes, "localhost", port) + handle + }, timeout.duration) + } + + def stop() = { + if (handle != null) { + handle.flatMap(_.unbind()).onComplete(_ => system.terminate()) + } + } +} + +object RouteService { + val defaultRoute = { + path("test") { + get { (ctx: RequestContext) => + ctx.complete("FAIL") + } + } + } +} diff --git a/instrumentation/apache-pekko-http-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/HttpServerRule.scala b/instrumentation/apache-pekko-http-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/HttpServerRule.scala new file mode 100644 index 0000000000..68cc3d1080 --- /dev/null +++ b/instrumentation/apache-pekko-http-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/HttpServerRule.scala @@ -0,0 +1,26 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.agent.instrumentation.org.apache.pekko.http + +import org.apache.pekko.http.scaladsl.server.Route +import org.junit.rules.ExternalResource + +import scala.beans.BeanProperty + +class HttpServerRule(@BeanProperty val port: Int, val route: Route) + extends ExternalResource { + val server = new HttpServer(route) + + override def before(): Unit = server.start(port) + + override def after(): Unit = server.stop() +} + +object HttpServerRule { + def apply(port: Int, route: Route) = new HttpServerRule(port, route) +} diff --git a/instrumentation/apache-pekko-http-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/PekkoHttpTestRoutes.scala b/instrumentation/apache-pekko-http-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/PekkoHttpTestRoutes.scala new file mode 100644 index 0000000000..fb37cd72e0 --- /dev/null +++ b/instrumentation/apache-pekko-http-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/PekkoHttpTestRoutes.scala @@ -0,0 +1,451 @@ +/* + * + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.agent.instrumentation.org.apache.pekko.http + +import org.apache.pekko.actor.{ActorRef, ActorSystem, Scheduler} +import org.apache.pekko.http.scaladsl.marshalling.ToResponseMarshallable +import org.apache.pekko.http.scaladsl.model.StatusCodes +import org.apache.pekko.http.scaladsl.model.headers.RawHeader +import org.apache.pekko.http.scaladsl.server.Directives._ +import org.apache.pekko.http.scaladsl.server._ +import org.apache.pekko.http.scaladsl.server.directives.BasicDirectives +import org.apache.pekko.pattern.{after, ask} +import org.apache.pekko.stream.ActorMaterializer +import org.apache.pekko.util.{ByteString, Timeout} +import com.agent.instrumentation.org.apache.pekko.http.StatusCheckActor.Ping +import com.newrelic.api.agent.NewRelic + +import scala.concurrent.duration._ +import scala.concurrent.{ExecutionContext, Future} +import scala.util.Success +import scala.language.postfixOps + +class PekkoHttpTestRoutes { + + implicit val system: ActorSystem = ActorSystem("pekkohttptest") + implicit val scheduler: Scheduler = system.scheduler + implicit val executor: ExecutionContext = system.dispatcher + implicit val materializer: ActorMaterializer = ActorMaterializer() + implicit val requestTimeout: Timeout = Timeout(30 seconds) + + val newrelicCheck: ActorRef = system.actorOf(StatusCheckActor.props, "StatusCheck") + + def customTx(s: String): Directive0 = mapResponse { resp => + NewRelic.addCustomParameter("attr1", "newrelic-test") + NewRelic.addCustomParameter("attr2", s) + resp + } + + def customDirective: Directive1[Option[String]] = { + optionalHeaderValueByName("X-NewRelic-Directive").flatMap { + case Some(directiveHeader) => + onComplete(AsyncApp.asyncTest(directiveHeader)).flatMap { + case Success(result) => BasicDirectives.provide(result) + case _ => + BasicDirectives.provide(None) + } + case None => BasicDirectives.provide(None) + } + } + + def pekkoStreamDirective: Directive1[String] = { + extractDataBytes.flatMap { data => + val body = data.runFold(ByteString.empty)(_ ++ _).map(_.utf8String) + + onSuccess(body).flatMap { payload => + provide(payload) + } + } + } + + def hostnameAndPort: Directive[(String, Int)] = Directive[(String, Int)] { inner => + ctx => + val authority = ctx.request.uri.authority + inner((authority.host.address(), authority.port))(ctx) + } + + val routes: Route = rejectEmptyResponse { + customDirective { _: Option[String] => + pathPrefix("custom-directive") { + pathPrefix("v2") { + pathPrefix("docs") { + parameters(Symbol("parameter")) { parameterId: String => + get { ctx: RequestContext => + ctx.complete("CustomDirectiveDocsParam: " + parameterId) + } + } ~ + path(Segment) { segment => + get { ctx: RequestContext => + ctx.complete("CustomDirectiveDocsSegment: " + segment) + } + } ~ + path(Segment / "details") { segment => + get { ctx: RequestContext => + ctx.complete("CustomDirectiveDocsSegmentDetails: " + segment) + } + } ~ + path(Segment / "details" / "test") { segment => + get { ctx: RequestContext => + ctx.complete("CustomDirectiveDocsSegmentDetailsTest: " + segment) + } + } + } + } + } ~ + path("callid" / Segment) { id => + val fut: Future[String] = (newrelicCheck ? Ping(id.toInt)).map { + case str: String => s"${str}_id" + case _ => "ERROR" + } + + onSuccess(fut) { response => + customTx("test") { + respondWithHeaders(RawHeader("Test-Header", "async-directive")) { + complete(response) + } + } + } + } ~ + pekkoStreamDirective { implicit data => { + path("prefix-first") { + get { ctx: RequestContext => + ctx.complete("prefix-first") + } + } ~ + path("prefix-first-future") { + onSuccess(Future { + "prefix-first-future" + }) { + result => complete(result) + } + } ~ + path("prefix-first-second") { + get { ctx: RequestContext => + ctx.complete("prefix-first-second") + } + } ~ + path("prefix-first-second-future") { + onSuccess(Future { + "prefix-first-second-future" + }) { + result => complete(result) + } + } ~ + path("simple" / "route") { + get { ctx: RequestContext => + ctx.complete("Simple Route") + } + } ~ + path("simple" / "route" / "future") { + onSuccess(Future { + "Simple Route Future" + }) { + result => complete(result) + } + } ~ + path("uuid" / JavaUUID) { uuid => + get { ctx: RequestContext => + ctx.complete("UUID: " + uuid.toString) + } + } ~ + path("uuid" / "future" / JavaUUID) { uuid => + onSuccess(Future { + "UUID Future: " + uuid.toString + }) { + result => complete(result) + } + } ~ + path("regex" / """\d+""".r) { digit => + get { ctx: RequestContext => + ctx.complete("Regex: " + digit) + } + } ~ + path("regex" / "future" / """\d+""".r) { digit => + onSuccess(Future { + "Regex Future: " + digit + }) { + result => complete(result) + } + } ~ + path("map" / Map("red" -> 1, "green" -> 2, "blue" -> 3)) { value => + get { ctx: RequestContext => + ctx.complete("Map: " + value) + } + } ~ + path("map" / "future" / Map("red" -> 1, "green" -> 2, "blue" -> 3)) { value => + onSuccess(Future { + "Map Future: " + value + }) { + result => complete(result) + } + } ~ + path("segment" / "foo" ~ Segment) { segment => + get { ctx: RequestContext => + ctx.complete("Segment: " + segment) + } + } ~ + path("segment" / "future" / "foo" ~ Segment) { segment => + onSuccess(Future { + "Segment Future: " + segment + }) { + result => complete(result) + } + } ~ + pathPrefix("pathend") { + pathEnd { + get { ctx: RequestContext => + ctx.complete("PathEnd") + } + } ~ + path(Segment) { segment => + get { ctx: RequestContext => + ctx.complete("PathEnd: " + segment) + } + } + } ~ + pathPrefix("pathendfuture") { + pathEnd { + onSuccess(Future { + "PathEndFuture" + }) { + result => complete(result) + } + } ~ + path(Segment) { segment => + onSuccess(Future { + "PathEndFuture: " + segment + }) { + result => complete(result) + } + } + } ~ + path("remainingpath" / RemainingPath) { remainingpath => + get { ctx: RequestContext => + ctx.complete("RemainingPath: " + remainingpath) + } + } ~ + path("futureremainingpath" / RemainingPath) { remainingpath => + onSuccess(Future { + "FutureRemainingPath: " + remainingpath + }) { + result => complete(result) + } + } ~ + path("remain" ~ Remaining) { remaining => + get { ctx: RequestContext => + ctx.complete("Remain: " + remaining) + } + } ~ + path("futureremain" ~ Remaining) { remaining => + onSuccess(Future { + "FutureRemain: " + remaining + }) { + result => complete(result) + } + } ~ + path("int" / IntNumber) { number => + get { ctx: RequestContext => + ctx.complete("IntNumber: " + number) + } + } ~ + path("intfuture" / IntNumber) { number => + onSuccess(Future { + "IntNumberFuture: " + number + }) { + result => complete(result) + } + } ~ + path("long" / LongNumber) { number => + get { ctx: RequestContext => + ctx.complete("LongNumber: " + number) + } + } ~ + path("longfuture" / LongNumber) { number => + onSuccess(Future { + "LongNumberFuture: " + number + }) { + result => complete(result) + } + } ~ + path("hexint" / HexIntNumber) { number => + get { ctx: RequestContext => + ctx.complete("HexIntNumber: " + number) + } + } ~ + path("hexintfuture" / HexIntNumber) { number => + onSuccess(Future { + "HexIntNumberFuture: " + number + }) { + result => complete(result) + } + } ~ + path("hexlong" / HexLongNumber) { number => + get { ctx: RequestContext => + ctx.complete("HexLongNumber: " + number) + } + } ~ + path("hexlongfuture" / HexLongNumber) { number => + onSuccess(Future { + "HexLongNumberFuture: " + number + }) { + result => complete(result) + } + } ~ + path("double" / DoubleNumber) { number => + get { ctx: RequestContext => + ctx.complete("DoubleNumber: " + number) + } + } ~ + path("double" / "future" / DoubleNumber) { number => + onSuccess(Future { + "DoubleNumberFuture: " + number + }) { + result => complete(result) + } + } ~ + path("segments" / Segments) { segments => + get { ctx: RequestContext => + val result = segments.mkString(",") + ctx.complete("Segments: " + result) + } + } ~ + path("futuresegments" / Segments) { segments => + onSuccess(Future { + val result = segments.mkString(",") + "FutureSegments: " + result + }) { + result => complete(result) + } + } ~ + path("repeat" / IntNumber.repeat(2, 3, separator = Slash) / "complex") { segments => + get { ctx: RequestContext => + val result = segments.mkString(",") + ctx.complete("Repeat: " + result) + } + } ~ + path("futurerepeat" / IntNumber.repeat(2, 3, separator = Slash) / "complex") { segments => + onSuccess(Future { + val result = segments.mkString(",") + "FutureRepeat: " + result + }) { + result => complete(result) + } + } ~ + path("zerorepeat" / IntNumber.repeat(0, 2, separator = Slash)) { segments => + get { ctx: RequestContext => + val result = segments.mkString(",") + ctx.complete("ZeroRepeat: " + result) + } + } ~ + path("pipe" / ("i" ~ IntNumber | "h" ~ HexIntNumber)) { number => + get { ctx: RequestContext => + ctx.complete("Pipe: " + number) + } + } ~ + path("futurepipe" / ("i" ~ IntNumber | "h" ~ HexIntNumber)) { number => + onSuccess(Future { + "FuturePipe: " + number + }) { + result => complete(result) + } + } ~ + path("pipe" / "optional" / "X" ~ IntNumber.? / ("edit" | "create")) { number => + get { ctx: RequestContext => + ctx.complete("Pipe + Optional: " + number.orNull) + } + } ~ + path("futurepipe" / "optional" / "X" ~ IntNumber.? / ("edit" | "create")) { number => + onSuccess(Future { + "FuturePipe + Optional: " + number.orNull + }) { + result => complete(result) + } + } ~ + pathPrefix("match" ~ !"nomatch") { + get { ctx: RequestContext => + ctx.complete("Negation") + } + } ~ + pathPrefix("futurematch" ~ !"nomatch") { + onSuccess(Future { + "FutureNegation" + }) { + result => complete(result) + } + } ~ + pathPrefix("v1") { + pathPrefix("containers") { + parameters(Symbol("parameter")) { parameterId: String => + get { ctx: RequestContext => + ctx.complete("ContainersParam: " + parameterId) + } + } ~ + path(Segment) { segment => + get { ctx: RequestContext => + ctx.complete("ContainersSegment: " + segment) + } + } ~ + path(Segment / "details") { segment => + get { ctx: RequestContext => + ctx.complete("ContainersSegmentDetails: " + segment) + } + } ~ + path(Segment / "details" / "test") { segment => + get { ctx: RequestContext => + ctx.complete("ContainersSegmentDetailsTest: " + segment) + } + } + } + } ~ + path("future" / IntNumber) { millis => + val futureResult = after(millis.millis, scheduler)(Future.successful(StatusCodes.OK)) + complete(ToResponseMarshallable.apply(futureResult)) + } ~ + path("hostandport") { + hostnameAndPort { + (_, _) => complete(StatusCodes.OK) + } + } ~ + path("test-error") { + throw new UnsupportedOperationException + } ~ + path("test-error-2") { + complete(StatusCodes.InternalServerError, "ErrorTest") + } ~ + pathPrefix("path-end") { + path(Remaining) { + path => { + complete(StatusCodes.OK, "Remaining: " + path) + } + } ~ + pathEnd { + get { + complete(StatusCodes.OK, "Get path end!") + } + } + } ~ + pathPrefix("path-prefix-end") { + path("first-case") { + complete(StatusCodes.OK, "First case") + } ~ + path(Remaining) { + path => { + complete(StatusCodes.OK, "Remaining: " + path) + } + } ~ + pathEnd { + get { + complete(StatusCodes.OK, "Get path end!") + } + } + } + } + } + } + } +} diff --git a/instrumentation/apache-pekko-http-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/StatusCheckActor.scala b/instrumentation/apache-pekko-http-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/StatusCheckActor.scala new file mode 100644 index 0000000000..9ace9db0b0 --- /dev/null +++ b/instrumentation/apache-pekko-http-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/StatusCheckActor.scala @@ -0,0 +1,43 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.agent.instrumentation.org.apache.pekko.http + +import org.apache.pekko.actor.{Actor, ActorRef, Props, Timers} + +import java.util.concurrent.TimeUnit +import scala.concurrent.duration.FiniteDuration +import scala.util.Random + +object StatusCheckActor { + def props: Props = Props(new StatusCheckActor) + + case class Ping(id: Int) + + case object Pong + +} + +class StatusCheckActor extends Actor with Timers { + + import StatusCheckActor._ + + val random: Random = Random + var lastSender: Option[ActorRef] = None + + timers.startPeriodicTimer("my-timer", Pong, FiniteDuration(1, TimeUnit.SECONDS)) + + override def receive: Receive = { + case Ping(id) if id < 100 => + sender ! "Ping_OK" + case Ping(id) => + lastSender = Some(sender) + case Pong => + lastSender.foreach(_ ! "Pong_OK") + lastSender = None + } +} diff --git a/settings.gradle b/settings.gradle index df50845a05..9cef988ff7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -92,6 +92,7 @@ include 'instrumentation:apache-log4j-1' include 'instrumentation:apache-log4j-2.6' include 'instrumentation:apache-log4j-2.11' include 'instrumentation:apache-pekko-1' +include 'instrumentation:apache-pekko-http-2.13_1' include 'instrumentation:apache-struts-2.0' include 'instrumentation:async-http-client-2.0.0' include 'instrumentation:async-http-client-2.1.0' From b97a04dc22f777170fd3d20e42834844f7b1be39 Mon Sep 17 00:00:00 2001 From: Kate Anderson Date: Tue, 26 Mar 2024 17:12:19 -0700 Subject: [PATCH 2/5] initial commit for pekko http core --- .../apache-pekko-http-core-2.13_1/README.md | 28 ++ .../build.gradle | 27 ++ .../PekkoHttpInboundHeaders.scala | 36 +++ .../pekkohttpcore/PekkoHttpUtils.scala | 50 ++++ .../pekkohttpcore/RequestWrapper.scala | 62 +++++ .../pekkohttpcore/ResponseFuture.scala | 54 ++++ .../pekkohttpcore/ResponseWrapper.scala | 37 +++ .../http/scaladsl/AsyncRequestHandler.scala | 55 ++++ .../http/scaladsl/HttpExtInstrumentation.java | 72 +++++ .../http/scaladsl/HttpInstrumentation.java | 46 ++++ .../http/scaladsl/IncomingConnection.java | 38 +++ .../http/scaladsl/SyncRequestHandler.scala | 59 ++++ .../http/core/PekkoHttpCoreTest.scala | 259 ++++++++++++++++++ .../http/core/PekkoServer.scala | 77 ++++++ .../http/core/PlayServer.scala | 70 +++++ settings.gradle | 1 + 16 files changed, 971 insertions(+) create mode 100644 instrumentation/apache-pekko-http-core-2.13_1/README.md create mode 100644 instrumentation/apache-pekko-http-core-2.13_1/build.gradle create mode 100644 instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/com/nr/instrumentation/pekkohttpcore/PekkoHttpInboundHeaders.scala create mode 100644 instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/com/nr/instrumentation/pekkohttpcore/PekkoHttpUtils.scala create mode 100644 instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/com/nr/instrumentation/pekkohttpcore/RequestWrapper.scala create mode 100644 instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/com/nr/instrumentation/pekkohttpcore/ResponseFuture.scala create mode 100644 instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/com/nr/instrumentation/pekkohttpcore/ResponseWrapper.scala create mode 100644 instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/AsyncRequestHandler.scala create mode 100644 instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/HttpExtInstrumentation.java create mode 100644 instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/HttpInstrumentation.java create mode 100644 instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/IncomingConnection.java create mode 100644 instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/SyncRequestHandler.scala create mode 100644 instrumentation/apache-pekko-http-core-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/core/PekkoHttpCoreTest.scala create mode 100644 instrumentation/apache-pekko-http-core-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/core/PekkoServer.scala create mode 100644 instrumentation/apache-pekko-http-core-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/core/PlayServer.scala diff --git a/instrumentation/apache-pekko-http-core-2.13_1/README.md b/instrumentation/apache-pekko-http-core-2.13_1/README.md new file mode 100644 index 0000000000..7c52df3430 --- /dev/null +++ b/instrumentation/apache-pekko-http-core-2.13_1/README.md @@ -0,0 +1,28 @@ +#Akka HTTP core instrumentation + +##HttpExt Instrumentation +Instrumentation for Akka HTTP Core is carried out in the `akka.http.scaladsl.HttpExt` class that serves as the +main entry point for a server. 2 convenience methods from `HttpExt` that can be used to start an HTTP server have +been instrumented, they are : + +- ` bindAndHandleAsync`: Convenience method which starts a new HTTP server at the given endpoint and uses handler that is a function recieving an `HttpRequest` and returning a `Future[HttpResponse]` +- ` bindAndHandleSync`: Convenience method which starts a new HTTP server at the given endpoint and uses handler that is a function recieving an `HttpRequest` and returning a `HttpResponse` + + +It has been decide that intrumentation is not extended for `bindAndHandle` which starts a new HTTP server using a +`akka.stream.scaladsl.Flow` instance. This is due to a clash in the Akka Http Routing DSL instrumentation. + + +Users wishing to start an HTTP Server from an `akka.stream.scaladsl.Flow` can use the following workaround + +```scala + val flow: Flow[HttpRequest, HttpResponse, NotUsed] = ??? + val asyncHandler: HttpRequest => Future[HttpResponse] = request => Source.single(request).via(flow).runWith(Sink.head) + Http().bindAndHandleAsync(asyncHandler, host, port) +``` + +This workaround is not needed for users using calling `bindAndHandle` using `akka.http.scaladsl.Route` from the +Akka Http Routing DSL. Instrumentation should work in the same way being called from the other conveniencs methods +to start an HTTP Server + + diff --git a/instrumentation/apache-pekko-http-core-2.13_1/build.gradle b/instrumentation/apache-pekko-http-core-2.13_1/build.gradle new file mode 100644 index 0000000000..2ebca6a7c9 --- /dev/null +++ b/instrumentation/apache-pekko-http-core-2.13_1/build.gradle @@ -0,0 +1,27 @@ +apply plugin: 'scala' + +isScalaProjectEnabled(project, "scala-2.13") + +jar { + manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.apache-pekko-http-core-2.13_1' } +} + +dependencies { + implementation(project(":agent-bridge")) + implementation("org.apache.pekko:pekko-http-core_2.13:1.0.1") + implementation("org.apache.pekko:pekko-stream_2.13:1.0.1") +} + +verifyInstrumentation { + passesOnly('org.apache.pekko:pekko-http-core_2.13:[1.0.0,)') { + implementation("org.apache.pekko:pekko-stream_2.13:1.0.1") + } +// fails('org.apache.pekko:pekko-http-core_2.12:[1.0.0,)') { +// implementation("org.apache.pekko:pekko-stream_2.12:2.5.23") +// } +} + +site { + title 'Pekko Http Core' + type 'Framework' +} diff --git a/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/com/nr/instrumentation/pekkohttpcore/PekkoHttpInboundHeaders.scala b/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/com/nr/instrumentation/pekkohttpcore/PekkoHttpInboundHeaders.scala new file mode 100644 index 0000000000..4517cce062 --- /dev/null +++ b/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/com/nr/instrumentation/pekkohttpcore/PekkoHttpInboundHeaders.scala @@ -0,0 +1,36 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.instrumentation.pekkohttpcore + +import java.util + +import org.apache.pekko.http.scaladsl.model.HttpRequest +import com.newrelic.api.agent.{ExtendedInboundHeaders, HeaderType} + +import scala.jdk.CollectionConverters._ + +class PekkoHttpInboundHeaders(val httpRequest: HttpRequest) extends ExtendedInboundHeaders { + + override def getHeaderType: HeaderType = HeaderType.HTTP + + override def getHeader(name: String): String = { + val header = httpRequest.getHeader(name) + if (!header.isPresent) { + return null + } + header.get().value() + } + + override def getHeaders(name: String): util.List[String] = { + val headers = httpRequest.headers.filter(header => header.is(name.toLowerCase)).map(header => header.value) + if (headers.isEmpty) { + return null + } + headers.asJava + } +} diff --git a/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/com/nr/instrumentation/pekkohttpcore/PekkoHttpUtils.scala b/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/com/nr/instrumentation/pekkohttpcore/PekkoHttpUtils.scala new file mode 100644 index 0000000000..3cda37812a --- /dev/null +++ b/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/com/nr/instrumentation/pekkohttpcore/PekkoHttpUtils.scala @@ -0,0 +1,50 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.instrumentation.pekkohttpcore + +import java.net.URI +import java.util.concurrent.Executors + +import org.apache.pekko.http.scaladsl.model.{HttpRequest, HttpResponse} +import com.newrelic.agent.bridge.AgentBridge +import com.newrelic.api.agent.weaver.Weaver +import com.newrelic.api.agent.{HttpParameters, Segment} + +import scala.concurrent.{ExecutionContext, ExecutionContextExecutor, Future} +import scala.util.{Failure, Success} + +object PekkoHttpUtils { + + implicit val executor: ExecutionContextExecutor = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(4)) + + def finishSegmentOnComplete(httpRequest: HttpRequest, httpResponseFuture: Future[HttpResponse], segment: Segment): Unit = { + httpResponseFuture onComplete { + case Success(response) => + try { + segment.reportAsExternal(HttpParameters + .library("PekkoHttpClient") + .uri(new URI(httpRequest.uri.toString())) + .procedure(httpRequest.method.value) + .inboundHeaders(new PekkoHttpInboundHeaders(httpRequest)) + .build()) + segment.end() + } catch { + case t: Throwable => AgentBridge.instrumentation.noticeInstrumentationError(t, Weaver.getImplementationTitle) + } + + case Failure(t) => + try { + // In the case of an error, just end the segment. + // We probably don't want to report an error here because it may just be a 404 + segment.end() + } catch { + case t: Throwable => AgentBridge.instrumentation.noticeInstrumentationError(t, Weaver.getImplementationTitle) + } + } + } +} diff --git a/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/com/nr/instrumentation/pekkohttpcore/RequestWrapper.scala b/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/com/nr/instrumentation/pekkohttpcore/RequestWrapper.scala new file mode 100644 index 0000000000..e90434c24f --- /dev/null +++ b/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/com/nr/instrumentation/pekkohttpcore/RequestWrapper.scala @@ -0,0 +1,62 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.instrumentation.pekkohttpcore + +import java.util + +import org.apache.pekko.http.scaladsl.model.HttpRequest +import com.newrelic.api.agent.{ExtendedRequest, HeaderType} + +import scala.jdk.CollectionConverters._ + +class RequestWrapper(request: HttpRequest) extends ExtendedRequest { + + def getMethod: String = { + request.method.name + } + + def getRequestURI: String = { + request.uri.path.toString() + } + + def getRemoteUser: String = { + null + } + + def getParameterNames: java.util.Enumeration[_] = { + request.uri.query().toMap.keysIterator.asJavaEnumeration + } + + def getParameterValues(name: String): Array[String] = { + request.uri.query().getAll(name).toArray + } + + def getAttribute(name: String): AnyRef = { + null + } + + def getCookieValue(name: String): String = { + request.cookies.find(cookie => cookie.name.equalsIgnoreCase(name)).map(cookie => cookie.value).orNull + } + + def getHeaderType: HeaderType = { + HeaderType.HTTP + } + + def getHeader(name: String): String = { + request.headers.find(header => header.is(name.toLowerCase)).map(header => header.value).orNull + } + + override def getHeaders(name: String): util.List[String] = { + val headers = request.headers.filter(header => header.is(name.toLowerCase)).map(header => header.value) + if (headers.isEmpty) { + return null + } + headers.asJava + } +} diff --git a/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/com/nr/instrumentation/pekkohttpcore/ResponseFuture.scala b/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/com/nr/instrumentation/pekkohttpcore/ResponseFuture.scala new file mode 100644 index 0000000000..e85e2e3431 --- /dev/null +++ b/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/com/nr/instrumentation/pekkohttpcore/ResponseFuture.scala @@ -0,0 +1,54 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.instrumentation.pekkohttpcore + +import java.util.logging.Level + +import org.apache.pekko.http.scaladsl.model.HttpResponse +import com.newrelic.agent.bridge.{AgentBridge, Token} +import com.newrelic.api.agent.{NewRelic, Transaction} +import com.newrelic.api.agent.weaver.Weaver + +import scala.concurrent.{ExecutionContext, Future} + +object ResponseFuture { + + def wrapResponse(token: Token)(implicit ec: ExecutionContext): (HttpResponse) => Future[HttpResponse] = { response:HttpResponse => { + Future { + var updatedResponse: HttpResponse = response + var localToken = token + + try { + val txn: Transaction = localToken.getTransaction + if (txn != null) { + val wrappedResponse = new ResponseWrapper(response) + txn.setWebResponse(wrappedResponse) + txn.addOutboundResponseHeaders() + txn.markResponseSent() + updatedResponse = wrappedResponse.response + + localToken.expire() + localToken = null + } + } catch { + case t: Throwable => AgentBridge.instrumentation.noticeInstrumentationError(t, Weaver.getImplementationTitle) + try { + localToken.expire() + localToken = null; + } + catch { + case t: Throwable => AgentBridge.instrumentation.noticeInstrumentationError(t, Weaver.getImplementationTitle) + } + + } + + updatedResponse + } + } + } +} diff --git a/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/com/nr/instrumentation/pekkohttpcore/ResponseWrapper.scala b/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/com/nr/instrumentation/pekkohttpcore/ResponseWrapper.scala new file mode 100644 index 0000000000..11c061361e --- /dev/null +++ b/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/com/nr/instrumentation/pekkohttpcore/ResponseWrapper.scala @@ -0,0 +1,37 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.instrumentation.pekkohttpcore + +import org.apache.pekko.http.scaladsl.model.HttpResponse +import org.apache.pekko.http.scaladsl.model.headers.RawHeader +import com.newrelic.api.agent.{HeaderType, ExtendedResponse} + +class ResponseWrapper(var response: HttpResponse) extends ExtendedResponse { + + def getStatus: Int = { + response.status.intValue + } + + def getStatusMessage: String = { + response.status.reason + } + + def getContentType: String = { + response.entity.contentType.value + } + + override def getHeaderType = HeaderType.HTTP + + override def setHeader(name: String, value: String): Unit = { + response = response.addHeader(new RawHeader(name, value)) + } + + def getContentLength: Long = { + response.entity.contentLengthOption.getOrElse(0L) + } +} diff --git a/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/AsyncRequestHandler.scala b/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/AsyncRequestHandler.scala new file mode 100644 index 0000000000..7cc31c6a4a --- /dev/null +++ b/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/AsyncRequestHandler.scala @@ -0,0 +1,55 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package org.apache.pekko.http.scaladsl + +import org.apache.pekko.http.scaladsl.model.{HttpRequest, HttpResponse} +import com.newrelic.agent.bridge.{AgentBridge, Token, TransactionNamePriority} +import com.newrelic.api.agent.weaver.Weaver +import com.newrelic.api.agent.{NewRelic, Trace} +import com.nr.instrumentation.pekkohttpcore.{RequestWrapper, ResponseFuture} + +import scala.concurrent.{ExecutionContext, Future} +import scala.runtime.AbstractFunction1 + +class AsyncRequestHandler(handler: HttpRequest => Future[HttpResponse])(implicit ec: ExecutionContext) extends AbstractFunction1[HttpRequest, Future[HttpResponse]] { + + val transactionCategory: String = "PekkoHttpCore" + + @Trace(dispatcher = true) + override def apply(param: HttpRequest): Future[HttpResponse] = { + + var futureResponse: Future[HttpResponse] = null + var token: Token = null + + try { + token = AgentBridge.getAgent.getTransaction.getToken + AgentBridge.getAgent.getTransaction.setTransactionName(TransactionNamePriority.SERVLET_NAME, true, transactionCategory, "pekkoHandler") + NewRelic.getAgent.getTracedMethod.setMetricName("Pekko", "RequestHandler") + + val wrappedRequest: RequestWrapper = new RequestWrapper(param) + NewRelic.getAgent().getTransaction().setWebRequest(wrappedRequest) + } catch { + case t: Throwable => { + AgentBridge.instrumentation.noticeInstrumentationError(t, Weaver.getImplementationTitle()) + } + } + + futureResponse = handler.apply(param) + + try { + // Modify the original response by passing it through our map function (since a copy + // is required due to the response headers being immutable). Return the (future) result of this map function. + futureResponse.flatMap(ResponseFuture.wrapResponse(token)) + } catch { + case t: Throwable => { + AgentBridge.instrumentation.noticeInstrumentationError(t, Weaver.getImplementationTitle()) + futureResponse + } + } + } +} diff --git a/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/HttpExtInstrumentation.java b/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/HttpExtInstrumentation.java new file mode 100644 index 0000000000..919867cd7a --- /dev/null +++ b/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/HttpExtInstrumentation.java @@ -0,0 +1,72 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package org.apache.pekko.http.scaladsl; + +import org.apache.pekko.event.LoggingAdapter; +import org.apache.pekko.http.scaladsl.model.HttpRequest; +import org.apache.pekko.http.scaladsl.model.HttpResponse; +import org.apache.pekko.http.scaladsl.settings.ConnectionPoolSettings; +import org.apache.pekko.http.scaladsl.settings.ServerSettings; +import org.apache.pekko.stream.Materializer; +import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.Segment; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import com.nr.instrumentation.pekkohttpcore.PekkoHttpUtils; +import scala.Function1; +import scala.concurrent.Future; + +@Weave(type = MatchType.ExactClass, originalName = "org.apache.pekko.http.scaladsl.HttpExt") +public class HttpExtInstrumentation { + + // This method only exists to ensure that this weave module doesn't match for versions of pekko-http-core-2.13 prior to 10.2.0. + // That said, as of 10.2.0 bind, bindAndHandle, bindAndHandleSync, and bindAndHandleAsync were all deprecated in favor of newServerAt: + // @deprecated("Use Http.newServerAt(...)...connectionSource() to create a source that can be materialized to a binding.", since = "10.2.0") + public ServerBuilder newServerAt(String interfaceString, int port) { + return Weaver.callOriginal(); + } + + public Future bindAndHandleAsync( + Function1> handler, + String interfaceString, int port, + ConnectionContext connectionContext, + ServerSettings settings, int parallelism, + LoggingAdapter adapter, Materializer mat) { + + AsyncRequestHandler wrapperHandler = new AsyncRequestHandler(handler, mat.executionContext()); + handler = wrapperHandler; + + return Weaver.callOriginal(); + } + + public Future bindAndHandleSync( + Function1 handler, + String interfaceString, int port, + ConnectionContext connectionContext, + ServerSettings settings, + LoggingAdapter adapter, Materializer mat) { + + SyncRequestHandler wrapperHandler = new SyncRequestHandler(handler); + handler = wrapperHandler; + + return Weaver.callOriginal(); + } + + public Future singleRequest(HttpRequest httpRequest, HttpsConnectionContext connectionContext, ConnectionPoolSettings poolSettings, + LoggingAdapter loggingAdapter) { + final Segment segment = NewRelic.getAgent().getTransaction().startSegment("Pekko", "singleRequest"); + + Future responseFuture = Weaver.callOriginal(); + + PekkoHttpUtils.finishSegmentOnComplete(httpRequest, responseFuture, segment); + + return responseFuture; + } + +} diff --git a/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/HttpInstrumentation.java b/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/HttpInstrumentation.java new file mode 100644 index 0000000000..ac9596e960 --- /dev/null +++ b/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/HttpInstrumentation.java @@ -0,0 +1,46 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package org.apache.pekko.http.scaladsl; + +import com.newrelic.agent.bridge.AgentBridge; +import com.newrelic.agent.bridge.ManifestUtils; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.WeaveAllConstructors; +import com.newrelic.api.agent.weaver.Weaver; +import scala.Function0; +import scala.Function1; +import scala.concurrent.Future; +import scala.concurrent.duration.FiniteDuration; +import scala.runtime.BoxedUnit; + +import java.net.InetSocketAddress; +import java.util.logging.Level; + +@Weave(type = MatchType.ExactClass, originalName = "org.apache.pekko.http.scaladsl.Http") +public class HttpInstrumentation { + + @Weave(type = MatchType.ExactClass, originalName = "org.apache.pekko.http.scaladsl.Http$ServerBinding") + public static class ServerBinding { + + public InetSocketAddress localAddress() { + return Weaver.callOriginal(); + } + + @WeaveAllConstructors + public ServerBinding() { + AgentBridge.getAgent().getLogger().log(Level.FINE, "Setting pekko-http port to: {0,number,#}", localAddress().getPort()); + AgentBridge.publicApi.setAppServerPort(localAddress().getPort()); + AgentBridge.publicApi.setServerInfo("Pekko HTTP", ManifestUtils.getVersionFromManifest(getClass(), "pekko-http-core", "10.2.0")); + + AgentBridge.instrumentation.retransformUninstrumentedClass(SyncRequestHandler.class); + AgentBridge.instrumentation.retransformUninstrumentedClass(AsyncRequestHandler.class); + } + } + +} diff --git a/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/IncomingConnection.java b/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/IncomingConnection.java new file mode 100644 index 0000000000..5243f99d32 --- /dev/null +++ b/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/IncomingConnection.java @@ -0,0 +1,38 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package org.apache.pekko.http.scaladsl; + +import org.apache.pekko.http.scaladsl.model.HttpRequest; +import org.apache.pekko.http.scaladsl.model.HttpResponse; +import org.apache.pekko.stream.Materializer; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import scala.Function1; +import scala.concurrent.Future; + +@Weave(originalName = "org.apache.pekko.http.scaladsl.Http$IncomingConnection") +public class IncomingConnection { + + public void handleWithSyncHandler(Function1 func, Materializer mat) { + + SyncRequestHandler wrapperHandler = new SyncRequestHandler(func); + func = wrapperHandler; + + Weaver.callOriginal(); + } + + public void handleWithAsyncHandler(Function1> func, int parallel, Materializer mat) { + + AsyncRequestHandler wrapperHandler = new AsyncRequestHandler(func, mat.executionContext()); + func = wrapperHandler; + + Weaver.callOriginal(); + } + + +} diff --git a/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/SyncRequestHandler.scala b/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/SyncRequestHandler.scala new file mode 100644 index 0000000000..53dc58130f --- /dev/null +++ b/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/SyncRequestHandler.scala @@ -0,0 +1,59 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package org.apache.pekko.http.scaladsl + +import org.apache.pekko.http.scaladsl.model.{HttpRequest, HttpResponse} +import com.newrelic.agent.bridge.{AgentBridge, TransactionNamePriority} +import com.newrelic.api.agent.weaver.Weaver +import com.newrelic.api.agent.{NewRelic, Trace, Transaction} +import com.nr.instrumentation.pekkohttpcore.{RequestWrapper, ResponseWrapper} + +import scala.runtime.AbstractFunction1 + +class SyncRequestHandler(handler: HttpRequest => HttpResponse) extends AbstractFunction1[HttpRequest, HttpResponse] { + + val transactionCategory :String = "PekkoHttpCore" + + @Trace(dispatcher = true) + override def apply(param: HttpRequest): HttpResponse = { + + try { + AgentBridge.getAgent.getTransaction.setTransactionName(TransactionNamePriority.SERVLET_NAME, true, transactionCategory, "pekkoHandler") + NewRelic.getAgent.getTracedMethod.setMetricName("Pekko", "RequestHandler") + + val wrappedRequest: RequestWrapper = new RequestWrapper(param) + NewRelic.getAgent().getTransaction().setWebRequest(wrappedRequest) + + } catch { + case t: Throwable => AgentBridge.instrumentation.noticeInstrumentationError(t, Weaver.getImplementationTitle()); + } + + val response: HttpResponse = handler.apply(param) + + try { + + var updatedResponse: HttpResponse = response + + val txn: Transaction = NewRelic.getAgent().getTransaction() + + if (txn != null) { + val wrappedResponse = new ResponseWrapper(response) + txn.setWebResponse(wrappedResponse) + txn.addOutboundResponseHeaders() + txn.markResponseSent() + updatedResponse = wrappedResponse.response + } + + updatedResponse + + } catch { + case t: Throwable => AgentBridge.instrumentation.noticeInstrumentationError(t, Weaver.getImplementationTitle()); + response + } + } +} diff --git a/instrumentation/apache-pekko-http-core-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/core/PekkoHttpCoreTest.scala b/instrumentation/apache-pekko-http-core-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/core/PekkoHttpCoreTest.scala new file mode 100644 index 0000000000..73cf5f975b --- /dev/null +++ b/instrumentation/apache-pekko-http-core-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/core/PekkoHttpCoreTest.scala @@ -0,0 +1,259 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.agent.instrumentation.org.apache.pekko.http.core + +import java.util +import java.util.concurrent.TimeUnit + +import org.apache.pekko.actor.ActorSystem +import org.apache.pekko.http.scaladsl.Http +import org.apache.pekko.http.scaladsl.model.headers.RawHeader +import org.apache.pekko.http.scaladsl.model.{HttpHeader, HttpRequest, HttpResponse} +import org.apache.pekko.stream.ActorMaterializer +import com.newrelic.agent.HeadersUtil +import com.newrelic.agent.introspec._ +import com.newrelic.agent.util.Obfuscator +import com.newrelic.api.agent.Trace +import org.junit.Assert.{assertNotNull, assertTrue} +import org.junit.runner.RunWith +import org.junit.{Assert, Test} + +import scala.concurrent.duration.DurationInt +import scala.concurrent.{Await, Future} + +@RunWith(classOf[InstrumentationTestRunner]) +@InstrumentationTestConfig(includePrefixes = Array("scala", "pekko")) +class PekkoHttpCoreTest { + + implicit val system: ActorSystem = ActorSystem() + implicit val materializer: ActorMaterializer = ActorMaterializer() + + val pekkoServer = new PekkoServer() + val playServer = new PlayServer() + var port: Int = InstrumentationTestRunner.getIntrospector.getRandomPort + val baseUrl: String = "http://localhost:" + port + + @Test + def syncHandlerPekkoServerTest(): Unit = { + pekkoServer.start(port, async = false) + + Http().singleRequest(HttpRequest(uri = baseUrl + "/ping")) + + val introspector: Introspector = InstrumentationTestRunner.getIntrospector + awaitFinishedTx(introspector, 1) + pekkoServer.stop() + Assert.assertEquals(1, introspector.getFinishedTransactionCount()) + val txName = introspector.getTransactionNames.iterator.next + Assert.assertEquals("WebTransaction/PekkoHttpCore/pekkoHandler", txName) + } + + @Test + def asyncHandlerPekkoServerTest(): Unit = { + pekkoServer.start(port, async = true) + Http().singleRequest(HttpRequest(uri = baseUrl + "/asyncPing")) + + val introspector: Introspector = InstrumentationTestRunner.getIntrospector + awaitFinishedTx(introspector, 1) + pekkoServer.stop() + Assert.assertEquals(1, introspector.getFinishedTransactionCount()) + val txName = introspector.getTransactionNames.iterator.next + Assert.assertEquals("WebTransaction/PekkoHttpCore/pekkoHandler", txName) + } + + @Test + def syncHandlerPlayServerTest(): Unit = { + playServer.start(port, async = false) + Http().singleRequest(HttpRequest(uri = baseUrl + "/ping")) + + val introspector: Introspector = InstrumentationTestRunner.getIntrospector + awaitFinishedTx(introspector, 1) + playServer.stop() + Assert.assertEquals(1, introspector.getFinishedTransactionCount()) + val txName = introspector.getTransactionNames.iterator.next + Assert.assertEquals("WebTransaction/PekkoHttpCore/pekkoHandler", txName) + } + + @Test + def asyncHandlerPlayServerTest(): Unit = { + playServer.start(port, async = true) + + Http().singleRequest(HttpRequest(uri = baseUrl + "/asyncPing")) + + val introspector: Introspector = InstrumentationTestRunner.getIntrospector + awaitFinishedTx(introspector, 1) + playServer.stop() + Assert.assertEquals(1, introspector.getFinishedTransactionCount()) + val txName = introspector.getTransactionNames.iterator.next + Assert.assertEquals("WebTransaction/PekkoHttpCore/pekkoHandler", txName) + } + + @Test + def syncHandlerPlayServerCatTest(): Unit = { + playServer.start(port, async = false) + + val idHeader = Obfuscator.obfuscateNameUsingKey("1xyz234#1xyz3333", "cafebabedeadbeef8675309babecafe1beefdead") + + val requestHeaders: Seq[HttpHeader] = List(new RawHeader(HeadersUtil.NEWRELIC_ID_HEADER, idHeader)) + val responseFuture: Future[HttpResponse] = + Http().singleRequest(HttpRequest(uri = baseUrl + "/ping", headers = collection.immutable.Seq[HttpHeader](requestHeaders: _*))) + + val result: HttpResponse = Await.result(responseFuture, new DurationInt(10).seconds) + var hasNewRelicHeader: Boolean = false + hasNewRelicHeader = result.headers.exists(header => header.name().contains(HeadersUtil.NEWRELIC_APP_DATA_HEADER)) + + Assert.assertTrue(hasNewRelicHeader) + + val introspector: Introspector = InstrumentationTestRunner.getIntrospector + awaitFinishedTx(introspector, 1) + playServer.stop() + Assert.assertEquals(1, introspector.getFinishedTransactionCount()) + val txName = introspector.getTransactionNames.iterator.next + Assert.assertEquals("WebTransaction/PekkoHttpCore/pekkoHandler", txName) + } + + @Test + def asyncHandlerPlayServerCatTest(): Unit = { + playServer.start(port, async = true) + + val idHeader = Obfuscator.obfuscateNameUsingKey("1xyz234#1xyz3333", "cafebabedeadbeef8675309babecafe1beefdead") + val requestHeaders: Seq[HttpHeader] = List(new RawHeader(HeadersUtil.NEWRELIC_ID_HEADER, idHeader)) + + val responseFuture: Future[HttpResponse] = + Http().singleRequest(HttpRequest(uri = baseUrl + "/asyncPing", headers = collection.immutable.Seq[HttpHeader](requestHeaders: _*))) + + val result: HttpResponse = Await.result(responseFuture, new DurationInt(1).seconds) + var hasNewRelicHeader: Boolean = false + hasNewRelicHeader = result.headers.exists(header => header.name().contains(HeadersUtil.NEWRELIC_APP_DATA_HEADER)) + Assert.assertTrue(hasNewRelicHeader) + + val introspector: Introspector = InstrumentationTestRunner.getIntrospector + awaitFinishedTx(introspector, 1) + playServer.stop() + Assert.assertEquals(1, introspector.getFinishedTransactionCount()) + val txName = introspector.getTransactionNames.iterator.next + Assert.assertEquals("WebTransaction/PekkoHttpCore/pekkoHandler", txName) + } + + @Test + def syncHandlerPekkoServerCatTest(): Unit = { + pekkoServer.start(port, async = false) + + val idHeader = Obfuscator.obfuscateNameUsingKey("1xyz234#1xyz3333", "cafebabedeadbeef8675309babecafe1beefdead") + val requestHeaders: Seq[HttpHeader] = List(new RawHeader(HeadersUtil.NEWRELIC_ID_HEADER, idHeader)) + + val responseFuture: Future[HttpResponse] = + Http().singleRequest(HttpRequest(uri = baseUrl + "/ping", headers = collection.immutable.Seq[HttpHeader](requestHeaders: _*))) + + val result: HttpResponse = Await.result(responseFuture, new DurationInt(10).seconds) + var hasNewRelicHeader: Boolean = false + hasNewRelicHeader = result.headers.exists(header => header.name().contains(HeadersUtil.NEWRELIC_APP_DATA_HEADER)) + Assert.assertTrue(hasNewRelicHeader) + + val introspector: Introspector = InstrumentationTestRunner.getIntrospector + awaitFinishedTx(introspector, 1) + pekkoServer.stop() + Assert.assertEquals(1, introspector.getFinishedTransactionCount()) + val txName = introspector.getTransactionNames.iterator.next + Assert.assertEquals("WebTransaction/PekkoHttpCore/pekkoHandler", txName) + } + + @Test + def asyncHandlerPekkoServerCatTest(): Unit = { + pekkoServer.start(port, async = true) + + val idHeader = Obfuscator.obfuscateNameUsingKey("1xyz234#1xyz3333", "cafebabedeadbeef8675309babecafe1beefdead") + val requestHeaders: Seq[HttpHeader] = List(new RawHeader(HeadersUtil.NEWRELIC_ID_HEADER, idHeader)) + + val responseFuture: Future[HttpResponse] = + Http().singleRequest(HttpRequest(uri = baseUrl + "/asyncPing", headers = collection.immutable.Seq[HttpHeader](requestHeaders: _*))) + + val result: HttpResponse = Await.result(responseFuture, new DurationInt(1).seconds) + var hasNewRelicHeader: Boolean = false + hasNewRelicHeader = result.headers.exists(header => header.name().contains(HeadersUtil.NEWRELIC_APP_DATA_HEADER)) + Assert.assertTrue(hasNewRelicHeader) + + val introspector: Introspector = InstrumentationTestRunner.getIntrospector + awaitFinishedTx(introspector, 1) + pekkoServer.stop() + Assert.assertEquals(1, introspector.getFinishedTransactionCount()) + val txName = introspector.getTransactionNames.iterator.next + Assert.assertEquals("WebTransaction/PekkoHttpCore/pekkoHandler", txName) + } + + @Test + def asyncHandlerPekkoServerHttpClientTest(): Unit = { + pekkoServer.start(port, async = true) + makeHttpRequest(true) + + verifyHttpClientMetrics() + } + + @Test + def syncHandlerPekkoServerHttpClientTest(): Unit = { + pekkoServer.start(port, async = false) + makeHttpRequest(false) + + verifyHttpClientMetrics() + } + + def verifyHttpClientMetrics() = { + val introspector: Introspector = InstrumentationTestRunner.getIntrospector + awaitFinishedTx(introspector, 2) + pekkoServer.stop() + Assert.assertEquals(2, introspector.getFinishedTransactionCount()) + val txNames = introspector.getTransactionNames + + val clientTx = "OtherTransaction/Custom/com.agent.instrumentation.org.apache.pekko.http.core.PekkoHttpCoreTest/makeHttpRequest" + val serverTx = "WebTransaction/PekkoHttpCore/pekkoHandler" + + // The @Trace(dispatcher = true) below where the Http call is made + Assert.assertTrue(txNames.contains(clientTx)) + // The WebTransaction from the PekkoHttp server itself + Assert.assertTrue(txNames.contains(serverTx)) + + Assert.assertEquals(1, MetricsHelper.getScopedMetricCount(clientTx, "External/localhost/PekkoHttpClient/GET")) + Assert.assertEquals(1, MetricsHelper.getUnscopedMetricCount("External/localhost/PekkoHttpClient/GET")) + + // external rollups + Assert.assertEquals(1, MetricsHelper.getUnscopedMetricCount("External/localhost/all")) + Assert.assertEquals(1, MetricsHelper.getUnscopedMetricCount("External/allOther")) + Assert.assertEquals(1, MetricsHelper.getUnscopedMetricCount("External/all")) + + // verify timing of External/all metrics + val externalMetrics: TracedMetricData = InstrumentationTestRunner.getIntrospector.getUnscopedMetrics.get("External/all") + assertNotNull(externalMetrics) + assertTrue(externalMetrics.getTotalTimeInSec > 0) + + val transactionEvents: util.Collection[TransactionEvent] = introspector.getTransactionEvents(clientTx) + Assert.assertEquals(1, transactionEvents.size) + val transactionEvent: TransactionEvent = transactionEvents.iterator.next + Assert.assertEquals(1, transactionEvent.getExternalCallCount) + Assert.assertTrue(transactionEvent.getExternalDurationInSec > 0) + + val externalRequests: util.Collection[ExternalRequest] = introspector.getExternalRequests(clientTx) + Assert.assertEquals(1, externalRequests.size) + val externalRequest: ExternalRequest = externalRequests.iterator.next + Assert.assertEquals(1, externalRequest.getCount) + Assert.assertEquals("localhost", externalRequest.getHostname) + Assert.assertEquals("PekkoHttpClient", externalRequest.getLibrary) + Assert.assertEquals("GET", externalRequest.getOperation) + } + + @Trace(dispatcher = true, nameTransaction = true) + private def makeHttpRequest(async: Boolean): Unit = { + Http().singleRequest(HttpRequest(uri = baseUrl + (if (async) "/asyncPing" else "/ping"))) + } + + private def awaitFinishedTx(introspector: Introspector, expectedTxCount: Int) = { + val timeout = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(30) // 30 seconds from now + while (System.currentTimeMillis() < timeout && introspector.getFinishedTransactionCount() <= expectedTxCount - 1) { + Thread.sleep(100) + } + Thread.sleep(100) + } +} diff --git a/instrumentation/apache-pekko-http-core-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/core/PekkoServer.scala b/instrumentation/apache-pekko-http-core-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/core/PekkoServer.scala new file mode 100644 index 0000000000..0a748bf5c9 --- /dev/null +++ b/instrumentation/apache-pekko-http-core-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/core/PekkoServer.scala @@ -0,0 +1,77 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.agent.instrumentation.org.apache.pekko.http.core + +import org.apache.pekko.actor.ActorSystem +import org.apache.pekko.event.Logging +import org.apache.pekko.http.scaladsl.Http +import org.apache.pekko.http.scaladsl.model.HttpMethods._ +import org.apache.pekko.http.scaladsl.model._ +import org.apache.pekko.stream.ActorMaterializer +import org.apache.pekko.stream.scaladsl.{Source, _} +import org.apache.pekko.util.Timeout +import com.typesafe.config.ConfigFactory + +import scala.concurrent.duration._ +import scala.concurrent.{Await, Future} +import scala.language.postfixOps + +//how the pekko http core docs' example sets up a server +class PekkoServer() { + implicit val system = ActorSystem() + implicit val executor = system.dispatcher + implicit val materializer = ActorMaterializer() + implicit val timeout: Timeout = 3 seconds + + val config = ConfigFactory.load() + val logger = Logging(system, getClass) + + var serverSource: Source[Http.IncomingConnection, Future[Http.ServerBinding]] = _ + var bindingFuture: Future[Http.ServerBinding] = _ + + def start(port: Int, async: Boolean) = { + + serverSource = Http().bind(interface = "localhost", port) + + if (async) { + + val asyncRequestHandler: HttpRequest => Future[HttpResponse] = { + case HttpRequest(GET, Uri.Path("/asyncPing"), _, _, _) => Future[HttpResponse](HttpResponse(entity = "Hoops!")) + } + + bindingFuture = serverSource.to(Sink.foreach { + connection => + println("accepted connection from: " + connection.remoteAddress) + connection handleWithAsyncHandler asyncRequestHandler + }).run() + } + else { + + val requestHandler: HttpRequest => HttpResponse = { + case HttpRequest(GET, Uri.Path("/ping"), _, _, _) => + HttpResponse(entity = "Hoops!") + } + + bindingFuture = serverSource.to(Sink.foreach { + connection => + println("accepted connection from: " + connection.remoteAddress) + connection handleWithSyncHandler requestHandler + }).run() + } + + Await.ready({ + bindingFuture + }, timeout.duration) + } + + def stop() = { + if (bindingFuture != null) { + bindingFuture.flatMap(_.unbind()).onComplete(_ => system.terminate()) + } + } +} diff --git a/instrumentation/apache-pekko-http-core-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/core/PlayServer.scala b/instrumentation/apache-pekko-http-core-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/core/PlayServer.scala new file mode 100644 index 0000000000..e717c5b18d --- /dev/null +++ b/instrumentation/apache-pekko-http-core-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/core/PlayServer.scala @@ -0,0 +1,70 @@ +/* + * + * * Copyright 2024 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.agent.instrumentation.org.apache.pekko.http.core + +import org.apache.pekko.actor.ActorSystem +import org.apache.pekko.event.Logging +import org.apache.pekko.http.scaladsl.Http +import org.apache.pekko.http.scaladsl.model.HttpMethods._ +import org.apache.pekko.http.scaladsl.model._ +import org.apache.pekko.stream.ActorMaterializer +import org.apache.pekko.stream.scaladsl.{Source, _} +import org.apache.pekko.util.Timeout +import com.typesafe.config.ConfigFactory + +import scala.concurrent.duration._ +import scala.concurrent.{Await, Future} +import scala.language.postfixOps + +//how play 2.6 sets up a server +class PlayServer() { + implicit val system = ActorSystem() + implicit val executor = system.dispatcher + implicit val materializer = ActorMaterializer() + implicit val timeout: Timeout = 3 seconds + + val config = ConfigFactory.load() + val logger = Logging(system, getClass) + + var bindingFuture: Future[Http.ServerBinding] = _ + + def start(port: Int, async: Boolean) = { + + if (async) { + + val asyncRequestHandler: HttpRequest => Future[HttpResponse] = { + case HttpRequest(GET, Uri.Path("/asyncPing"), _, _, _) => + Future[HttpResponse](HttpResponse(entity = "Hoops!")) + } + + bindingFuture = Http().bindAndHandleAsync(asyncRequestHandler, interface = "localhost", port) + + } + else { + + val requestHandler: HttpRequest => HttpResponse = { + case HttpRequest(GET, Uri.Path("/ping"), _, _, _) => + HttpResponse(entity = "Boops!") + } + + bindingFuture = Http().bindAndHandleSync(requestHandler, interface = "localhost", port) + } + + Await.ready({ + bindingFuture + }, timeout.duration) + } + + def stop() = { + if (bindingFuture != null) { + bindingFuture.flatMap(_.unbind()).onComplete(_ => { + system.terminate() + }) + } + } +} diff --git a/settings.gradle b/settings.gradle index 9cef988ff7..14b56ff72c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -93,6 +93,7 @@ include 'instrumentation:apache-log4j-2.6' include 'instrumentation:apache-log4j-2.11' include 'instrumentation:apache-pekko-1' include 'instrumentation:apache-pekko-http-2.13_1' +include 'instrumentation:apache-pekko-http-core-2.13_1' include 'instrumentation:apache-struts-2.0' include 'instrumentation:async-http-client-2.0.0' include 'instrumentation:async-http-client-2.1.0' From be8756a05a8fbfd104274eaf57519b955edd8deb Mon Sep 17 00:00:00 2001 From: Kate Anderson Date: Tue, 2 Apr 2024 12:29:16 -0700 Subject: [PATCH 3/5] make pekko http updates --- instrumentation/apache-pekko-http-2.13_1/build.gradle | 2 +- .../org.apache.pekko/http/PekkoHttpRoutesTest.java | 2 +- .../org.apache.pekko/http/PekkoResponseWrapperTest.java | 2 +- .../org.apache.pekko/http/core/PekkoServer.scala | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/instrumentation/apache-pekko-http-2.13_1/build.gradle b/instrumentation/apache-pekko-http-2.13_1/build.gradle index c0bd3216d1..992320a278 100644 --- a/instrumentation/apache-pekko-http-2.13_1/build.gradle +++ b/instrumentation/apache-pekko-http-2.13_1/build.gradle @@ -15,7 +15,7 @@ dependencies { implementation(project(":newrelic-weaver-scala-api")) implementation("org.apache.pekko:pekko-http_2.13:1.0.1") implementation("org.apache.pekko:pekko-stream_2.13:1.0.1") - implementation("org.apache.pekko:pekko-actor_2.13:1.0.2") + implementation("org.apache.pekko:pekko-actor_2.13:1.0.1") testImplementation(project(":instrumentation:apache-pekko-1")) { transitive = false } testImplementation(project(":instrumentation:scala-2.13.0")) { transitive = false } diff --git a/instrumentation/apache-pekko-http-2.13_1/src/test/java/com/agent/instrumentation/org.apache.pekko/http/PekkoHttpRoutesTest.java b/instrumentation/apache-pekko-http-2.13_1/src/test/java/com/agent/instrumentation/org.apache.pekko/http/PekkoHttpRoutesTest.java index 025062ddf4..2c3da90ffb 100644 --- a/instrumentation/apache-pekko-http-2.13_1/src/test/java/com/agent/instrumentation/org.apache.pekko/http/PekkoHttpRoutesTest.java +++ b/instrumentation/apache-pekko-http-2.13_1/src/test/java/com/agent/instrumentation/org.apache.pekko/http/PekkoHttpRoutesTest.java @@ -28,7 +28,7 @@ import static org.hamcrest.Matchers.containsString; @RunWith(InstrumentationTestRunner.class) -@InstrumentationTestConfig(includePrefixes = { "pekko", "scala" }) +@InstrumentationTestConfig(includePrefixes = { "org.apache.pekko", "scala" }) public class PekkoHttpRoutesTest { private static final long TIMEOUT = 30000; diff --git a/instrumentation/apache-pekko-http-2.13_1/src/test/java/com/agent/instrumentation/org.apache.pekko/http/PekkoResponseWrapperTest.java b/instrumentation/apache-pekko-http-2.13_1/src/test/java/com/agent/instrumentation/org.apache.pekko/http/PekkoResponseWrapperTest.java index 5e5d9eb7be..ba3496bcba 100644 --- a/instrumentation/apache-pekko-http-2.13_1/src/test/java/com/agent/instrumentation/org.apache.pekko/http/PekkoResponseWrapperTest.java +++ b/instrumentation/apache-pekko-http-2.13_1/src/test/java/com/agent/instrumentation/org.apache.pekko/http/PekkoResponseWrapperTest.java @@ -25,7 +25,7 @@ import static org.junit.Assert.assertTrue; @RunWith(InstrumentationTestRunner.class) -@InstrumentationTestConfig(includePrefixes = { "pekko", "scala" }) +@InstrumentationTestConfig(includePrefixes = { "org.apache.pekko", "scala" }) public class PekkoResponseWrapperTest { private static final long TIMEOUT = 30000; diff --git a/instrumentation/apache-pekko-http-core-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/core/PekkoServer.scala b/instrumentation/apache-pekko-http-core-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/core/PekkoServer.scala index 0a748bf5c9..47f19aeb79 100644 --- a/instrumentation/apache-pekko-http-core-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/core/PekkoServer.scala +++ b/instrumentation/apache-pekko-http-core-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/core/PekkoServer.scala @@ -36,7 +36,7 @@ class PekkoServer() { def start(port: Int, async: Boolean) = { - serverSource = Http().bind(interface = "localhost", port) + serverSource = Http().newServerAt(interface = "localhost", port).connectionSource() if (async) { From 1a9a5cdbad0dc986ea403f855db2429028adfd89 Mon Sep 17 00:00:00 2001 From: Kate Anderson Date: Thu, 11 Apr 2024 15:47:41 -0700 Subject: [PATCH 4/5] Update pekko-http and pekko-http-core modules --- .../NewRelicRequestContextWrapper.scala | 6 ++---- .../server/PekkoHttpContextFunction.scala | 4 +--- .../directives/PekkoExecutionDirectives.java | 20 +++++++++---------- .../org.apache.pekko/http/HttpServer.scala | 4 +--- .../http/PekkoHttpTestRoutes.scala | 1 - .../apache-pekko-http-core-2.13_1/README.md | 19 ++++++++++++------ .../build.gradle | 3 --- .../http/scaladsl/HttpExtInstrumentation.java | 9 ++------- .../http/core/PekkoHttpCoreTest.scala | 8 +++----- .../http/core/PekkoServer.scala | 2 +- 10 files changed, 33 insertions(+), 43 deletions(-) diff --git a/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/NewRelicRequestContextWrapper.scala b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/NewRelicRequestContextWrapper.scala index 687883cd0c..8c47a7b148 100644 --- a/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/NewRelicRequestContextWrapper.scala +++ b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/NewRelicRequestContextWrapper.scala @@ -55,8 +55,6 @@ class NewRelicRequestContextWrapper(originalRequestContext: Object, underlyingRequestContext.reconfigure(executionContext, materializer, log, settings) } - //INCOMPLETE --- COME BACK LATER - @Trace(async = true) override def complete(trm: ToResponseMarshallable): Future[RouteResult] = { try { @@ -71,7 +69,7 @@ class NewRelicRequestContextWrapper(originalRequestContext: Object, result })(executionContext) } catch { - case t: Throwable => AgentBridge.instrumentation.noticeInstrumentationError(t, "pekko-http-2.4.5") + case t: Throwable => AgentBridge.instrumentation.noticeInstrumentationError(t, "pekko-http-1") underlyingRequestContext.complete(trm) } } @@ -83,7 +81,7 @@ class NewRelicRequestContextWrapper(originalRequestContext: Object, token.linkAndExpire() } } catch { - case t: Throwable => AgentBridge.instrumentation.noticeInstrumentationError(t, "pekko-http-2.4.5") + case t: Throwable => AgentBridge.instrumentation.noticeInstrumentationError(t, "pekko-http-1") } } diff --git a/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/PekkoHttpContextFunction.scala b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/PekkoHttpContextFunction.scala index 1f7d705917..9d2e05b078 100644 --- a/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/PekkoHttpContextFunction.scala +++ b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/PekkoHttpContextFunction.scala @@ -34,8 +34,6 @@ object PekkoHttpContextFunction { } -// REVISIT THIS AND UNPACK IT - class ContextWrapper(original: Function1[RequestContext, Future[RouteResult]]) extends AbstractFunction1[RequestContext, Future[RouteResult]] { @Trace(dispatcher = true) @@ -55,7 +53,7 @@ class ContextWrapper(original: Function1[RequestContext, Future[RouteResult]]) e original.apply(newCtx) } catch { case t: Throwable => { - AgentBridge.instrumentation.noticeInstrumentationError(t, "pekko-http-2.4.5") + AgentBridge.instrumentation.noticeInstrumentationError(t, "pekko-http-1") original.apply(ctx) } } diff --git a/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/directives/PekkoExecutionDirectives.java b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/directives/PekkoExecutionDirectives.java index 699264371f..88962fef97 100644 --- a/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/directives/PekkoExecutionDirectives.java +++ b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/directives/PekkoExecutionDirectives.java @@ -7,26 +7,26 @@ package org.apache.pekko.http.scaladsl.server.directives; -import org.apache.pekko.http.scaladsl.server.PekkoHttpContextFunction; -import org.apache.pekko.http.scaladsl.server.ExceptionHandler; -import org.apache.pekko.http.scaladsl.server.RejectionHandler; -import org.apache.pekko.http.scaladsl.server.RequestContext; -import org.apache.pekko.http.scaladsl.server.RouteResult; +import org.apache.pekko.http.scaladsl.model.HttpRequest; +import org.apache.pekko.http.scaladsl.model.HttpResponse; +import org.apache.pekko.http.scaladsl.server.*; import org.apache.pekko.http.scaladsl.settings.ParserSettings; import org.apache.pekko.http.scaladsl.settings.RoutingSettings; import com.newrelic.api.agent.weaver.Weave; import com.newrelic.api.agent.weaver.Weaver; +import org.apache.pekko.stream.Materializer; import scala.Function1; +import scala.concurrent.ExecutionContextExecutor; import scala.concurrent.Future; @Weave(originalName = "org.apache.pekko.http.scaladsl.server.Route$") public class PekkoExecutionDirectives { - public Function1> seal(Function1> f1, - RoutingSettings routingSettings, ParserSettings parserSettings, RejectionHandler rejectionHandler, - ExceptionHandler exceptionHandler) { - Function1> result = Weaver.callOriginal(); - return PekkoHttpContextFunction.contextWrapper(result); + private Function1> createAsyncHandler(Function1> sealedRoute, RoutingLog routingLog, RoutingSettings routingSettings, + ParserSettings parserSettings, ExecutionContextExecutor ec, Materializer mat){ + sealedRoute = PekkoHttpContextFunction.contextWrapper(sealedRoute); + return Weaver.callOriginal(); } } diff --git a/instrumentation/apache-pekko-http-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/HttpServer.scala b/instrumentation/apache-pekko-http-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/HttpServer.scala index 121cda754a..e5ebd49dad 100644 --- a/instrumentation/apache-pekko-http-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/HttpServer.scala +++ b/instrumentation/apache-pekko-http-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/HttpServer.scala @@ -13,7 +13,6 @@ import org.apache.pekko.http.scaladsl.Http import org.apache.pekko.http.scaladsl.Http.ServerBinding import org.apache.pekko.http.scaladsl.server.Directives._ import org.apache.pekko.http.scaladsl.server.{RequestContext, Route} -import org.apache.pekko.stream.ActorMaterializer import org.apache.pekko.util.Timeout import com.typesafe.config.ConfigFactory @@ -24,7 +23,6 @@ import scala.language.postfixOps class HttpServer(val routes: Route = RouteService.defaultRoute) { implicit val system = ActorSystem() implicit val executor = system.dispatcher - implicit val materializer = ActorMaterializer() implicit val timeout: Timeout = 60 seconds val config = ConfigFactory.load() @@ -34,7 +32,7 @@ class HttpServer(val routes: Route = RouteService.defaultRoute) { def start(port: Int) = { Await.ready({ - handle = Http().bindAndHandle(routes, "localhost", port) + handle = Http().newServerAt("localhost", port).bind(routes) handle }, timeout.duration) } diff --git a/instrumentation/apache-pekko-http-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/PekkoHttpTestRoutes.scala b/instrumentation/apache-pekko-http-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/PekkoHttpTestRoutes.scala index fb37cd72e0..56e1542c57 100644 --- a/instrumentation/apache-pekko-http-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/PekkoHttpTestRoutes.scala +++ b/instrumentation/apache-pekko-http-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/PekkoHttpTestRoutes.scala @@ -30,7 +30,6 @@ class PekkoHttpTestRoutes { implicit val system: ActorSystem = ActorSystem("pekkohttptest") implicit val scheduler: Scheduler = system.scheduler implicit val executor: ExecutionContext = system.dispatcher - implicit val materializer: ActorMaterializer = ActorMaterializer() implicit val requestTimeout: Timeout = Timeout(30 seconds) val newrelicCheck: ActorRef = system.actorOf(StatusCheckActor.props, "StatusCheck") diff --git a/instrumentation/apache-pekko-http-core-2.13_1/README.md b/instrumentation/apache-pekko-http-core-2.13_1/README.md index 7c52df3430..d86f13f9d6 100644 --- a/instrumentation/apache-pekko-http-core-2.13_1/README.md +++ b/instrumentation/apache-pekko-http-core-2.13_1/README.md @@ -1,7 +1,14 @@ -#Akka HTTP core instrumentation +#Pekko HTTP core instrumentation + +This instrumentation is a lift of `akka-http-core-2.13_10.2.0`. + +As of `Pekko Http Core 1.0.0`, `bindAndHandleAsync` and `bindAndHandleSync` have both been deprecated and replaced by +`Http().newServerAt().bind()`. However, these methods still exist and are instrumented where used, +so the documentation below (also taken from Akka) is maintained for historical purposes. + ##HttpExt Instrumentation -Instrumentation for Akka HTTP Core is carried out in the `akka.http.scaladsl.HttpExt` class that serves as the +Instrumentation for Pekko HTTP Core is carried out in the `pekko.http.scaladsl.HttpExt` class that serves as the main entry point for a server. 2 convenience methods from `HttpExt` that can be used to start an HTTP server have been instrumented, they are : @@ -9,11 +16,11 @@ been instrumented, they are : - ` bindAndHandleSync`: Convenience method which starts a new HTTP server at the given endpoint and uses handler that is a function recieving an `HttpRequest` and returning a `HttpResponse` -It has been decide that intrumentation is not extended for `bindAndHandle` which starts a new HTTP server using a -`akka.stream.scaladsl.Flow` instance. This is due to a clash in the Akka Http Routing DSL instrumentation. +It has been decided that intrumentation is not extended for `bindAndHandle` which starts a new HTTP server using a +`pekko.stream.scaladsl.Flow` instance. This is due to a clash in the Akka Http Routing DSL instrumentation. -Users wishing to start an HTTP Server from an `akka.stream.scaladsl.Flow` can use the following workaround +Users wishing to start an HTTP Server from an `pekko.stream.scaladsl.Flow` can use the following workaround ```scala val flow: Flow[HttpRequest, HttpResponse, NotUsed] = ??? @@ -22,7 +29,7 @@ Users wishing to start an HTTP Server from an `akka.stream.scaladsl.Flow` can us ``` This workaround is not needed for users using calling `bindAndHandle` using `akka.http.scaladsl.Route` from the -Akka Http Routing DSL. Instrumentation should work in the same way being called from the other conveniencs methods +Pekko Http Routing DSL. Instrumentation should work in the same way being called from the other conveniencs methods to start an HTTP Server diff --git a/instrumentation/apache-pekko-http-core-2.13_1/build.gradle b/instrumentation/apache-pekko-http-core-2.13_1/build.gradle index 2ebca6a7c9..8636c90614 100644 --- a/instrumentation/apache-pekko-http-core-2.13_1/build.gradle +++ b/instrumentation/apache-pekko-http-core-2.13_1/build.gradle @@ -16,9 +16,6 @@ verifyInstrumentation { passesOnly('org.apache.pekko:pekko-http-core_2.13:[1.0.0,)') { implementation("org.apache.pekko:pekko-stream_2.13:1.0.1") } -// fails('org.apache.pekko:pekko-http-core_2.12:[1.0.0,)') { -// implementation("org.apache.pekko:pekko-stream_2.12:2.5.23") -// } } site { diff --git a/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/HttpExtInstrumentation.java b/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/HttpExtInstrumentation.java index 919867cd7a..d1bd5fe331 100644 --- a/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/HttpExtInstrumentation.java +++ b/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/HttpExtInstrumentation.java @@ -25,13 +25,8 @@ @Weave(type = MatchType.ExactClass, originalName = "org.apache.pekko.http.scaladsl.HttpExt") public class HttpExtInstrumentation { - // This method only exists to ensure that this weave module doesn't match for versions of pekko-http-core-2.13 prior to 10.2.0. - // That said, as of 10.2.0 bind, bindAndHandle, bindAndHandleSync, and bindAndHandleAsync were all deprecated in favor of newServerAt: - // @deprecated("Use Http.newServerAt(...)...connectionSource() to create a source that can be materialized to a binding.", since = "10.2.0") - public ServerBuilder newServerAt(String interfaceString, int port) { - return Weaver.callOriginal(); - } - + // These methods are deprecated but still exist in Pekko Http Core 1.0.0. + // They have been replaced by Http().newServerAt().bind(). public Future bindAndHandleAsync( Function1> handler, String interfaceString, int port, diff --git a/instrumentation/apache-pekko-http-core-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/core/PekkoHttpCoreTest.scala b/instrumentation/apache-pekko-http-core-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/core/PekkoHttpCoreTest.scala index 73cf5f975b..3d3591a157 100644 --- a/instrumentation/apache-pekko-http-core-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/core/PekkoHttpCoreTest.scala +++ b/instrumentation/apache-pekko-http-core-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/core/PekkoHttpCoreTest.scala @@ -9,12 +9,11 @@ package com.agent.instrumentation.org.apache.pekko.http.core import java.util import java.util.concurrent.TimeUnit - import org.apache.pekko.actor.ActorSystem import org.apache.pekko.http.scaladsl.Http import org.apache.pekko.http.scaladsl.model.headers.RawHeader import org.apache.pekko.http.scaladsl.model.{HttpHeader, HttpRequest, HttpResponse} -import org.apache.pekko.stream.ActorMaterializer +import org.apache.pekko.stream.{ActorMaterializer, Materializer} import com.newrelic.agent.HeadersUtil import com.newrelic.agent.introspec._ import com.newrelic.agent.util.Obfuscator @@ -27,11 +26,10 @@ import scala.concurrent.duration.DurationInt import scala.concurrent.{Await, Future} @RunWith(classOf[InstrumentationTestRunner]) -@InstrumentationTestConfig(includePrefixes = Array("scala", "pekko")) +@InstrumentationTestConfig(includePrefixes = Array("scala", "org.apache.pekko")) class PekkoHttpCoreTest { implicit val system: ActorSystem = ActorSystem() - implicit val materializer: ActorMaterializer = ActorMaterializer() val pekkoServer = new PekkoServer() val playServer = new PlayServer() @@ -42,7 +40,7 @@ class PekkoHttpCoreTest { def syncHandlerPekkoServerTest(): Unit = { pekkoServer.start(port, async = false) - Http().singleRequest(HttpRequest(uri = baseUrl + "/ping")) + val response: Future[HttpResponse] = Http().singleRequest(HttpRequest(uri = baseUrl + "/ping")) val introspector: Introspector = InstrumentationTestRunner.getIntrospector awaitFinishedTx(introspector, 1) diff --git a/instrumentation/apache-pekko-http-core-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/core/PekkoServer.scala b/instrumentation/apache-pekko-http-core-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/core/PekkoServer.scala index 47f19aeb79..91fed5a964 100644 --- a/instrumentation/apache-pekko-http-core-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/core/PekkoServer.scala +++ b/instrumentation/apache-pekko-http-core-2.13_1/src/test/scala/com/agent/instrumentation/org.apache.pekko/http/core/PekkoServer.scala @@ -25,7 +25,7 @@ import scala.language.postfixOps class PekkoServer() { implicit val system = ActorSystem() implicit val executor = system.dispatcher - implicit val materializer = ActorMaterializer() + implicit val timeout: Timeout = 3 seconds val config = ConfigFactory.load() From 20d23eb80a8d64f01853e028846c649ff245e28a Mon Sep 17 00:00:00 2001 From: Kate Anderson Date: Fri, 12 Apr 2024 09:50:34 -0700 Subject: [PATCH 5/5] Fix typos, other updates in pekko-http pekko-http-core --- instrumentation/apache-pekko-http-2.13_1/README.md | 6 ++++++ .../http/scaladsl/server/PekkoHttpContextFunction.scala | 1 - instrumentation/apache-pekko-http-core-2.13_1/build.gradle | 5 ++++- .../org.apache.pekko/http/scaladsl/HttpInstrumentation.java | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 instrumentation/apache-pekko-http-2.13_1/README.md diff --git a/instrumentation/apache-pekko-http-2.13_1/README.md b/instrumentation/apache-pekko-http-2.13_1/README.md new file mode 100644 index 0000000000..5b0cea43e9 --- /dev/null +++ b/instrumentation/apache-pekko-http-2.13_1/README.md @@ -0,0 +1,6 @@ +## Pekko Http Instrumentation + +This module adds support for `pekko-http`. + +Pekko Http is a fork of Akka Http 10.2.0. Consequently, this instrumentation is a direct lift of the existing `akka-http-2.13_10.1.8` instrumentation module. + diff --git a/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/PekkoHttpContextFunction.scala b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/PekkoHttpContextFunction.scala index 9d2e05b078..4abbaf0bce 100644 --- a/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/PekkoHttpContextFunction.scala +++ b/instrumentation/apache-pekko-http-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/server/PekkoHttpContextFunction.scala @@ -41,7 +41,6 @@ class ContextWrapper(original: Function1[RequestContext, Future[RouteResult]]) e try { val tracedMethod = AgentBridge.getAgent.getTracedMethod tracedMethod.setMetricName("PekkoHttp") - // Pekko-http 10.1.5 uses CallbackRunnable and we lose transaction context between Directives AgentBridge.getAgent.getTracedMethod.setTrackCallbackRunnable(true); val token = AgentBridge.getAgent.getTransaction(false).getToken PathMatcherUtils.setHttpRequest(ctx.request) diff --git a/instrumentation/apache-pekko-http-core-2.13_1/build.gradle b/instrumentation/apache-pekko-http-core-2.13_1/build.gradle index 8636c90614..94c6f4b809 100644 --- a/instrumentation/apache-pekko-http-core-2.13_1/build.gradle +++ b/instrumentation/apache-pekko-http-core-2.13_1/build.gradle @@ -14,7 +14,10 @@ dependencies { verifyInstrumentation { passesOnly('org.apache.pekko:pekko-http-core_2.13:[1.0.0,)') { - implementation("org.apache.pekko:pekko-stream_2.13:1.0.1") + implementation("org.apache.pekko:pekko-stream_2.13:1.0.0") + } + passesOnly('org.apache.pekko:pekko-http-core_3:[1.0.0,)') { + implementation("org.apache.pekko:pekko-stream_3:1.0.0") } } diff --git a/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/HttpInstrumentation.java b/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/HttpInstrumentation.java index ac9596e960..7ab830ff32 100644 --- a/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/HttpInstrumentation.java +++ b/instrumentation/apache-pekko-http-core-2.13_1/src/main/scala/org.apache.pekko/http/scaladsl/HttpInstrumentation.java @@ -36,7 +36,7 @@ public InetSocketAddress localAddress() { public ServerBinding() { AgentBridge.getAgent().getLogger().log(Level.FINE, "Setting pekko-http port to: {0,number,#}", localAddress().getPort()); AgentBridge.publicApi.setAppServerPort(localAddress().getPort()); - AgentBridge.publicApi.setServerInfo("Pekko HTTP", ManifestUtils.getVersionFromManifest(getClass(), "pekko-http-core", "10.2.0")); + AgentBridge.publicApi.setServerInfo("Pekko HTTP", ManifestUtils.getVersionFromManifest(getClass(), "pekko-http-core", "1.0.0")); AgentBridge.instrumentation.retransformUninstrumentedClass(SyncRequestHandler.class); AgentBridge.instrumentation.retransformUninstrumentedClass(AsyncRequestHandler.class);