Skip to content

Commit 3e98406

Browse files
authored
Add Jakarta EE 9 servlet module (#202)
* Add Jakarta Servlet module * Add project, formatting * Update ci.yml
1 parent f1869cc commit 3e98406

File tree

5 files changed

+208
-2
lines changed

5 files changed

+208
-2
lines changed

.github/workflows/ci.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,11 @@ jobs:
6363

6464
- name: Make target directories
6565
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v'))
66-
run: mkdir -p money-java-servlet/target money-aspectj/target money-http-client/target money-otlp-http-exporter/target money-api/target money-kafka/target money-otel-handler/target money-otel-jaeger-exporter/target money-otlp-exporter/target money-otel-zipkin-exporter/target money-akka/target money-otel-formatters/target money-spring/target money-core/target money-otel-logging-exporter/target money-otel-inmemory-exporter/target money-wire/target project/target
66+
run: mkdir -p money-java-servlet/target money-aspectj/target money-http-client/target money-otlp-http-exporter/target money-api/target money-kafka/target money-otel-handler/target money-otel-jaeger-exporter/target money-otlp-exporter/target money-otel-zipkin-exporter/target money-akka/target money-otel-formatters/target money-spring/target money-core/target money-otel-logging-exporter/target money-otel-inmemory-exporter/target money-jakarta-servlet/target money-wire/target project/target
6767

6868
- name: Compress target directories
6969
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v'))
70-
run: tar cf targets.tar money-java-servlet/target money-aspectj/target money-http-client/target money-otlp-http-exporter/target money-api/target money-kafka/target money-otel-handler/target money-otel-jaeger-exporter/target money-otlp-exporter/target money-otel-zipkin-exporter/target money-akka/target money-otel-formatters/target money-spring/target money-core/target money-otel-logging-exporter/target money-otel-inmemory-exporter/target money-wire/target project/target
70+
run: tar cf targets.tar money-java-servlet/target money-aspectj/target money-http-client/target money-otlp-http-exporter/target money-api/target money-kafka/target money-otel-handler/target money-otel-jaeger-exporter/target money-otlp-exporter/target money-otel-zipkin-exporter/target money-akka/target money-otel-formatters/target money-spring/target money-core/target money-otel-logging-exporter/target money-otel-inmemory-exporter/target money-jakarta-servlet/target money-wire/target project/target
7171

7272
- name: Upload target directories
7373
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v'))

build.sbt

+13
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ lazy val money =
4242
moneyAspectj,
4343
moneyHttpClient,
4444
moneyJavaServlet,
45+
moneyJakartaServlet,
4546
moneyWire,
4647
moneyKafka,
4748
moneySpring,
@@ -151,6 +152,18 @@ lazy val moneyJavaServlet =
151152
)
152153
.dependsOn(moneyCore % "test->test;compile->compile")
153154

155+
lazy val moneyJakartaServlet =
156+
Project("money-jakarta-servlet", file("./money-jakarta-servlet"))
157+
.enablePlugins(AutomateHeaderPlugin)
158+
.settings(projectSettings: _*)
159+
.settings(
160+
libraryDependencies ++=
161+
Seq(
162+
jakartaServlet
163+
) ++ commonTestDependencies
164+
)
165+
.dependsOn(moneyCore % "test->test;compile->compile")
166+
154167
lazy val moneyWire =
155168
Project("money-wire", file("./money-wire"))
156169
.enablePlugins(AutomateHeaderPlugin)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright 2012 Comcast Cable Communications Management, LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.comcast.money.jakarta.servlet
18+
19+
import com.comcast.money.core.Money
20+
import io.opentelemetry.context.{ Context, Scope }
21+
import org.slf4j.LoggerFactory
22+
23+
import jakarta.servlet._
24+
import jakarta.servlet.http.{ HttpServletRequest, HttpServletRequestWrapper, HttpServletResponse }
25+
import scala.collection.JavaConverters._
26+
27+
/**
28+
* A Java Servlet 2.5 Filter. Examines the inbound http request, and will set the
29+
* trace context for the request if the money trace header or X-B3 style headers are found
30+
*/
31+
class TraceFilter extends Filter {
32+
33+
private val logger = LoggerFactory.getLogger(classOf[TraceFilter])
34+
private val tracer = Money.Environment.tracer
35+
private val formatter = Money.Environment.formatter
36+
37+
override def init(filterConfig: FilterConfig): Unit = {}
38+
39+
override def destroy(): Unit = {}
40+
41+
private val spanName = "servlet"
42+
43+
override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = {
44+
45+
val httpRequest = new HttpServletRequestWrapper(request.asInstanceOf[HttpServletRequest])
46+
47+
val headerNames: Iterable[String] = httpRequest.getHeaderNames.asScala.toIterable.asInstanceOf[Iterable[String]]
48+
val scope: Scope = formatter.fromHttpHeaders(headerNames, httpRequest.getHeader, logger.warn) match {
49+
case Some(spanId) =>
50+
val span = tracer.spanFactory.newSpan(spanId, spanName)
51+
Context.root()
52+
.`with`(span)
53+
.makeCurrent()
54+
case None => () => ()
55+
}
56+
57+
try {
58+
val httpResponse = response.asInstanceOf[HttpServletResponse]
59+
formatter.setResponseHeaders(httpRequest.getHeader, httpResponse.addHeader)
60+
61+
chain.doFilter(request, response)
62+
} finally {
63+
scope.close()
64+
}
65+
}
66+
67+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
* Copyright 2012 Comcast Cable Communications Management, LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.comcast.money.jakarta.servlet
18+
19+
import com.comcast.money.api.{ Span, SpanId }
20+
import com.comcast.money.core.formatters.FormatterUtils.randomRemoteSpanId
21+
import com.comcast.money.core.internal.SpanLocal
22+
import org.mockito.Mockito._
23+
import org.mockito.stubbing.OngoingStubbing
24+
import org.scalatest.OptionValues._
25+
import org.scalatest.matchers.should.Matchers
26+
import org.scalatest.wordspec.AnyWordSpec
27+
import org.scalatest.{ BeforeAndAfter, OneInstancePerTest }
28+
import org.scalatestplus.mockito.MockitoSugar
29+
30+
import java.util.Collections
31+
import jakarta.servlet.http.{ HttpServletRequest, HttpServletResponse }
32+
import jakarta.servlet.{ FilterChain, FilterConfig, ServletRequest, ServletResponse }
33+
34+
class TraceFilterSpec extends AnyWordSpec with Matchers with OneInstancePerTest with BeforeAndAfter with MockitoSugar {
35+
36+
val mockRequest = mock[HttpServletRequest]
37+
val mockResponse = mock[HttpServletResponse]
38+
val mockFilterChain = mock[FilterChain]
39+
val existingSpanId = randomRemoteSpanId()
40+
val underTest = new TraceFilter()
41+
val MoneyTraceFormat = "trace-id=%s;parent-id=%s;span-id=%s"
42+
val filterChain: FilterChain = (_: ServletRequest, _: ServletResponse) => capturedSpan = SpanLocal.current
43+
var capturedSpan: Option[Span] = None
44+
45+
def traceParentHeader(spanId: SpanId): String = {
46+
val traceId = spanId.traceId.replace("-", "").toLowerCase
47+
f"00-$traceId%s-${spanId.selfId}%016x-00"
48+
}
49+
50+
before {
51+
capturedSpan = None
52+
val empty: java.util.Enumeration[_] = Collections.emptyEnumeration()
53+
// The raw type seems to confuse the Scala compiler so the cast is required to compile successfully
54+
when(mockRequest.getHeaderNames).asInstanceOf[OngoingStubbing[java.util.Enumeration[_]]].thenReturn(empty)
55+
}
56+
57+
"A TraceFilter" should {
58+
"clear the trace context when an http request arrives" in {
59+
underTest.doFilter(mockRequest, mockResponse, filterChain)
60+
SpanLocal.current shouldBe None
61+
}
62+
63+
"always call the filter chain" in {
64+
underTest.doFilter(mockRequest, mockResponse, mockFilterChain)
65+
verify(mockFilterChain).doFilter(mockRequest, mockResponse)
66+
}
67+
68+
"set the trace context to the money trace header if present" in {
69+
when(mockRequest.getHeader("X-MoneyTrace"))
70+
.thenReturn(MoneyTraceFormat.format(existingSpanId.traceId, existingSpanId.parentId, existingSpanId.selfId))
71+
underTest.doFilter(mockRequest, mockResponse, filterChain)
72+
capturedSpan.value.info.id shouldEqual existingSpanId
73+
}
74+
75+
"set the trace context to the traceparent header if present" in {
76+
when(mockRequest.getHeader("traceparent"))
77+
.thenReturn(traceParentHeader(existingSpanId))
78+
underTest.doFilter(mockRequest, mockResponse, filterChain)
79+
80+
val actualSpanId = capturedSpan.value.info.id
81+
actualSpanId.traceId shouldEqual existingSpanId.traceId
82+
actualSpanId.parentId shouldEqual existingSpanId.selfId
83+
}
84+
85+
"prefer the money trace header over the W3C Trace Context header" in {
86+
when(mockRequest.getHeader("X-MoneyTrace"))
87+
.thenReturn(MoneyTraceFormat.format(existingSpanId.traceId, existingSpanId.parentId, existingSpanId.selfId))
88+
when(mockRequest.getHeader("traceparent"))
89+
.thenReturn(traceParentHeader(SpanId.createNew()))
90+
underTest.doFilter(mockRequest, mockResponse, filterChain)
91+
capturedSpan.value.info.id shouldEqual existingSpanId
92+
}
93+
94+
"not set the trace context if the money trace header could not be parsed" in {
95+
when(mockRequest.getHeader("X-MoneyTrace")).thenReturn("can't parse this")
96+
underTest.doFilter(mockRequest, mockResponse, filterChain)
97+
capturedSpan shouldBe None
98+
}
99+
100+
"adds Money header to response" in {
101+
when(mockRequest.getHeader("X-MoneyTrace"))
102+
.thenReturn(MoneyTraceFormat.format(existingSpanId.traceId, existingSpanId.parentId, existingSpanId.selfId))
103+
underTest.doFilter(mockRequest, mockResponse, mockFilterChain)
104+
verify(mockResponse).addHeader(
105+
"X-MoneyTrace",
106+
MoneyTraceFormat.format(existingSpanId.traceId, existingSpanId.parentId, existingSpanId.selfId))
107+
}
108+
109+
"adds Trace Context header to response" in {
110+
when(mockRequest.getHeader("traceparent"))
111+
.thenReturn(traceParentHeader(existingSpanId))
112+
underTest.doFilter(mockRequest, mockResponse, mockFilterChain)
113+
verify(mockResponse).addHeader(
114+
"traceparent",
115+
traceParentHeader(existingSpanId))
116+
}
117+
118+
"loves us some test coverage" in {
119+
val mockConf = mock[FilterConfig]
120+
underTest.init(mockConf)
121+
underTest.destroy()
122+
}
123+
}
124+
}

project/Dependencies.scala

+2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ object Dependencies {
4343
// Javax servlet - note: the group id and artfacit id have changed in 3.0
4444
val javaxServlet = "javax.servlet" % "servlet-api" % "2.5"
4545

46+
val jakartaServlet = "jakarta.servlet" % "jakarta.servlet-api" % "5.0.0"
47+
4648
// Kafka, exclude dependencies that we will not need, should work for 2.10 and 2.11
4749
val kafka = ("org.apache.kafka" %% "kafka" % "2.4.0")
4850
.exclude("javax.jms", "jms")

0 commit comments

Comments
 (0)