Files
frankenphp/metrics.go
2024-10-23 22:33:58 +02:00

335 lines
8.5 KiB
Go

package frankenphp
import (
"path/filepath"
"regexp"
"sync"
"time"
"github.com/prometheus/client_golang/prometheus"
)
var metricsNameRegex = regexp.MustCompile(`\W+`)
var metricsNameFixRegex = regexp.MustCompile(`^_+|_+$`)
const (
StopReasonCrash = iota
StopReasonRestart
StopReasonShutdown
)
type StopReason int
type Metrics interface {
// StartWorker collects started workers
StartWorker(name string)
// ReadyWorker collects ready workers
ReadyWorker(name string)
// StopWorker collects stopped workers
StopWorker(name string, reason StopReason)
// TotalWorkers collects expected workers
TotalWorkers(name string, num int)
// TotalThreads collects total threads
TotalThreads(num int)
// StartRequest collects started requests
StartRequest()
// StopRequest collects stopped requests
StopRequest()
// StopWorkerRequest collects stopped worker requests
StopWorkerRequest(name string, duration time.Duration)
// StartWorkerRequest collects started worker requests
StartWorkerRequest(name string)
Shutdown()
}
type nullMetrics struct{}
func (n nullMetrics) StartWorker(string) {
}
func (n nullMetrics) ReadyWorker(string) {
}
func (n nullMetrics) StopWorker(string, StopReason) {
}
func (n nullMetrics) TotalWorkers(string, int) {
}
func (n nullMetrics) TotalThreads(int) {
}
func (n nullMetrics) StartRequest() {
}
func (n nullMetrics) StopRequest() {
}
func (n nullMetrics) StopWorkerRequest(string, time.Duration) {
}
func (n nullMetrics) StartWorkerRequest(string) {
}
func (n nullMetrics) Shutdown() {
}
type PrometheusMetrics struct {
registry prometheus.Registerer
totalThreads prometheus.Counter
busyThreads prometheus.Gauge
totalWorkers map[string]prometheus.Gauge
busyWorkers map[string]prometheus.Gauge
readyWorkers map[string]prometheus.Gauge
workerCrashes map[string]prometheus.Counter
workerRestarts map[string]prometheus.Counter
workerRequestTime map[string]prometheus.Counter
workerRequestCount map[string]prometheus.Counter
mu sync.Mutex
}
func (m *PrometheusMetrics) StartWorker(name string) {
m.busyThreads.Inc()
// tests do not register workers before starting them
if _, ok := m.totalWorkers[name]; !ok {
return
}
m.totalWorkers[name].Inc()
}
func (m *PrometheusMetrics) ReadyWorker(name string) {
if _, ok := m.totalWorkers[name]; !ok {
return
}
m.readyWorkers[name].Inc()
}
func (m *PrometheusMetrics) StopWorker(name string, reason StopReason) {
m.busyThreads.Dec()
// tests do not register workers before starting them
if _, ok := m.totalWorkers[name]; !ok {
return
}
m.totalWorkers[name].Dec()
m.readyWorkers[name].Dec()
if reason == StopReasonCrash {
m.workerCrashes[name].Inc()
} else if reason == StopReasonRestart {
m.workerRestarts[name].Inc()
} else if reason == StopReasonShutdown {
m.totalWorkers[name].Dec()
}
}
func (m *PrometheusMetrics) getIdentity(name string) (string, error) {
actualName, err := filepath.Abs(name)
if err != nil {
return name, err
}
return actualName, nil
}
func (m *PrometheusMetrics) TotalWorkers(name string, _ int) {
m.mu.Lock()
defer m.mu.Unlock()
identity, err := m.getIdentity(name)
if err != nil {
// do not create metrics, let error propagate when worker is started
return
}
subsystem := getWorkerNameForMetrics(name)
if _, ok := m.totalWorkers[identity]; !ok {
m.totalWorkers[identity] = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: "frankenphp",
Subsystem: subsystem,
Name: "total_workers",
Help: "Total number of PHP workers for this worker",
})
m.registry.MustRegister(m.totalWorkers[identity])
}
if _, ok := m.workerCrashes[identity]; !ok {
m.workerCrashes[identity] = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: "frankenphp",
Subsystem: subsystem,
Name: "worker_crashes",
Help: "Number of PHP worker crashes for this worker",
})
m.registry.MustRegister(m.workerCrashes[identity])
}
if _, ok := m.workerRestarts[identity]; !ok {
m.workerRestarts[identity] = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: "frankenphp",
Subsystem: subsystem,
Name: "worker_restarts",
Help: "Number of PHP worker restarts for this worker",
})
m.registry.MustRegister(m.workerRestarts[identity])
}
if _, ok := m.readyWorkers[identity]; !ok {
m.readyWorkers[identity] = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: "frankenphp",
Subsystem: subsystem,
Name: "ready_workers",
Help: "Running workers that have successfully called frankenphp_handle_request at least once",
})
m.registry.MustRegister(m.readyWorkers[identity])
}
if _, ok := m.busyWorkers[identity]; !ok {
m.busyWorkers[identity] = prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: "frankenphp",
Subsystem: subsystem,
Name: "busy_workers",
Help: "Number of busy PHP workers for this worker",
})
m.registry.MustRegister(m.busyWorkers[identity])
}
if _, ok := m.workerRequestTime[identity]; !ok {
m.workerRequestTime[identity] = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: "frankenphp",
Subsystem: subsystem,
Name: "worker_request_time",
})
m.registry.MustRegister(m.workerRequestTime[identity])
}
if _, ok := m.workerRequestCount[identity]; !ok {
m.workerRequestCount[identity] = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: "frankenphp",
Subsystem: subsystem,
Name: "worker_request_count",
})
m.registry.MustRegister(m.workerRequestCount[identity])
}
}
func (m *PrometheusMetrics) TotalThreads(num int) {
m.totalThreads.Add(float64(num))
}
func (m *PrometheusMetrics) StartRequest() {
m.busyThreads.Inc()
}
func (m *PrometheusMetrics) StopRequest() {
m.busyThreads.Dec()
}
func (m *PrometheusMetrics) StopWorkerRequest(name string, duration time.Duration) {
if _, ok := m.workerRequestTime[name]; !ok {
return
}
m.workerRequestCount[name].Inc()
m.busyWorkers[name].Dec()
m.workerRequestTime[name].Add(duration.Seconds())
}
func (m *PrometheusMetrics) StartWorkerRequest(name string) {
if _, ok := m.busyWorkers[name]; !ok {
return
}
m.busyWorkers[name].Inc()
}
func (m *PrometheusMetrics) Shutdown() {
m.registry.Unregister(m.totalThreads)
m.registry.Unregister(m.busyThreads)
for _, g := range m.totalWorkers {
m.registry.Unregister(g)
}
for _, g := range m.busyWorkers {
m.registry.Unregister(g)
}
for _, c := range m.workerRequestTime {
m.registry.Unregister(c)
}
for _, c := range m.workerRequestCount {
m.registry.Unregister(c)
}
for _, c := range m.workerCrashes {
m.registry.Unregister(c)
}
for _, c := range m.workerRestarts {
m.registry.Unregister(c)
}
for _, g := range m.readyWorkers {
m.registry.Unregister(g)
}
m.totalThreads = prometheus.NewCounter(prometheus.CounterOpts{
Name: "frankenphp_total_threads",
Help: "Total number of PHP threads",
})
m.busyThreads = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "frankenphp_busy_threads",
Help: "Number of busy PHP threads",
})
m.totalWorkers = map[string]prometheus.Gauge{}
m.busyWorkers = map[string]prometheus.Gauge{}
m.workerRequestTime = map[string]prometheus.Counter{}
m.workerRequestCount = map[string]prometheus.Counter{}
m.workerRestarts = map[string]prometheus.Counter{}
m.workerCrashes = map[string]prometheus.Counter{}
m.readyWorkers = map[string]prometheus.Gauge{}
m.registry.MustRegister(m.totalThreads)
m.registry.MustRegister(m.busyThreads)
}
func getWorkerNameForMetrics(name string) string {
name = metricsNameRegex.ReplaceAllString(name, "_")
name = metricsNameFixRegex.ReplaceAllString(name, "")
return name
}
func NewPrometheusMetrics(registry prometheus.Registerer) *PrometheusMetrics {
if registry == nil {
registry = prometheus.NewRegistry()
}
m := &PrometheusMetrics{
registry: registry,
totalThreads: prometheus.NewCounter(prometheus.CounterOpts{
Name: "frankenphp_total_threads",
Help: "Total number of PHP threads",
}),
busyThreads: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "frankenphp_busy_threads",
Help: "Number of busy PHP threads",
}),
totalWorkers: map[string]prometheus.Gauge{},
busyWorkers: map[string]prometheus.Gauge{},
workerRequestTime: map[string]prometheus.Counter{},
workerRequestCount: map[string]prometheus.Counter{},
workerRestarts: map[string]prometheus.Counter{},
workerCrashes: map[string]prometheus.Counter{},
readyWorkers: map[string]prometheus.Gauge{},
}
m.registry.MustRegister(m.totalThreads)
m.registry.MustRegister(m.busyThreads)
return m
}