1
0
mirror of https://github.com/axllent/mailpit.git synced 2025-08-13 20:04:49 +02:00

Feature: Add Prometheus exporter (#505)

This commit is contained in:
Ben Edmunds
2025-06-06 03:33:49 +01:00
committed by GitHub
parent 020d5b0fcb
commit 82d7bdc971
6 changed files with 257 additions and 0 deletions

View File

@@ -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")

View File

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

8
go.mod
View File

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

20
go.sum
View File

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

View File

@@ -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"
}

View File

@@ -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")