Skip to content

Commit

Permalink
Add Jakarta Servlet module
Browse files Browse the repository at this point in the history
  • Loading branch information
HaloFour committed Jun 26, 2024
1 parent f1869cc commit 6ea4936
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 0 deletions.
12 changes: 12 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,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 6ea4936

Please sign in to comment.