From 82d7bdc97114b03f54290f8a609cf502258fdf09 Mon Sep 17 00:00:00 2001 From: Ben Edmunds Date: Fri, 6 Jun 2025 03:33:49 +0100 Subject: [PATCH] Feature: Add Prometheus exporter (#505) --- cmd/root.go | 17 +++ config/config.go | 16 +++ go.mod | 8 ++ go.sum | 20 ++++ internal/prometheus/metrics.go | 188 +++++++++++++++++++++++++++++++++ server/server.go | 8 ++ 6 files changed, 257 insertions(+) create mode 100644 internal/prometheus/metrics.go diff --git a/cmd/root.go b/cmd/root.go index b4cbcee..6e7d167 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -9,6 +9,7 @@ import ( "github.com/axllent/mailpit/config" "github.com/axllent/mailpit/internal/auth" "github.com/axllent/mailpit/internal/logger" + "github.com/axllent/mailpit/internal/prometheus" "github.com/axllent/mailpit/internal/smtpd" "github.com/axllent/mailpit/internal/smtpd/chaos" "github.com/axllent/mailpit/internal/storage" @@ -39,6 +40,14 @@ Documentation: os.Exit(1) } + // Start Prometheus metrics if enabled + switch prometheus.GetMode() { + case "integrated": + prometheus.StartUpdater() + case "separate": + go prometheus.StartSeparateServer() + } + go server.Listen() if err := smtpd.Listen(); err != nil { @@ -112,6 +121,9 @@ func init() { rootCmd.Flags().StringVar(&config.SendAPIAuthFile, "send-api-auth-file", config.SendAPIAuthFile, "A password file for Send API authentication") rootCmd.Flags().BoolVar(&config.SendAPIAuthAcceptAny, "send-api-auth-accept-any", config.SendAPIAuthAcceptAny, "Accept any username and password for the Send API endpoint, including none") + // Prometheus Metrics + rootCmd.Flags().StringVar(&config.PrometheusListen, "enable-prometheus", config.PrometheusListen, "Enable Prometheus metrics: false=disabled, 'true'=use web port, address=separate server (':9090')") + // SMTP server rootCmd.Flags().StringVarP(&config.SMTPListen, "smtp", "s", config.SMTPListen, "SMTP bind interface and port") rootCmd.Flags().StringVar(&config.SMTPAuthFile, "smtp-auth-file", config.SMTPAuthFile, "A password file for SMTP authentication") @@ -262,6 +274,11 @@ func initConfigFromEnv() { config.SendAPIAuthAcceptAny = true } + // Prometheus Metrics + if len(os.Getenv("MP_ENABLE_PROMETHEUS")) > 0 { + config.PrometheusListen = os.Getenv("MP_ENABLE_PROMETHEUS") + } + // SMTP server if len(os.Getenv("MP_SMTP_BIND_ADDR")) > 0 { config.SMTPListen = os.Getenv("MP_SMTP_BIND_ADDR") diff --git a/config/config.go b/config/config.go index dfd85ad..a11f69c 100644 --- a/config/config.go +++ b/config/config.go @@ -191,6 +191,10 @@ var ( // AllowUntrustedTLS allows untrusted HTTPS connections link checking & screenshot generation AllowUntrustedTLS bool + // PrometheusListen address for Prometheus metrics server + // Empty = disabled, "true"= use existing web server, address = separate server + PrometheusListen string + // Version is the default application version, updated on release Version = "dev" @@ -567,5 +571,17 @@ func VerifyConfig() error { logger.Log().Info("demo mode enabled") } + // Prometheus configuration validation + if PrometheusListen != "" { + mode := strings.ToLower(strings.TrimSpace(PrometheusListen)) + if mode != "true" && mode != "false" { + // Validate as address for separate server mode + _, err := net.ResolveTCPAddr("tcp", PrometheusListen) + if err != nil { + return fmt.Errorf("[prometheus] %s", err.Error()) + } + } + } + return nil } diff --git a/go.mod b/go.mod index 4377847..51b1387 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/leporo/sqlf v1.4.0 github.com/lithammer/shortuuid/v4 v4.2.0 github.com/mneis/go-telnet v0.0.0-20221017141824-6f643e477c62 + github.com/prometheus/client_golang v1.22.0 github.com/rqlite/gorqlite v0.0.0-20250128004930-114c7828b55a github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.9.1 @@ -36,7 +37,9 @@ require ( require ( github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect + github.com/beorn7/perks v1.0.1 // indirect github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect github.com/google/uuid v1.6.0 // indirect @@ -45,9 +48,13 @@ require ( github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/olekukonko/tablewriter v1.0.6 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/reiver/go-oi v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect @@ -58,6 +65,7 @@ require ( golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect golang.org/x/image v0.27.0 // indirect golang.org/x/sys v0.33.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect modernc.org/libc v1.65.8 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect diff --git a/go.sum b/go.sum index d9b418b..f01626a 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,12 @@ github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhP github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw= github.com/axllent/semver v0.0.1 h1:QqF+KSGxgj8QZzSXAvKFqjGWE5792ksOnQhludToK8E= github.com/axllent/semver v0.0.1/go.mod h1:2xSPzvG8n9mRfdtxSvWvfTfQGWfHsMsHO1iZnKATMSc= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI= github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -25,6 +29,8 @@ github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b h1:EY/KpStFl60qA17CptGXhwfZ+k1sFNJIUNR8DdbcuUk= github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -45,6 +51,8 @@ github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zt github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kovidgoyal/imaging v1.6.4 h1:K0idhRPXnRrJBKnBYcTfI1HTWSNDeAn7hYDvf9I0dCk= github.com/kovidgoyal/imaging v1.6.4/go.mod h1:bEIgsaZmXlvFfkv/CUxr9rJook6AQkJnpB5EPosRfRY= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leporo/sqlf v1.4.0 h1:SyWnX/8GSGOzVmanG0Ub1c04mR9nNl6Tq3IeFKX2/4c= github.com/leporo/sqlf v1.4.0/go.mod h1:pgN9yKsAnQ+2ewhbZogr98RcasUjPsHF3oXwPPhHvBw= github.com/lithammer/shortuuid/v4 v4.2.0 h1:LMFOzVB3996a7b8aBuEXxqOBflbfPQAiVzkIcHO0h8c= @@ -59,6 +67,8 @@ github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwp github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mneis/go-telnet v0.0.0-20221017141824-6f643e477c62 h1:XMG5DklHoioVYysfYglOB7vRBg/LOUJZy2mq2QyedLg= github.com/mneis/go-telnet v0.0.0-20221017141824-6f643e477c62/go.mod h1:niAM5cni0I/47IFA995xQfeK58Mkbb7FHJjacY4OGQg= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= @@ -67,6 +77,14 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/reiver/go-oi v1.0.0 h1:nvECWD7LF+vOs8leNGV/ww+F2iZKf3EYjYZ527turzM= github.com/reiver/go-oi v1.0.0/go.mod h1:RrDBct90BAhoDTxB1fenZwfykqeGvhI6LsNfStJoEkI= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= @@ -190,6 +208,8 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/prometheus/metrics.go b/internal/prometheus/metrics.go new file mode 100644 index 0000000..bd40c06 --- /dev/null +++ b/internal/prometheus/metrics.go @@ -0,0 +1,188 @@ +// Package prometheus provides Prometheus metrics for Mailpit +package prometheus + +import ( + "net/http" + "strings" + "time" + + "github.com/axllent/mailpit/config" + "github.com/axllent/mailpit/internal/logger" + "github.com/axllent/mailpit/internal/stats" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +var ( + // Registry is the Prometheus registry for Mailpit metrics + Registry = prometheus.NewRegistry() + + // Metrics + totalMessages prometheus.Gauge + unreadMessages prometheus.Gauge + databaseSize prometheus.Gauge + messagesDeleted prometheus.Counter + smtpAccepted prometheus.Counter + smtpRejected prometheus.Counter + smtpIgnored prometheus.Counter + smtpAcceptedSize prometheus.Counter + uptime prometheus.Gauge + memoryUsage prometheus.Gauge + tagCounters *prometheus.GaugeVec +) + +// InitMetrics initializes all Prometheus metrics +func InitMetrics() { + // Create metrics + totalMessages = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "mailpit_messages", + Help: "Total number of messages in the database", + }) + + unreadMessages = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "mailpit_messages_unread", + Help: "Number of unread messages in the database", + }) + + databaseSize = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "mailpit_database_size_bytes", + Help: "Size of the database in bytes", + }) + + messagesDeleted = prometheus.NewCounter(prometheus.CounterOpts{ + Name: "mailpit_messages_deleted_total", + Help: "Total number of messages deleted", + }) + + smtpAccepted = prometheus.NewCounter(prometheus.CounterOpts{ + Name: "mailpit_smtp_accepted_total", + Help: "Total number of SMTP messages accepted", + }) + + smtpRejected = prometheus.NewCounter(prometheus.CounterOpts{ + Name: "mailpit_smtp_rejected_total", + Help: "Total number of SMTP messages rejected", + }) + + smtpIgnored = prometheus.NewCounter(prometheus.CounterOpts{ + Name: "mailpit_smtp_ignored_total", + Help: "Total number of SMTP messages ignored (duplicates)", + }) + + smtpAcceptedSize = prometheus.NewCounter(prometheus.CounterOpts{ + Name: "mailpit_smtp_accepted_size_bytes_total", + Help: "Total size of accepted SMTP messages in bytes", + }) + + uptime = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "mailpit_uptime_seconds", + Help: "Uptime of Mailpit in seconds", + }) + + memoryUsage = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "mailpit_memory_usage_bytes", + Help: "Memory usage in bytes", + }) + + tagCounters = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "mailpit_tag_messages", + Help: "Number of messages per tag", + }, + []string{"tag"}, + ) + + // Register metrics + Registry.MustRegister(totalMessages) + Registry.MustRegister(unreadMessages) + Registry.MustRegister(databaseSize) + Registry.MustRegister(messagesDeleted) + Registry.MustRegister(smtpAccepted) + Registry.MustRegister(smtpRejected) + Registry.MustRegister(smtpIgnored) + Registry.MustRegister(smtpAcceptedSize) + Registry.MustRegister(uptime) + Registry.MustRegister(memoryUsage) + Registry.MustRegister(tagCounters) +} + +// UpdateMetrics updates all metrics with current values +func UpdateMetrics() { + info := stats.Load() + + totalMessages.Set(float64(info.Messages)) + unreadMessages.Set(float64(info.Unread)) + databaseSize.Set(float64(info.DatabaseSize)) + messagesDeleted.Add(float64(info.RuntimeStats.MessagesDeleted)) + smtpAccepted.Add(float64(info.RuntimeStats.SMTPAccepted)) + smtpRejected.Add(float64(info.RuntimeStats.SMTPRejected)) + smtpIgnored.Add(float64(info.RuntimeStats.SMTPIgnored)) + smtpAcceptedSize.Add(float64(info.RuntimeStats.SMTPAcceptedSize)) + uptime.Set(float64(info.RuntimeStats.Uptime)) + memoryUsage.Set(float64(info.RuntimeStats.Memory)) + + // Reset tag counters + tagCounters.Reset() + + // Update tag counters + for tag, count := range info.Tags { + tagCounters.WithLabelValues(tag).Set(float64(count)) + } +} + +// Returns the Prometheus handler & disables double compression in middleware +func GetHandler() http.Handler { + return promhttp.HandlerFor(Registry, promhttp.HandlerOpts{ + DisableCompression: true, + }) +} + +// StartUpdater starts the periodic metrics update routine +func StartUpdater() { + InitMetrics() + UpdateMetrics() + + // Start periodic updates + go func() { + ticker := time.NewTicker(15 * time.Second) + defer ticker.Stop() + + for range ticker.C { + UpdateMetrics() + } + }() +} + +// StartSeparateServer starts a separate HTTP server for Prometheus metrics +func StartSeparateServer() { + StartUpdater() + + logger.Log().Infof("[prometheus] metrics server listening on %s", config.PrometheusListen) + + // Create a dedicated mux for the metrics server + mux := http.NewServeMux() + mux.Handle("/metrics", promhttp.HandlerFor(Registry, promhttp.HandlerOpts{})) + + // Create a dedicated server instance + server := &http.Server{ + Addr: config.PrometheusListen, + Handler: mux, + } + + // Start HTTP server + if err := server.ListenAndServe(); err != nil { + logger.Log().Errorf("[prometheus] metrics server error: %s", err.Error()) + } +} + +// GetMode returns the Prometheus run mode +func GetMode() string { + mode := strings.ToLower(strings.TrimSpace(config.PrometheusListen)) + if mode == "false" { + return "disabled" + } + if mode == "true" { + return "integrated" + } + return "separate" +} diff --git a/server/server.go b/server/server.go index 45458d3..d2c2b84 100644 --- a/server/server.go +++ b/server/server.go @@ -19,6 +19,7 @@ import ( "github.com/axllent/mailpit/internal/auth" "github.com/axllent/mailpit/internal/logger" "github.com/axllent/mailpit/internal/pop3" + "github.com/axllent/mailpit/internal/prometheus" "github.com/axllent/mailpit/internal/stats" "github.com/axllent/mailpit/internal/storage" "github.com/axllent/mailpit/internal/tools" @@ -182,6 +183,13 @@ func apiRoutes() *mux.Router { r.HandleFunc(config.Webroot+"api/v1/chaos", middleWareFunc(apiv1.GetChaos)).Methods("GET") r.HandleFunc(config.Webroot+"api/v1/chaos", middleWareFunc(apiv1.SetChaos)).Methods("PUT") + // Prometheus metrics (if enabled and using existing server) + if prometheus.GetMode() == "integrated" { + r.HandleFunc(config.Webroot+"metrics", middleWareFunc(func(w http.ResponseWriter, r *http.Request) { + prometheus.GetHandler().ServeHTTP(w, r) + })).Methods("GET") + } + // web UI websocket r.HandleFunc(config.Webroot+"api/events", apiWebsocket).Methods("GET")