github.com/safing/portbase@v0.19.5/metrics/metric.go (about)

     1  package metrics
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"regexp"
     7  	"sort"
     8  	"strings"
     9  
    10  	vm "github.com/VictoriaMetrics/metrics"
    11  
    12  	"github.com/safing/portbase/api"
    13  	"github.com/safing/portbase/config"
    14  )
    15  
    16  // PrometheusFormatRequirement is required format defined by prometheus for
    17  // metric and label names.
    18  const (
    19  	prometheusBaseFormt         = "[a-zA-Z_][a-zA-Z0-9_]*"
    20  	PrometheusFormatRequirement = "^" + prometheusBaseFormt + "$"
    21  )
    22  
    23  var prometheusFormat = regexp.MustCompile(PrometheusFormatRequirement)
    24  
    25  // Metric represents one or more metrics.
    26  type Metric interface {
    27  	ID() string
    28  	LabeledID() string
    29  	Opts() *Options
    30  	WritePrometheus(w io.Writer)
    31  }
    32  
    33  type metricBase struct {
    34  	Identifier        string
    35  	Labels            map[string]string
    36  	LabeledIdentifier string
    37  	Options           *Options
    38  	set               *vm.Set
    39  }
    40  
    41  // Options can be used to set advanced metric settings.
    42  type Options struct {
    43  	// Name defines an optional human readable name for the metric.
    44  	Name string
    45  
    46  	// InternalID specifies an alternative internal ID that will be used when
    47  	// exposing the metric via the API in a structured format.
    48  	InternalID string
    49  
    50  	// AlertLimit defines an upper limit that triggers an alert.
    51  	AlertLimit float64
    52  
    53  	// AlertTimeframe defines an optional timeframe in seconds for which the
    54  	// AlertLimit should be interpreted in.
    55  	AlertTimeframe float64
    56  
    57  	// Permission defines the permission that is required to read the metric.
    58  	Permission api.Permission
    59  
    60  	// ExpertiseLevel defines the expertise level that the metric is meant for.
    61  	ExpertiseLevel config.ExpertiseLevel
    62  
    63  	// Persist enabled persisting the metric on shutdown and loading the previous
    64  	// value at start. This is only supported for counters.
    65  	Persist bool
    66  }
    67  
    68  func newMetricBase(id string, labels map[string]string, opts Options) (*metricBase, error) {
    69  	// Check formats.
    70  	if !prometheusFormat.MatchString(strings.ReplaceAll(id, "/", "_")) {
    71  		return nil, fmt.Errorf("metric name %q must match %s", id, PrometheusFormatRequirement)
    72  	}
    73  	for labelName := range labels {
    74  		if !prometheusFormat.MatchString(labelName) {
    75  			return nil, fmt.Errorf("metric label name %q must match %s", labelName, PrometheusFormatRequirement)
    76  		}
    77  	}
    78  
    79  	// Check permission.
    80  	if opts.Permission < api.PermitAnyone {
    81  		// Default to PermitUser.
    82  		opts.Permission = api.PermitUser
    83  	}
    84  
    85  	// Ensure that labels is a map.
    86  	if labels == nil {
    87  		labels = make(map[string]string)
    88  	}
    89  
    90  	// Create metric base.
    91  	base := &metricBase{
    92  		Identifier: id,
    93  		Labels:     labels,
    94  		Options:    &opts,
    95  		set:        vm.NewSet(),
    96  	}
    97  	base.LabeledIdentifier = base.buildLabeledID()
    98  	return base, nil
    99  }
   100  
   101  // ID returns the given ID of the metric.
   102  func (m *metricBase) ID() string {
   103  	return m.Identifier
   104  }
   105  
   106  // LabeledID returns the Prometheus-compatible labeled ID of the metric.
   107  func (m *metricBase) LabeledID() string {
   108  	return m.LabeledIdentifier
   109  }
   110  
   111  // Opts returns the metric options. They  may not be modified.
   112  func (m *metricBase) Opts() *Options {
   113  	return m.Options
   114  }
   115  
   116  // WritePrometheus writes the metric in the prometheus format to the given writer.
   117  func (m *metricBase) WritePrometheus(w io.Writer) {
   118  	m.set.WritePrometheus(w)
   119  }
   120  
   121  func (m *metricBase) buildLabeledID() string {
   122  	// Because we use the namespace and the global flags here, we need to flag
   123  	// them as immutable.
   124  	registryLock.Lock()
   125  	defer registryLock.Unlock()
   126  	firstMetricRegistered = true
   127  
   128  	// Build ID from Identifier.
   129  	metricID := strings.TrimSpace(strings.ReplaceAll(m.Identifier, "/", "_"))
   130  
   131  	// Add namespace to ID.
   132  	if metricNamespace != "" {
   133  		metricID = metricNamespace + "_" + metricID
   134  	}
   135  
   136  	// Return now if no labels are defined.
   137  	if len(globalLabels) == 0 && len(m.Labels) == 0 {
   138  		return metricID
   139  	}
   140  
   141  	// Add global labels to the custom ones, if they don't exist yet.
   142  	for labelName, labelValue := range globalLabels {
   143  		if _, ok := m.Labels[labelName]; !ok {
   144  			m.Labels[labelName] = labelValue
   145  		}
   146  	}
   147  
   148  	// Render labels into a slice and sort them in order to make the labeled ID
   149  	// reproducible.
   150  	labels := make([]string, 0, len(m.Labels))
   151  	for labelName, labelValue := range m.Labels {
   152  		labels = append(labels, fmt.Sprintf("%s=%q", labelName, labelValue))
   153  	}
   154  	sort.Strings(labels)
   155  
   156  	// Return fully labaled ID.
   157  	return fmt.Sprintf("%s{%s}", metricID, strings.Join(labels, ","))
   158  }
   159  
   160  // Split metrics into sets, according to the API Auth Levels, which will also correspond to the UI Mode levels. SPN // nodes will also allow public access to metrics with the permission "PermitAnyone".
   161  // Save "life-long" metrics on shutdown and load them at start.
   162  // Generate the correct metric name and labels.
   163  // Expose metrics via http, but also via the runtime DB in order to push metrics to the UI.
   164  // The UI will have to parse the prometheus metrics format and will not be able to immediately present historical data, // but data will have to be built.
   165  // Provide the option to push metrics to a prometheus push gateway, this is especially helpful when gathering data from // loads of SPN nodes.