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 {
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.

View File

@ -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 {