backend/app/utils/RequestLoggingFilter.scala (78 lines of code) (raw):

package utils import java.net.URI import org.apache.pekko.stream.Materializer import org.slf4j.LoggerFactory import play.api.mvc.{Filter, RequestHeader, Result} import scala.concurrent.{ExecutionContext, Future} import scala.util.{Failure, Success} import scala.jdk.CollectionConverters._ import net.logstash.logback.marker.Markers.appendEntries import net.logstash.logback.marker.LogstashMarker import play.api.mvc.Results.InternalServerError import services.AppConfig class RequestLoggingFilter(override val mat: Materializer)(implicit ec: ExecutionContext) extends Filter { val logger = LoggerFactory.getLogger("access") override def apply(next: (RequestHeader) => Future[Result])(rh: RequestHeader): Future[Result] = { val start = System.currentTimeMillis() val result = next(rh) result onComplete { case Success(response) => val duration = System.currentTimeMillis() - start val (structuredMessage, message) = RequestLoggingFilter.buildSuccessMessage(rh, response, duration) if (rh.path != "/healthcheck") { logger.info(structuredMessage, message) } case Failure(err) => val duration = System.currentTimeMillis() - start val (structuredFailureMessage, failureMessage) = RequestLoggingFilter.buildFailureMessage(rh, duration, err) logger.error(structuredFailureMessage, failureMessage) } result } } object RequestLoggingFilter { def buildSuccessMessage(request: RequestHeader, response: Result, duration: Long): (LogstashMarker, String) = { val originIp = request.headers.get("X-Forwarded-For").getOrElse(request.remoteAddress) val referer = request.headers.get("Referer").getOrElse("") val length = response.header.headers.getOrElse("Content-Length", 0) val uri = request.uri val marker = appendEntries(Map( "originIp" -> originIp, "method" -> request.method, "uri" -> uri, "version" -> request.version, "status" -> response.header.status, "length" -> length, "referer" -> referer, "duration" -> s"${duration}ms" ).asJava) val messageString = s"""$originIp - "${request.method} $uri ${request.version}" ${response.header.status} $length "$referer" ${duration}ms""" (marker, messageString) } def buildFailureMessage(request: RequestHeader, duration: Long, error: Throwable): (LogstashMarker, String) = { val originIp = request.headers.get("X-Forwarded-For").getOrElse(request.remoteAddress) val referer = request.headers.get("Referer").getOrElse("") val uri = request.uri val marker = appendEntries(Map( "originIp" -> originIp, "method" -> request.method, "uri" -> uri, "version" -> request.version, "referer" -> referer, "duration" -> s"${duration}ms" ).asJava) val messageString = s"""$originIp - "${request.method} $uri ${request.version}" ERROR: ${error.toString} "$referer" ${duration}ms""" (marker, messageString) } } class ReadOnlyFilter(appConfig: AppConfig, override val mat: Materializer) extends Filter { override def apply(next: (RequestHeader) => Future[Result])(rh: RequestHeader): Future[Result] = { val whitelistPaths = Set("/api/auth/token") val disallowedHttpMethods = Set("POST", "DELETE", "PUT") if (!whitelistPaths.contains(rh.path) && appConfig.readOnly && disallowedHttpMethods.contains(rh.method)) { Future.successful(InternalServerError(s"Application in read only mode")) } else { next(rh) } } }