pkg/exporter/probe/metrics.go (194 lines of code) (raw):
package probe
import (
"errors"
"fmt"
"reflect"
log "github.com/sirupsen/logrus"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/exp/maps"
)
const LegacyMetricsNamespace = "inspector"
const MetricsNamespace = "kubeskoop"
var (
availableMetricsProbes = make(map[string]*metricsProbeCreator)
ErrUndeclaredMetrics = errors.New("undeclared metrics")
)
type metricsProbeCreator struct {
f reflect.Value
s *reflect.Type
}
func newMetricProbeCreator(creator interface{}) (*metricsProbeCreator, error) {
t := reflect.TypeOf(creator)
err := validateProbeCreatorReturnValue[MetricsProbe](t)
if err != nil {
return nil, err
}
if t.NumIn() > 1 {
return nil, fmt.Errorf("input parameter count of creator should be either 0 or 1")
}
ret := &metricsProbeCreator{
f: reflect.ValueOf(creator),
}
if t.NumIn() == 1 {
st := t.In(0)
if err := validateParamTypeMapOrStruct(st); err != nil {
return nil, err
}
ret.s = &st
}
return ret, nil
}
func (m *metricsProbeCreator) Call(args map[string]interface{}) (mp MetricsProbe, err error) {
var in []reflect.Value
if m.s != nil {
s, e := createStructFromTypeWithArgs(*m.s, args)
if e != nil {
return nil, e
}
in = append(in, s)
}
result := m.f.Call(in)
// return parameter count and type has been checked in newMetricProbeCreator
if result[0].Interface() == nil {
mp = nil
} else {
mp = result[0].Interface().(MetricsProbe)
}
if result[1].Interface() == nil {
err = nil
} else {
err = result[1].Interface().(error)
}
return
}
// MustRegisterMetricsProbe registers the metrics probe by given name and creator.
// The creator is a function that creates MetricProbe. Return values of the creator
// must be (MetricsProbe, error). The creator can accept no parameter, or struct/map as a parameter.
// When the creator specifies the parameter, the configuration of the probe in the configuration file
// will be passed to the creator when the probe is created. For example:
//
// The creator accepts no extra args.
//
// func metricsProbeCreator() (MetricsProbe, error)
//
// The creator accepts struct "probeArgs" as args. Names of struct fields are case-insensitive.
//
// // Config in yaml
// args:
// argA: test
// argB: 20
// argC:
// - a
// // Struct definition
// type probeArgs struct {
// ArgA string
// ArgB int
// ArgC []string
// }
// // The creator function:
// func metricsProbeCreator(args probeArgs) (MetricsProbe, error)
//
// The creator can also use a map with string keys as parameters.
// However, if you use a type other than interface{} as the value type, errors may occur
// during the configuration parsing process.
//
// func metricsProbeCreator(args map[string]string) (MetricsProbe, error)
// func metricsProbeCreator(args map[string]interface{} (MetricsProbe, error)
func MustRegisterMetricsProbe(name string, creator interface{}) {
if _, ok := availableMetricsProbes[name]; ok {
panic(fmt.Errorf("duplicated metric probe %s", name))
}
c, err := newMetricProbeCreator(creator)
if err != nil {
panic(fmt.Errorf("error register metric probe %s: %s", name, err))
}
availableMetricsProbes[name] = c
}
func CreateMetricsProbe(name string, args map[string]interface{}) (MetricsProbe, error) {
creator, ok := availableMetricsProbes[name]
if !ok {
return nil, fmt.Errorf("undefined probe %s", name)
}
return creator.Call(args)
}
func ListMetricsProbes() []string {
var ret []string
for key := range availableMetricsProbes {
ret = append(ret, key)
}
return ret
}
type Emit func(name string, labels []string, val float64)
type Collector func(emit Emit) error
type SingleMetricsOpts struct {
Name string
Help string
ConstLabels map[string]string
VariableLabels []string
ValueType prometheus.ValueType
}
type BatchMetricsOpts struct {
Namespace string
Subsystem string
ConstLabels map[string]string
VariableLabels []string
SingleMetricsOpts []SingleMetricsOpts
}
type metricsInfo struct {
desc *prometheus.Desc
valueType prometheus.ValueType
}
type BatchMetrics struct {
name string
infoMap map[string]*metricsInfo
ProbeCollector Collector
}
func NewBatchMetrics(opts BatchMetricsOpts, probeCollector Collector) *BatchMetrics {
m := make(map[string]*metricsInfo)
for _, metrics := range opts.SingleMetricsOpts {
constLabels, variableLables := mergeLabels(opts, metrics)
desc := prometheus.NewDesc(
prometheus.BuildFQName(opts.Namespace, opts.Subsystem, metrics.Name),
metrics.Help,
variableLables,
constLabels,
)
m[metrics.Name] = &metricsInfo{
desc: desc,
valueType: metrics.ValueType,
}
}
return &BatchMetrics{
name: fmt.Sprintf("%s_%s", opts.Namespace, opts.Subsystem),
infoMap: m,
ProbeCollector: probeCollector,
}
}
func (b *BatchMetrics) Describe(descs chan<- *prometheus.Desc) {
for _, info := range b.infoMap {
descs <- info.desc
}
}
func (b *BatchMetrics) Collect(metrics chan<- prometheus.Metric) {
emit := func(name string, labels []string, val float64) {
info, ok := b.infoMap[name]
if !ok {
log.Errorf("%s undeclared metrics %s", b.name, name)
return
}
m, err := prometheus.NewConstMetric(info.desc, info.valueType, val, labels...)
if err != nil {
log.Errorf("%s failed create metrics, err: %v", b.name, err)
return
}
metrics <- m
}
err := b.ProbeCollector(emit)
if err != nil {
log.Errorf("%s error collect, err: %v", b.name, err)
return
}
}
func mergeLabels(opts BatchMetricsOpts, metrics SingleMetricsOpts) (map[string]string, []string) {
constLabels := mergeMap(opts.ConstLabels, metrics.ConstLabels)
variableLabels := mergeArray(opts.VariableLabels, metrics.VariableLabels)
return constLabels, variableLabels
}
func mergeArray(labels []string, labels2 []string) []string {
m := make(map[string]bool)
for _, s := range labels {
m[s] = true
}
for _, s := range labels2 {
if _, ok := m[s]; ok {
//to avoid duplicated label
panic(fmt.Sprintf("metric label %s already declared in BatchMetricsOpts", s))
}
}
var ret []string
ret = append(ret, labels...)
ret = append(ret, labels2...)
return ret
}
// if a key exists in both maps, value in labels2 will be keep
func mergeMap(labels map[string]string, labels2 map[string]string) map[string]string {
ret := make(map[string]string)
maps.Copy(ret, labels)
maps.Copy(ret, labels2)
return ret
}
type combinedMetricsProbe struct {
Probe
prometheus.Collector
}
func NewMetricsProbe(name string, simpleProbe SimpleProbe, collector prometheus.Collector) MetricsProbe {
return &combinedMetricsProbe{
Probe: NewProbe(name, simpleProbe),
Collector: collector,
}
}
var _ prometheus.Collector = &BatchMetrics{}