|
1 | 1 | package com.ovoenergy.natchez.extras.datadog |
2 | 2 |
|
3 | 3 | import cats.effect._ |
4 | | -import cats.effect.unsafe.implicits._ |
5 | 4 | import cats.instances.list._ |
6 | 5 | import cats.syntax.traverse._ |
7 | 6 | import com.ovoenergy.natchez.extras.datadog.Datadog.entryPoint |
8 | 7 | import com.ovoenergy.natchez.extras.datadog.DatadogTags.SpanType.{Cache, Db, Web} |
9 | 8 | import com.ovoenergy.natchez.extras.datadog.DatadogTags.spanType |
| 9 | +import munit.CatsEffectSuite |
10 | 10 | import natchez.EntryPoint |
11 | 11 | import org.http4s.Request |
12 | 12 | import org.http4s.circe.CirceEntityDecoder._ |
13 | 13 | import org.http4s.syntax.literals._ |
14 | | -import org.scalatest.{Inspectors, OptionValues} |
15 | | -import org.scalatest.matchers.should.Matchers |
16 | | -import org.scalatest.wordspec.AnyWordSpec |
17 | 14 |
|
18 | 15 | import scala.concurrent.duration._ |
19 | 16 |
|
20 | 17 | /** |
21 | 18 | * This tests both the datadog span code itself and the submission of metrics over HTTP |
22 | 19 | * Could be expanded but even just these tests exposed concurrency issues with my original code. |
23 | 20 | */ |
24 | | -class DatadogTest extends AnyWordSpec with Matchers with OptionValues { |
| 21 | +class DatadogTest extends CatsEffectSuite { |
25 | 22 |
|
26 | 23 | def run(f: EntryPoint[IO] => IO[Unit]): IO[List[Request[IO]]] = |
27 | 24 | TestClient[IO].flatMap(c => entryPoint(c.client, "test", "blah").use(f) >> c.requests) |
28 | 25 |
|
29 | | - "Datadog span" should { |
30 | | - |
31 | | - "Obtain the agent host from the parameter" in { |
32 | | - ( |
33 | | - for { |
34 | | - client <- TestClient[IO] |
35 | | - ep = entryPoint(client.client, "a", "b", agentHost = uri"http://example.com") |
36 | | - _ <- ep.use(_.root("foo").use(_ => IO.unit)) |
37 | | - requests <- client.requests |
38 | | - } yield requests.map(_.uri) shouldBe List( |
39 | | - uri"http://example.com/v0.3/traces" |
40 | | - ) |
41 | | - ).unsafeRunSync() |
42 | | - } |
43 | | - |
44 | | - "Allow you to modify trace tokens" in { |
45 | | - ( |
46 | | - for { |
47 | | - client <- TestClient[IO] |
48 | | - ep = entryPoint(client.client, "a", "b", agentHost = uri"http://example.com") |
49 | | - kernel <- ep.use(_.root("foo").use(s => s.put("traceToken" -> "foo") >> s.kernel)) |
50 | | - } yield kernel.toHeaders.get("X-Trace-Token") shouldBe Some("foo") |
51 | | - ).unsafeRunSync() |
52 | | - } |
53 | | - |
54 | | - "Continue to send HTTP calls even if one of them fails" in { |
55 | | - |
56 | | - val test: EntryPoint[IO] => IO[Unit] = |
57 | | - ep => |
58 | | - ep.root("first").use(_ => IO.unit) >> |
59 | | - IO.sleep(1.second) >> |
60 | | - ep.root("second").use(_ => IO.unit) |
61 | | - ( |
62 | | - for { |
63 | | - client <- TestClient[IO] |
64 | | - _ <- client.respondWith(IO.raiseError(new Exception)) |
65 | | - ep = entryPoint(client.client, "a", "b", agentHost = uri"http://example.com") |
66 | | - _ <- ep.use(test) |
67 | | - requests <- client.requests |
68 | | - } yield requests.length shouldBe 2 |
69 | | - ).unsafeRunSync() |
70 | | - } |
| 26 | + test("Obtain the agent host from the parameter") { |
| 27 | + assertIO( |
| 28 | + returns = List(uri"http://example.com/v0.3/traces"), |
| 29 | + obtained = for { |
| 30 | + client <- TestClient[IO] |
| 31 | + ep = entryPoint(client.client, "a", "b", agentHost = uri"http://example.com") |
| 32 | + _ <- ep.use(_.root("foo").use(_ => IO.unit)) |
| 33 | + requests <- client.requests |
| 34 | + } yield requests.map(_.uri) |
| 35 | + ) |
| 36 | + } |
71 | 37 |
|
72 | | - "Submit the right info to Datadog when closed" in { |
| 38 | + test("Allow you to modify trace tokens") { |
| 39 | + assertIO( |
| 40 | + returns = Some("foo"), |
| 41 | + obtained = for { |
| 42 | + client <- TestClient[IO] |
| 43 | + ep = entryPoint(client.client, "a", "b", agentHost = uri"http://example.com") |
| 44 | + kernel <- ep.use(_.root("foo").use(s => s.put("traceToken" -> "foo") >> s.kernel)) |
| 45 | + } yield kernel.toHeaders.get("X-Trace-Token") |
| 46 | + ) |
| 47 | + } |
73 | 48 |
|
74 | | - val res = run(_.root("bar:res").use(_.put("k" -> "v") >> IO.sleep(1.milli))).unsafeRunSync() |
75 | | - val span = res.flatTraverse(_.as[List[List[SubmittableSpan]]]).unsafeRunSync().flatten.head |
| 49 | + test("Continue to send HTTP calls even if one of them fails") { |
| 50 | + |
| 51 | + val test: EntryPoint[IO] => IO[Unit] = |
| 52 | + ep => |
| 53 | + ep.root("first").use(_ => IO.unit) >> |
| 54 | + IO.sleep(1.second) >> |
| 55 | + ep.root("second").use(_ => IO.unit) |
| 56 | + |
| 57 | + assertIO( |
| 58 | + returns = 2, |
| 59 | + obtained = for { |
| 60 | + client <- TestClient[IO] |
| 61 | + _ <- client.respondWith(IO.raiseError(new Exception)) |
| 62 | + ep = entryPoint(client.client, "a", "b", agentHost = uri"http://example.com") |
| 63 | + _ <- ep.use(test) |
| 64 | + requests <- client.requests |
| 65 | + } yield requests.length |
| 66 | + ) |
| 67 | + } |
76 | 68 |
|
77 | | - span.name shouldBe "bar" |
78 | | - span.service shouldBe "test" |
79 | | - span.resource shouldBe "res" |
80 | | - span.`type` shouldBe None |
81 | | - span.duration > 0 shouldBe true |
82 | | - span.meta.get("k") shouldBe Some("v") |
83 | | - span.meta.contains("traceToken") shouldBe true |
| 69 | + test("Submit the right info to Datadog when closed") { |
| 70 | + for { |
| 71 | + res <- run(_.root("bar:res").use(_.put("k" -> "v") >> IO.sleep(1.milli))) |
| 72 | + spans <- res.flatTraverse(_.as[List[List[SubmittableSpan]]]) |
| 73 | + } yield { |
| 74 | + val span = spans.flatten.head |
| 75 | + assertEquals(span.name, "bar") |
| 76 | + assertEquals(span.service, "test") |
| 77 | + assertEquals(span.resource, "res") |
| 78 | + assertEquals(span.`type`, None) |
| 79 | + assertEquals(span.duration > 0, true) |
| 80 | + assertEquals(span.meta.get("k"), Some("v")) |
| 81 | + assertEquals(span.meta.contains("traceToken"), true) |
84 | 82 | } |
| 83 | + } |
85 | 84 |
|
86 | | - "Only include the sampling priority metric on the root span" in { |
87 | | - val res = run(_.root("root").use(_.span("span").use(_ => IO.sleep(1.milli)))).unsafeRunSync() |
88 | | - val spans = res.flatTraverse(_.as[List[List[SubmittableSpan]]]).unsafeRunSync().flatten |
89 | | - spans.find(_.parentId.isEmpty).value.metrics shouldBe Map("_sampling_priority_v1" -> 2.0) |
90 | | - spans.find(_.parentId.isDefined).value.metrics shouldBe Map.empty |
| 85 | + test("Only include the sampling priority metric on the root span") { |
| 86 | + for { |
| 87 | + res <- run(_.root("root").use(_.span("span").use(_ => IO.sleep(1.milli)))) |
| 88 | + spans <- res.flatTraverse(_.as[List[List[SubmittableSpan]]]).map(_.flatten) |
| 89 | + } yield { |
| 90 | + assertEquals(spans.find(_.parentId.isEmpty).map(_.metrics), Some(Map("_sampling_priority_v1" -> 2.0))) |
| 91 | + assertEquals(spans.find(_.parentId.isDefined).map(_.metrics), Some(Map.empty[String, Double])) |
91 | 92 | } |
| 93 | + } |
92 | 94 |
|
93 | | - "Infer the right span.type from any tags set" in { |
94 | | - Inspectors.forAll(List(Web, Cache, Db)) { typ => |
95 | | - val res = run(_.root("bar:res").use(_.put(spanType(typ)))).unsafeRunSync() |
96 | | - val span = res.flatTraverse(_.as[List[List[SubmittableSpan]]]).unsafeRunSync().flatten.head |
97 | | - span.`type` shouldBe Some(typ) |
98 | | - } |
| 95 | + test("Infer the right span.type from any tags set") { |
| 96 | + List(Web, Cache, Db).traverse { typ => |
| 97 | + assertIO( |
| 98 | + returns = Some(typ), |
| 99 | + obtained = for { |
| 100 | + res <- run(_.root("bar:res").use(_.put(spanType(typ)))) |
| 101 | + span <- res.flatTraverse(_.as[List[List[SubmittableSpan]]]) |
| 102 | + } yield span.flatten.head.`type` |
| 103 | + ) |
99 | 104 | } |
| 105 | + } |
100 | 106 |
|
101 | | - "Submit multiple spans across multiple calls when span() is called" in { |
102 | | - val res = run(_.root("bar").use(_.span("subspan").use(_ => IO.sleep(1.second)))).unsafeRunSync() |
103 | | - val spans = res.flatTraverse(_.as[List[List[SubmittableSpan]]]).unsafeRunSync().flatten |
104 | | - spans.map(_.traceId).distinct.length shouldBe 1 |
105 | | - spans.map(_.spanId).distinct.length shouldBe 2 |
| 107 | + test("Submit multiple spans across multiple calls when span() is called") { |
| 108 | + for { |
| 109 | + res <- run(_.root("bar").use(_.span("subspan").use(_ => IO.sleep(1.second)))) |
| 110 | + spans <- res.flatTraverse(_.as[List[List[SubmittableSpan]]]).map(_.flatten) |
| 111 | + } yield { |
| 112 | + assertEquals(spans.map(_.traceId).distinct.length, 1) |
| 113 | + assertEquals(spans.map(_.spanId).distinct.length, 2) |
106 | 114 | } |
| 115 | + } |
107 | 116 |
|
108 | | - "Allow you to override the service name and resource with colons" in { |
109 | | - val res = run(_.root("svc:name:res").use(_ => IO.unit)).unsafeRunSync() |
110 | | - val spans = res.flatTraverse(_.as[List[List[SubmittableSpan]]]).unsafeRunSync().flatten |
111 | | - spans.head.resource shouldBe "res" |
112 | | - spans.head.service shouldBe "svc" |
113 | | - spans.head.name shouldBe "name" |
| 117 | + test("Allow you to override the service name and resource with colons") { |
| 118 | + for { |
| 119 | + res <- run(_.root("svc:name:res").use(_ => IO.unit)) |
| 120 | + spans <- res.flatTraverse(_.as[List[List[SubmittableSpan]]]).map(_.flatten) |
| 121 | + } yield { |
| 122 | + assertEquals(spans.head.resource, "res") |
| 123 | + assertEquals(spans.head.service, "svc") |
| 124 | + assertEquals(spans.head.name, "name") |
114 | 125 | } |
| 126 | + } |
115 | 127 |
|
116 | | - "Inherit metadata into subspans but only at the time of creation" in { |
117 | | - val res = run( |
118 | | - _.root("bar:res").use { root => |
119 | | - root.put("foo" -> "bar") >> root.span("sub").use(_.put("baz" -> "qux")) |
120 | | - } |
121 | | - ).unsafeRunSync() |
122 | | - |
123 | | - val spans = res.flatTraverse(_.as[List[List[SubmittableSpan]]]).unsafeRunSync().flatten |
| 128 | + test("Inherit metadata into subspans but only at the time of creation") { |
| 129 | + run( |
| 130 | + _.root("bar:res").use { root => |
| 131 | + root.put("foo" -> "bar") >> root.span("sub").use(_.put("baz" -> "qux")) |
| 132 | + } |
| 133 | + ).flatMap(_.flatTraverse(_.as[List[List[SubmittableSpan]]]).map(_.flatten)).map { spans => |
124 | 134 | val rootSpan = spans.find(_.name == "bar").get |
125 | 135 | val subSpan = spans.find(_.name == "sub").get |
126 | | - |
127 | | - subSpan.meta.view.filterKeys(_ != "traceToken").toMap shouldBe Map("foo" -> "bar", "baz" -> "qux") |
128 | | - rootSpan.meta.view.filterKeys(_ != "traceToken").toMap shouldBe Map("foo" -> "bar") |
| 136 | + assertEquals( |
| 137 | + subSpan.meta.view.filterKeys(_ != "traceToken").toMap, |
| 138 | + Map("foo" -> "bar", "baz" -> "qux") |
| 139 | + ) |
| 140 | + assertEquals( |
| 141 | + rootSpan.meta.view.filterKeys(_ != "traceToken").toMap, |
| 142 | + Map("foo" -> "bar") |
| 143 | + ) |
129 | 144 | } |
130 | 145 | } |
131 | 146 | } |
0 commit comments