Hello
I’m having trouble understanding how to implement Logging & debugging — tapir 1.x documentation
In fact, if I manage to get this working for me, I’d like to shot a PR to provide some examples in the repo.
Say I have a very simple GET /hello?name=[string]
endpoint, the same you get from https://adopt-tapir.softwaremill.com/.
If I omit the mandatory name parameter, as expected, I get a 400 error
❯ curl localhost:8080/hello
Invalid value for: query parameter name (missing)
And, I get some debug-level logs
23:49:09.067 [default-pekko.actor.default-dispatcher-4] DEBUG s.t.s.p.PekkoHttpServerInterpreter$ - Request: GET /hello, handled by: GET /hello, took: 49ms; decode failure: Missing, on input: ?name; response: 400
However, I don’t want this to be on a debug level. Instead, I want to println
it.
Note: I want to println in this simple example to avoid any slf4j complications, though I suspect the issue lies there anyways
My main looks like
package com.example
import org.apache.pekko.actor.ActorSystem
import org.apache.pekko.http.scaladsl.Http
import sttp.tapir.server.pekkohttp.{PekkoHttpServerInterpreter, PekkoHttpServerOptions}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.io.StdIn
@main def run(): Unit =
implicit val actorSystem: ActorSystem = ActorSystem()
val serverOptions: PekkoHttpServerOptions =
PekkoHttpServerOptions
.customiseInterceptors
.metricsInterceptor(Endpoints.prometheusMetrics.metricsInterceptor())
.options
val route = PekkoHttpServerInterpreter(serverOptions).toRoute(Endpoints.all)
val port = sys.env.get("http.port").map(_.toInt).getOrElse(8080)
val bindingFuture = Http()
.newServerAt("localhost", port)
.bindFlow(route)
.map { binding =>
println(s"Go to http://localhost:${binding.localAddress.getPort}/docs to open SwaggerUI. Press ENTER key to exit.")
binding
}
StdIn.readLine()
bindingFuture.flatMap(_.unbind()).onComplete(_ => actorSystem.terminate())
, so I add a log interceptor
val serverOptions: PekkoHttpServerOptions =
PekkoHttpServerOptions
.customiseInterceptors
.metricsInterceptor(Endpoints.prometheusMetrics.metricsInterceptor())
.serverLog(
PekkoHttpServerOptions.defaultSlf4jServerLog
.doLogAllDecodeFailures { (message, maybeThrowable) =>
Future.successful {
println("Tapirs are amazing animals!")
println(message)
}
}
.logAllDecodeFailures(true) // just to be sure
)
.options
However, this setting seems to be completely ignored.
Note that this is a toy example, in a real word scenario it’s of course a bit more complex. In my case, I want to “promote” the log level from debug to warn.
Actually, I’ve never managed to make this work despite using tapir for 2 years. My workaround is adding logging to a custom rejection handler, which works. However, that is sub-optimal because
- Separation of concerns: the logging interceptor should… deal with logging
- Ideally, I’d have access to the whole context, but when customizing the default rejection handler, you don’t have that
To be more precise, you only have access to message
and statusCode
in the response
method when using DefaultDecodeFailureHandler
. You can hack it at the cost of truly ugly code by logging when defining the failureMessage
failureMessage = (context: DecodeFailureContext) => {
logger.warn(
s"""Failed to decode request
| Request: ${context.request.showShort}
| Error: ${FailureMessages.failureMessage(context)}
|""".stripMargin
)
FailureMessages.failureMessage(context)
}
Anyhow, back at the topic at hand: what am I doing wrong?
All I want is to simply enable logging when a request decoding fails. Ideally, I’d like to be able to pass a function (context: DecodeFailureContext) => F[Unit]
so I can do whatever, but I’m fine with the default “decorators” and just customizing the error message
def doLogAllDecodeFailures(f: (String, Option[Throwable]) => F[Unit]): DefaultServerLog[F]
Thanks!