Skip to content

Commit 7408c25

Browse files
committed
Merge branch 'feature/prometheus' into develop
2 parents 020d5b0 + a6b5f5f commit 7408c25

File tree

6 files changed

+259
-0
lines changed

6 files changed

+259
-0
lines changed

cmd/root.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/axllent/mailpit/config"
1010
"github.com/axllent/mailpit/internal/auth"
1111
"github.com/axllent/mailpit/internal/logger"
12+
"github.com/axllent/mailpit/internal/prometheus"
1213
"github.com/axllent/mailpit/internal/smtpd"
1314
"github.com/axllent/mailpit/internal/smtpd/chaos"
1415
"github.com/axllent/mailpit/internal/storage"
@@ -39,6 +40,14 @@ Documentation:
3940
os.Exit(1)
4041
}
4142

43+
// Start Prometheus metrics if enabled
44+
switch prometheus.GetMode() {
45+
case "integrated":
46+
prometheus.StartUpdater()
47+
case "separate":
48+
go prometheus.StartSeparateServer()
49+
}
50+
4251
go server.Listen()
4352

4453
if err := smtpd.Listen(); err != nil {
@@ -112,6 +121,9 @@ func init() {
112121
rootCmd.Flags().StringVar(&config.SendAPIAuthFile, "send-api-auth-file", config.SendAPIAuthFile, "A password file for Send API authentication")
113122
rootCmd.Flags().BoolVar(&config.SendAPIAuthAcceptAny, "send-api-auth-accept-any", config.SendAPIAuthAcceptAny, "Accept any username and password for the Send API endpoint, including none")
114123

124+
// Prometheus metrics
125+
rootCmd.Flags().StringVar(&config.PrometheusListen, "enable-prometheus", config.PrometheusListen, "Enable Prometheus metrics: true|false|<bind interface & port> (eg:':9090')")
126+
115127
// SMTP server
116128
rootCmd.Flags().StringVarP(&config.SMTPListen, "smtp", "s", config.SMTPListen, "SMTP bind interface and port")
117129
rootCmd.Flags().StringVar(&config.SMTPAuthFile, "smtp-auth-file", config.SMTPAuthFile, "A password file for SMTP authentication")
@@ -262,6 +274,11 @@ func initConfigFromEnv() {
262274
config.SendAPIAuthAcceptAny = true
263275
}
264276

277+
// Prometheus Metrics
278+
if len(os.Getenv("MP_ENABLE_PROMETHEUS")) > 0 {
279+
config.PrometheusListen = os.Getenv("MP_ENABLE_PROMETHEUS")
280+
}
281+
265282
// SMTP server
266283
if len(os.Getenv("MP_SMTP_BIND_ADDR")) > 0 {
267284
config.SMTPListen = os.Getenv("MP_SMTP_BIND_ADDR")

config/config.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,10 @@ var (
191191
// AllowUntrustedTLS allows untrusted HTTPS connections link checking & screenshot generation
192192
AllowUntrustedTLS bool
193193

194+
// PrometheusListen address for Prometheus metrics server
195+
// Empty = disabled, true= use existing web server, address = separate server
196+
PrometheusListen string
197+
194198
// Version is the default application version, updated on release
195199
Version = "dev"
196200

@@ -358,6 +362,20 @@ func VerifyConfig() error {
358362
logger.Log().Info("[send-api] disabling authentication")
359363
}
360364

365+
// Prometheus configuration validation
366+
if PrometheusListen != "" {
367+
mode := strings.ToLower(strings.TrimSpace(PrometheusListen))
368+
if mode != "true" && mode != "false" {
369+
// Validate as address for separate server mode
370+
_, err := net.ResolveTCPAddr("tcp", PrometheusListen)
371+
if err != nil {
372+
return fmt.Errorf("[prometheus] %s", err.Error())
373+
}
374+
} else if mode == "true" {
375+
logger.Log().Info("[prometheus] enabling metrics")
376+
}
377+
}
378+
361379
// SMTP server
362380
if SMTPTLSCert != "" && SMTPTLSKey == "" || SMTPTLSCert == "" && SMTPTLSKey != "" {
363381
return errors.New("[smtp] you must provide both an SMTP TLS certificate and a key")

go.mod

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ require (
2121
github.com/leporo/sqlf v1.4.0
2222
github.com/lithammer/shortuuid/v4 v4.2.0
2323
github.com/mneis/go-telnet v0.0.0-20221017141824-6f643e477c62
24+
github.com/prometheus/client_golang v1.22.0
2425
github.com/rqlite/gorqlite v0.0.0-20250128004930-114c7828b55a
2526
github.com/sirupsen/logrus v1.9.3
2627
github.com/spf13/cobra v1.9.1
@@ -36,7 +37,9 @@ require (
3637
require (
3738
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 // indirect
3839
github.com/andybalholm/cascadia v1.3.3 // indirect
40+
github.com/beorn7/perks v1.0.1 // indirect
3941
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
42+
github.com/cespare/xxhash/v2 v2.3.0 // indirect
4043
github.com/dustin/go-humanize v1.0.1 // indirect
4144
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect
4245
github.com/google/uuid v1.6.0 // indirect
@@ -45,9 +48,13 @@ require (
4548
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 // indirect
4649
github.com/mattn/go-isatty v0.0.20 // indirect
4750
github.com/mattn/go-runewidth v0.0.16 // indirect
51+
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
4852
github.com/ncruces/go-strftime v0.1.9 // indirect
4953
github.com/olekukonko/tablewriter v1.0.6 // indirect
5054
github.com/pkg/errors v0.9.1 // indirect
55+
github.com/prometheus/client_model v0.6.1 // indirect
56+
github.com/prometheus/common v0.62.0 // indirect
57+
github.com/prometheus/procfs v0.15.1 // indirect
5158
github.com/reiver/go-oi v1.0.0 // indirect
5259
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
5360
github.com/rivo/uniseg v0.4.7 // indirect
@@ -58,6 +65,7 @@ require (
5865
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
5966
golang.org/x/image v0.27.0 // indirect
6067
golang.org/x/sys v0.33.0 // indirect
68+
google.golang.org/protobuf v1.36.5 // indirect
6169
modernc.org/libc v1.65.8 // indirect
6270
modernc.org/mathutil v1.7.1 // indirect
6371
modernc.org/memory v1.11.0 // indirect

go.sum

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@ github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhP
88
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
99
github.com/axllent/semver v0.0.1 h1:QqF+KSGxgj8QZzSXAvKFqjGWE5792ksOnQhludToK8E=
1010
github.com/axllent/semver v0.0.1/go.mod h1:2xSPzvG8n9mRfdtxSvWvfTfQGWfHsMsHO1iZnKATMSc=
11+
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
12+
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
1113
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
1214
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
15+
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
16+
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
1317
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
1418
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1519
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
2529
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b h1:EY/KpStFl60qA17CptGXhwfZ+k1sFNJIUNR8DdbcuUk=
2630
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
2731
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
32+
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
33+
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
2834
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
2935
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
3036
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
4551
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
4652
github.com/kovidgoyal/imaging v1.6.4 h1:K0idhRPXnRrJBKnBYcTfI1HTWSNDeAn7hYDvf9I0dCk=
4753
github.com/kovidgoyal/imaging v1.6.4/go.mod h1:bEIgsaZmXlvFfkv/CUxr9rJook6AQkJnpB5EPosRfRY=
54+
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
55+
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
4856
github.com/leporo/sqlf v1.4.0 h1:SyWnX/8GSGOzVmanG0Ub1c04mR9nNl6Tq3IeFKX2/4c=
4957
github.com/leporo/sqlf v1.4.0/go.mod h1:pgN9yKsAnQ+2ewhbZogr98RcasUjPsHF3oXwPPhHvBw=
5058
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
5967
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
6068
github.com/mneis/go-telnet v0.0.0-20221017141824-6f643e477c62 h1:XMG5DklHoioVYysfYglOB7vRBg/LOUJZy2mq2QyedLg=
6169
github.com/mneis/go-telnet v0.0.0-20221017141824-6f643e477c62/go.mod h1:niAM5cni0I/47IFA995xQfeK58Mkbb7FHJjacY4OGQg=
70+
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
71+
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
6272
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
6373
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
6474
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
@@ -67,6 +77,14 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
6777
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
6878
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
6979
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
80+
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
81+
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
82+
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
83+
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
84+
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
85+
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
86+
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
87+
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
7088
github.com/reiver/go-oi v1.0.0 h1:nvECWD7LF+vOs8leNGV/ww+F2iZKf3EYjYZ527turzM=
7189
github.com/reiver/go-oi v1.0.0/go.mod h1:RrDBct90BAhoDTxB1fenZwfykqeGvhI6LsNfStJoEkI=
7290
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
190208
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
191209
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
192210
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
211+
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
212+
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
193213
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
194214
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
195215
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

internal/prometheus/metrics.go

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
// Package prometheus provides Prometheus metrics for Mailpit
2+
package prometheus
3+
4+
import (
5+
"net/http"
6+
"strings"
7+
"time"
8+
9+
"github.com/axllent/mailpit/config"
10+
"github.com/axllent/mailpit/internal/logger"
11+
"github.com/axllent/mailpit/internal/stats"
12+
"github.com/prometheus/client_golang/prometheus"
13+
"github.com/prometheus/client_golang/prometheus/promhttp"
14+
)
15+
16+
var (
17+
// Registry is the Prometheus registry for Mailpit metrics
18+
Registry = prometheus.NewRegistry()
19+
20+
// Metrics
21+
totalMessages prometheus.Gauge
22+
unreadMessages prometheus.Gauge
23+
databaseSize prometheus.Gauge
24+
messagesDeleted prometheus.Counter
25+
smtpAccepted prometheus.Counter
26+
smtpRejected prometheus.Counter
27+
smtpIgnored prometheus.Counter
28+
smtpAcceptedSize prometheus.Counter
29+
uptime prometheus.Gauge
30+
memoryUsage prometheus.Gauge
31+
tagCounters *prometheus.GaugeVec
32+
)
33+
34+
// InitMetrics initializes all Prometheus metrics
35+
func InitMetrics() {
36+
// Create metrics
37+
totalMessages = prometheus.NewGauge(prometheus.GaugeOpts{
38+
Name: "mailpit_messages",
39+
Help: "Total number of messages in the database",
40+
})
41+
42+
unreadMessages = prometheus.NewGauge(prometheus.GaugeOpts{
43+
Name: "mailpit_messages_unread",
44+
Help: "Number of unread messages in the database",
45+
})
46+
47+
databaseSize = prometheus.NewGauge(prometheus.GaugeOpts{
48+
Name: "mailpit_database_size_bytes",
49+
Help: "Size of the database in bytes",
50+
})
51+
52+
messagesDeleted = prometheus.NewCounter(prometheus.CounterOpts{
53+
Name: "mailpit_messages_deleted_total",
54+
Help: "Total number of messages deleted",
55+
})
56+
57+
smtpAccepted = prometheus.NewCounter(prometheus.CounterOpts{
58+
Name: "mailpit_smtp_accepted_total",
59+
Help: "Total number of SMTP messages accepted",
60+
})
61+
62+
smtpRejected = prometheus.NewCounter(prometheus.CounterOpts{
63+
Name: "mailpit_smtp_rejected_total",
64+
Help: "Total number of SMTP messages rejected",
65+
})
66+
67+
smtpIgnored = prometheus.NewCounter(prometheus.CounterOpts{
68+
Name: "mailpit_smtp_ignored_total",
69+
Help: "Total number of SMTP messages ignored (duplicates)",
70+
})
71+
72+
smtpAcceptedSize = prometheus.NewCounter(prometheus.CounterOpts{
73+
Name: "mailpit_smtp_accepted_size_bytes_total",
74+
Help: "Total size of accepted SMTP messages in bytes",
75+
})
76+
77+
uptime = prometheus.NewGauge(prometheus.GaugeOpts{
78+
Name: "mailpit_uptime_seconds",
79+
Help: "Uptime of Mailpit in seconds",
80+
})
81+
82+
memoryUsage = prometheus.NewGauge(prometheus.GaugeOpts{
83+
Name: "mailpit_memory_usage_bytes",
84+
Help: "Memory usage in bytes",
85+
})
86+
87+
tagCounters = prometheus.NewGaugeVec(
88+
prometheus.GaugeOpts{
89+
Name: "mailpit_tag_messages",
90+
Help: "Number of messages per tag",
91+
},
92+
[]string{"tag"},
93+
)
94+
95+
// Register metrics
96+
Registry.MustRegister(totalMessages)
97+
Registry.MustRegister(unreadMessages)
98+
Registry.MustRegister(databaseSize)
99+
Registry.MustRegister(messagesDeleted)
100+
Registry.MustRegister(smtpAccepted)
101+
Registry.MustRegister(smtpRejected)
102+
Registry.MustRegister(smtpIgnored)
103+
Registry.MustRegister(smtpAcceptedSize)
104+
Registry.MustRegister(uptime)
105+
Registry.MustRegister(memoryUsage)
106+
Registry.MustRegister(tagCounters)
107+
}
108+
109+
// UpdateMetrics updates all metrics with current values
110+
func UpdateMetrics() {
111+
info := stats.Load()
112+
113+
totalMessages.Set(float64(info.Messages))
114+
unreadMessages.Set(float64(info.Unread))
115+
databaseSize.Set(float64(info.DatabaseSize))
116+
messagesDeleted.Add(float64(info.RuntimeStats.MessagesDeleted))
117+
smtpAccepted.Add(float64(info.RuntimeStats.SMTPAccepted))
118+
smtpRejected.Add(float64(info.RuntimeStats.SMTPRejected))
119+
smtpIgnored.Add(float64(info.RuntimeStats.SMTPIgnored))
120+
smtpAcceptedSize.Add(float64(info.RuntimeStats.SMTPAcceptedSize))
121+
uptime.Set(float64(info.RuntimeStats.Uptime))
122+
memoryUsage.Set(float64(info.RuntimeStats.Memory))
123+
124+
// Reset tag counters
125+
tagCounters.Reset()
126+
127+
// Update tag counters
128+
for tag, count := range info.Tags {
129+
tagCounters.WithLabelValues(tag).Set(float64(count))
130+
}
131+
}
132+
133+
// Returns the Prometheus handler & disables double compression in middleware
134+
func GetHandler() http.Handler {
135+
return promhttp.HandlerFor(Registry, promhttp.HandlerOpts{
136+
DisableCompression: true,
137+
})
138+
}
139+
140+
// StartUpdater starts the periodic metrics update routine
141+
func StartUpdater() {
142+
InitMetrics()
143+
UpdateMetrics()
144+
145+
// Start periodic updates
146+
go func() {
147+
ticker := time.NewTicker(15 * time.Second)
148+
defer ticker.Stop()
149+
150+
for range ticker.C {
151+
UpdateMetrics()
152+
}
153+
}()
154+
}
155+
156+
// StartSeparateServer starts a separate HTTP server for Prometheus metrics
157+
func StartSeparateServer() {
158+
StartUpdater()
159+
160+
logger.Log().Infof("[prometheus] metrics server listening on %s", config.PrometheusListen)
161+
162+
// Create a dedicated mux for the metrics server
163+
mux := http.NewServeMux()
164+
mux.Handle("/metrics", promhttp.HandlerFor(Registry, promhttp.HandlerOpts{}))
165+
166+
// Create a dedicated server instance
167+
server := &http.Server{
168+
Addr: config.PrometheusListen,
169+
Handler: mux,
170+
}
171+
172+
// Start HTTP server
173+
if err := server.ListenAndServe(); err != nil {
174+
logger.Log().Errorf("[prometheus] metrics server error: %s", err.Error())
175+
}
176+
}
177+
178+
// GetMode returns the Prometheus run mode
179+
func GetMode() string {
180+
mode := strings.ToLower(strings.TrimSpace(config.PrometheusListen))
181+
if mode == "false" {
182+
return "disabled"
183+
}
184+
if mode == "true" {
185+
return "integrated"
186+
}
187+
return "separate"
188+
}

server/server.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/axllent/mailpit/internal/auth"
2020
"github.com/axllent/mailpit/internal/logger"
2121
"github.com/axllent/mailpit/internal/pop3"
22+
"github.com/axllent/mailpit/internal/prometheus"
2223
"github.com/axllent/mailpit/internal/stats"
2324
"github.com/axllent/mailpit/internal/storage"
2425
"github.com/axllent/mailpit/internal/tools"
@@ -182,6 +183,13 @@ func apiRoutes() *mux.Router {
182183
r.HandleFunc(config.Webroot+"api/v1/chaos", middleWareFunc(apiv1.GetChaos)).Methods("GET")
183184
r.HandleFunc(config.Webroot+"api/v1/chaos", middleWareFunc(apiv1.SetChaos)).Methods("PUT")
184185

186+
// Prometheus metrics (if enabled and using existing server)
187+
if prometheus.GetMode() == "integrated" {
188+
r.HandleFunc(config.Webroot+"metrics", middleWareFunc(func(w http.ResponseWriter, r *http.Request) {
189+
prometheus.GetHandler().ServeHTTP(w, r)
190+
})).Methods("GET")
191+
}
192+
185193
// web UI websocket
186194
r.HandleFunc(config.Webroot+"api/events", apiWebsocket).Methods("GET")
187195

0 commit comments

Comments
 (0)