Search code examples
scalaprometheusziotapir

Scala, Tapir, ZIO - add metrics interceptor with default error handler


Have a question regarding Prometheus metrics in Tapir and ZIO. I have a simple code:

val metrics = PrometheusMetrics.default[Task]()
val options: ZioHttpServerOptions[Any] = ZioHttpServerOptions
    .customiseInterceptors
    .metricsInterceptor(metrics.metricsInterceptor())
    .options

and it works correct when I call localhost:8080/metrics, I see metrics.

But when I added default error handler:

val metrics = PrometheusMetrics.default[Task]()
def failureResponse(msg: String): ValuedEndpointOutput[_]=
   ValuedEndpointOutput(jsonBody[MyFailure], MyFailure(msg))
val options: ZioHttpServerOptions[Any] = ZioHttpServerOptions
    .customiseInterceptors
    .metricsInterceptor(metrics.metricsInterceptor())
    .defaultHandlers(failureResponse, notFoundWhenRejected = true)
    .options

It doesn't work. Instead of metrics I see now error (404) which was caught during request to localhost:8080/metrics. Honestly, don't know why. Is it possible to fix it somehow and keep error handler along with metrics interceptor?

EDIT: Metrics endpoint:

def metricsEndpoint = ZioHttpInterpreter(options).toHttp(metrics.metricsEndpoint)

Solution

  • This problem is most probably due to separately interpreting the "main" endpoints and the metrics endpoint as ZIO Http's Http value.

    Consider the following:

    val mainHttp = ZioHttpInterpreter(options).toHttp(mainEndpoints)
    val metricsHttp = ZioHttpInterpreter(options).toHttp(metricsEndpoints)
    
    Server.start(8080, mainHttp <> metricsHttp)
    

    If the notFoundWhenRejected = true option is used, when the request /metrics comes in, it is first handled by mainHttp. However, that value doesn't know how to handle this request - hence it is rejected. But as we specified the mentioned option, rejections are turned into 404s, hence the answer.

    The default value for that option is false. In this situation, the /metrics request is rejected by the mainHttp value, but this isn't converted into a 404 response, instead processing continues with metricsHttp.

    The proper solution, to have both /metrics working, and the notFoundWhenRejected = true option, is to interpret all endpoints at once. Then, the 404 will be returned only when none of the endpoint (neither the main, nor the metrics one) matches the request:

    val http = ZioHttpInterpreter(options)
      .toHttp(mainEndpoints ++ metricsEndpoints)
    
    Server.start(8080, http)