Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

+ kamon-okhttp: introduce the new module kamon-okhttp to provide instrumentation for OkHttp #808

Merged
merged 3 commits into from
Aug 22, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ lazy val instrumentation = (project in file("instrumentation"))
`kamon-system-metrics`,
`kamon-akka`,
`kamon-akka-http`,
`kamon-play`
`kamon-play`,
`kamon-okhttp`,
)


Expand Down Expand Up @@ -386,6 +387,23 @@ lazy val `kamon-play` = (project in file("instrumentation/kamon-play"))
)


lazy val `kamon-okhttp` = (project in file("instrumentation/kamon-okhttp"))
.disablePlugins(AssemblyPlugin)
.enablePlugins(JavaAgent)
.settings(instrumentationSettings)
.settings(
libraryDependencies ++= Seq(
kanelaAgent % "provided",
"com.squareup.okhttp3" % "okhttp" % "3.14.9" % "provided",

scalatest % "test",
logbackClassic % "test",
"org.eclipse.jetty" % "jetty-server" % "9.4.25.v20191220" % "test",
"org.eclipse.jetty" % "jetty-servlet" % "9.4.25.v20191220" % "test",
)
).dependsOn(`kamon-core`, `kamon-executors`, `kamon-testkit` % "test")


/**
* Reporters
*/
Expand Down Expand Up @@ -597,5 +615,6 @@ val `kamon-bundle` = (project in file("bundle/kamon-bundle"))
`kamon-system-metrics` % "shaded",
`kamon-akka` % "shaded",
`kamon-akka-http` % "shaded",
`kamon-play` % "shaded"
`kamon-play` % "shaded",
`kamon-okhttp` % "shaded",
)
106 changes: 106 additions & 0 deletions instrumentation/kamon-okhttp/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# ============================================ #
# kamon okhttp3 client reference configuration #
# ============================================ #

kamon.instrumentation.okhttp {

http-client {
#
# Configuration for HTTP context propagation.
#
propagation {

# Enables or disables HTTP context propagation on this HTTP server instrumentation. Please note that if
# propagation is disabled then some distributed tracing features will not be work as expected (e.g. Spans can
# be created and reported but will not be linked across boundaries nor take trace identifiers from tags).
enabled = yes

# HTTP propagation channel to b used by this instrumentation. Take a look at the kamon.propagation.http.default
# configuration for more details on how to configure the detault HTTP context propagation.
channel = "default"
}

tracing {

# Enables HTTP request tracing. When enabled the instrumentation will create Spans for outgoing requests
# and finish them when the response is received from the server.
enabled = yes

# Enables collection of span metrics using the `span.processing-time` metric.
span-metrics = on

# Select which tags should be included as span and span metric tags. The possible options are:
# - span: the tag is added as a Span tag (i.e. using span.tag(...))
# - metric: the tag is added a a Span metric tag (i.e. using span.tagMetric(...))
# - off: the tag is not used.
#
tags {

# Use the http.url tag.
url = span

# Use the http.method tag.
method = metric

# Use the http.status_code tag.
status-code = metric

# Copy tags from the context into the Spans with the specified purpouse. For example, to copy a customer_type
# tag from the context into the HTTP Server Span created by the instrumentation, the following configuration
# should be added:
#
# from-context {
# customer_type = span
# }
#
from-context {

}
}

operations {

# The default operation name to be used when creating Spans to handle the HTTP client requests. The HTTP
# Client instrumentation will always try to use the HTTP Operation Name Generator configured bellow to get
# a name, but if it fails to generate it then this name will be used.
default = "http.client.request"

# FQCN for a HttpOperationNameGenerator implementation, or ony of the following shorthand forms:
# - hostname: Uses the request Host as the operation name.
# - method: Uses the request HTTP method as the operation name.
#
name-generator = "kamon.okhttp3.instrumentation.OkHttpOperationNameGenerator"
}
}
}
}

kamon {
okhttp {
# Fully qualified name of the implementation of kamon.okhttp3.NameGenerator that will be used for assigning names
# names to Spans.
name-generator = kamon.okhttp3.DefaultNameGenerator
# Metrics for okhttp
metrics {
enabled = true
}
}
}

kanela {
show-banner = true
cspinetta marked this conversation as resolved.
Show resolved Hide resolved
log-level = "ERROR"

modules {
okhttp3-module {
name = "OkHttp 3 Instrumentation Module"
stoppable = false
cspinetta marked this conversation as resolved.
Show resolved Hide resolved
instrumentations = [
"kamon.okhttp3.instrumentation.OkHttpInstrumentation"
]
within = [
"okhttp3..*"
]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* =========================================================================================
* Copyright © 2013-2020 the kamon project <http://kamon.io/>
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions
* and limitations under the License.
* =========================================================================================
*/

package kamon.okhttp3.instrumentation

import java.util

import kamon.Kamon
import kamon.context.HttpPropagation.HeaderWriter
import kamon.instrumentation.http.{HttpClientInstrumentation, HttpMessage}
import okhttp3.{Request, Response}

import scala.collection.immutable.Map
import scala.collection.{JavaConverters, mutable}

object KamonOkHttpTracing {
private val httpClientConfig = Kamon.config.getConfig("kamon.instrumentation.okhttp.http-client")
private val instrumentation = HttpClientInstrumentation.from(httpClientConfig, "okhttp-client")

def withNewSpan(request: Request): HttpClientInstrumentation.RequestHandler[Request] = {
instrumentation.createHandler(getRequestBuilder(request), Kamon.currentContext)
}

def successContinuation(requestHandler: HttpClientInstrumentation.RequestHandler[Request], response: Response): Response = {
requestHandler.processResponse(toKamonResponse(response))
response
}

def failureContinuation(requestHandler: HttpClientInstrumentation.RequestHandler[Request], error: Throwable): Unit = {
requestHandler.span.fail(error)
requestHandler.span.finish()
}

def getRequestBuilder(request: Request): HttpMessage.RequestBuilder[Request] = new HttpMessage.RequestBuilder[Request]() {
private val _headers = mutable.Map[String, String]()

override def read(header: String): Option[String] = Option.apply(request.header(header))

override def readAll: Map[String, String] = {
JavaConverters
.mapAsScalaMapConverter(request.headers.toMultimap)
.asScala
.mapValues((values: util.List[String]) => values.get(0))
.toMap
}

override def url: String = request.url.toString

override def path: String = request.url.uri.getPath

override def method: String = request.method

override def host: String = request.url.host

override def port: Int = request.url.port

override def write(header: String, value: String): Unit = {
_headers += (header -> value)
}

override def build: Request = {
val newHeadersMap = request.headers.newBuilder
_headers.foreach { case (key, value) => newHeadersMap.add(key, value) }
request.newBuilder.headers(newHeadersMap.build).build
}
}

def toKamonResponse(response: Response): HttpMessage.Response = new HttpMessage.Response() {
override def statusCode: Int = response.code()
}

trait HeaderHandler extends HeaderWriter {
private val _headers = mutable.Map[String, String]()

override def write(header: String, value: String): Unit = {
_headers += (header -> value)
}

def headers: mutable.Map[String, String] = _headers
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* =========================================================================================
* Copyright © 2013-2020 the kamon project <http://kamon.io/>
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions
* and limitations under the License.
* =========================================================================================
*/

package kamon.okhttp3.instrumentation

import okhttp3.{Interceptor, Response}

final class KamonTracingInterceptor extends Interceptor {

override def intercept(chain: Interceptor.Chain): Response = {
val clientRequestHandler = KamonOkHttpTracing.withNewSpan(chain.request)
val request = clientRequestHandler.request
try {
val response = chain.proceed(request)
KamonOkHttpTracing.successContinuation(clientRequestHandler, response)
} catch {
case error: Throwable =>
KamonOkHttpTracing.failureContinuation(clientRequestHandler, error)
throw error
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* =========================================================================================
* Copyright © 2013-2020 the kamon project <http://kamon.io/>
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions
* and limitations under the License.
* =========================================================================================
*/

package kamon.okhttp3.instrumentation

import kanela.agent.api.instrumentation.InstrumentationBuilder
import kanela.agent.libs.net.bytebuddy.asm.Advice
import okhttp3.OkHttpClient

class OkHttpInstrumentation extends InstrumentationBuilder {

/**
* Instrument:
*
* okhttp3.OkHttpClient::constructor
*/
onType("okhttp3.OkHttpClient")
.advise(isConstructor() and takesOneArgumentOf("okhttp3.OkHttpClient$Builder"), classOf[OkHttpClientBuilderAdvisor])
}

/**
* Avisor for okhttp3.OkHttpClient::constructor(OkHttpClient.Builder)
*/
class OkHttpClientBuilderAdvisor

object OkHttpClientBuilderAdvisor {

import scala.collection.JavaConverters._

@Advice.OnMethodEnter(suppress = classOf[Throwable])
def addKamonInterceptor(@Advice.Argument(0) builder: OkHttpClient.Builder): Unit = {
val interceptors = builder.networkInterceptors.asScala
if (!interceptors.exists(_.isInstanceOf[KamonTracingInterceptor])) {
builder.addNetworkInterceptor(new KamonTracingInterceptor)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package kamon.okhttp3.instrumentation

import kamon.instrumentation.http.{HttpMessage, HttpOperationNameGenerator}

class OkHttpOperationNameGenerator extends HttpOperationNameGenerator {
override def name(request: HttpMessage.Request): Option[String] = {
Option(request.url)
}
}
12 changes: 12 additions & 0 deletions instrumentation/kamon-okhttp/src/test/resources/logback.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<configuration>
<statusListener class="ch.qos.logback.core.status.NopStatusListener"/>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>

<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
Loading