github.com/Jeffail/benthos/v3@v3.65.0/lib/metrics/stdout.go (about)

     1  package metrics
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"runtime"
     7  	"strconv"
     8  	"strings"
     9  	"sync/atomic"
    10  	"time"
    11  
    12  	"github.com/Jeffail/benthos/v3/internal/docs"
    13  	"github.com/Jeffail/benthos/v3/lib/log"
    14  	"github.com/Jeffail/gabs/v2"
    15  )
    16  
    17  //------------------------------------------------------------------------------
    18  
    19  func init() {
    20  	Constructors[TypeStdout] = TypeSpec{
    21  		constructor: NewStdout,
    22  		Status:      docs.StatusBeta,
    23  		Summary: `
    24  Prints aggregated metrics as JSON objects to stdout.`,
    25  		Description: `
    26  When Benthos shuts down all aggregated metrics are printed. If a
    27  ` + "`push_interval`" + ` is specified then metrics are also printed
    28  periodically.`,
    29  		FieldSpecs: docs.FieldSpecs{
    30  			docs.FieldCommon("push_interval", "An optional period of time to continuously print metrics."),
    31  			docs.FieldCommon("static_fields", "A map of static fields to add to each flushed metric object.").Map().HasDefault(map[string]interface{}{
    32  				"@service": "benthos",
    33  			}).HasType(docs.FieldTypeUnknown),
    34  			docs.FieldCommon("flush_metrics", "Whether counters and timing metrics should be reset to 0 each time metrics are printed."),
    35  			pathMappingDocs(false, false),
    36  		},
    37  	}
    38  }
    39  
    40  //------------------------------------------------------------------------------
    41  
    42  // StdoutConfig contains configuration parameters for the Stdout metrics
    43  // aggregator.
    44  type StdoutConfig struct {
    45  	PushInterval string                 `json:"push_interval" yaml:"push_interval"`
    46  	StaticFields map[string]interface{} `json:"static_fields" yaml:"static_fields"`
    47  	FlushMetrics bool                   `json:"flush_metrics" yaml:"flush_metrics"`
    48  	PathMapping  string                 `json:"path_mapping" yaml:"path_mapping"`
    49  }
    50  
    51  // NewStdoutConfig returns a new StdoutConfig with default values.
    52  func NewStdoutConfig() StdoutConfig {
    53  	return StdoutConfig{
    54  		PushInterval: "",
    55  		StaticFields: map[string]interface{}{
    56  			"@service": "benthos",
    57  		},
    58  		FlushMetrics: false,
    59  		PathMapping:  "",
    60  	}
    61  }
    62  
    63  //------------------------------------------------------------------------------
    64  
    65  // Stdout is an object with capability to hold internal stats and emit them as
    66  // individual JSON objects via stdout.
    67  type Stdout struct {
    68  	local     *Local
    69  	timestamp time.Time
    70  	log       log.Modular
    71  
    72  	pathMapping *pathMapping
    73  	config      StdoutConfig
    74  	closedChan  chan struct{}
    75  	running     int32
    76  
    77  	staticFields []byte
    78  }
    79  
    80  // NewStdout creates and returns a new Stdout metric object.
    81  func NewStdout(config Config, opts ...func(Type)) (Type, error) {
    82  	t := &Stdout{
    83  		local:      NewLocal(),
    84  		timestamp:  time.Now(),
    85  		config:     config.Stdout,
    86  		closedChan: make(chan struct{}),
    87  		running:    1,
    88  	}
    89  
    90  	//TODO: add field interpolation here
    91  	sf, err := json.Marshal(config.Stdout.StaticFields)
    92  	if err != nil {
    93  		return t, fmt.Errorf("failed to parse static fields: %v", err)
    94  	}
    95  	t.staticFields = sf
    96  
    97  	for _, opt := range opts {
    98  		opt(t)
    99  	}
   100  
   101  	if t.pathMapping, err = newPathMapping(t.config.PathMapping, t.log); err != nil {
   102  		return nil, fmt.Errorf("failed to init path mapping: %v", err)
   103  	}
   104  
   105  	if len(t.config.PushInterval) > 0 {
   106  		interval, err := time.ParseDuration(t.config.PushInterval)
   107  		if err != nil {
   108  			return nil, fmt.Errorf("failed to parse push interval: %v", err)
   109  		}
   110  		go func() {
   111  			for {
   112  				select {
   113  				case <-t.closedChan:
   114  					return
   115  				case <-time.After(interval):
   116  					t.publishMetrics()
   117  				}
   118  			}
   119  		}()
   120  	}
   121  
   122  	return t, nil
   123  }
   124  
   125  //------------------------------------------------------------------------------
   126  
   127  // writeMetric prints a metric object with any configured extras merged in to
   128  // t.
   129  func (s *Stdout) writeMetric(metricSet *gabs.Container) {
   130  	base, _ := gabs.ParseJSON(s.staticFields)
   131  	base.SetP(time.Now().Format(time.RFC3339), "@timestamp")
   132  	base.Merge(metricSet)
   133  
   134  	fmt.Printf("%s\n", base.String())
   135  }
   136  
   137  // publishMetrics
   138  func (s *Stdout) publishMetrics() {
   139  	counterObjs := make(map[string]*gabs.Container)
   140  
   141  	var counters map[string]int64
   142  	var timings map[string]int64
   143  	if s.config.FlushMetrics {
   144  		counters = s.local.FlushCounters()
   145  		timings = s.local.FlushTimings()
   146  	} else {
   147  		counters = s.local.GetCounters()
   148  		timings = s.local.GetTimings()
   149  	}
   150  
   151  	s.constructMetrics(counterObjs, counters)
   152  	s.constructMetrics(counterObjs, timings)
   153  
   154  	system := make(map[string]int64)
   155  	uptime := time.Since(s.timestamp).Milliseconds()
   156  	goroutines := runtime.NumGoroutine()
   157  	system["system.uptime"] = uptime
   158  	system["system.goroutines"] = int64(goroutines)
   159  	s.constructMetrics(counterObjs, system)
   160  
   161  	for _, o := range counterObjs {
   162  		s.writeMetric(o)
   163  	}
   164  }
   165  
   166  // constructMetrics groups individual Benthos metrics contained in a map into
   167  // a container for each component instance.  For example,
   168  // pipeline.processor.1.count and pipeline.processor.1.error would be grouped
   169  // into a single pipeline.processor.1 object.
   170  func (s *Stdout) constructMetrics(co map[string]*gabs.Container, metrics map[string]int64) {
   171  	for k, v := range metrics {
   172  		parts := strings.Split(k, ".")
   173  		var objKey string
   174  		var valKey string
   175  		// walk key parts backwards building up objects of instances of processor/broker/etc
   176  		for i := len(parts) - 1; i >= 0; i-- {
   177  			if _, err := strconv.Atoi(parts[i]); err == nil {
   178  				// part is a reference to an index of a processor/broker/etc
   179  				objKey = strings.Join(parts[:i+1], ".")
   180  				valKey = strings.Join(parts[i+1:], ".")
   181  				break
   182  			}
   183  		}
   184  
   185  		if objKey == "" {
   186  			// key is not referencing an 'instance' of a processor/broker/etc
   187  			objKey = parts[0]
   188  			valKey = strings.Join(parts[0:], ".")
   189  		}
   190  
   191  		_, exists := co[objKey]
   192  		if !exists {
   193  			co[objKey] = gabs.New()
   194  			co[objKey].SetP(objKey, "metric")
   195  			co[objKey].SetP(parts[0], "component")
   196  
   197  		}
   198  		co[objKey].SetP(v, valKey)
   199  	}
   200  }
   201  
   202  // GetCounter returns a stat counter object for a path.
   203  func (s *Stdout) GetCounter(path string) StatCounter {
   204  	if path = s.pathMapping.mapPathNoTags(path); path == "" {
   205  		return DudStat{}
   206  	}
   207  	return s.local.GetCounter(path)
   208  }
   209  
   210  // GetCounterVec returns a stat counter object for a path with the labels
   211  // discarded.
   212  func (s *Stdout) GetCounterVec(path string, n []string) StatCounterVec {
   213  	path = s.pathMapping.mapPathNoTags(path)
   214  	return fakeCounterVec(func([]string) StatCounter {
   215  		if path == "" {
   216  			return DudStat{}
   217  		}
   218  		return s.local.GetCounter(path)
   219  	})
   220  }
   221  
   222  // GetTimer returns a stat timer object for a path.
   223  func (s *Stdout) GetTimer(path string) StatTimer {
   224  	if path = s.pathMapping.mapPathNoTags(path); path == "" {
   225  		return DudStat{}
   226  	}
   227  	return s.local.GetTimer(path)
   228  }
   229  
   230  // GetTimerVec returns a stat timer object for a path with the labels
   231  // discarded.
   232  func (s *Stdout) GetTimerVec(path string, n []string) StatTimerVec {
   233  	path = s.pathMapping.mapPathNoTags(path)
   234  	return fakeTimerVec(func([]string) StatTimer {
   235  		if path == "" {
   236  			return DudStat{}
   237  		}
   238  		return s.local.GetTimer(path)
   239  	})
   240  }
   241  
   242  // GetGauge returns a stat gauge object for a path.
   243  func (s *Stdout) GetGauge(path string) StatGauge {
   244  	if path = s.pathMapping.mapPathNoTags(path); path == "" {
   245  		return DudStat{}
   246  	}
   247  	return s.local.GetGauge(path)
   248  }
   249  
   250  // GetGaugeVec returns a stat timer object for a path with the labels
   251  // discarded.
   252  func (s *Stdout) GetGaugeVec(path string, n []string) StatGaugeVec {
   253  	path = s.pathMapping.mapPathNoTags(path)
   254  	return fakeGaugeVec(func([]string) StatGauge {
   255  		if path == "" {
   256  			return DudStat{}
   257  		}
   258  		return s.local.GetGauge(path)
   259  	})
   260  }
   261  
   262  // SetLogger does nothing.
   263  func (s *Stdout) SetLogger(log log.Modular) {
   264  	s.log = log
   265  }
   266  
   267  // Close stops the Stdout object from aggregating metrics and does a publish
   268  // (write to stdout) of metrics.
   269  func (s *Stdout) Close() error {
   270  	if atomic.CompareAndSwapInt32(&s.running, 1, 0) {
   271  		close(s.closedChan)
   272  	}
   273  	s.publishMetrics()
   274  
   275  	return nil
   276  }
   277  
   278  //------------------------------------------------------------------------------