safing-portmaster/base/metrics/metric.go
Daniel Hååvi 80664d1a27
Restructure modules ()
* Move portbase into monorepo

* Add new simple module mgr

* [WIP] Switch to new simple module mgr

* Add StateMgr and more worker variants

* [WIP] Switch more modules

* [WIP] Switch more modules

* [WIP] swtich more modules

* [WIP] switch all SPN modules

* [WIP] switch all service modules

* [WIP] Convert all workers to the new module system

* [WIP] add new task system to module manager

* [WIP] Add second take for scheduling workers

* [WIP] Add FIXME for bugs in new scheduler

* [WIP] Add minor improvements to scheduler

* [WIP] Add new worker scheduler

* [WIP] Fix more bug related to new module system

* [WIP] Fix start handing of the new module system

* [WIP] Improve startup process

* [WIP] Fix minor issues

* [WIP] Fix missing subsystem in settings

* [WIP] Initialize managers in constructor

* [WIP] Move module event initialization to constrictors

* [WIP] Fix setting for enabling and disabling the SPN module

* [WIP] Move API registeration into module construction

* [WIP] Update states mgr for all modules

* [WIP] Add CmdLine operation support

* Add state helper methods to module group and instance

* Add notification and module status handling to status package

* Fix starting issues

* Remove pilot widget and update security lock to new status data

* Remove debug logs

* Improve http server shutdown

* Add workaround for cleanly shutting down firewall+netquery

* Improve logging

* Add syncing states with notifications for new module system

* Improve starting, stopping, shutdown; resolve FIXMEs/TODOs

* [WIP] Fix most unit tests

* Review new module system and fix minor issues

* Push shutdown and restart events again via API

* Set sleep mode via interface

* Update example/template module

* [WIP] Fix spn/cabin unit test

* Remove deprecated UI elements

* Make log output more similar for the logging transition phase

* Switch spn hub and observer cmds to new module system

* Fix log sources

* Make worker mgr less error prone

* Fix tests and minor issues

* Fix observation hub

* Improve shutdown and restart handling

* Split up big connection.go source file

* Move varint and dsd packages to structures repo

* Improve expansion test

* Fix linter warnings

* Fix interception module on windows

* Fix linter errors

---------

Co-authored-by: Vladimir Stoilov <vladimir@safing.io>
2024-08-09 18:15:48 +03:00

165 lines
4.9 KiB
Go

package metrics
import (
"fmt"
"io"
"regexp"
"sort"
"strings"
vm "github.com/VictoriaMetrics/metrics"
"github.com/safing/portmaster/base/api"
"github.com/safing/portmaster/base/config"
)
// PrometheusFormatRequirement is required format defined by prometheus for
// metric and label names.
const (
prometheusBaseFormt = "[a-zA-Z_][a-zA-Z0-9_]*"
PrometheusFormatRequirement = "^" + prometheusBaseFormt + "$"
)
var prometheusFormat = regexp.MustCompile(PrometheusFormatRequirement)
// Metric represents one or more metrics.
type Metric interface {
ID() string
LabeledID() string
Opts() *Options
WritePrometheus(w io.Writer)
}
type metricBase struct {
Identifier string
Labels map[string]string
LabeledIdentifier string
Options *Options
set *vm.Set
}
// Options can be used to set advanced metric settings.
type Options struct {
// Name defines an optional human readable name for the metric.
Name string
// InternalID specifies an alternative internal ID that will be used when
// exposing the metric via the API in a structured format.
InternalID string
// AlertLimit defines an upper limit that triggers an alert.
AlertLimit float64
// AlertTimeframe defines an optional timeframe in seconds for which the
// AlertLimit should be interpreted in.
AlertTimeframe float64
// Permission defines the permission that is required to read the metric.
Permission api.Permission
// ExpertiseLevel defines the expertise level that the metric is meant for.
ExpertiseLevel config.ExpertiseLevel
// Persist enabled persisting the metric on shutdown and loading the previous
// value at start. This is only supported for counters.
Persist bool
}
func newMetricBase(id string, labels map[string]string, opts Options) (*metricBase, error) {
// Check formats.
if !prometheusFormat.MatchString(strings.ReplaceAll(id, "/", "_")) {
return nil, fmt.Errorf("metric name %q must match %s", id, PrometheusFormatRequirement)
}
for labelName := range labels {
if !prometheusFormat.MatchString(labelName) {
return nil, fmt.Errorf("metric label name %q must match %s", labelName, PrometheusFormatRequirement)
}
}
// Check permission.
if opts.Permission < api.PermitAnyone {
// Default to PermitUser.
opts.Permission = api.PermitUser
}
// Ensure that labels is a map.
if labels == nil {
labels = make(map[string]string)
}
// Create metric base.
base := &metricBase{
Identifier: id,
Labels: labels,
Options: &opts,
set: vm.NewSet(),
}
base.LabeledIdentifier = base.buildLabeledID()
return base, nil
}
// ID returns the given ID of the metric.
func (m *metricBase) ID() string {
return m.Identifier
}
// LabeledID returns the Prometheus-compatible labeled ID of the metric.
func (m *metricBase) LabeledID() string {
return m.LabeledIdentifier
}
// Opts returns the metric options. They may not be modified.
func (m *metricBase) Opts() *Options {
return m.Options
}
// WritePrometheus writes the metric in the prometheus format to the given writer.
func (m *metricBase) WritePrometheus(w io.Writer) {
m.set.WritePrometheus(w)
}
func (m *metricBase) buildLabeledID() string {
// Because we use the namespace and the global flags here, we need to flag
// them as immutable.
registryLock.Lock()
defer registryLock.Unlock()
firstMetricRegistered = true
// Build ID from Identifier.
metricID := strings.TrimSpace(strings.ReplaceAll(m.Identifier, "/", "_"))
// Add namespace to ID.
if metricNamespace != "" {
metricID = metricNamespace + "_" + metricID
}
// Return now if no labels are defined.
if len(globalLabels) == 0 && len(m.Labels) == 0 {
return metricID
}
// Add global labels to the custom ones, if they don't exist yet.
for labelName, labelValue := range globalLabels {
if _, ok := m.Labels[labelName]; !ok {
m.Labels[labelName] = labelValue
}
}
// Render labels into a slice and sort them in order to make the labeled ID
// reproducible.
labels := make([]string, 0, len(m.Labels))
for labelName, labelValue := range m.Labels {
labels = append(labels, fmt.Sprintf("%s=%q", labelName, labelValue))
}
sort.Strings(labels)
// Return fully labaled ID.
return fmt.Sprintf("%s{%s}", metricID, strings.Join(labels, ","))
}
// 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".
// Save "life-long" metrics on shutdown and load them at start.
// Generate the correct metric name and labels.
// Expose metrics via http, but also via the runtime DB in order to push metrics to the UI.
// 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.
// Provide the option to push metrics to a prometheus push gateway, this is especially helpful when gathering data from // loads of SPN nodes.