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>
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 @@
 			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 @@
 			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 @@
 			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 @@
 	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 @@
 	// 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 @@
 	"fmt"
 	"io"
 	"log"
+	"log/slog"
 	"net/http"
 	"net/http/httptest"
+	"os"
 	"strings"
 	"testing"
 	"time"
@@ -130,25 +132,30 @@
 	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 {