github.com/wfusion/gofusion@v1.1.14/metrics/metrics.go (about) 1 package metrics 2 3 import ( 4 "context" 5 "fmt" 6 "runtime" 7 "strings" 8 "sync" 9 "syscall" 10 "time" 11 12 "github.com/Rican7/retry" 13 "github.com/Rican7/retry/strategy" 14 "github.com/panjf2000/ants/v2" 15 "github.com/pkg/errors" 16 17 "github.com/wfusion/gofusion/common/constant" 18 "github.com/wfusion/gofusion/common/infra/metrics" 19 "github.com/wfusion/gofusion/common/infra/metrics/prometheus" 20 "github.com/wfusion/gofusion/common/utils" 21 "github.com/wfusion/gofusion/common/utils/inspect" 22 "github.com/wfusion/gofusion/config" 23 ) 24 25 const ( 26 defaultQueueLimit = 16 * 1024 27 ) 28 29 var ( 30 rwlock = new(sync.RWMutex) 31 appInstances map[string]map[string]map[string]Sink 32 cfgsMap map[string]map[string]*cfg 33 ) 34 35 type abstract struct { 36 *metrics.Metrics 37 38 ctx context.Context 39 job string 40 name string 41 appName string 42 log metrics.Logger 43 constLabels []metrics.Label 44 45 stop chan struct{} 46 queue chan *task 47 queuePool *ants.Pool 48 49 dispatcher map[string]func(...any) 50 } 51 52 type task struct { 53 ctx context.Context 54 key []string 55 val any 56 opts []utils.OptionExtender 57 labels []metrics.Label 58 method string 59 } 60 61 func (t *task) String() string { 62 label := make(map[string]string, len(t.labels)) 63 for _, l := range t.labels { 64 label[l.Name] = l.Value 65 } 66 67 return fmt.Sprintf("%s:%s:%+v(%+v)", t.method, strings.Join(t.key, constant.Dot), t.val, label) 68 } 69 70 func newMetrics(ctx context.Context, appName, name, job string, sink metrics.MetricSink, conf *cfg) *abstract { 71 metricsConfig := metrics.DefaultConfig(config.Use(appName).AppName()) 72 if conf.c.EnableServiceLabel { 73 metricsConfig.EnableHostname = true 74 metricsConfig.EnableHostnameLabel = true 75 metricsConfig.EnableServiceLabel = true 76 metricsConfig.EnableClientIPLabel = true 77 } else { 78 metricsConfig.EnableHostname = false 79 metricsConfig.EnableHostnameLabel = false 80 metricsConfig.EnableServiceLabel = false 81 metricsConfig.EnableClientIPLabel = false 82 } 83 84 m, err := metrics.New(metricsConfig, sink) 85 if err != nil { 86 panic(errors.Errorf("initialize metrics failed: %s", err)) 87 } 88 89 limit := defaultQueueLimit 90 if conf.c.QueueLimit > 0 { 91 limit = conf.c.QueueLimit 92 } 93 if conf.c.QueueConcurrency == 0 { 94 conf.c.QueueConcurrency = runtime.NumCPU() 95 } 96 97 constLabels := make([]metrics.Label, 0, len(conf.c.Labels)) 98 for k, v := range conf.c.Labels { 99 constLabels = append(constLabels, metrics.Label{Name: k, Value: v}) 100 } 101 102 a := &abstract{ 103 Metrics: m, 104 105 ctx: ctx, 106 constLabels: constLabels, 107 job: job, 108 name: name, 109 appName: appName, 110 log: conf.logger, 111 stop: make(chan struct{}), 112 queue: make(chan *task, limit), 113 queuePool: utils.Must(ants.NewPool(conf.c.QueueConcurrency)), 114 } 115 116 a.dispatcher = map[string]func(...any){ 117 "Gauge": utils.WrapFunc(a.setGaugeWithLabels), 118 "Counter": utils.WrapFunc(a.incrCounterWithLabels), 119 "Sample": utils.WrapFunc(a.addSampleWithLabels), 120 "MeasureSince": utils.WrapFunc(a.measureSinceWithLabels), 121 } 122 123 a.start() 124 125 return a 126 } 127 128 func (a *abstract) SetGauge(ctx context.Context, key []string, val float64, opts ...utils.OptionExtender) { 129 a.send(ctx, "Gauge", key, val, opts...) 130 } 131 func (a *abstract) IncrCounter(ctx context.Context, key []string, val float64, opts ...utils.OptionExtender) { 132 a.send(ctx, "Counter", key, val, opts...) 133 } 134 func (a *abstract) AddSample(ctx context.Context, key []string, val float64, opts ...utils.OptionExtender) { 135 a.send(ctx, "Sample", key, val, opts...) 136 } 137 func (a *abstract) MeasureSince(ctx context.Context, key []string, start time.Time, opts ...utils.OptionExtender) { 138 a.send(ctx, "MeasureSince", key, start, opts...) 139 } 140 func (a *abstract) IsEnableServiceLabel() bool { return a.EnableServiceLabel } 141 142 func (a *abstract) getProxy() any { 143 return inspect.GetField[metrics.MetricSink](a.Metrics, "sink") 144 } 145 func (a *abstract) shutdown() { 146 if _, ok := utils.IsChannelClosed(a.stop); ok { 147 return 148 } 149 150 close(a.stop) 151 a.Metrics.Shutdown() 152 } 153 154 func (a *abstract) send(ctx context.Context, method string, key []string, val any, opts ...utils.OptionExtender) { 155 opt := utils.ApplyOptions[option](opts...) 156 157 t := &task{ 158 ctx: ctx, 159 key: key, 160 val: val, 161 opts: append(opts, a.convertOpts(opts...)...), 162 labels: a.convertLabels(opt.labels), 163 method: method, 164 } 165 166 switch { 167 case opt.timeout > 0: 168 timeoutCtx, cancel := context.WithTimeout(a.ctx, opt.timeout) 169 defer cancel() 170 171 select { 172 case a.queue <- t: 173 case <-ctx.Done(): 174 if a.log != nil { 175 a.log.Info(ctx, "%v [Gofusion] %s %s %s async send task canceled due to context done", 176 syscall.Getpid(), config.Use(a.appName).AppName(), config.ComponentMetrics, a.name) 177 } 178 case <-timeoutCtx.Done(): 179 if a.log != nil { 180 a.log.Warn(ctx, "%v [Gofusion] %s %s %s async send task canceled due to timeout %s", 181 syscall.Getpid(), config.Use(a.appName).AppName(), config.ComponentMetrics, a.name, opt.timeout) 182 } 183 case <-a.stop: 184 close(a.queue) 185 if a.log != nil { 186 a.log.Info(ctx, "%v [Gofusion] %s %s %s async send task canceled due to metrics stopped", 187 syscall.Getpid(), config.Use(a.appName).AppName(), config.ComponentMetrics, a.name) 188 } 189 case <-a.ctx.Done(): 190 if a.log != nil { 191 a.log.Info(ctx, "%v [Gofusion] %s %s %s async send task canceled due to app exited", 192 syscall.Getpid(), config.Use(a.appName).AppName(), config.ComponentMetrics, a.name) 193 } 194 } 195 case opt.timeout < 0: 196 select { 197 case a.queue <- t: 198 case <-ctx.Done(): 199 if a.log != nil { 200 a.log.Info(ctx, "%v [Gofusion] %s %s %s async send task canceled due to context done", 201 syscall.Getpid(), config.Use(a.appName).AppName(), config.ComponentMetrics, a.name) 202 } 203 case <-a.stop: 204 close(a.queue) 205 if a.log != nil { 206 a.log.Info(ctx, "%v [Gofusion] %s %s %s async send task canceled due to metrics stopped", 207 syscall.Getpid(), config.Use(a.appName).AppName(), config.ComponentMetrics, a.name) 208 } 209 case <-a.ctx.Done(): 210 if a.log != nil { 211 a.log.Info(ctx, "%v [Gofusion] %s %s %s async send task canceled due to app exited", 212 syscall.Getpid(), config.Use(a.appName).AppName(), config.ComponentMetrics, a.name) 213 } 214 } 215 default: 216 select { 217 case a.queue <- t: 218 case <-ctx.Done(): 219 if a.log != nil { 220 a.log.Info(ctx, "%v [Gofusion] %s %s %s async send task canceled due to context done", 221 syscall.Getpid(), config.Use(a.appName).AppName(), config.ComponentMetrics, a.name) 222 } 223 case <-a.ctx.Done(): 224 if a.log != nil { 225 a.log.Info(ctx, "%v [Gofusion] %s %s %s async send task canceled due to app exited", 226 syscall.Getpid(), config.Use(a.appName).AppName(), config.ComponentMetrics, a.name) 227 } 228 case <-a.stop: 229 close(a.queue) 230 if a.log != nil { 231 a.log.Info(ctx, "%v [Gofusion] %s %s %s async send task canceled due to metrics stopped", 232 syscall.Getpid(), config.Use(a.appName).AppName(), config.ComponentMetrics, a.name) 233 } 234 default: 235 if a.log != nil { 236 a.log.Warn(ctx, "%v [Gofusion] %s %s %s async send task canceled due to exceed the queue limit", 237 syscall.Getpid(), config.Use(a.appName).AppName(), config.ComponentMetrics, a.name) 238 } 239 } 240 } 241 } 242 func (a *abstract) start() { 243 go func() { 244 appName := config.Use(a.appName).AppName() 245 _ = retry.Retry(func(attempt uint) (err error) { 246 _, err = utils.Catch(func() { 247 for { 248 select { 249 case <-a.ctx.Done(): 250 if a.log != nil { 251 a.log.Info(a.ctx, "%v [Gofusion] %s %s %s process exited due to context done", 252 syscall.Getpid(), config.Use(a.appName).AppName(), config.ComponentMetrics, a.name) 253 return 254 } 255 case task, ok := <-a.queue: 256 if !ok { 257 a.log.Info(a.ctx, "%v [Gofusion] %s %s %s process exited due to queue closed", 258 syscall.Getpid(), config.Use(a.appName).AppName(), config.ComponentMetrics, a.name) 259 return 260 } 261 262 if err := a.queuePool.Submit(func() { _ = a.process(task) }); err != nil && a.log != nil { 263 a.log.Error(task.ctx, "%v [Gofusion] %s %s %s submit process %s error: %s", 264 syscall.Getpid(), config.Use(a.appName).AppName(), config.ComponentMetrics, 265 a.name, task, err) 266 } 267 } 268 } 269 }) 270 if err != nil { 271 a.log.Warn(a.ctx, "%v [Gofusion] %s %s %s dispatcher exited with error: %s", 272 syscall.Getpid(), appName, config.ComponentMetrics, a.name, err) 273 } 274 return 275 }, strategy.Limit(10000)) 276 }() 277 } 278 func (a *abstract) process(task *task) (err error) { 279 _, err = utils.Catch(func() (err error) { 280 handler, ok := a.dispatcher[task.method] 281 if !ok { 282 return errors.Errorf("method %s not found", task.method) 283 } 284 params := []any{task.key, task.val, append(task.labels, a.constLabels...)} 285 params = append(params, utils.SliceMapping(task.opts, func(o utils.OptionExtender) any { return o })...) 286 handler(params...) 287 return 288 }) 289 if err != nil && a.log != nil { 290 a.log.Error(task.ctx, "%v [Gofusion] %s %s %s process %s catch error: %s", 291 syscall.Getpid(), config.Use(a.appName).AppName(), config.ComponentMetrics, a.name, task, err) 292 } 293 return 294 } 295 296 func (a *abstract) setGaugeWithLabels(key []string, v any, labels []metrics.Label, opts ...utils.OptionExtender) { 297 val, ok := v.(float64) 298 if !ok { 299 return 300 } 301 opt := utils.ApplyOptions[option](opts...) 302 if opt.precision { 303 a.Metrics.SetPrecisionGaugeWithLabels(key, val, labels, opts...) 304 } else { 305 a.Metrics.SetGaugeWithLabels(key, float32(val), labels, opts...) 306 } 307 } 308 func (a *abstract) incrCounterWithLabels(key []string, v any, labels []metrics.Label, opts ...utils.OptionExtender) { 309 val, ok := v.(float64) 310 if !ok { 311 return 312 } 313 a.Metrics.IncrCounterWithLabels(key, float32(val), labels, opts...) 314 } 315 func (a *abstract) addSampleWithLabels(key []string, v any, labels []metrics.Label, opts ...utils.OptionExtender) { 316 val, ok := v.(float64) 317 if !ok { 318 return 319 } 320 opt := utils.ApplyOptions[option](opts...) 321 if opt.precision { 322 a.Metrics.AddPrecisionSampleWithLabels(key, val, labels, opts...) 323 } else { 324 a.Metrics.AddSampleWithLabels(key, float32(val), labels, opts...) 325 } 326 } 327 func (a *abstract) measureSinceWithLabels(key []string, v any, labels []metrics.Label, opts ...utils.OptionExtender) { 328 start, ok := v.(time.Time) 329 if !ok { 330 return 331 } 332 a.Metrics.MeasureSinceWithLabels(key, start, labels, opts...) 333 } 334 335 func (a *abstract) convertLabels(src []Label) (dst []metrics.Label) { 336 return utils.SliceMapping(src, func(l Label) metrics.Label { 337 return metrics.Label{ 338 Name: l.Key, 339 Value: l.Value, 340 } 341 }) 342 } 343 func (a *abstract) convertOpts(src ...utils.OptionExtender) (dst []utils.OptionExtender) { 344 if src == nil { 345 return 346 } 347 348 dst = make([]utils.OptionExtender, 0, len(src)) 349 opt := utils.ApplyOptions[option](src...) 350 if opt.precision { 351 dst = append(dst, metrics.Precision()) 352 } 353 if len(opt.prometheusBuckets) > 0 { 354 dst = append(dst, prometheus.Bucket(opt.prometheusBuckets)) 355 } 356 357 return 358 }