Tapir logging format

I am trying to send our tapir logs to Honeycomb. Currently we are using Http4sDefaultServerLog logger but I can’t tell what format the logs are. The Honeycomb parsers options I have are these:

I’m not sure which one to choose.

If there’s no good parser for these default logs tapir is outputting, how can I change the format of the logs?

Hi, you can’t customize logs produced by tapir by passing a function (String, Option[Throwable]) => F[Unit] to tapir configuration.

For example, it might look like the below, where logger is just an instance of the logger you use in your application.

def logHandler(msg: String, error: Option[Throwable]) = error match {
   case Some(err) => logger.error(err)(msg)
   case _ => logger.info(msg)
}

Then you should just pass this function to options that are passed to the interpreter:

val options = Http4sServerOptions.defaultServerLog[IO]
      .doLogWhenHandled(logHandler)

val routes = Http4sServerInterpreter[IO]().toRoutes(endpoints)
1 Like

Hi @katlasik,
I’ve made a little progress since my post. I’ve managed to transform all the logs being outputted to Json, using the logback encoder LogstashEncoder. So now I can chose a Honeycomb parser.

I added the logging options you indicated but I’m still facing an issue. The problem is specifically with the “message” attribute:

[info] {"timeStamp":"2023-04-06T22:32:53.21052-06:00","message":"Request: GET /health/ping, handled by: GET /health/ping, took: 6ms; response: 200","logger":"ntrs.tpv.perf.routes.Routes","thread":"io-compute-2","level":"INFO"}

Notice that the request details are inside the “message” attribute. This gets parsed by Honeycomb as an entire string which makes it hard to query. Is there a way to split log the entire request/response details into more granular attributes instead of it all being embedded in a single string?

Any pointers would be greatly appreciated. Thanks.

You can put additional fields to the structured log with MDC. Add it with MDC.put("yourFieldName", "field"), but you then have to remove that field with MDC.remove("yourFieldName"). I think the best place to get all information you need is requestHandled from ServerLog trait (Http4sDefaultServerLog extends it). So you have to basically provide your own implementation of ServerLog.

The implementation of the requestHandled method could look similar to:

  override def requestHandled(ctx: DecodeSuccessContext[F, _, _, _], response: ServerResponse[_], token: Long): F[Unit] =
    if (logWhenHandled)
      Sync[F].delay(MDS.put("method", ctx.request.method)) >> //add any fields you need
      doLogWhenHandled(
        s"Request: ${showRequest(ctx.request)}, handled by: ${showEndpoint(ctx.endpoint)}${took(token)}; response: ${showResponse(response)}",
        None
      )>> Sync[F].delay(MDS.remove("method")) //remove field at the end
    else noLog

Hi @katlasik,
Thanks for your hints and examples. I ended up writing my own class that extends ServerLog and overrode each of the methods required with various custom MDC.puts. Fore example:

  override def requestReceived(request: ServerRequest, token: TOKEN): F[Unit] = {
    MDC.put("requestReceived", request.showShort).pure[F] >>
      MDC.put("method", request.method.method).pure[F] >>
      MDC.put("endpoint", request.uri.toString).pure[F] >>
      MDC.put("host", request.header("Host").getOrElse("unknown")).pure[F]

After that I was able to pass in my custom logging class to the Http4sServerInterpreter:

    val apiLogger = new ApiLogging[F]

    val serverOptions = Http4sServerOptions.customiseInterceptors
      .serverLog(apiLogger)
      .options

    Http4sServerInterpreter[F](serverOptions = serverOptions)
      .toRoutes(
        endpoints
      )
      .orNotFound

It’s not a pretty solution and I do wonder if there’s another way to do this with log4cats or some other library. But for now I’m getting what I need in terms of custom logging.

Thanks!

1 Like