Add structured logging support to promhttp

In order to better support the standard library `log/slog` add a
new interface to the `promhttp` `HandlerOpts`.

Signed-off-by: SuperQ <superq@gmail.com>
This commit is contained in:
SuperQ 2024-08-30 15:41:06 +02:00
parent 97aa0493eb
commit c65233230d
No known key found for this signature in database
GPG Key ID: C646B23C9E3245F1
2 changed files with 34 additions and 9 deletions

View File

@ -168,6 +168,9 @@ func HandlerForTransactional(reg prometheus.TransactionalGatherer, opts HandlerO
if opts.ErrorLog != nil { if opts.ErrorLog != nil {
opts.ErrorLog.Println("error gathering metrics:", err) opts.ErrorLog.Println("error gathering metrics:", err)
} }
if opts.StructuredErrorLog != nil {
opts.StructuredErrorLog.Error("error gathering metrics", "error", err)
}
errCnt.WithLabelValues("gathering").Inc() errCnt.WithLabelValues("gathering").Inc()
switch opts.ErrorHandling { switch opts.ErrorHandling {
case PanicOnError: case PanicOnError:
@ -197,6 +200,9 @@ func HandlerForTransactional(reg prometheus.TransactionalGatherer, opts HandlerO
if opts.ErrorLog != nil { if opts.ErrorLog != nil {
opts.ErrorLog.Println("error getting writer", err) opts.ErrorLog.Println("error getting writer", err)
} }
if opts.StructuredErrorLog != nil {
opts.StructuredErrorLog.Error("error getting writer", "error", err)
}
w = io.Writer(rsp) w = io.Writer(rsp)
encodingHeader = string(Identity) encodingHeader = string(Identity)
} }
@ -218,6 +224,9 @@ func HandlerForTransactional(reg prometheus.TransactionalGatherer, opts HandlerO
if opts.ErrorLog != nil { if opts.ErrorLog != nil {
opts.ErrorLog.Println("error encoding and sending metric family:", err) 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() errCnt.WithLabelValues("encoding").Inc()
switch opts.ErrorHandling { switch opts.ErrorHandling {
case PanicOnError: case PanicOnError:
@ -344,6 +353,12 @@ type Logger interface {
Println(v ...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 // HandlerOpts specifies options how to serve metrics via an http.Handler. The
// zero value of HandlerOpts is a reasonable default. // zero value of HandlerOpts is a reasonable default.
type HandlerOpts struct { type HandlerOpts struct {
@ -354,6 +369,9 @@ type HandlerOpts struct {
// latter, create a Logger implementation that detects a // latter, create a Logger implementation that detects a
// prometheus.MultiError and formats the contained errors into one line. // prometheus.MultiError and formats the contained errors into one line.
ErrorLog Logger ErrorLog Logger
// StructuredErrorLog StructuredLogger specifies an optional structured log
// handler.
StructuredErrorLog StructuredLogger
// ErrorHandling defines how errors are handled. Note that errors are // ErrorHandling defines how errors are handled. Note that errors are
// logged regardless of the configured ErrorHandling provided ErrorLog // logged regardless of the configured ErrorHandling provided ErrorLog
// is not nil. // is not nil.

View File

@ -20,8 +20,10 @@ import (
"fmt" "fmt"
"io" "io"
"log" "log"
"log/slog"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os"
"strings" "strings"
"testing" "testing"
"time" "time"
@ -130,25 +132,30 @@ func TestHandlerErrorHandling(t *testing.T) {
logBuf := &bytes.Buffer{} logBuf := &bytes.Buffer{}
logger := log.New(logBuf, "", 0) logger := log.New(logBuf, "", 0)
slogger := slog.New(slog.NewTextHandler(os.Stderr, nil))
writer := httptest.NewRecorder() writer := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "/", nil) request, _ := http.NewRequest("GET", "/", nil)
request.Header.Add("Accept", "test/plain") request.Header.Add("Accept", "test/plain")
mReg := &mockTransactionGatherer{g: reg} mReg := &mockTransactionGatherer{g: reg}
errorHandler := HandlerForTransactional(mReg, HandlerOpts{ errorHandler := HandlerForTransactional(mReg, HandlerOpts{
ErrorLog: logger, ErrorLog: logger,
ErrorHandling: HTTPErrorOnError, StructuredErrorLog: slogger,
Registry: reg, ErrorHandling: HTTPErrorOnError,
Registry: reg,
}) })
continueHandler := HandlerForTransactional(mReg, HandlerOpts{ continueHandler := HandlerForTransactional(mReg, HandlerOpts{
ErrorLog: logger, ErrorLog: logger,
ErrorHandling: ContinueOnError, StructuredErrorLog: slogger,
Registry: reg, ErrorHandling: ContinueOnError,
Registry: reg,
}) })
panicHandler := HandlerForTransactional(mReg, HandlerOpts{ panicHandler := HandlerForTransactional(mReg, HandlerOpts{
ErrorLog: logger, ErrorLog: logger,
ErrorHandling: PanicOnError, StructuredErrorLog: slogger,
Registry: reg, ErrorHandling: PanicOnError,
Registry: reg,
}) })
// Expect gatherer not touched. // Expect gatherer not touched.
if got := mReg.gatherInvoked; got != 0 { if got := mReg.gatherInvoked; got != 0 {