blob: 2dbad292f8c76efd55d83c7c3e7a9a015521512a [file] [log] [blame]
// Package metrics implements a Prometheus metrics submission interface for BMDB
// client components. A Metrics object can be attached to a BMDB object, which
// will make all BMDB sessions/transactions/work statistics be submitted to that
// Metrics object.
package metrics
import (
"github.com/prometheus/client_golang/prometheus"
"source.monogon.dev/cloud/bmaas/bmdb/model"
)
// Processor describes some cloud component and possibly sub-component which acts
// upon the BMDB. When starting a BMDB session, this Processor can be provided to
// contextualize the metrics emitted by this session. Because the selected
// Processor ends up directly as a Prometheus metric label, it must be
// low-cardinality - thus all possible values are defined as an enum here. If a
// Session is not configured with a Processor, the default (ProcessorUnknown)
// will be used.
type Processor string
const (
ProcessorUnknown Processor = ""
ProcessorShepherdInitializer Processor = "shepherd-initializer"
ProcessorShepherdProvisioner Processor = "shepherd-provisioner"
ProcessorShepherdRecoverer Processor = "shepherd-recoverer"
ProcessorShepherdUpdater Processor = "shepherd-updater"
ProcessorBMSRV Processor = "bmsrv"
)
// String returns the Prometheus label value for use with the 'processor' label
// key.
func (p Processor) String() string {
switch p {
case ProcessorUnknown:
return "unknown"
default:
return string(p)
}
}
// MetricsSet contains all the Prometheus metrics objects related to a BMDB
// client.
//
// The MetricsSet object is goroutine-safe.
//
// An empty MetricsSet object is not valid, and should be instead constructed
// using New.
//
// A nil MetricsSet object is valid and represents a no-op metrics recorder
// that's never collected.
type MetricsSet struct {
sessionStarted *prometheus.CounterVec
transactionExecuted *prometheus.CounterVec
transactionRetried *prometheus.CounterVec
transactionFailed *prometheus.CounterVec
workStarted *prometheus.CounterVec
workFinished *prometheus.CounterVec
}
func processorCounter(name, help string, labels ...string) *prometheus.CounterVec {
labels = append([]string{"processor"}, labels...)
return prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: name,
Help: help,
},
labels,
)
}
// New creates a new BMDB MetricsSet object which can be then attached to a BMDB
// object by calling BMDB.EnableMetrics on the MetricsSet object.
//
// The given registry must be a valid Prometheus registry, and all metrics
// contained in this MetricsSet object will be registered into it.
//
// The MetricsSet object can be shared between multiple BMDB object.
//
// The MetricsSet object is goroutine-safe.
func New(registry *prometheus.Registry) *MetricsSet {
m := &MetricsSet{
sessionStarted: processorCounter("bmdb_session_started", "How many sessions this worker started"),
transactionExecuted: processorCounter("bmdb_transaction_executed", "How many transactions were performed by this worker"),
transactionRetried: processorCounter("bmdb_transaction_retried", "How many transaction retries were performed by this worker"),
transactionFailed: processorCounter("bmdb_transaction_failed", "How many transactions failed permanently on this worker"),
workStarted: processorCounter("bmdb_work_started", "How many work items were performed by this worker, partitioned by process", "process"),
workFinished: processorCounter("bmdb_work_finished", "How many work items were finished by this worker, partitioned by process and result", "process", "result"),
}
registry.MustRegister(
m.sessionStarted,
m.transactionExecuted,
m.transactionRetried,
m.transactionFailed,
m.workStarted,
m.workFinished,
)
return m
}
// ProcessorRecorder wraps a MetricsSet object with the context of some
// Processor. It exposes methods that record specific events into the managed
// Metrics.
//
// The ProcessorRecorder object is goroutine safe.
//
// An empty ProcessorRecorder object is not valid, and should be instead
// constructed using Metrics.Recorder.
//
// A nil ProcessorRecorder object is valid and represents a no-op metrics
// recorder.
type ProcessorRecorder struct {
m *MetricsSet
labels prometheus.Labels
}
// Recorder builds a ProcessorRecorder for the given Metrics and a given
// Processor.
func (m *MetricsSet) Recorder(p Processor) *ProcessorRecorder {
if m == nil {
return nil
}
return &ProcessorRecorder{
m: m,
labels: prometheus.Labels{
"processor": p.String(),
},
}
}
// OnTransactionStarted should be called any time a BMDB client starts or
// re-starts a BMDB Transaction. The attempt should either be '1' (for the first
// attempt) or a number larger than 1 for any subsequent attempt (i.e. retry) of
// a transaction.
func (r *ProcessorRecorder) OnTransactionStarted(attempt int64) {
if r == nil {
return
}
if attempt == 1 {
r.m.transactionExecuted.With(r.labels).Inc()
} else {
r.m.transactionRetried.With(r.labels).Inc()
}
}
// OnTransactionFailed should be called any time a BMDB client fails a
// BMDB Transaction permanently.
func (r *ProcessorRecorder) OnTransactionFailed() {
if r == nil {
return
}
r.m.transactionFailed.With(r.labels).Inc()
}
// OnSessionStarted should be called any time a BMDB client opens a new BMDB
// Session.
func (r *ProcessorRecorder) OnSessionStarted() {
if r == nil {
return
}
r.m.sessionStarted.With(r.labels).Inc()
}
// ProcessRecorder wraps a ProcessorRecorder with an additional model.Process.
// The resulting object can then record work-specific events.
//
// The PusherWithProcess object is goroutine-safe.
type ProcessRecorder struct {
*ProcessorRecorder
labels prometheus.Labels
}
// WithProcess wraps a given Pusher with a Process.
//
// The resulting PusherWithProcess object is goroutine-safe.
func (r *ProcessorRecorder) WithProcess(process model.Process) *ProcessRecorder {
if r == nil {
return nil
}
return &ProcessRecorder{
ProcessorRecorder: r,
labels: prometheus.Labels{
"processor": r.labels["processor"],
"process": string(process),
},
}
}
// OnWorkStarted should be called any time a BMDB client starts a new Work item.
func (r *ProcessRecorder) OnWorkStarted() {
if r == nil {
return
}
r.m.workStarted.With(r.labels).Inc()
}
type WorkResult string
const (
WorkResultFinished WorkResult = "finished"
WorkResultCanceled WorkResult = "canceled"
WorkResultFailed WorkResult = "failed"
)
// OnWorkFinished should be called any time a BMDB client finishes, cancels or
// fails a Work item.
func (r *ProcessRecorder) OnWorkFinished(result WorkResult) {
if r == nil {
return
}
r.m.workFinished.MustCurryWith(r.labels).With(prometheus.Labels{"result": string(result)}).Inc()
}