Skip to content

Commit a80c6a9

Browse files
committed
Move span creation to middleware
1 parent 300082d commit a80c6a9

3 files changed

Lines changed: 82 additions & 97 deletions

File tree

modules/http4s/src/main/scala/natchez/http4s/NatchezMiddleware.scala

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,27 @@ import cats.effect.{MonadCancel, Outcome}
1010
import cats.effect.syntax.all._
1111
import Outcome._
1212
import org.http4s.HttpRoutes
13-
import natchez.{Trace, TraceValue, Tags}
13+
import org.typelevel.ci.CIString
14+
import natchez.{Kernel, Span, Trace, TraceValue, Tags}
15+
import org.http4s.Request
1416
import org.http4s.Response
1517
import org.http4s.client.Client
1618
import java.io.ByteArrayOutputStream
1719
import java.io.PrintStream
1820
import cats.effect.Resource
1921

22+
/**
23+
* @define excludedHeaders
24+
* All headers except security (Authorization, Cookie, Set-Cookie)
25+
* and payload (Content-Length, ContentType, Content-Range, Trailer, Transfer-Encoding)
26+
* are passed to Kernel by default.
27+
*
28+
* @define isKernelHeader should an HTTP header be passed to Kernel or not
29+
*
30+
* @define spanName compute the span name from the request
31+
*
32+
* @define modifySpanOptions modify default span creation options
33+
*/
2034
object NatchezMiddleware {
2135
import syntax.kernel._
2236

@@ -27,7 +41,8 @@ object NatchezMiddleware {
2741
server(routes)
2842

2943
/**
30-
* A middleware that adds the following standard fields to the current span:
44+
* A middleware that creates a per request span.
45+
* It also adds following standard fields to newly created span:
3146
*
3247
* - "http.method" -> "GET", "PUT", etc.
3348
* - "http.url" -> request URI (not URL)
@@ -39,8 +54,20 @@ object NatchezMiddleware {
3954
* - "error.message" -> Exception message
4055
* - "error.stacktrace" -> Exception stack trace as a multi-line string
4156
* - "cancelled" -> true // only present in case of cancellation
57+
*
58+
* @note $excludedHeaders
59+
*
60+
* @param isKernelHeader $isKernelHeader
61+
* @param spanName $spanName
62+
* @param modifySpanOptions $modifySpanOptions
4263
*/
43-
def server[F[_]: Trace](routes: HttpRoutes[F])(
64+
65+
def server[F[_]: Trace](
66+
routes: HttpRoutes[F],
67+
isKernelHeader: CIString => Boolean = name => !ExcludedHeaders.contains(name),
68+
spanName: Request[F] => String = (req: Request[F]) => req.uri.path.toString,
69+
modifySpanOptions: Span.Options => Span.Options = identity
70+
)(
4471
implicit ev: MonadCancel[F, Throwable]
4572
): HttpRoutes[F] =
4673
Kleisli { req =>
@@ -71,17 +98,25 @@ object NatchezMiddleware {
7198
new String(baos.toByteArray, "UTF-8")
7299
}
73100
)
74-
75-
routes(req).guaranteeCase {
76-
case Canceled() => OptionT.liftF(addRequestFields *> Trace[F].put(("cancelled", TraceValue.BooleanValue(true)), Tags.error(true)))
77-
case Errored(e) => OptionT.liftF(addRequestFields *> addErrorFields(e))
78-
case Succeeded(fa) => OptionT.liftF {
79-
fa.value.flatMap {
80-
case Some(resp) => addRequestFields *> addResponseFields(resp)
81-
case None => MonadCancel[F].unit
101+
val kernelHeaders = req.headers.headers
102+
.collect {
103+
case header if isKernelHeader(header.name) => header.name -> header.value
104+
}
105+
.toMap
106+
val kernel = Kernel(kernelHeaders)
107+
OptionT(
108+
Trace[F].span(spanName(req), modifySpanOptions(Span.Options.Defaults.withParentKernel(kernel).withSpanKind(Span.SpanKind.Server))) {
109+
addRequestFields >> routes(req).value.guaranteeCase {
110+
case Canceled() => Trace[F].put(("cancelled", TraceValue.BooleanValue(true)), Tags.error(true))
111+
case Errored(e) => addErrorFields(e)
112+
case Succeeded(fa) =>
113+
fa.flatMap {
114+
case Some(resp) => addResponseFields(resp)
115+
case None => ev.unit
116+
}
82117
}
83118
}
84-
}
119+
)
85120
}
86121

87122
/**
@@ -113,4 +148,25 @@ object NatchezMiddleware {
113148
}
114149
}
115150

151+
val ExcludedHeaders: Set[CIString] = {
152+
import org.http4s.headers._
153+
import org.typelevel.ci._
154+
155+
val payload = Set(
156+
`Content-Length`.name,
157+
ci"Content-Type",
158+
`Content-Range`.name,
159+
ci"Trailer",
160+
`Transfer-Encoding`.name,
161+
)
162+
163+
val security = Set(
164+
Authorization.name,
165+
Cookie.name,
166+
`Set-Cookie`.name,
167+
)
168+
169+
payload ++ security
170+
}
171+
116172
}

modules/http4s/src/main/scala/natchez/http4s/syntax/EntryPointOps.scala

Lines changed: 13 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -6,140 +6,69 @@ package natchez.http4s.syntax
66

77
import cats.~>
88
import cats.data.{ Kleisli, OptionT }
9-
import cats.data.Kleisli.applyK
109
import cats.effect.MonadCancel
11-
import natchez.{ EntryPoint, Kernel, Span }
1210
import org.http4s.HttpRoutes
1311
import cats.effect.Resource
1412
import org.http4s.server.websocket.WebSocketBuilder2
1513
import org.typelevel.ci.CIString
14+
import natchez.EntryPoint
15+
import natchez.Span
1616

17-
/**
18-
* @define excludedHeaders
19-
* All headers except security (Authorization, Cookie, Set-Cookie)
20-
* and payload (Content-Length, ContentType, Content-Range, Trailer, Transfer-Encoding)
21-
* are passed to Kernel by default.
22-
*
23-
* @define isKernelHeader should an HTTP header be passed to Kernel or not
24-
*
25-
* @define spanName compute the span name from the request
26-
*
27-
* @define spanOptions options used in span creation
28-
*/
2917
trait EntryPointOps[F[_]] { outer =>
3018

3119
def self: EntryPoint[F]
3220

3321
/**
34-
* Given an entry point and HTTP Routes in Kleisli[F, Span[F], *] return routes in F. A new span
35-
* is created with by default the URI path as the name, either as a continuation of the incoming trace, if
36-
* any, or as a new root.
22+
* Given an entry point and HTTP Routes in Kleisli[F, Span[F], *] return routes in F.
23+
* A span that is injected is a RootsSpan (smilarly to Trace.ioTraceForEntryPoint)
3724
*
38-
* @note $excludedHeaders
39-
*
40-
* @param isKernelHeader $isKernelHeader
41-
* @param spanName $spanName
42-
* @param spanOptions $spanOptions
4325
*/
4426
def liftT(
4527
routes: HttpRoutes[Kleisli[F, Span[F], *]],
46-
isKernelHeader: CIString => Boolean = name => !EntryPointOps.ExcludedHeaders.contains(name),
47-
spanName: org.http4s.Request[F] => String = _.uri.path.toString,
48-
spanOptions: Span.Options = Span.Options.Defaults,
4928
)(implicit ev: MonadCancel[F, Throwable]): HttpRoutes[F] =
5029
Kleisli { req =>
51-
val kernelHeaders = req.headers.headers
52-
.collect {
53-
case header if isKernelHeader(header.name) => header.name -> header.value
54-
}
55-
.toMap
56-
57-
val kernel = Kernel(kernelHeaders)
58-
val spanR = self.continueOrElseRoot(spanName(req), kernel, spanOptions)
59-
OptionT {
60-
spanR.use { span =>
61-
routes.run(req.mapK(lift)).mapK(applyK(span)).map(_.mapK(applyK(span))).value
62-
}
63-
}
30+
val root = Span.makeRoots(self)
31+
OptionT(routes.run(req.mapK(Kleisli.liftK)).mapK(Kleisli.applyK(root)).map(_.mapK(Kleisli.applyK(root))).value)
6432
}
6533

34+
6635
/**
6736
* Lift an `HttpRoutes`-yielding resource that consumes `Span`s into the bare effect. We do this
6837
* by ignoring any tracing that happens during allocation and freeing of the `HttpRoutes`
6938
* resource. The reasoning is that such a resource typically lives for the lifetime of the
7039
* application and it's of little use to keep a span open that long.
7140
*
72-
* @note $excludedHeaders
73-
*
74-
* @param isKernelHeader $isKernelHeader
7541
*/
7642
def liftR(
7743
routes: Resource[Kleisli[F, Span[F], *], HttpRoutes[Kleisli[F, Span[F], *]]],
78-
isKernelHeader: CIString => Boolean = name => !EntryPointOps.ExcludedHeaders.contains(name)
7944
)(implicit ev: MonadCancel[F, Throwable]): Resource[F, HttpRoutes[F]] =
80-
routes.map(liftT(_, isKernelHeader)).mapK(Span.dropTracing)
45+
routes.map(liftT).mapK(Span.dropTracing)
8146

8247
/**
8348
* Given an entry point and a function from `WebSocketBuilder2` to HTTP Routes in
84-
* Kleisli[F, Span[F], *] return a function from `WebSocketBuilder2` to routes in F. A new span
85-
* is created with the URI path as the name, either as a continuation of the incoming trace, if
86-
* any, or as a new root.
87-
*
88-
* @note $excludedHeaders
89-
*
90-
* @param isKernelHeader $isKernelHeader
49+
* Kleisli[F, Span[F], *] return a function from `WebSocketBuilder2` to routes in F.
9150
*/
9251
def wsLiftT(
93-
routes: WebSocketBuilder2[Kleisli[F, Span[F], *]] => HttpRoutes[Kleisli[F, Span[F], *]],
94-
isKernelHeader: CIString => Boolean = name => !EntryPointOps.ExcludedHeaders.contains(name),
95-
spanName: org.http4s.Request[F] => String = _.uri.path.toString,
96-
spanOptions: Span.Options = Span.Options.Defaults
52+
routes: WebSocketBuilder2[Kleisli[F, Span[F], *]] => HttpRoutes[Kleisli[F, Span[F], *]]
9753
)(implicit ev: MonadCancel[F, Throwable]): WebSocketBuilder2[F] => HttpRoutes[F] = wsb =>
98-
liftT(routes(wsb.imapK(lift)(Span.dropTracing)), isKernelHeader, spanName, spanOptions)
54+
liftT(routes(wsb.imapK(lift)(Span.dropTracing)))
9955

10056
/**
10157
* Lift a `WebSocketBuilder2 => HttpRoutes`-yielding resource that consumes `Span`s into the bare
10258
* effect. We do this by ignoring any tracing that happens during allocation and freeing of the
10359
* `HttpRoutes` resource. The reasoning is that such a resource typically lives for the lifetime
10460
* of the application and it's of little use to keep a span open that long.
105-
*
106-
* @note $excludedHeaders
107-
*
108-
* @param isKernelHeader $isKernelHeader
10961
*/
11062
def wsLiftR(
111-
routes: Resource[Kleisli[F, Span[F], *], WebSocketBuilder2[Kleisli[F, Span[F], *]] => HttpRoutes[Kleisli[F, Span[F], *]]],
112-
isKernelHeader: CIString => Boolean = name => !EntryPointOps.ExcludedHeaders.contains(name),
113-
spanName: org.http4s.Request[F] => String = _.uri.path.toString,
114-
spanOptions: Span.Options = Span.Options.Defaults
63+
routes: Resource[Kleisli[F, Span[F], *], WebSocketBuilder2[Kleisli[F, Span[F], *]] => HttpRoutes[Kleisli[F, Span[F], *]]]
11564
)(implicit ev: MonadCancel[F, Throwable]): Resource[F, WebSocketBuilder2[F] => HttpRoutes[F]] =
116-
routes.map(wsLiftT(_, isKernelHeader, spanName, spanOptions)).mapK(Span.dropTracing)
65+
routes.map(wsLiftT).mapK(Span.dropTracing)
11766

11867
private val lift: F ~> Kleisli[F, Span[F], *] =
11968
Kleisli.liftK
12069
}
12170

12271
object EntryPointOps {
123-
val ExcludedHeaders: Set[CIString] = {
124-
import org.http4s.headers._
125-
import org.typelevel.ci._
126-
127-
val payload = Set(
128-
`Content-Length`.name,
129-
ci"Content-Type",
130-
`Content-Range`.name,
131-
ci"Trailer",
132-
`Transfer-Encoding`.name,
133-
)
134-
135-
val security = Set(
136-
Authorization.name,
137-
Cookie.name,
138-
`Set-Cookie`.name,
139-
)
140-
141-
payload ++ security
142-
}
14372
}
14473

14574
trait ToEntryPointOps {

modules/http4s/src/test/scala/natchez/http4s/NatchezMiddlewareSuite.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,14 +97,14 @@ class NatchezMiddlewareSuite extends InMemorySuite {
9797

9898
List(
9999
(Lineage.Root, NatchezCommand.CreateRootSpan("/hello/some-name", requestKernel, Span.Options.Defaults)),
100+
(Lineage.Root, NatchezCommand.Put(requestTags)),
100101
(Lineage.Root, NatchezCommand.CreateSpan("call-proxy", None, Span.Options.Defaults)),
101102
(Lineage.Root / "call-proxy", NatchezCommand.CreateSpan("http4s-client-request", None, Span.Options.Defaults)),
102103
(Lineage.Root / "call-proxy" / "http4s-client-request", NatchezCommand.AskKernel(requestKernel)),
103104
(Lineage.Root / "call-proxy" / "http4s-client-request", NatchezCommand.Put(clientRequestTags)),
104105
(Lineage.Root / "call-proxy" / "http4s-client-request", NatchezCommand.Put(clientResponseTags)),
105106
(Lineage.Root / "call-proxy", NatchezCommand.ReleaseSpan("http4s-client-request")),
106107
(Lineage.Root, NatchezCommand.ReleaseSpan("call-proxy")),
107-
(Lineage.Root, NatchezCommand.Put(requestTags)),
108108
(Lineage.Root, NatchezCommand.Put(responseTags)),
109109
(Lineage.Root, NatchezCommand.ReleaseRootSpan("/hello/some-name"))
110110
)

0 commit comments

Comments
 (0)