Skip to content

Commit

Permalink
Add Jakarta EE 9 servlet module (#202)
Browse files Browse the repository at this point in the history
* Add Jakarta Servlet module

* Add project, formatting

* Update ci.yml
  • Loading branch information
HaloFour authored Jun 26, 2024
1 parent f1869cc commit 3e98406
Show file tree
Hide file tree
Showing 5 changed files with 208 additions and 2 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,11 @@ jobs:

- name: Make target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v'))
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
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

- name: Compress target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v'))
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
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

- name: Upload target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v'))
Expand Down
13 changes: 13 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ lazy val money =
moneyAspectj,
moneyHttpClient,
moneyJavaServlet,
moneyJakartaServlet,
moneyWire,
moneyKafka,
moneySpring,
Expand Down Expand Up @@ -151,6 +152,18 @@ lazy val moneyJavaServlet =
)
.dependsOn(moneyCore % "test->test;compile->compile")

lazy val moneyJakartaServlet =
Project("money-jakarta-servlet", file("./money-jakarta-servlet"))
.enablePlugins(AutomateHeaderPlugin)
.settings(projectSettings: _*)
.settings(
libraryDependencies ++=
Seq(
jakartaServlet
) ++ commonTestDependencies
)
.dependsOn(moneyCore % "test->test;compile->compile")

lazy val moneyWire =
Project("money-wire", file("./money-wire"))
.enablePlugins(AutomateHeaderPlugin)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright 2012 Comcast Cable Communications Management, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.comcast.money.jakarta.servlet

import com.comcast.money.core.Money
import io.opentelemetry.context.{ Context, Scope }
import org.slf4j.LoggerFactory

import jakarta.servlet._
import jakarta.servlet.http.{ HttpServletRequest, HttpServletRequestWrapper, HttpServletResponse }
import scala.collection.JavaConverters._

/**
* A Java Servlet 2.5 Filter. Examines the inbound http request, and will set the
* trace context for the request if the money trace header or X-B3 style headers are found
*/
class TraceFilter extends Filter {

private val logger = LoggerFactory.getLogger(classOf[TraceFilter])
private val tracer = Money.Environment.tracer
private val formatter = Money.Environment.formatter

override def init(filterConfig: FilterConfig): Unit = {}

override def destroy(): Unit = {}

private val spanName = "servlet"

override def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain): Unit = {

val httpRequest = new HttpServletRequestWrapper(request.asInstanceOf[HttpServletRequest])

val headerNames: Iterable[String] = httpRequest.getHeaderNames.asScala.toIterable.asInstanceOf[Iterable[String]]
val scope: Scope = formatter.fromHttpHeaders(headerNames, httpRequest.getHeader, logger.warn) match {
case Some(spanId) =>
val span = tracer.spanFactory.newSpan(spanId, spanName)
Context.root()
.`with`(span)
.makeCurrent()
case None => () => ()
}

try {
val httpResponse = response.asInstanceOf[HttpServletResponse]
formatter.setResponseHeaders(httpRequest.getHeader, httpResponse.addHeader)

chain.doFilter(request, response)
} finally {
scope.close()
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Copyright 2012 Comcast Cable Communications Management, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.comcast.money.jakarta.servlet

import com.comcast.money.api.{ Span, SpanId }
import com.comcast.money.core.formatters.FormatterUtils.randomRemoteSpanId
import com.comcast.money.core.internal.SpanLocal
import org.mockito.Mockito._
import org.mockito.stubbing.OngoingStubbing
import org.scalatest.OptionValues._
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import org.scalatest.{ BeforeAndAfter, OneInstancePerTest }
import org.scalatestplus.mockito.MockitoSugar

import java.util.Collections
import jakarta.servlet.http.{ HttpServletRequest, HttpServletResponse }
import jakarta.servlet.{ FilterChain, FilterConfig, ServletRequest, ServletResponse }

class TraceFilterSpec extends AnyWordSpec with Matchers with OneInstancePerTest with BeforeAndAfter with MockitoSugar {

val mockRequest = mock[HttpServletRequest]
val mockResponse = mock[HttpServletResponse]
val mockFilterChain = mock[FilterChain]
val existingSpanId = randomRemoteSpanId()
val underTest = new TraceFilter()
val MoneyTraceFormat = "trace-id=%s;parent-id=%s;span-id=%s"
val filterChain: FilterChain = (_: ServletRequest, _: ServletResponse) => capturedSpan = SpanLocal.current
var capturedSpan: Option[Span] = None

def traceParentHeader(spanId: SpanId): String = {
val traceId = spanId.traceId.replace("-", "").toLowerCase
f"00-$traceId%s-${spanId.selfId}%016x-00"
}

before {
capturedSpan = None
val empty: java.util.Enumeration[_] = Collections.emptyEnumeration()
// The raw type seems to confuse the Scala compiler so the cast is required to compile successfully
when(mockRequest.getHeaderNames).asInstanceOf[OngoingStubbing[java.util.Enumeration[_]]].thenReturn(empty)
}

"A TraceFilter" should {
"clear the trace context when an http request arrives" in {
underTest.doFilter(mockRequest, mockResponse, filterChain)
SpanLocal.current shouldBe None
}

"always call the filter chain" in {
underTest.doFilter(mockRequest, mockResponse, mockFilterChain)
verify(mockFilterChain).doFilter(mockRequest, mockResponse)
}

"set the trace context to the money trace header if present" in {
when(mockRequest.getHeader("X-MoneyTrace"))
.thenReturn(MoneyTraceFormat.format(existingSpanId.traceId, existingSpanId.parentId, existingSpanId.selfId))
underTest.doFilter(mockRequest, mockResponse, filterChain)
capturedSpan.value.info.id shouldEqual existingSpanId
}

"set the trace context to the traceparent header if present" in {
when(mockRequest.getHeader("traceparent"))
.thenReturn(traceParentHeader(existingSpanId))
underTest.doFilter(mockRequest, mockResponse, filterChain)

val actualSpanId = capturedSpan.value.info.id
actualSpanId.traceId shouldEqual existingSpanId.traceId
actualSpanId.parentId shouldEqual existingSpanId.selfId
}

"prefer the money trace header over the W3C Trace Context header" in {
when(mockRequest.getHeader("X-MoneyTrace"))
.thenReturn(MoneyTraceFormat.format(existingSpanId.traceId, existingSpanId.parentId, existingSpanId.selfId))
when(mockRequest.getHeader("traceparent"))
.thenReturn(traceParentHeader(SpanId.createNew()))
underTest.doFilter(mockRequest, mockResponse, filterChain)
capturedSpan.value.info.id shouldEqual existingSpanId
}

"not set the trace context if the money trace header could not be parsed" in {
when(mockRequest.getHeader("X-MoneyTrace")).thenReturn("can't parse this")
underTest.doFilter(mockRequest, mockResponse, filterChain)
capturedSpan shouldBe None
}

"adds Money header to response" in {
when(mockRequest.getHeader("X-MoneyTrace"))
.thenReturn(MoneyTraceFormat.format(existingSpanId.traceId, existingSpanId.parentId, existingSpanId.selfId))
underTest.doFilter(mockRequest, mockResponse, mockFilterChain)
verify(mockResponse).addHeader(
"X-MoneyTrace",
MoneyTraceFormat.format(existingSpanId.traceId, existingSpanId.parentId, existingSpanId.selfId))
}

"adds Trace Context header to response" in {
when(mockRequest.getHeader("traceparent"))
.thenReturn(traceParentHeader(existingSpanId))
underTest.doFilter(mockRequest, mockResponse, mockFilterChain)
verify(mockResponse).addHeader(
"traceparent",
traceParentHeader(existingSpanId))
}

"loves us some test coverage" in {
val mockConf = mock[FilterConfig]
underTest.init(mockConf)
underTest.destroy()
}
}
}
2 changes: 2 additions & 0 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ object Dependencies {
// Javax servlet - note: the group id and artfacit id have changed in 3.0
val javaxServlet = "javax.servlet" % "servlet-api" % "2.5"

val jakartaServlet = "jakarta.servlet" % "jakarta.servlet-api" % "5.0.0"

// Kafka, exclude dependencies that we will not need, should work for 2.10 and 2.11
val kafka = ("org.apache.kafka" %% "kafka" % "2.4.0")
.exclude("javax.jms", "jms")
Expand Down

0 comments on commit 3e98406

Please sign in to comment.