metrics

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 4, 2024 License: MIT Imports: 20 Imported by: 1

README

go-simple-metrics

This library provides a simple metrics package which can be used to instrument code, expose application metrics, and profile runtime performance in a flexible manner. It is derived from the fantastic and widely used version of @armon's go-metrics. Several configurable sinks allow metrics to be exported to observability systems, often via statsd-like bridges.

See the section on Design Philosophies below to understand the guiding design principles for this library.

NOTE: This is an early version of this library so caution should be used before using it in production. There are no guarantees that breaking changes may not occur in early updates.

GoDoc

Examples

Here is an example of using the package:

func SlowMethod() {
    // Profiling the runtime of a method
    defer metrics.MeasureSince("SlowMethod", time.Now(), metrics.L("method_name", "slow"))

    // do something slow
}

// Configure a statsite sink as the global metrics sink
sink, _ := metrics.NewStatsiteSink("statsite:8125")
metrics.NewGlobal(sink, func(cfg *metrics.Config) {
    cfg.ServiceName = "service-name"
})

// count an occurrence
metrics.Incr("method-counter", 1)

// sample an observed value, add some labels
metrics.Sample("queue-latency", 43.5, metrics.L("name", "msgs"), metrics.L("type", "sqs"))

Sinks

The metrics package makes use of a MetricSink interface to support delivery to any type of backend. Currently, the following sinks are provided:

  • StatsiteSink : Sinks to a statsite instance (TCP)
  • Datadog: Sinks to a DataDog dogstatsd instance.
  • PrometheusSink: Sinks to a Prometheus metrics endpoint (exposed via HTTP for scrapes)
  • InmemSink : Provides in-memory aggregation, can be used to export stats or for testing
  • FanoutSink : Sinks to multiple sinks. Enables writing to multiple statsite instances for example.
  • BlackholeSink : Sinks to nowhere

In addition to the sinks, the InmemSignal can be used to catch a signal, and dump a formatted output of recent metrics. For example, when a process gets a SIGUSR1, it can dump to stderr recent performance metrics for debugging.

Simple Instrumentation Methods

Gauge: SetGauge()

A gauge represents the current value of some observed property, for example the current number of items pending in a queue. While multiple observations may be recorded over an interval, typically a sink will keep only the last recorded value.

Counter: Incr()

Counters represent occurrences of an event over time and are typically graphed as a rate. For example, the total number of messages read from a queue.

Histogram: Sample()

Histograms track aggregations and quantile values over multiple observed values. For example, the number of bytes in each message received from a queue, recorded per-message, may be tracked as a histogram. Aggregation sinks may typically calculate quantile metrics like 95th percentile.

Timer: MeasureSince()

Timers are a special purpose histogram for tracking time metrics, like latency. The MeasureSince method makes it easy to record the time spent since some the start of an event. When invoked with a defer as the example above shows, it makes it easy to record the time of a code block.

Distribution: Observe()

A distribution is a specific type of histogram that provides some additional quantile flexibility and accuracy. It is mostly provided for the Datadog sink, other sinks at the moment implement distributions on top of the histogram support.

Memoized Metrics

In most scenarios the one-liner methods above should be enough to instrument any block of code quickly and easily. However, in tight-loop hot paths there can be some overhead to construct the metric names and labels and sanitize the names for the resulting sink. In these cases where multiple observations from the same metric name and labels are expected, you can use a memoized version of the metric and emit new observations as needed with lower overhead.

For example, using the memoized counter in a message processing loop:


func pollMessages(messages chan<- string) {
	c := metrics.NewCounter("msg-count", metrics.L("queue", "msgs"))
	for msg := range messages {
		c.Incr(1)
		fmt.Println("received ", msg)
	}
}

There are similar methods for all metric types: NewGauge, NewHistogram, NewTimer, NewDistribution.

Persisted and Aggregated Metrics

Finally, there are two special metric types, PersistedGauge and AggregatedCounter, that can be used for even further performance improvements. Both of these elide updates to the sink across individual metric observations, instead publishing aggregated updates to the sink once per publishing interval. This can help when you want to further reduce the overhead of a 1:1 ratio of observation and sink update.

CAUTION: Because these metrics batch updates, there is a chance for some data loss in the case of an unhandled crash of an application.

Persisted Gauges

Persisted gauges maintain an observed value internally and publish the last seen value on the publishing interval. The behavior is the same as using the SetGauge method, however instead of each call posting a value to the sink and the sink (e.g. agent) keeping the last value, this happens before the sink.

For example, if you wanted to track the active number of open connections on a busy server:

type watcher struct {
	gauge metrics.PersistentGauge
}

func (w *watcher) OnStateChange(conn net.Conn, state http.ConnState) {
	switch state {
	case http.StateNew:
		w.gauge.Incr(1)
	case http.StateHijacked, http.StateClosed:
		w.gauge.Decr(1)
	}
}

w := &watcher{
	gauge: metrics.NewPersistentGauge("server.active-conns", metrics.L("port", "443")),
}

s := &http.Server{
	ConnState: w.OnStateChange
}

// run server

// when finished, you must stop the gauge
w.gauge.Stop()
Aggregated Counters

An aggregated counter can be useful for extremely hot-path metric instrumentation. It aggregates the total increment delta internally and publishes the current delta on each report interval. Unlike the Persisted Gauge, an Aggregated Counter will reset its value to zero on each reporting interval.

Here's the example from earlier using an aggregated counter instead:


func pollMessages(messages chan<- string) {
	c := metrics.NewAggregatedCounter("msg-count", metrics.L("queue", "msgs"))
	for msg := range messages {
		c.Incr(1)
		fmt.Println("received ", msg)
	}
	// must stop the counter when finished to stop reporting
	c.Stop()
}

Benchmarking

We run three benchmarks comparing the following:

  • Simple Counter: running Incr() in a loop
  • Memoized Counter: creating a counter using NewCounter() and testing counter.Incr()
  • Aggregated Counter: creating an aggregated counter with NewAggregatedCounter() and testing counter.Incr()
BenchmarkSimpleCounter-8       	 1503106	      1593 ns/op	     904 B/op	      24 allocs/op
BenchmarkMemoizedCounter-8     	24951338	        93.65 ns/op	      80 B/op	       1 allocs/op
BenchmarkAggregatedCounter-8   	388790562	         6.050 ns/op	       0 B/op	       0 allocs/op

Design Philosophy

This library draws on a few principles required for modern metrics instrumentation and observability.

Metrics are multidimensional

Call them tags or labels, but multidimensional metrics are the standard and observability systems must treat them as first-class citizens. Similarly, they should be the default for instrumentation libraries, allowing the ability to configure them globally (e.g., enriched with infra dimensions) as well as per-metric based on application context.

Instrumentation should be succinct

Instrumenting code should not distract from the content of the code itself. It should be clear to a reviewer where and how the code has been instrumented, but readability of the application must not be sacrificed.

Quick to instrument

Instrumenting foreign code blocks is particularly useful when dropping into a codebase for the first time, possibly under pressure of an ongoing incident. You must be able to instrument quickly without having to understand larger structures of the code or refactor major portions to inject the right instrumentation. Instrumentation should only require a single line of code in most circumstances.

Low impact

Generally developers should not have to worry whether instrumenting a block of code may negatively impact the performance of their application in a severe way. Instrumentation must be lightweight enough that in most scenarios there is little impact to introducing it to the code. In extreme cases where that may not be possible there should be graduated levels of instrumentation methods available to use in those hot paths, even if they may conflict partially with the previous two design points.

Documentation

Index

Constants

View Source
const (
	// DefaultSignal is used with DefaultInmemSignal
	DefaultSignal = syscall.SIGUSR1
)

Variables

This section is empty.

Functions

func Incr

func Incr[V StatValue](key string, val V, labels ...Label)

Incr increments a counter

func MeasureSince

func MeasureSince(key string, start time.Time, labels ...Label)

MeasureSince records the time elapsed since an event, often as a histogram

func Observe

func Observe[V StatValue](key string, val V, labels ...Label)

Observe records an observation as part of a distribution

func Sample

func Sample[V StatValue](key string, val V, labels ...Label)

Sample records an observation in a histogram

func SetGauge

func SetGauge[V StatValue](key string, val V, labels ...Label)

SetGauge records the current observed value

func Shutdown

func Shutdown()

Shutdown disables metric collection, then blocks while attempting to flush metrics to storage. WARNING: Not all MetricSink backends support this functionality, and calling this will cause them to leak resources. This is intended for use immediately prior to application exit.

Types

type AggregateSample

type AggregateSample struct {
	Count       int       // The count of emitted pairs
	Rate        float64   // The values rate per time unit (usually 1 second)
	Sum         float64   // The sum of values
	SumSq       float64   `json:"-"` // The sum of squared values
	Min         float64   // Minimum value
	Max         float64   // Maximum value
	LastUpdated time.Time `json:"-"` // When value was last updated
}

AggregateSample is used to hold aggregate metrics about a sample

func (*AggregateSample) Ingest

func (a *AggregateSample) Ingest(v float64, rateDenom float64)

Ingest is used to update a sample

func (*AggregateSample) Mean

func (a *AggregateSample) Mean() float64

Computes a mean of the values

func (*AggregateSample) Stddev

func (a *AggregateSample) Stddev() float64

Computes a Stddev of the values

func (*AggregateSample) String

func (a *AggregateSample) String() string

type AggregatedCounter

type AggregatedCounter interface {
	Stop()
	Incr(delta int64)
}

An AggregatedCounter can be useful for extremely hot-path metric instrumentation. It aggregates the total increment delta internally and publishes the current delta on each report interval. Unlike the PersistentGauge, an AggregatedCounter will reset its value to zero on each reporting interval.

func NewAggregatedCounter

func NewAggregatedCounter(key string, labels ...Label) AggregatedCounter

type BlackholeSink

type BlackholeSink struct{}

BlackholeSink is used to just blackhole messages

func (*BlackholeSink) BuildMetricEmitter

func (s *BlackholeSink) BuildMetricEmitter(_ MetricType, _ []string, _ []Label) MetricEmitter

type Config

type Config struct {
	ServiceName          string        // Name of service, added to labels if EnableServiceLabel is set
	HostName             string        // Hostname to use. If not provided and EnableHostname, it will be os.Hostname
	EnableHostnameLabel  bool          // Enable adding hostname to labels
	EnableServiceLabel   bool          // Enable adding service to labels
	EnableServicePrefix  bool          // Enable adding service to the metrics key
	EnableRuntimeMetrics bool          // Enables profiling of runtime metrics (GC, Goroutines, Memory)
	EnableTypePrefix     bool          // Prefixes key with a type ("counter", "gauge", "timer")
	TimerGranularity     time.Duration // Granularity of timers.
	ProfileInterval      time.Duration // Interval to profile runtime metrics
	PersistentInterval   time.Duration // Interval to publish persisted metrics

	BaseLabels []Label // Default labels applied to all measurements

	AllowedPrefixes []string // A list of metric prefixes to allow, with '.' as the separator
	BlockedPrefixes []string // A list of metric prefixes to block, with '.' as the separator
	AllowedLabels   []string // A list of metric labels to allow, with '.' as the separator
	BlockedLabels   []string // A list of metric labels to block, with '.' as the separator
	FilterDefault   bool     // Whether to allow metrics by default
}

Config is used to configure metrics settings

type ConfigOption added in v0.0.3

type ConfigOption func(cfg *Config)

type Counter

type Counter interface {
	Incr(val float64)
}

func NewCounter

func NewCounter(key string, labels ...Label) Counter

NewCounter creates a memoized counter

type Distribution

type Distribution interface {
	Observe(val float64)
}

func NewDistribution

func NewDistribution(key string, labels ...Label) Distribution

NewDistribution creates a memoized histogram

type Encoder

type Encoder interface {
	Encode(interface{}) error
}

type FanoutSink

type FanoutSink struct {
	Sinks []MetricSink
}

FanoutSink is used to sink to fanout values to multiple sinks

func (FanoutSink) BuildMetricEmitter

func (fh FanoutSink) BuildMetricEmitter(mType MetricType, keys []string, labels []Label) MetricEmitter

func (FanoutSink) Shutdown

func (fh FanoutSink) Shutdown()

type Gauge

type Gauge interface {
	Set(val float64)
}

func NewGauge

func NewGauge(key string, labels ...Label) Gauge

NewGauge creates a memoized gauge

type GaugeValue

type GaugeValue struct {
	Name  string
	Hash  string `json:"-"`
	Value float64

	Labels        []Label           `json:"-"`
	DisplayLabels map[string]string `json:"Labels"`
}

type Histogram

type Histogram interface {
	Sample(val float64)
}

func NewHistogram

func NewHistogram(key string, labels ...Label) Histogram

NewHistogram creates a memoized histogram

type InmemSignal

type InmemSignal struct {
	// contains filtered or unexported fields
}

InmemSignal is used to listen for a given signal, and when received, to dump the current metrics from the InmemSink to an io.Writer

func DefaultInmemSignal

func DefaultInmemSignal(inmem *InmemSink) *InmemSignal

DefaultInmemSignal returns a new InmemSignal that responds to SIGUSR1 and writes output to stderr. Windows uses SIGBREAK

func NewInmemSignal

func NewInmemSignal(inmem *InmemSink, sig syscall.Signal, w io.Writer) *InmemSignal

NewInmemSignal creates a new InmemSignal which listens for a given signal, and dumps the current metrics out to a writer

func (*InmemSignal) Stop

func (i *InmemSignal) Stop()

Stop is used to stop the InmemSignal from listening

type InmemSink

type InmemSink struct {
	// contains filtered or unexported fields
}

InmemSink provides a MetricSink that does in-memory aggregation without sending metrics over a network. It can be embedded within an application to provide profiling information.

func NewInmemSink

func NewInmemSink(interval, retain time.Duration) *InmemSink

NewInmemSink is used to construct a new in-memory sink. Uses an aggregation interval and maximum retention period.

func (*InmemSink) BuildMetricEmitter

func (i *InmemSink) BuildMetricEmitter(mType MetricType, keys []string, labels []Label) MetricEmitter

func (*InmemSink) Data

func (i *InmemSink) Data() []*IntervalMetrics

Data is used to retrieve all the aggregated metrics Intervals may be in use, and a read lock should be acquired

func (*InmemSink) DisplayMetrics

func (i *InmemSink) DisplayMetrics(resp http.ResponseWriter, req *http.Request) (interface{}, error)

DisplayMetrics returns a summary of the metrics from the most recent finished interval.

func (*InmemSink) Stream

func (i *InmemSink) Stream(ctx context.Context, encoder Encoder)

Stream writes metrics using encoder.Encode each time an interval ends. Runs until the request context is cancelled, or the encoder returns an error. The caller is responsible for logging any errors from encoder.

type IntervalMetrics

type IntervalMetrics struct {
	sync.RWMutex

	// The start time of the interval
	Interval time.Time

	// Gauges maps the key to the last set value
	Gauges map[string]GaugeValue

	// Counters maps the string key to a sum of the counter
	// values
	Counters map[string]SampledValue

	// Samples maps the key to an AggregateSample,
	// which has the rolled up view of a sample
	Samples map[string]SampledValue
	// contains filtered or unexported fields
}

IntervalMetrics stores the aggregated metrics for a specific interval

func NewIntervalMetrics

func NewIntervalMetrics(intv time.Time) *IntervalMetrics

NewIntervalMetrics creates a new IntervalMetrics for a given interval

type Label

type Label struct {
	Name  string
	Value string
}

func L

func L(name string, value string) Label

L is a shorthand for creating a label

type MetricEmitter

type MetricEmitter func(val float64)

type MetricSink

type MetricSink interface {
	BuildMetricEmitter(mType MetricType, keys []string, labels []Label) MetricEmitter
}

The MetricSink interface is used to transmit metrics information to an external system

func NewInmemSinkFromURL

func NewInmemSinkFromURL(u *url.URL) (MetricSink, error)

NewInmemSinkFromURL creates an InmemSink from a URL. It is used (and tested) from NewMetricSinkFromURL.

func NewMetricSinkFromURL

func NewMetricSinkFromURL(urlStr string) (MetricSink, error)

NewMetricSinkFromURL allows a generic URL input to configure any of the supported sinks. The scheme of the URL identifies the type of the sink, the and query parameters are used to set options.

"statsd://" - Initializes a StatsdSink. The host and port are passed through as the "addr" of the sink

"statsite://" - Initializes a StatsiteSink. The host and port become the "addr" of the sink

"inmem://" - Initializes an InmemSink. The host and port are ignored. The "interval" and "duration" query parameters must be specified with valid durations, see NewInmemSink for details.

func NewStatsiteSinkFromURL

func NewStatsiteSinkFromURL(u *url.URL) (MetricSink, error)

NewStatsiteSinkFromURL creates an StatsiteSink from a URL. It is used (and tested) from NewMetricSinkFromURL.

type MetricType

type MetricType int
const (
	MetricTypeCounter MetricType = iota
	MetricTypeGauge
	MetricTypeTimer
	MetricTypeHistogram
	MetricTypeDistribution
)

type Metrics

type Metrics struct {
	// contains filtered or unexported fields
}

Metrics represents an instance of a metrics sink that can be used to emit

func Default

func Default() *Metrics

Default returns the shared global metrics instance.

func New

func New(sink MetricSink, opts ...ConfigOption) (*Metrics, error)

New is used to create a new instance of Metrics

func NewGlobal

func NewGlobal(sink MetricSink, opts ...ConfigOption) (*Metrics, error)

NewGlobal is the same as New, but it assigns the metrics object to be used globally as well as returning it.

func (*Metrics) Incr

func (m *Metrics) Incr(key string, val float64, labels ...Label)

func (*Metrics) MeasureSince

func (m *Metrics) MeasureSince(key string, start time.Time, labels ...Label)

func (*Metrics) NewAggregatedCounter

func (m *Metrics) NewAggregatedCounter(key string, labels ...Label) AggregatedCounter

func (*Metrics) NewCounter

func (m *Metrics) NewCounter(key string, labels ...Label) Counter

func (*Metrics) NewDistribution

func (m *Metrics) NewDistribution(key string, labels ...Label) Distribution

func (*Metrics) NewGauge

func (m *Metrics) NewGauge(key string, labels ...Label) Gauge

func (*Metrics) NewHistogram

func (m *Metrics) NewHistogram(key string, labels ...Label) Histogram

func (*Metrics) NewPersistentGauge

func (m *Metrics) NewPersistentGauge(key string, labels ...Label) PersistentGauge

func (*Metrics) NewTimer

func (m *Metrics) NewTimer(key string, labels ...Label) Timer

func (*Metrics) Observe

func (m *Metrics) Observe(key string, val float64, labels ...Label)

func (*Metrics) Sample

func (m *Metrics) Sample(key string, val float64, labels ...Label)

func (*Metrics) SetGauge

func (m *Metrics) SetGauge(key string, val float64, labels ...Label)

Proxy all the methods to the globalMetrics instance

func (*Metrics) Shutdown

func (m *Metrics) Shutdown()

type MetricsSummary

type MetricsSummary struct {
	Timestamp string
	Gauges    []GaugeValue
	Counters  []SampledValue
	Samples   []SampledValue
}

MetricsSummary holds a roll-up of metrics info for a given interval

type PersistentGauge

type PersistentGauge interface {
	Stop()
	Set(val int64) int64
	Incr(val int64) int64
	Decr(val int64) int64
}

A PersistentGauge maintains an observed value internally and publish the last seen value on the publishing interval. The behavior is the same as using the SetGauge method, however instead of each call posting a value to the sink and the sink (e.g. agent) keeping the last value, this happens before the sink.

func NewPersistentGauge

func NewPersistentGauge(key string, labels ...Label) PersistentGauge

type SampledValue

type SampledValue struct {
	Name string
	Hash string `json:"-"`
	*AggregateSample
	Mean   float64
	Stddev float64

	Labels        []Label           `json:"-"`
	DisplayLabels map[string]string `json:"Labels"`
}

type ShutdownSink

type ShutdownSink interface {
	MetricSink

	// Shutdown the metric sink, flush metrics to storage, and cleanup resources.
	// Called immediately prior to application exit. Implementations must block
	// until metrics are flushed to storage.
	Shutdown()
}

type StatValue

type StatValue interface {
	int | int8 | int32 | int64 | float32 | float64
}

StatValue defines the allowed values for the simple interface

type StatsiteSink

type StatsiteSink struct {
	// contains filtered or unexported fields
}

StatsiteSink provides a MetricSink that can be used with a statsite metrics server

func NewStatsiteSink

func NewStatsiteSink(addr string) (*StatsiteSink, error)

NewStatsiteSink is used to create a new StatsiteSink

func (*StatsiteSink) BuildMetricEmitter

func (s *StatsiteSink) BuildMetricEmitter(mType MetricType, keys []string, labels []Label) MetricEmitter

func (*StatsiteSink) Shutdown

func (s *StatsiteSink) Shutdown()

Close is used to stop flushing to statsite

type Timer

type Timer interface {
	MeasureSince(start time.Time)
}

func NewTimer

func NewTimer(key string, labels ...Label) Timer

NewTimer creates a memoized timer

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL