Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
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
26 changes: 16 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,24 @@

### Getting Started

The `kamon-http4s` module brings traces and metrics to your [http4s][4] based applications.
The `kamon-http4s-<version>` module brings traces and metrics to your [http4s][4] based applications.

Kamon <b>kamon-http4s</b> is currently available for Scala 2.12 and 2.13.
It is currently available for Scala 2.12 and 2.13. The current version supports Kamon 2.2 and
is published for http4s 0.22, 0.23 and 1.0.

Supported releases and dependencies are shown below.
| kamon | kamon-http4s | status | jdk | scala | http4s
|:-----:|:------:|:------:|:----:|--------------:|-------
| 2.2.x | 2.2.0 | stable | 8+ | 2.12, 2.13 | 0.22.x
| 2.2.x | 2.2.0 | stable | 8+ | 2.12, 2.13 | 0.23.x
| 2.2.x | 2.2.0 | stable | 8+ | 2.12, 2.13 | 1.0.x

To get started with sbt, simply add the following to your `build.sbt` file, for instance for http4s 0.23:

```scala
libraryDependencies += "io.kamon" %% "kamon-http4s-0.23" % "2.2.0"
```

The releases and dependencies for the legacy module `kamon-http4s` (without http4s version) are shown below.

| kamon-http4s | status | jdk | scala | http4s
|:------:|:------:|:----:|--------------:|-------
Expand All @@ -20,13 +33,6 @@ Supported releases and dependencies are shown below.
| 2.0.1 | stable | 8+ | 2.12, 2.13 | 0.21.x


To get started with SBT, simply add the following to your `build.sbt`
file:

```scala
libraryDependencies += "io.kamon" %% "kamon-http4s" % "2.0.1"
```

## Metrics and Tracing for http4s in 2 steps

### The Server
Expand Down
73 changes: 52 additions & 21 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,59 @@
* =========================================================================================
*/

val kamonCore = "io.kamon" %% "kamon-core" % "2.1.0"
val kamonTestkit = "io.kamon" %% "kamon-testkit" % "2.1.0"
val kamonCommon = "io.kamon" %% "kamon-instrumentation-common" % "2.1.0"
val kamonCore = "io.kamon" %% "kamon-core" % "2.2.3"
val kamonTestkit = "io.kamon" %% "kamon-testkit" % "2.2.3"
val kamonCommon = "io.kamon" %% "kamon-instrumentation-common" % "2.2.3"

val server = "org.http4s" %% "http4s-blaze-server" % "0.21.3"
val client = "org.http4s" %% "http4s-blaze-client" % "0.21.3"
val dsl = "org.http4s" %% "http4s-dsl" % "0.21.3"
def http4sDeps(version: String) = Seq(
"org.http4s" %% "http4s-blaze-server" % version % Provided,
"org.http4s" %% "http4s-blaze-client" % version % Provided,
"org.http4s" %% "http4s-dsl" % version % Provided
)


lazy val root = (project in file("."))
.settings(Seq(
name := "kamon-http4s",
scalaVersion := "2.13.1",
crossScalaVersions := Seq("2.12.11", "2.13.1")))
.settings(resolvers += Resolver.bintrayRepo("kamon-io", "snapshots"))
.settings(resolvers += Resolver.mavenLocal)
.settings(scalacOptions ++= (CrossVersion.partialVersion(scalaVersion.value) match {
lazy val shared = Seq(
scalaVersion := "2.13.6",
crossScalaVersions := Seq("2.12.14", "2.13.6"),
scalacOptions ++= (CrossVersion.partialVersion(scalaVersion.value) match {
case Some((2, 12)) => Seq("-Ypartial-unification", "-language:higherKinds")
case _ => "-language:higherKinds" :: Nil
}))
case _ => "-language:higherKinds" :: Nil
}),
libraryDependencies ++=
compileScope(kamonCore, kamonCommon) ++
testScope(scalatest, kamonTestkit, logbackClassic)
)

lazy val `kamon-http4s-0_22` = project
.in(file("modules/0.22"))
.settings(
shared,
name := "kamon-http4s-0.22",
libraryDependencies ++= http4sDeps("0.22.1")
)

lazy val `kamon-http4s-0_23` = project
.in(file("modules/0.23"))
.settings(
shared,
name := "kamon-http4s-0.23",
libraryDependencies ++= http4sDeps("0.23.0")
)

lazy val `kamon-http4s-1_0` = project
.in(file("modules/1.0"))
.settings(
shared,
name := "kamon-http4s-1.0",
libraryDependencies ++= http4sDeps("1.0.0-M23")
)

lazy val root = project
.in(file("."))
.settings(
libraryDependencies ++=
compileScope(kamonCore, kamonCommon) ++
providedScope(server, client, dsl) ++
testScope(scalatest, kamonTestkit, logbackClassic))
shared,
name := "kamon-http4s",
publish / skip := true,
Test / parallelExecution := false,
Global / concurrentRestrictions += Tags.limit(Tags.Test, 1)
)
.aggregate(`kamon-http4s-0_22`, `kamon-http4s-0_23`, `kamon-http4s-1_0`)
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,10 @@ object KamonSupport {

Kamon.onReconfigure(newConfig => _instrumentation = instrumentation(newConfig))


def apply[F[_]](underlying: Client[F])(implicit F:Sync[F]): Client[F] = Client { request =>

for {
ctx <- Resource.liftF(F.delay(Kamon.currentContext()))
k <- kamonClient(underlying)(request)(ctx)(_instrumentation)
} yield k
def apply[F[_]](underlying: Client[F])(implicit F: Sync[F]): Client[F] = Client { request =>
// this needs to run on the same thread as the caller, so can't be suspended in F
val ctx = Kamon.currentContext()
kamonClient(underlying)(request)(ctx)(_instrumentation)
}


Expand All @@ -54,9 +51,9 @@ object KamonSupport {
(instrumentation: HttpClientInstrumentation)
(implicit F:Sync[F]): Resource[F, Response[F]] =
for {
requestHandler <- Resource.liftF(F.delay(instrumentation.createHandler(getRequestBuilder(request), ctx)))
requestHandler <- Resource.eval(F.delay(instrumentation.createHandler(getRequestBuilder(request), ctx)))
response <- underlying.run(requestHandler.request).attempt
trackedResponse <- Resource.liftF(handleResponse(response, requestHandler))
trackedResponse <- Resource.eval(handleResponse(response, requestHandler))
} yield trackedResponse

def handleResponse[F[_]](
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ object KamonSupport {

private def getHandler[F[_]](instrumentation: HttpServerInstrumentation)(request: Request[F])(implicit F: Sync[F]): Resource[F, RequestHandler] =
for {
handler <- Resource.liftF(F.delay(instrumentation.createHandler(buildRequestMessage(request))))
handler <- Resource.eval(F.delay(instrumentation.createHandler(buildRequestMessage(request))))
_ <- processRequest(handler)
_ <- withContext(handler)
} yield handler
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,57 +3,57 @@ package kamon
import org.http4s.{Header, Headers, Request, Response, Status}
import kamon.instrumentation.http.HttpMessage
import kamon.instrumentation.http.HttpMessage.ResponseBuilder
import org.http4s.util.CaseInsensitiveString
import org.typelevel.ci.CIString

package object http4s {


def buildRequestMessage[F[_]](inner: Request[F]): HttpMessage.Request = new HttpMessage.Request {
override def url: String = inner.uri.toString()

override def path: String = inner.uri.path
override def path: String = inner.uri.path.renderString

override def method: String = inner.method.name

override def host: String = inner.uri.authority.map(_.host.value).getOrElse("")

override def port: Int = inner.uri.authority.flatMap(_.port).getOrElse(0)

override def read(header: String): Option[String] = inner.headers.get(CaseInsensitiveString(header)).map(_.value)
override def read(header: String): Option[String] = inner.headers.get(CIString(header)).map(_.head.value)

override def readAll(): Map[String, String] = {
val builder = Map.newBuilder[String, String]
inner.headers.foreach(h => builder += (h.name.value -> h.value))
inner.headers.foreach(h => builder += (h.name.toString -> h.value))
builder.result()
}
}

def errorResponseBuilder[F[_]]: HttpMessage.ResponseBuilder[Response[F]] = new ResponseBuilder[Response[F]] {
override def write(header: String, value: String): Unit = ()
override def statusCode: Int = 500
override def build(): Response[F] = new Response[F](status = Status.InternalServerError)
override def build(): Response[F] = Response[F](status = Status.InternalServerError)
}

//TODO both of these
def notFoundResponseBuilder[F[_]]: HttpMessage.ResponseBuilder[Response[F]] = new ResponseBuilder[Response[F]] {
private var _headers = Headers.empty

override def write(header: String, value: String): Unit =
_headers = _headers.put(Header(header, value))
_headers = _headers.put(Header.Raw(CIString(header), value))

override def statusCode: Int = 404
override def build(): Response[F] = new Response[F](status = Status.NotFound, headers = _headers)
override def build(): Response[F] = Response[F](status = Status.NotFound, headers = _headers)
}

def getResponseBuilder[F[_]](response: Response[F]) = new HttpMessage.ResponseBuilder[Response[F]] {
def getResponseBuilder[F[_]](response: Response[F]): ResponseBuilder[Response[F]] = new HttpMessage.ResponseBuilder[Response[F]] {
private var _headers = response.headers

override def statusCode: Int = response.status.code

override def build(): Response[F] = response.withHeaders(_headers)

override def write(header: String, value: String): Unit =
_headers = _headers.put(Header(header, value))
_headers = _headers.put(Header.Raw(CIString(header), value))
}


Expand All @@ -63,23 +63,23 @@ package object http4s {
override def build(): Request[F] = request.withHeaders(_headers)

override def write(header: String, value: String): Unit =
_headers = _headers.put(Header(header, value))
_headers = _headers.put(Header.Raw(CIString(header), value))

override def url: String = request.uri.toString()

override def path: String = request.uri.path
override def path: String = request.uri.path.renderString

override def method: String = request.method.name

override def host: String = request.uri.authority.map(_.host.value).getOrElse("")

override def port: Int = request.uri.authority.flatMap(_.port).getOrElse(0)

override def read(header: String): Option[String] = _headers.get(CaseInsensitiveString(header)).map(_.value)
override def read(header: String): Option[String] = _headers.get(CIString(header)).map(_.head.value)

override def readAll(): Map[String, String] = {
val builder = Map.newBuilder[String, String]
request.headers.foreach(h => builder += (h.name.value -> h.value))
request.headers.foreach(h => builder += (h.name.toString -> h.value))
builder.result()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,21 @@

package kamon.http4s

import java.net.ConnectException

import cats.effect.{IO, Resource}
import kamon.Kamon
import kamon.http4s.middleware.client.KamonSupport
import kamon.tag.Lookups.{plain, plainLong}
import kamon.testkit.TestSpanReporter
import kamon.trace.Span
import org.http4s.{HttpRoutes, Response}
import org.http4s.client._
import org.http4s.dsl.io._
import org.http4s.implicits._
import org.http4s.{HttpRoutes, Response}
import org.scalatest.concurrent.Eventually
import org.scalatest.time.SpanSugar
import org.scalatest.{BeforeAndAfterAll, Matchers, OptionValues, WordSpec}
import kamon.tag.Lookups.{plainLong, plain}

import java.net.ConnectException

class ClientInstrumentationSpec extends WordSpec
with Matchers
Expand All @@ -57,7 +56,7 @@ class ClientInstrumentationSpec extends WordSpec
client.expect[String]("/tracing/ok").unsafeRunSync() shouldBe "ok"
}

eventually(timeout(2 seconds)) {
eventually(timeout(3 seconds)) {
val span = testSpanReporter().nextSpan().value

span.operationName shouldBe "/tracing/ok"
Expand All @@ -74,7 +73,7 @@ class ClientInstrumentationSpec extends WordSpec
"close and finish a span even if an exception is thrown by the client" in {
val okSpan = Kamon.spanBuilder("client-exception").start()
val client: Client[IO] = KamonSupport[IO](
Client(_ => Resource.liftF(IO.raiseError[Response[IO]](new ConnectException("Connection Refused."))))
Client(_ => Resource.eval(IO.raiseError[Response[IO]](new ConnectException("Connection Refused."))))
)

Kamon.runWithSpan(okSpan) {
Expand All @@ -83,7 +82,7 @@ class ClientInstrumentationSpec extends WordSpec
}
}

eventually(timeout(2 seconds)) {
eventually(timeout(3 seconds)) {
val span = testSpanReporter().nextSpan().value
span.operationName shouldBe "/tracing/ok"
span.kind shouldBe Span.Kind.Client
Expand All @@ -102,7 +101,7 @@ class ClientInstrumentationSpec extends WordSpec
client.expect[String]("/tracing/not-found").attempt.unsafeRunSync().isLeft shouldBe true
}

eventually(timeout(2 seconds)) {
eventually(timeout(3 seconds)) {
val span = testSpanReporter().nextSpan().value
span.operationName shouldBe "/tracing/not-found"
span.kind shouldBe Span.Kind.Client
Expand All @@ -124,7 +123,7 @@ class ClientInstrumentationSpec extends WordSpec
client.expect[String]("/tracing/error").attempt.unsafeRunSync().isLeft shouldBe true
}

eventually(timeout(2 seconds)) {
eventually(timeout(3 seconds)) {
val span = testSpanReporter().nextSpan().value

span.operationName shouldBe "/tracing/error"
Expand Down
Loading