Skip to content

Commit

Permalink
Initial instrumentation for Jedis
Browse files Browse the repository at this point in the history
  • Loading branch information
SimunKaracic committed May 17, 2021
1 parent 7bf8705 commit a6b1344
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 0 deletions.
15 changes: 15 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,21 @@ lazy val `kamon-tapir` = (project in file("instrumentation/kamon-tapir"))
)
).dependsOn(`kamon-core`, `kamon-akka-http`, `kamon-testkit` % "test")

lazy val `kamon-redis` = (project in file("instrumentation/kamon-redis"))
.disablePlugins(AssemblyPlugin)
.enablePlugins(JavaAgent)
.settings(instrumentationSettings)
.settings(
libraryDependencies ++= Seq(
kanelaAgent % "provided",
"redis.clients" % "jedis" % "3.6.0" % "provided",

scalatest % "test",
logbackClassic % "test",
"org.testcontainers" % "testcontainers" % "1.15.3" % "test",
)
).dependsOn(`kamon-core`, `kamon-testkit` % "test")

/**
* Reporters
*/
Expand Down
15 changes: 15 additions & 0 deletions instrumentation/kamon-redis/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
kanela.modules {
redis {
name = "Redis Instrumentation"
# Does it provide metrics? No.
description = "Provides tracing and metrics for the Jedis library"

instrumentations = [
"kamon.instrumentation.jedis.JedisInstrumentation",
]

within = [
"redis.clients.jedis..*",
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package kamon.instrumentation.jedis

import kamon.Kamon
import kamon.context.Storage.Scope
import kamon.trace.Span
import kamon.trace.Trace.SamplingDecision
import redis.clients.jedis.commands.ProtocolCommand
import kanela.agent.api.instrumentation.InstrumentationBuilder
import kanela.agent.libs.net.bytebuddy.asm.Advice

class JedisInstrumentation extends InstrumentationBuilder {
onType("redis.clients.jedis.Protocol")
.advise(method("sendCommand").and(withArgument(1, classOf[ProtocolCommand])), classOf[SendCommandAdvice])

onType("redis.clients.jedis.Jedis")
// we're gonna have a looot of these
.advise(method("set"), classOf[JedisCommandAdvice])
.advise(method("get"), classOf[JedisCommandAdvice])
}

class JedisCommandAdvice
object JedisCommandAdvice {
@Advice.OnMethodEnter()
def enter = {
// operation name default should be in conf
val span = Kamon.clientSpanBuilder("redis.command.unknown", "redis.client.jedis")
.samplingDecision(SamplingDecision.Unknown)
.start()

(Kamon.storeContext(Kamon.currentContext().withEntry(Span.Key, span)), span)
}

@Advice.OnMethodExit(onThrowable = classOf[Throwable])
def exit(@Advice.Enter enter: (Scope, Span),
@Advice.Thrown t: Throwable) = {
val (scope, span) = enter
// when should I fail the span?
// is this throwable thing ok?
// how do I even test that it's good?
if (t != null) {
span.fail(t)
} else {
span.finish()
}
scope.close()
}
}

class SendCommandAdvice
object SendCommandAdvice {
@Advice.OnMethodEnter()
def enter(@Advice.Argument(1) command: ProtocolCommand) = {
val span = Kamon.currentSpan()
if (span.operationName() == "redis.command.unknown") {
span.name(s"redis.command.${command}")
.takeSamplingDecision()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package kamon.instrumentation.jedis

import kamon.metric.MeasurementUnit.time.seconds
import kamon.testkit.{MetricInspection, TestSpanReporter}
import kamon.trace.Span.Kind
import org.scalatest.{BeforeAndAfterAll, Matchers, OptionValues, WordSpec}
import org.scalatest.concurrent.{Eventually, ScalaFutures}
import org.testcontainers.containers.GenericContainer
import org.testcontainers.utility.DockerImageName
import redis.clients.jedis.Jedis

import scala.collection.convert.ImplicitConversions.`collection AsScalaIterable`
import scala.concurrent.duration.DurationInt


class JedisInstrumentationSpec extends WordSpec
with Matchers
with ScalaFutures
with Eventually
with BeforeAndAfterAll
with MetricInspection.Syntax
with OptionValues
with TestSpanReporter {

var container: GenericContainer[Nothing] = _
override def beforeAll = {
val REDIS_IMAGE = DockerImageName.parse("redis")
container = new GenericContainer(REDIS_IMAGE).withExposedPorts(6379)

container.start()
}

override def afterAll= {
container.stop()
}

"the Jedis instrumentation" should {
"generate a client span for get and set commands" in {
val jedis = new Jedis(container.getHost, container.getFirstMappedPort)
jedis.set("foo", "bar")

eventually(timeout(10.seconds)) {
val span = testSpanReporter().nextSpan().get
span.operationName shouldBe "redis.command.SET"
span.kind shouldBe Kind.Client
}

jedis.get("foo")
eventually(timeout(10.seconds)) {
val span = testSpanReporter().nextSpan().get
span.operationName shouldBe "redis.command.GET"
span.kind shouldBe Kind.Client
}
}
}
}

0 comments on commit a6b1344

Please sign in to comment.