| // 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() |
| } |