Skip to content

Commit 21117d4

Browse files
authored
Merge pull request #1052 from SimunKaracic/rediscala-initial
Rediscala initial
2 parents ba3eb83 + 3ed143e commit 21117d4

File tree

5 files changed

+190
-66
lines changed

5 files changed

+190
-66
lines changed

build.sbt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,7 @@ lazy val `kamon-redis` = (project in file("instrumentation/kamon-redis"))
511511
kanelaAgent % "provided",
512512
"redis.clients" % "jedis" % "3.6.0" % "provided",
513513
"io.lettuce" % "lettuce-core" % "6.1.2.RELEASE" % "provided",
514+
"com.github.etaty" %% "rediscala" % "1.9.0" % "provided",
514515

515516
scalatest % "test",
516517
logbackClassic % "test",
Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,29 @@
11
kanela.modules {
22
redis {
33
name = "Redis Instrumentation"
4-
description = "Provides tracing for Jedis and Lettuce libraries"
4+
description = "Provides tracing for Jedis, Lettuce and Rediscala libraries"
55

66
instrumentations = [
77
"kamon.instrumentation.jedis.JedisInstrumentation",
88
"kamon.instrumentation.lettuce.LettuceInstrumentation",
9+
"kamon.instrumentation.rediscala.RediscalaInstrumentation",
910
]
1011

1112
within = [
1213
"redis.clients.jedis.Protocol",
1314
"io.lettuce.core..*",
15+
"redis..*",
1416
]
1517
}
1618
}
19+
20+
21+
# when using multiple clients, the extension will be alphabetical
22+
# e.g. $a, $b, $c.
23+
# so add exclude clauses as needed
24+
kamon.instrumentation.akka.filters.actors.trace {
25+
excludes += "*/user/RedisClient-$a/**"
26+
excludes += "*/user/RedisClient-$a"
27+
excludes += "*/user/RedisBlockingClient-$a/**"
28+
excludes += "*/user/RedisBlockingClient-$a"
29+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package kamon.instrumentation.rediscala
2+
3+
import kamon.Kamon
4+
import kamon.trace.Span
5+
import kamon.util.CallingThreadExecutionContext
6+
import kanela.agent.api.instrumentation.InstrumentationBuilder
7+
import kanela.agent.libs.net.bytebuddy.asm.Advice
8+
9+
import scala.concurrent.Future
10+
import scala.util.{Failure, Success}
11+
12+
class RediscalaInstrumentation extends InstrumentationBuilder {
13+
onTypes("redis.Request", "redis.ActorRequest", "redis.BufferedRequest",
14+
"redis.commands.BLists", "redis.RoundRobinPoolRequest", "ActorRequest")
15+
.advise(method("send").and(takesArguments(1)), classOf[RequestInstrumentation])
16+
17+
onTypes("redis.ActorRequest$class")
18+
.advise(method("send"), classOf[ActorRequestAdvice])
19+
20+
}
21+
22+
class RequestInstrumentation
23+
object RequestInstrumentation {
24+
@Advice.OnMethodEnter()
25+
def enter(@Advice.Argument(0) command: Any): Span = {
26+
val spanName = s"redis.command.${command.getClass.getSimpleName}"
27+
28+
Kamon.clientSpanBuilder(spanName, "redis.client.rediscala")
29+
.start()
30+
}
31+
32+
@Advice.OnMethodExit(onThrowable = classOf[Throwable], suppress = classOf[Throwable])
33+
def exit(@Advice.Enter span: Span,
34+
@Advice.Thrown t: Throwable,
35+
@Advice.Return future: Future[_]) = {
36+
if (t != null) {
37+
span.fail(t);
38+
}
39+
40+
future.onComplete {
41+
case Success(_value) =>
42+
span.finish()
43+
44+
case Failure(exception) =>
45+
span.fail(exception)
46+
span.finish()
47+
48+
}(CallingThreadExecutionContext)
49+
}
50+
}
51+
52+
class RoundRobinRequestInstrumentation
53+
54+
object RoundRobinRequestInstrumentation {
55+
@Advice.OnMethodEnter()
56+
def enter(@Advice.Argument(1) command: Any): Span = {
57+
println("Entering round robin")
58+
val spanName = s"redis.command.${command.getClass.getSimpleName}"
59+
Kamon.clientSpanBuilder(spanName, "redis.client.rediscala")
60+
.start()
61+
}
62+
63+
@Advice.OnMethodExit(onThrowable = classOf[Throwable], suppress = classOf[Throwable])
64+
def exit(@Advice.Enter span: Span,
65+
@Advice.Thrown t: Throwable,
66+
@Advice.Return future: Future[_]) = {
67+
println("Exiting round robin")
68+
if (t != null) {
69+
span.fail(t);
70+
}
71+
72+
future.onComplete {
73+
case Success(_value) =>
74+
span.finish()
75+
76+
case Failure(exception) =>
77+
span.fail(exception)
78+
span.finish()
79+
80+
}(CallingThreadExecutionContext)
81+
}
82+
}
83+
84+
class ActorRequestAdvice
85+
object ActorRequestAdvice {
86+
@Advice.OnMethodEnter()
87+
def enter(@Advice.Argument(1) command: Any): Span = {
88+
val spanName = s"redis.command.${command.getClass.getSimpleName}"
89+
Kamon.clientSpanBuilder(spanName, "redis.client.rediscala")
90+
.start()
91+
}
92+
93+
@Advice.OnMethodExit(onThrowable = classOf[Throwable], suppress = classOf[Throwable])
94+
def exit(@Advice.Enter span: Span,
95+
@Advice.Thrown t: Throwable,
96+
@Advice.Return future: Future[_]) = {
97+
if (t != null) {
98+
span.fail(t);
99+
}
100+
101+
future.onComplete {
102+
case Success(_value) =>
103+
span.finish()
104+
105+
case Failure(exception) =>
106+
span.fail(exception)
107+
span.finish()
108+
109+
}(CallingThreadExecutionContext)
110+
}
111+
}

instrumentation/kamon-redis/src/test/scala/kamon/instrumentation/lettuce/LettuceInstrumentationSpec.scala renamed to instrumentation/kamon-redis/src/test/scala/kamon/instrumentation/combined/RedisInstrumentationsSpec.scala

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
1-
package kamon.instrumentation.lettuce
1+
package kamon.instrumentation.combined
22

3-
import io.lettuce.core.RedisClient
3+
4+
import io.lettuce.core.{RedisClient => LettuceClient}
45
import kamon.testkit.{MetricInspection, TestSpanReporter}
56
import kamon.trace.Span.Kind
67
import org.scalatest.concurrent.{Eventually, ScalaFutures}
78
import org.scalatest.{BeforeAndAfterAll, Matchers, OptionValues, WordSpec}
9+
import org.slf4j.LoggerFactory
810
import org.testcontainers.containers.GenericContainer
911
import org.testcontainers.utility.DockerImageName
12+
import redis.clients.jedis.Jedis
13+
import redis.{RedisBlockingClient, RedisClient}
1014

1115
import java.time.Duration
1216
import scala.concurrent.duration.DurationInt
1317
import scala.util.control.NonFatal
1418

15-
class LettuceInstrumentationSpec extends WordSpec
19+
20+
class RedisInstrumentationsSpec extends WordSpec
1621
with Matchers
1722
with ScalaFutures
1823
with Eventually
@@ -21,20 +26,45 @@ class LettuceInstrumentationSpec extends WordSpec
2126
with OptionValues
2227
with TestSpanReporter {
2328

29+
private val logger = LoggerFactory.getLogger(classOf[RedisInstrumentationsSpec])
2430
var container: GenericContainer[Nothing] = _
31+
2532
override def beforeAll: Unit = {
2633
val REDIS_IMAGE = DockerImageName.parse("redis")
2734
container = new GenericContainer(REDIS_IMAGE).withExposedPorts(6379)
35+
2836
container.start()
2937
}
3038

3139
override def afterAll: Unit = {
3240
container.stop()
3341
}
3442

43+
"the Jedis instrumentation" should {
44+
"generate a client span for get and set commands" in {
45+
val jedis = new Jedis(container.getHost, container.getFirstMappedPort)
46+
jedis.set("foo", "bar")
47+
48+
eventually(timeout(2.seconds)) {
49+
val span = testSpanReporter().nextSpan().get
50+
span.operationName shouldBe "redis.command.SET"
51+
span.kind shouldBe Kind.Client
52+
}
53+
54+
testSpanReporter().clear()
55+
56+
jedis.get("foo")
57+
eventually(timeout(2.seconds)) {
58+
val span = testSpanReporter().nextSpan().get
59+
span.operationName shouldBe "redis.command.GET"
60+
span.kind shouldBe Kind.Client
61+
}
62+
}
63+
}
64+
3565
"the Lettuce instrumentation" should {
3666
"generate a client span for async commands" in {
37-
val client = RedisClient.create(s"redis://${container.getHost}:${container.getFirstMappedPort}")
67+
val client = LettuceClient.create(s"redis://${container.getHost}:${container.getFirstMappedPort}")
3868
val connection = client.connect
3969
val asyncCommands = connection.async()
4070
asyncCommands.set("key", "Hello, Redis!")
@@ -49,7 +79,7 @@ class LettuceInstrumentationSpec extends WordSpec
4979
}
5080

5181
"generate a client span for sync commands" in {
52-
val client = RedisClient.create(s"redis://${container.getHost}:${container.getFirstMappedPort}")
82+
val client = LettuceClient.create(s"redis://${container.getHost}:${container.getFirstMappedPort}")
5383
val connection = client.connect
5484
val commands = connection.sync()
5585

@@ -65,7 +95,7 @@ class LettuceInstrumentationSpec extends WordSpec
6595
}
6696

6797
"fail a span that times out" in {
68-
val client = RedisClient.create(s"redis://${container.getHost}:${container.getFirstMappedPort}")
98+
val client = LettuceClient.create(s"redis://${container.getHost}:${container.getFirstMappedPort}")
6999
client.setDefaultTimeout(Duration.ofNanos(1))
70100
val connection = client.connect
71101
val commands = connection.sync()
@@ -86,4 +116,32 @@ class LettuceInstrumentationSpec extends WordSpec
86116
client.shutdown()
87117
}
88118
}
119+
120+
"the Rediscala instrumentation" should {
121+
implicit val akkaSystem = akka.actor.ActorSystem()
122+
"generate only one client span for commands" in {
123+
val client = RedisClient(host = container.getHost, port = container.getFirstMappedPort)
124+
client.set("a", "a")
125+
126+
eventually(timeout(30.seconds)) {
127+
val span = testSpanReporter().nextSpan().value
128+
span.operationName shouldBe "redis.command.Set"
129+
span.hasError shouldBe false
130+
}
131+
client.shutdown()
132+
}
133+
134+
"generate only one client span when using the blocking client" in {
135+
val blockingClient = RedisBlockingClient(host = container.getHost, port = container.getFirstMappedPort)
136+
blockingClient.blpop(Seq("a", "b", "c"))
137+
138+
eventually(timeout(30.seconds)) {
139+
val span = testSpanReporter().nextSpan().value
140+
span.operationName shouldBe "redis.command.Blpop"
141+
span.hasError shouldBe true
142+
}
143+
blockingClient.stop()
144+
}
145+
146+
}
89147
}

instrumentation/kamon-redis/src/test/scala/kamon/instrumentation/jedis/JedisInstrumentationSpec.scala

Lines changed: 0 additions & 59 deletions
This file was deleted.

0 commit comments

Comments
 (0)