http4s-otel4s-middleware

Filtering and errors

Choose which requests to record

Metrics, tracing and propagation each have a request filter. A PerRequestFilter receives a RequestPrelude with request metadata, including the method, URI and headers. It runs each time the client acquires a response or the server runs the app. The response and body are not available to the filter.

import org.http4s.otel4s.middleware.{PerRequestConfig, PerRequestFilter}
import org.http4s.otel4s.middleware.metrics.client.ClientMetricsConfig
import org.http4s.otel4s.middleware.client.ClientTracingConfig
import org.http4s.otel4s.middleware.trace.client.{ClientSpanDataProvider, UriRedactor}
import org.http4s.otel4s.middleware.trace.redact.QueryRedactor

val excludeHealth: PerRequestFilter = request => {
  if (request.uri.path.renderString == "/health") {
    PerRequestConfig.Disabled
  } else {
    PerRequestConfig.Enabled
  }
}

val redactor = new UriRedactor.OnlyRedactUserInfo with QueryRedactor.RedactSensitive {}
val spanDataProvider = ClientSpanDataProvider.openTelemetry(redactor)
val clientConfig = ClientMetricsConfig.default.withRequestFilter(excludeHealth)
val tracingConfig = ClientTracingConfig(spanDataProvider)
  .withRequestFilter(excludeHealth)
  .withPropagationFilter(PerRequestFilter.alwaysEnabled)

In this example, /health requests produce neither metrics nor spans. withPropagationFilter(alwaysEnabled) allows context injection only for requests that pass the tracing filter.

Use and or or to combine filters. Use alwaysEnabled or neverEnabled to allow or disable every request. On the server, withReversePropagationFilter controls context injection into response headers.

Choose which responses count as errors

Default Client Server
Metrics and tracing error responses 4xx and 5xx 5xx
Cancellation alone Neutral Neutral

Customize metrics and tracing separately with withErrorClassifier:

import org.http4s.Status
import org.http4s.otel4s.middleware.ErrorClassifier

val policy = ErrorClassifier.default.excluding(Status.NotFound)
val customClientConfig = clientConfig.withErrorClassifier(policy)
val customTracingConfig = tracingConfig.withErrorClassifier(policy)

ErrorClassifier.serverError selects 5xx, and ErrorClassifier.never disables response-status error classification. Use included to add statuses and excluding to remove them. Combine classifiers with and or or.

Exceptions and cancellation

The combined middleware and metrics-only middleware record exceptions raised while acquiring a response, running the server app or evaluating a body. These exceptions take precedence over errors based on the response status. ErrorClassifier.never does not hide them. If the middleware sees a body failure, it records it even if code outside the middleware catches the exception.

The middleware can only record failures it sees. For example, if a backend ends a stream normally after a transport failure, the middleware cannot tell that the transport failed.

Errors in caller code and later client backend cleanup do not count as HTTP failures. Cancellation alone does not set the span status to Error or add error.type. A response status that already counts as an error still does.

See Streaming lifecycle for when the middleware finishes recording.