github.com/safing/portbase@v0.19.5/metrics/module.go (about) 1 package metrics 2 3 import ( 4 "errors" 5 "fmt" 6 "sort" 7 "sync" 8 9 "github.com/safing/portbase/modules" 10 ) 11 12 var ( 13 module *modules.Module 14 15 registry []Metric 16 registryLock sync.RWMutex 17 18 firstMetricRegistered bool 19 metricNamespace string 20 globalLabels = make(map[string]string) 21 22 // ErrAlreadyStarted is returned when an operation is only valid before the 23 // first metric is registered, and is called after. 24 ErrAlreadyStarted = errors.New("can only be changed before first metric is registered") 25 26 // ErrAlreadyRegistered is returned when a metric with the same ID is 27 // registered again. 28 ErrAlreadyRegistered = errors.New("metric already registered") 29 30 // ErrAlreadySet is returned when a value is already set and cannot be changed. 31 ErrAlreadySet = errors.New("already set") 32 33 // ErrInvalidOptions is returned when invalid options where provided. 34 ErrInvalidOptions = errors.New("invalid options") 35 ) 36 37 func init() { 38 module = modules.Register("metrics", prep, start, stop, "config", "database", "api") 39 } 40 41 func prep() error { 42 return prepConfig() 43 } 44 45 func start() error { 46 // Add metric instance name as global variable if set. 47 if instanceOption() != "" { 48 if err := AddGlobalLabel("instance", instanceOption()); err != nil { 49 return err 50 } 51 } 52 53 if err := registerInfoMetric(); err != nil { 54 return err 55 } 56 57 if err := registerRuntimeMetric(); err != nil { 58 return err 59 } 60 61 if err := registerHostMetrics(); err != nil { 62 return err 63 } 64 65 if err := registerLogMetrics(); err != nil { 66 return err 67 } 68 69 if err := registerAPI(); err != nil { 70 return err 71 } 72 73 if pushOption() != "" { 74 module.StartServiceWorker("metric pusher", 0, metricsWriter) 75 } 76 77 return nil 78 } 79 80 func stop() error { 81 // Wait until the metrics pusher is done, as it may have started reporting 82 // and may report a higher number than we store to disk. For persistent 83 // metrics it can then happen that the first report is lower than the 84 // previous report, making prometheus think that all that happened since the 85 // last report, due to the automatic restart detection. 86 87 // The registry is read locked when writing metrics. 88 // Write lock the registry to make sure all writes are finished. 89 registryLock.Lock() 90 registryLock.Unlock() //nolint:staticcheck 91 92 storePersistentMetrics() 93 94 return nil 95 } 96 97 func register(m Metric) error { 98 registryLock.Lock() 99 defer registryLock.Unlock() 100 101 // Check if metric ID is already registered. 102 for _, registeredMetric := range registry { 103 if m.LabeledID() == registeredMetric.LabeledID() { 104 return ErrAlreadyRegistered 105 } 106 if m.Opts().InternalID != "" && 107 m.Opts().InternalID == registeredMetric.Opts().InternalID { 108 return fmt.Errorf("%w with this internal ID", ErrAlreadyRegistered) 109 } 110 } 111 112 // Add new metric to registry and sort it. 113 registry = append(registry, m) 114 sort.Sort(byLabeledID(registry)) 115 116 // Set flag that first metric is now registered. 117 firstMetricRegistered = true 118 119 if module.Status() < modules.StatusStarting { 120 return fmt.Errorf("registering metric %q too early", m.ID()) 121 } 122 123 return nil 124 } 125 126 // SetNamespace sets the namespace for all metrics. It is prefixed to all 127 // metric IDs. 128 // It must be set before any metric is registered. 129 // Does not affect golang runtime metrics. 130 func SetNamespace(namespace string) error { 131 // Lock registry and check if a first metric is already registered. 132 registryLock.Lock() 133 defer registryLock.Unlock() 134 if firstMetricRegistered { 135 return ErrAlreadyStarted 136 } 137 138 // Check if the namespace is already set. 139 if metricNamespace != "" { 140 return ErrAlreadySet 141 } 142 143 metricNamespace = namespace 144 return nil 145 } 146 147 // AddGlobalLabel adds a global label to all metrics. 148 // Global labels must be added before any metric is registered. 149 // Does not affect golang runtime metrics. 150 func AddGlobalLabel(name, value string) error { 151 // Lock registry and check if a first metric is already registered. 152 registryLock.Lock() 153 defer registryLock.Unlock() 154 if firstMetricRegistered { 155 return ErrAlreadyStarted 156 } 157 158 // Check format. 159 if !prometheusFormat.MatchString(name) { 160 return fmt.Errorf("metric label name %q must match %s", name, PrometheusFormatRequirement) 161 } 162 163 globalLabels[name] = value 164 return nil 165 } 166 167 type byLabeledID []Metric 168 169 func (r byLabeledID) Len() int { return len(r) } 170 func (r byLabeledID) Less(i, j int) bool { return r[i].LabeledID() < r[j].LabeledID() } 171 func (r byLabeledID) Swap(i, j int) { r[i], r[j] = r[j], r[i] }