/
NatchezMiddleware.scala
116 lines (105 loc) · 3.96 KB
/
NatchezMiddleware.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
// Copyright (c) 2021 by Rob Norris
// This software is licensed under the MIT License (MIT).
// For more information see LICENSE or https://opensource.org/licenses/MIT
package natchez.http4s
import cats.data.{ Kleisli, OptionT }
import cats.syntax.all._
import cats.effect.{MonadCancel, Outcome}
import cats.effect.syntax.all._
import Outcome._
import org.http4s.HttpRoutes
import natchez.{Trace, TraceValue, Tags}
import org.http4s.Response
import org.http4s.client.Client
import java.io.ByteArrayOutputStream
import java.io.PrintStream
import cats.effect.Resource
object NatchezMiddleware {
import syntax.kernel._
@deprecated("Use NatchezMiddleware.server(routes)", "0.0.3")
def apply[F[_]: Trace](routes: HttpRoutes[F])(
implicit ev: MonadCancel[F, Throwable]
): HttpRoutes[F] =
server(routes)
/**
* A middleware that adds the following standard fields to the current span:
*
* - "http.method" -> "GET", "PUT", etc.
* - "http.url" -> request URI (not URL)
* - "http.status_code" -> "200", "403", etc. // why is this a string?
* - "error" -> true // only present in case of error
*
* In addition the following non-standard fields are added in case of error:
*
* - "error.message" -> Exception message
* - "error.stacktrace" -> Exception stack trace as a multi-line string
* - "cancelled" -> true // only present in case of cancellation
*/
def server[F[_]: Trace](routes: HttpRoutes[F])(
implicit ev: MonadCancel[F, Throwable]
): HttpRoutes[F] =
Kleisli { req =>
val addRequestFields: F[Unit] =
Trace[F].put(
Tags.http.method(req.method.name),
Tags.http.url(req.uri.renderString),
)
def addResponseFields(res: Response[F]): F[Unit] =
Trace[F].put(
Tags.http.status_code(res.status.code.toString)
)
def addErrorFields(e: Throwable): F[Unit] =
Trace[F].put(
Tags.error(true),
"error.message" -> e.getMessage(),
"error.stacktrace" -> {
val baos = new ByteArrayOutputStream
val fs = new AnsiFilterStream(baos)
val ps = new PrintStream(fs, true, "UTF-8")
e.printStackTrace(ps)
ps.close
fs.close
baos.close
new String(baos.toByteArray, "UTF-8")
}
)
routes(req).guaranteeCase {
case Canceled() => OptionT.liftF(addRequestFields *> Trace[F].put(("cancelled", TraceValue.BooleanValue(true)), Tags.error(true)))
case Errored(e) => OptionT.liftF(addRequestFields *> addErrorFields(e))
case Succeeded(fa) => OptionT.liftF {
fa.value.flatMap {
case Some(resp) => addRequestFields *> addResponseFields(resp)
case None => MonadCancel[F].unit
}
}
}
}
/**
* A middleware that adds the current span's kernel to outgoing requests, performs requests in
* a span called `http4s-client-request`, and adds the following fields to that span.
*
* - "client.http.method" -> "GET", "PUT", etc.
* - "client.http.uri" -> request URI
* - "client.http.status_code" -> "200", "403", etc. // why is this a string?
*
*/
def client[F[_]: Trace](client: Client[F])(
implicit ev: MonadCancel[F, Throwable]
): Client[F] =
Client { req =>
Resource.applyFull { cancelable =>
Trace[F].span("http4s-client-request") {
for {
knl <- Trace[F].kernel
_ <- Trace[F].put(
"client.http.uri" -> req.uri.toString(),
"client.http.method" -> req.method.toString
)
reqʹ = req.withHeaders(knl.toHttp4sHeaders ++ req.headers) // prioritize request headers over kernel ones
rsrc <- cancelable(client.run(reqʹ).allocatedCase)
_ <- Trace[F].put("client.http.status_code" -> rsrc._1.status.code.toString())
} yield rsrc
}
}
}
}