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  }