mirror of
https://github.com/axllent/mailpit.git
synced 2026-06-28 06:56:06 +00:00
219 lines
5.7 KiB
Go
219 lines
5.7 KiB
Go
// Package prometheus provides Prometheus metrics for Mailpit
|
|
package prometheus
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/axllent/mailpit/config"
|
|
"github.com/axllent/mailpit/internal/logger"
|
|
"github.com/axllent/mailpit/internal/stats"
|
|
)
|
|
|
|
type gauge struct {
|
|
mu sync.RWMutex
|
|
val float64
|
|
}
|
|
|
|
func (g *gauge) Set(v float64) {
|
|
g.mu.Lock()
|
|
g.val = v
|
|
g.mu.Unlock()
|
|
}
|
|
|
|
func (g *gauge) get() float64 {
|
|
g.mu.RLock()
|
|
defer g.mu.RUnlock()
|
|
return g.val
|
|
}
|
|
|
|
type gaugeVec struct {
|
|
mu sync.RWMutex
|
|
label string
|
|
vals map[string]float64
|
|
}
|
|
|
|
func newGaugeVec(label string) *gaugeVec {
|
|
return &gaugeVec{label: label, vals: make(map[string]float64)}
|
|
}
|
|
|
|
func (v *gaugeVec) Set(labelVal string, val float64) {
|
|
v.mu.Lock()
|
|
v.vals[labelVal] = val
|
|
v.mu.Unlock()
|
|
}
|
|
|
|
func (v *gaugeVec) Reset() {
|
|
v.mu.Lock()
|
|
v.vals = make(map[string]float64)
|
|
v.mu.Unlock()
|
|
}
|
|
|
|
type entry struct {
|
|
name string
|
|
help string
|
|
typ string
|
|
g *gauge
|
|
vec *gaugeVec
|
|
}
|
|
|
|
var (
|
|
regMu sync.RWMutex
|
|
registry []entry
|
|
|
|
totalMessages = &gauge{}
|
|
unreadMessages = &gauge{}
|
|
databaseSize = &gauge{}
|
|
messagesDeleted = &gauge{}
|
|
smtpAccepted = &gauge{}
|
|
smtpRejected = &gauge{}
|
|
smtpIgnored = &gauge{}
|
|
smtpAcceptedSize = &gauge{}
|
|
uptime = &gauge{}
|
|
memoryUsage = &gauge{}
|
|
tagCounters = newGaugeVec("tag")
|
|
)
|
|
|
|
func register(name, help, typ string, g *gauge, vec *gaugeVec) {
|
|
regMu.Lock()
|
|
registry = append(registry, entry{name: name, help: help, typ: typ, g: g, vec: vec})
|
|
regMu.Unlock()
|
|
}
|
|
|
|
func initMetrics() {
|
|
register("mailpit_database_size_bytes", "Size of the database in bytes", "gauge", databaseSize, nil)
|
|
register("mailpit_memory_usage_bytes", "Memory usage in bytes", "gauge", memoryUsage, nil)
|
|
register("mailpit_messages", "Total number of messages in the database", "gauge", totalMessages, nil)
|
|
register("mailpit_messages_deleted_total", "Total number of messages deleted", "counter", messagesDeleted, nil)
|
|
register("mailpit_messages_unread", "Number of unread messages in the database", "gauge", unreadMessages, nil)
|
|
register("mailpit_smtp_accepted_size_bytes_total", "Total size of accepted SMTP messages in bytes", "counter", smtpAcceptedSize, nil)
|
|
register("mailpit_smtp_accepted_total", "Total number of SMTP messages accepted", "counter", smtpAccepted, nil)
|
|
register("mailpit_smtp_ignored_total", "Total number of SMTP messages ignored (duplicates)", "counter", smtpIgnored, nil)
|
|
register("mailpit_smtp_rejected_total", "Total number of SMTP messages rejected", "counter", smtpRejected, nil)
|
|
register("mailpit_tag_messages", "Number of messages per tag", "gauge", nil, tagCounters)
|
|
register("mailpit_uptime_seconds", "Uptime of Mailpit in seconds", "gauge", uptime, nil)
|
|
}
|
|
|
|
func updateMetrics() {
|
|
info := stats.Load(false)
|
|
|
|
totalMessages.Set(float64(info.Messages))
|
|
unreadMessages.Set(float64(info.Unread))
|
|
databaseSize.Set(float64(info.DatabaseSize))
|
|
messagesDeleted.Set(float64(info.RuntimeStats.MessagesDeleted))
|
|
smtpAccepted.Set(float64(info.RuntimeStats.SMTPAccepted))
|
|
smtpRejected.Set(float64(info.RuntimeStats.SMTPRejected))
|
|
smtpIgnored.Set(float64(info.RuntimeStats.SMTPIgnored))
|
|
smtpAcceptedSize.Set(float64(info.RuntimeStats.SMTPAcceptedSize))
|
|
uptime.Set(float64(info.RuntimeStats.Uptime))
|
|
memoryUsage.Set(float64(info.RuntimeStats.Memory))
|
|
|
|
tagCounters.Reset()
|
|
for tag, count := range info.Tags {
|
|
tagCounters.Set(tag, float64(count))
|
|
}
|
|
}
|
|
|
|
func writeMetrics(w io.Writer) {
|
|
regMu.RLock()
|
|
entries := make([]entry, len(registry))
|
|
copy(entries, registry)
|
|
regMu.RUnlock()
|
|
|
|
sort.Slice(entries, func(i, j int) bool {
|
|
return entries[i].name < entries[j].name
|
|
})
|
|
|
|
for _, e := range entries {
|
|
fmt.Fprintf(w, "# HELP %s %s\n# TYPE %s %s\n", e.name, e.help, e.name, e.typ)
|
|
if e.g != nil {
|
|
fmt.Fprintf(w, "%s %s\n", e.name, formatFloat(e.g.get()))
|
|
} else {
|
|
e.vec.mu.RLock()
|
|
keys := make([]string, 0, len(e.vec.vals))
|
|
snapshot := make(map[string]float64, len(e.vec.vals))
|
|
for k, v := range e.vec.vals {
|
|
keys = append(keys, k)
|
|
snapshot[k] = v
|
|
}
|
|
e.vec.mu.RUnlock()
|
|
sort.Strings(keys)
|
|
for _, k := range keys {
|
|
fmt.Fprintf(w, "%s{%s=\"%s\"} %s\n", e.name, e.vec.label, escapeLabelValue(k), formatFloat(snapshot[k]))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func escapeLabelValue(s string) string {
|
|
s = strings.ReplaceAll(s, `\`, `\\`)
|
|
s = strings.ReplaceAll(s, "\n", `\n`)
|
|
s = strings.ReplaceAll(s, `"`, `\"`)
|
|
return s
|
|
}
|
|
|
|
func formatFloat(v float64) string {
|
|
return strconv.FormatFloat(v, 'g', -1, 64)
|
|
}
|
|
|
|
// GetHandler returns the Prometheus metrics HTTP handler
|
|
func GetHandler() http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/plain; version=0.0.4; charset=utf-8")
|
|
writeMetrics(w)
|
|
})
|
|
}
|
|
|
|
// StartUpdater starts the periodic metrics update routine
|
|
func StartUpdater() {
|
|
initMetrics()
|
|
updateMetrics()
|
|
|
|
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)
|
|
|
|
mux := http.NewServeMux()
|
|
mux.Handle("/metrics", GetHandler())
|
|
|
|
server := &http.Server{
|
|
Addr: config.PrometheusListen,
|
|
Handler: mux,
|
|
ReadHeaderTimeout: 5 * time.Second,
|
|
}
|
|
|
|
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))
|
|
switch mode {
|
|
case "false", "":
|
|
return "disabled"
|
|
case "true":
|
|
return "integrated"
|
|
default:
|
|
return "separate"
|
|
}
|
|
}
|