newswires/app/lib/RequestLoggingFilter.scala (99 lines of code) (raw):
package lib
import net.logstash.logback.marker.Markers.appendEntries
import org.apache.pekko.stream.Materializer
import play.api.mvc.{AnyContent, Filter, Request, RequestHeader, Result}
import play.api.{Logger, Logging, MarkerContext}
import java.util.UUID
import scala.concurrent.{ExecutionContext, Future}
import scala.jdk.CollectionConverters._
import scala.util.{Failure, Success}
object RequestLoggingFilter {
private val requestIdHeader = "x-newswires-request-id"
def getRequestId[T](req: Request[T]): String =
req.headers.get(requestIdHeader).getOrElse(UUID.randomUUID().toString)
}
class RequestLoggingFilter(implicit
val mat: Materializer,
ec: ExecutionContext
) extends Filter {
private val logger = Logger("request")
override def apply(
next: RequestHeader => Future[Result]
)(request: RequestHeader): Future[Result] = {
val start = System.currentTimeMillis()
val withID = request.withHeaders(
request.headers.replace(
RequestLoggingFilter.requestIdHeader -> UUID.randomUUID().toString
)
)
val resultFuture = next(withID)
resultFuture onComplete {
case Success(response) =>
val duration = System.currentTimeMillis() - start
log(withID, Right(response), duration)
case Failure(err) =>
val duration = System.currentTimeMillis() - start
log(withID, Left(err), duration)
}
resultFuture
}
private def log(
request: RequestHeader,
outcome: Either[Throwable, Result],
duration: Long
): Unit = {
val originIp =
request.headers.get("X-Forwarded-For").getOrElse(request.remoteAddress)
val referer = request.headers.get("Referer").getOrElse("-")
val queryStringMap = request.queryString.map { case (k, v) =>
s"query_string.$k" -> v.mkString(", ")
}
val mandatoryMarkers = Map(
"origin" -> originIp,
"method" -> request.method,
"duration" -> duration,
"path" -> request.path
) ++ queryStringMap
val optionalMarkers = Map(
"status" -> outcome.map(_.header.status).toOption,
"requestId" -> request.headers.get(RequestLoggingFilter.requestIdHeader),
"referrer" -> referer
).collect { case (key, Some(value)) =>
key -> value
}
val markers = MarkerContext(
appendEntries((mandatoryMarkers ++ optionalMarkers).asJava)
)
outcome.fold(
throwable => {
logger.info(
s"""$originIp - "${request.method} ${request.uri} ${request.version}" ERROR "$referer" ${duration}ms"""
)(markers)
logger.error(s"Error for ${request.method} ${request.uri}", throwable)
},
response => {
val length = response.header.headers.getOrElse("Content-Length", 0)
logger.info(
s"""$originIp - "${request.method} ${request.uri} ${request.version}" ${response.header.status} $length "$referer" ${duration}ms"""
)(markers)
}
)
}
}
trait RequestLogging extends Logging {
protected def buildLogMarker[T](
methodName: String
)(implicit r: Request[T]): LogMarker = {
LogMarker(
"requestId" -> RequestLoggingFilter.getRequestId(r),
"requestType" -> methodName
)
}
}
trait RequestHelpers {
val apiKey: String
protected def hasApiKey(request: Request[AnyContent]): Boolean =
request
.getQueryString("api-key")
.contains(apiKey)
}