diff --git a/prometheus/promhttp/http.go b/prometheus/promhttp/http.go index e598e66..e914d69 100644 --- a/prometheus/promhttp/http.go +++ b/prometheus/promhttp/http.go @@ -168,6 +168,9 @@ func HandlerForTransactional(reg prometheus.TransactionalGatherer, opts HandlerO if opts.ErrorLog != nil { opts.ErrorLog.Println("error gathering metrics:", err) } + if opts.StructuredErrorLog != nil { + opts.StructuredErrorLog.Error("error gathering metrics", "error", err) + } errCnt.WithLabelValues("gathering").Inc() switch opts.ErrorHandling { case PanicOnError: @@ -197,6 +200,9 @@ func HandlerForTransactional(reg prometheus.TransactionalGatherer, opts HandlerO if opts.ErrorLog != nil { opts.ErrorLog.Println("error getting writer", err) } + if opts.StructuredErrorLog != nil { + opts.StructuredErrorLog.Error("error getting writer", "error", err) + } w = io.Writer(rsp) encodingHeader = string(Identity) } @@ -218,6 +224,9 @@ func HandlerForTransactional(reg prometheus.TransactionalGatherer, opts HandlerO if opts.ErrorLog != nil { opts.ErrorLog.Println("error encoding and sending metric family:", err) } + if opts.StructuredErrorLog != nil { + opts.StructuredErrorLog.Error("error encoding and sending metric family", "error", err) + } errCnt.WithLabelValues("encoding").Inc() switch opts.ErrorHandling { case PanicOnError: @@ -344,6 +353,12 @@ type Logger interface { Println(v ...interface{}) } +// StructuredLogger is a minimal interface HandlerOpts needs for structured +// logging. This is implementd by the standard library log/slog.Logger type. +type StructuredLogger interface { + Error(msg string, args ...any) +} + // HandlerOpts specifies options how to serve metrics via an http.Handler. The // zero value of HandlerOpts is a reasonable default. type HandlerOpts struct { @@ -354,6 +369,9 @@ type HandlerOpts struct { // latter, create a Logger implementation that detects a // prometheus.MultiError and formats the contained errors into one line. ErrorLog Logger + // StructuredErrorLog StructuredLogger specifies an optional structured log + // handler. + StructuredErrorLog StructuredLogger // ErrorHandling defines how errors are handled. Note that errors are // logged regardless of the configured ErrorHandling provided ErrorLog // is not nil. diff --git a/prometheus/promhttp/http_test.go b/prometheus/promhttp/http_test.go index 3ad2d1d..35a6bf2 100644 --- a/prometheus/promhttp/http_test.go +++ b/prometheus/promhttp/http_test.go @@ -20,8 +20,10 @@ import ( "fmt" "io" "log" + "log/slog" "net/http" "net/http/httptest" + "os" "strings" "testing" "time" @@ -130,25 +132,30 @@ func TestHandlerErrorHandling(t *testing.T) { logBuf := &bytes.Buffer{} logger := log.New(logBuf, "", 0) + slogger := slog.New(slog.NewTextHandler(os.Stderr, nil)) + writer := httptest.NewRecorder() request, _ := http.NewRequest("GET", "/", nil) request.Header.Add("Accept", "test/plain") mReg := &mockTransactionGatherer{g: reg} errorHandler := HandlerForTransactional(mReg, HandlerOpts{ - ErrorLog: logger, - ErrorHandling: HTTPErrorOnError, - Registry: reg, + ErrorLog: logger, + StructuredErrorLog: slogger, + ErrorHandling: HTTPErrorOnError, + Registry: reg, }) continueHandler := HandlerForTransactional(mReg, HandlerOpts{ - ErrorLog: logger, - ErrorHandling: ContinueOnError, - Registry: reg, + ErrorLog: logger, + StructuredErrorLog: slogger, + ErrorHandling: ContinueOnError, + Registry: reg, }) panicHandler := HandlerForTransactional(mReg, HandlerOpts{ - ErrorLog: logger, - ErrorHandling: PanicOnError, - Registry: reg, + ErrorLog: logger, + StructuredErrorLog: slogger, + ErrorHandling: PanicOnError, + Registry: reg, }) // Expect gatherer not touched. if got := mReg.gatherInvoked; got != 0 {