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.