Skip to content

Commit

Permalink
Merge pull request #362 from GDownes/sttp-client-instrumentation
Browse files Browse the repository at this point in the history
STTP 2 & 3 Scala HTTP Client Instrumentation
  • Loading branch information
twcrone authored Aug 20, 2021
2 parents bc66314 + 6b3aa10 commit 8422a9f
Show file tree
Hide file tree
Showing 82 changed files with 2,788 additions and 0 deletions.
30 changes: 30 additions & 0 deletions instrumentation/sttp-2.12_2.2.3/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
apply plugin: 'scala'

dependencies {
implementation(project(":newrelic-api"))
implementation(project(":agent-bridge"))
implementation(project(":newrelic-weaver-api"))
implementation(project(":newrelic-weaver-scala-api"))
implementation("org.scala-lang:scala-library:2.12.14")
implementation("com.softwaremill.sttp.client:core_2.12:2.2.3")
}

jar {
manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.sttp-2.12_2.2.3', 'Implementation-Title-Alias': 'sttp_instrumentation' }
}

verifyInstrumentation {
passes 'com.softwaremill.sttp.client:core_2.12:[2.2.3,)'
excludeRegex ".*(RC|M)[0-9]*"
}

test {
onlyIf {
!project.hasProperty('test7')
}
}

site {
title 'Scala'
type 'Other'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
*
* * Copyright 2020 New Relic Corporation. All rights reserved.
* * SPDX-License-Identifier: Apache-2.0
*
*/

package com.nr.agent.instrumentation.sttp

import com.nr.agent.instrumentation.sttp.SttpUtils.{finishSegment, startSegment}
import sttp.client.monad.MonadError
import sttp.client.ws.WebSocketResponse
import sttp.client.{FollowRedirectsBackend, Identity, NothingT, Request, Response, SttpBackend}

class DelegateIdentity(delegate: FollowRedirectsBackend[Identity, Any, Any]) extends SttpBackend[Identity, Nothing, NothingT] {
override def send[T](request: Request[T, Nothing]): Identity[Response[T]] = {
val segment = startSegment(request)

val response = delegate.send(request)

finishSegment(request, segment, response)

response
}

override def openWebsocket[T, WS_RESULT](request: Request[T, Nothing], handler: NothingT[WS_RESULT]): Identity[WebSocketResponse[WS_RESULT]] = delegate.openWebsocket(request, handler)

override def close(): Identity[Unit] = delegate.close()

override def responseMonad: MonadError[Identity] = delegate.responseMonad
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
*
* * Copyright 2020 New Relic Corporation. All rights reserved.
* * SPDX-License-Identifier: Apache-2.0
*
*/

package com.nr.agent.instrumentation.sttp

import com.newrelic.api.agent.{ExtendedInboundHeaders, HeaderType}
import sttp.client.Response
import collection.JavaConverters._

import java.util

class InboundHttpHeaders[T](response: Response[T]) extends ExtendedInboundHeaders {
override def getHeader(name: String): String = response.header(name).orNull

override def getHeaders(name: String): util.List[String] = response.headers.map(x => x.name).toList.asJava

override def getHeaderType: HeaderType = HeaderType.HTTP
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
*
* * Copyright 2020 New Relic Corporation. All rights reserved.
* * SPDX-License-Identifier: Apache-2.0
*
*/

package com.nr.agent.instrumentation.sttp

import com.newrelic.api.agent.{HeaderType, OutboundHeaders}
import sttp.client.Request

class OutboundHttpHeaders[T, S](val request: Request[T, S]) extends OutboundHeaders {
override def getHeaderType = HeaderType.HTTP

/**
* Sets a response header with the given name and value.
* NO-OP Sttp Request Headers are immutable and so can't be set from here
*/
override def setHeader(name: String, value: String): Unit = request.header(name, value)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
*
* * Copyright 2020 New Relic Corporation. All rights reserved.
* * SPDX-License-Identifier: Apache-2.0
*
*/

package com.nr.agent.instrumentation.sttp

import com.newrelic.api.agent.{HttpParameters, NewRelic, Segment, TransactionNamePriority}
import sttp.client.{Request, Response}

import java.net.URI

object SttpUtils {

def startSegment[R, T](request: Request[T, R]): Segment = {
val segment = NewRelic.getAgent.getTransaction.startSegment("SttpBackend", "send")
segment.addOutboundRequestHeaders(new OutboundHttpHeaders(request))
segment
}

def finishSegment[R, T](request: Request[T, R], segment: Segment, response: Response[T]): Unit = {
segment.reportAsExternal(HttpParameters
.library("Sttp")
.uri(new URI(request.uri.toString()))
.procedure(request.method.method)
.inboundHeaders(new InboundHttpHeaders(response))
.build())
segment.end()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
*
* * Copyright 2020 New Relic Corporation. All rights reserved.
* * SPDX-License-Identifier: Apache-2.0
*
*/

package sttp.client

import com.newrelic.api.agent.weaver.Weaver
import com.newrelic.api.agent.weaver.scala.{ScalaMatchType, ScalaWeave}
import com.nr.agent.instrumentation.sttp.DelegateIdentity
import sttp.client.HttpURLConnectionBackend.EncodingHandler

import java.net.{HttpURLConnection, URL, URLConnection}

@ScalaWeave(`type` = ScalaMatchType.Object, `originalName` = "sttp.client.HttpURLConnectionBackend")
class HttpURLConnectionBackend_Instrumentation {
def apply(
options: SttpBackendOptions,
customizeConnection: HttpURLConnection => Unit,
createURL: String => URL,
openConnection: (URL, Option[java.net.Proxy]) => URLConnection,
customEncodingHandler: EncodingHandler
): SttpBackend[Identity, Nothing, NothingT] = new DelegateIdentity(Weaver.callOriginal())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
*
* * Copyright 2021 New Relic Corporation. All rights reserved.
* * SPDX-License-Identifier: Apache-2.0
*
*/

package com.nr.agent.instrumentation.sttp

import com.newrelic.agent.introspec.internal.HttpServerRule
import com.newrelic.agent.introspec.{InstrumentationTestConfig, InstrumentationTestRunner, Introspector}
import com.nr.agent.instrumentation.sttp.SttpTestUtils.{getSegments, getTraces, makeRequest}
import org.junit.runner.RunWith
import org.junit.{Assert, Rule, Test}
import sttp.client.{HttpURLConnectionBackend, _}

import java.util.concurrent.TimeUnit

@RunWith(classOf[InstrumentationTestRunner])
@InstrumentationTestConfig(includePrefixes = Array("none"))
class BackendRequestNoInstrumentation {

val _server = new HttpServerRule()

@Rule
implicit def server: HttpServerRule = _server

@Test
def httpURLConnectionBackend(): Unit = {
//Given
implicit val introspector: Introspector = InstrumentationTestRunner.getIntrospector
implicit val backend: SttpBackend[Identity, Nothing, NothingT] = HttpURLConnectionBackend()

//When
val response = makeRequest

//Then
introspector.getFinishedTransactionCount(TimeUnit.SECONDS.toMillis(10))

val traces = getTraces()
val segments = getSegments(traces)

Assert.assertTrue("Successful response", response.code.isSuccess)
Assert.assertEquals("Transactions", 1, introspector.getTransactionNames.size)
Assert.assertEquals("Traces", 1, traces.size)
Assert.assertEquals("Segments", 1, segments.size)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
*
* * Copyright 2021 New Relic Corporation. All rights reserved.
* * SPDX-License-Identifier: Apache-2.0
*
*/

package com.nr.agent.instrumentation.sttp

import com.newrelic.agent.introspec.internal.HttpServerRule
import com.newrelic.agent.introspec.{InstrumentationTestConfig, InstrumentationTestRunner, Introspector}
import com.nr.agent.instrumentation.sttp.SttpTestUtils.{getSegments, getTraces, makeRequest}
import org.junit.runner.RunWith
import org.junit.{Assert, Rule, Test}
import sttp.client.{HttpURLConnectionBackend, _}

import java.util.concurrent.TimeUnit

@RunWith(classOf[InstrumentationTestRunner])
@InstrumentationTestConfig(includePrefixes = Array("sttp"))
class BackendRequestSttpInstrumentation {

val _server = new HttpServerRule()

@Rule
implicit def server: HttpServerRule = _server

@Test
def httpURLConnectionBackend(): Unit = {
//Given
implicit val introspector: Introspector = InstrumentationTestRunner.getIntrospector
implicit val backend: SttpBackend[Identity, Nothing, NothingT] = HttpURLConnectionBackend()

//When
val response = makeRequest

//Then
introspector.getFinishedTransactionCount(TimeUnit.SECONDS.toMillis(10))

val traces = getTraces()
val segments = getSegments(traces)

Assert.assertTrue("Successful response", response.code.isSuccess)
Assert.assertEquals("Transactions", 1, introspector.getTransactionNames.size)
Assert.assertEquals("Traces", 1, traces.size)
Assert.assertEquals("Segments", 2, segments.size)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
*
* * Copyright 2020 New Relic Corporation. All rights reserved.
* * SPDX-License-Identifier: Apache-2.0
*
*/

package com.nr.agent.instrumentation.sttp

import com.newrelic.agent.introspec.internal.HttpServerRule
import com.newrelic.agent.introspec.{Introspector, TraceSegment, TransactionTrace}
import com.newrelic.api.agent.Trace
import sttp.client.{NothingT, Response, SttpBackend, UriContext, basicRequest}

import collection.JavaConverters._
import scala.language.higherKinds

object SttpTestUtils {

@Trace(dispatcher = true)
def makeRequest[F[_]](implicit backend: SttpBackend[F, Nothing, NothingT], server: HttpServerRule): F[Response[Either[String, String]]] = {
basicRequest.get(uri"${server.getEndPoint}?no-transaction=1").send()
}

def getTraces()(implicit introspector: Introspector): Iterable[TransactionTrace] =
introspector.getTransactionNames.asScala.flatMap(transactionName => introspector.getTransactionTracesForTransaction(transactionName).asScala)

def getSegments(traces : Iterable[TransactionTrace]): Iterable[TraceSegment] =
traces.flatMap(trace => this.getSegments(trace.getInitialTraceSegment))

private def getSegments(segment: TraceSegment): List[TraceSegment] = {
val childSegments = segment.getChildren.asScala.flatMap(childSegment => getSegments(childSegment)).toList
segment :: childSegments
}
}
31 changes: 31 additions & 0 deletions instrumentation/sttp-2.13_2.2.3/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
apply plugin: 'scala'

dependencies {
implementation(project(":newrelic-api"))
implementation(project(":agent-bridge"))
implementation(project(":newrelic-weaver-api"))
implementation(project(":newrelic-weaver-scala-api"))
implementation("org.scala-lang:scala-library:2.13.5")
implementation("com.softwaremill.sttp.client:core_2.13:2.2.3")
}

jar {
manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.sttp-2.13_2.2.3', 'Implementation-Title-Alias': 'sttp_instrumentation' }
}

verifyInstrumentation {
passes 'com.softwaremill.sttp.client:core_2.13:[2.2.3,)'
passes 'com.softwaremill.sttp.client:core_3:[2.2.3,)'
excludeRegex ".*(RC|M)[0-9]*"
}

test {
onlyIf {
!project.hasProperty('test7')
}
}

site {
title 'Scala'
type 'Other'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
*
* * Copyright 2020 New Relic Corporation. All rights reserved.
* * SPDX-License-Identifier: Apache-2.0
*
*/

package com.nr.agent.instrumentation.sttp

import com.nr.agent.instrumentation.sttp.SttpUtils.{finishSegment, startSegment}
import sttp.client.monad.MonadError
import sttp.client.ws.WebSocketResponse
import sttp.client.{FollowRedirectsBackend, Identity, NothingT, Request, Response, SttpBackend}

class DelegateIdentity(delegate: FollowRedirectsBackend[Identity, Any, Any]) extends SttpBackend[Identity, Nothing, NothingT] {
override def send[T](request: Request[T, Nothing]): Identity[Response[T]] = {
val segment = startSegment(request)

val response = delegate.send(request)

finishSegment(request, segment, response)

response
}

override def openWebsocket[T, WS_RESULT](request: Request[T, Nothing], handler: NothingT[WS_RESULT]): Identity[WebSocketResponse[WS_RESULT]] = delegate.openWebsocket(request, handler)

override def close(): Identity[Unit] = delegate.close()

override def responseMonad: MonadError[Identity] = delegate.responseMonad
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
*
* * Copyright 2020 New Relic Corporation. All rights reserved.
* * SPDX-License-Identifier: Apache-2.0
*
*/

package com.nr.agent.instrumentation.sttp

import com.newrelic.api.agent.{ExtendedInboundHeaders, HeaderType}
import sttp.client.Response
import collection.JavaConverters._

import java.util

class InboundHttpHeaders[T](response: Response[T]) extends ExtendedInboundHeaders {
override def getHeader(name: String): String = response.header(name).orNull

override def getHeaders(name: String): util.List[String] = response.headers.map(x => x.name).toList.asJava

override def getHeaderType: HeaderType = HeaderType.HTTP
}
Loading

0 comments on commit 8422a9f

Please sign in to comment.