├── .circleci └── config.yml ├── .github ├── CODEOWNERS └── workflows │ └── build-test.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── circonus ├── circonus.go └── circonus_test.go ├── compat ├── armon.go ├── circonus │ ├── armon.go │ └── hashicorp.go ├── datadog │ ├── armon.go │ └── hashicorp.go ├── hashicorp.go └── prometheus │ ├── armon.go │ └── hashicorp.go ├── const_js.go ├── const_unix.go ├── const_windows.go ├── datadog ├── dogstatsd.go └── dogstatsd_test.go ├── go.mod ├── go.sum ├── inmem.go ├── inmem_endpoint.go ├── inmem_endpoint_test.go ├── inmem_signal.go ├── inmem_signal_test.go ├── inmem_test.go ├── metrics.go ├── metrics_test.go ├── prometheus ├── prometheus.go └── prometheus_test.go ├── sink.go ├── sink_test.go ├── start.go ├── start_test.go ├── statsd.go ├── statsd_test.go ├── statsite.go └── statsite_test.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MIT 3 | 4 | version: 2.1 5 | 6 | orbs: 7 | go: gotest/tools@0.0.13 8 | 9 | workflows: 10 | ci: 11 | jobs: 12 | - go/test: 13 | name: test-golang-1.16 14 | executor: 15 | name: go/golang 16 | tag: "1.16" 17 | cgo-enabled: "1" 18 | go-test-flags: -race 19 | - go/test: 20 | name: test-golang-1.17 21 | executor: 22 | name: go/golang 23 | tag: "1.17" 24 | cgo-enabled: "1" 25 | go-test-flags: -race 26 | - go/test: 27 | name: test-golang-1.18 28 | executor: 29 | name: go/golang 30 | tag: "1.18" 31 | cgo-enabled: "1" 32 | go-test-flags: -race 33 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Each line is a file pattern followed by one or more owners. 2 | # More on CODEOWNERS files: https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners 3 | 4 | # Default owner 5 | * @hashicorp/team-ip-compliance 6 | 7 | # Add override rules below. Each line is a file/folder pattern followed by one or more owners. 8 | # Being an owner means those groups or individuals will be added as reviewers to PRs affecting 9 | # those areas of the code. 10 | # Examples: 11 | # /docs/ @docs-team 12 | # *.js @js-team 13 | # *.go @go-team -------------------------------------------------------------------------------- /.github/workflows/build-test.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout Code 12 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 13 | - name: Setup Go 14 | uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a 15 | with: 16 | go-version: '1.23' 17 | - name: Run Tests 18 | run: go test ./... 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | 24 | /metrics.out 25 | 26 | .idea 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MIT 3 | 4 | language: go 5 | 6 | go: 7 | - "1.x" 8 | 9 | env: 10 | - GO111MODULE=on 11 | 12 | install: 13 | - go get ./... 14 | 15 | script: 16 | - go test ./... 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 HashiCorp, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-metrics 2 | ========== 3 | 4 | This library provides a `metrics` package which can be used to instrument code, 5 | expose application metrics, and profile runtime performance in a flexible manner. 6 | 7 | Current API: [![GoDoc](https://godoc.org/github.com/hashicorp/go-metrics?status.svg)](https://godoc.org/github.com/hashicorp/go-metrics) 8 | 9 | Sinks 10 | ----- 11 | 12 | The `metrics` package makes use of a `MetricSink` interface to support delivery 13 | to any type of backend. Currently the following sinks are provided: 14 | 15 | * StatsiteSink : Sinks to a [statsite](https://github.com/statsite/statsite/) instance (TCP) 16 | * StatsdSink: Sinks to a [StatsD](https://github.com/statsd/statsd/) / statsite instance (UDP) 17 | * PrometheusSink: Sinks to a [Prometheus](http://prometheus.io/) metrics endpoint (exposed via HTTP for scrapes) 18 | * InmemSink : Provides in-memory aggregation, can be used to export stats 19 | * FanoutSink : Sinks to multiple sinks. Enables writing to multiple statsite instances for example. 20 | * BlackholeSink : Sinks to nowhere 21 | 22 | In addition to the sinks, the `InmemSignal` can be used to catch a signal, 23 | and dump a formatted output of recent metrics. For example, when a process gets 24 | a SIGUSR1, it can dump to stderr recent performance metrics for debugging. 25 | 26 | Labels 27 | ------ 28 | 29 | Most metrics do have an equivalent ending with `WithLabels`, such methods 30 | allow to push metrics with labels and use some features of underlying Sinks 31 | (ex: translated into Prometheus labels). 32 | 33 | Since some of these labels may increase the cardinality of metrics, the 34 | library allows filtering labels using a allow/block list filtering system 35 | which is global to all metrics. 36 | 37 | * If `Config.AllowedLabels` is not nil, then only labels specified in this value will be sent to underlying Sink, otherwise, all labels are sent by default. 38 | * If `Config.BlockedLabels` is not nil, any label specified in this value will not be sent to underlying Sinks. 39 | 40 | By default, both `Config.AllowedLabels` and `Config.BlockedLabels` are nil, meaning that 41 | no tags are filtered at all, but it allows a user to globally block some tags with high 42 | cardinality at the application level. 43 | 44 | Backwards Compatibility 45 | ----------------------- 46 | v0.5.0 of the library renamed the Go module from `github.com/armon/go-metrics` to `github.com/hashicorp/go-metrics`. 47 | While this did not introduce any breaking changes to the API, the change did subtly break backwards compatibility. 48 | 49 | In essence, Go treats a renamed module as entirely distinct and will happily compile both modules into the same binary. 50 | Due to most uses of the go-metrics library involving emitting metrics via the global metrics handler, having two global 51 | metrics handlers could cause a subset of metrics to be effectively lost. As an example, if your application configures 52 | go-metrics exporting via the `armon` namespace, then any metrics sent to go-metrics via the `hashicorp` namespaced module 53 | will never get exported. 54 | 55 | Eventually all usage of `armon/go-metrics` should be replaced with usage of `hashicorp/go-metrics`. However, a single 56 | point-in-time coordinated update across all libraries that an application may depend on isn't always feasible. To facilitate migrations, 57 | a `github.com/hashicorp/go-metrics/compat` package has been introduced. This package and sub-packages are API compatible with 58 | `armon/go-metrics`. Libraries should be updated to use this package for emitting metrics via the global handlers. Internally, 59 | the package will route metrics to either `armon/go-metrics` or `hashicorp/go-metrics`. This is achieved at a global level 60 | within an application via the use of Go build tags. 61 | 62 | **Build Tags** 63 | * `armonmetrics` - Using this tag will cause metrics to be routed to `armon/go-metrics` 64 | * `hashicorpmetrics` - Using this tag will cause all metrics to be routed to `hashicorp/go-metrics` 65 | 66 | If no build tag is specified, the default behavior is to use `armon/go-metrics`. The overall migration path would be as follows: 67 | 68 | 1. Upgrade libraries using `armon/go-metrics` to consume `hashicorp/go-metrics/compat` instead. 69 | 2. Update library dependencies of applications that use `armon/go-metrics`. 70 | * This doesn't need to be one big atomic update but can be slower due to the default behavior remaining unaltered. 71 | * At this point all metrics will still be emitted to `armon/go-metrics` 72 | 3. Update the application to use `hashicorp/go-metrics` 73 | * Replace all application imports of `github.com/armon/go-metrics` with `github.com/hashicorp/go-metrics` 74 | * Libraries are unaltered at this stage. 75 | * Instrument your build system to build with the `hashicorpmetrics` tag. 76 | 77 | Your migration is effectively finished and your application is now exclusively using `hashicorp/go-metrics`. A future release of the library 78 | will change the default behavior to use `hashicorp/go-metrics` instead of `armon/go-metrics`. At that point in time, any application that 79 | needs more time before performing the migration must instrument their build system to include the `armonmetrics` tag. A subsequent release 80 | after that will eventually remove the compatibility layer all together. The rough timeline for this will be mid-2025 for changing the default 81 | behavior and then the end of 2025 for removal of the compatibility layer. 82 | 83 | 84 | Examples 85 | -------- 86 | 87 | Here is an example of using the package: 88 | 89 | ```go 90 | func SlowMethod() { 91 | // Profiling the runtime of a method 92 | defer metrics.MeasureSince([]string{"SlowMethod"}, time.Now()) 93 | } 94 | 95 | // Configure a statsite sink as the global metrics sink 96 | sink, _ := metrics.NewStatsiteSink("statsite:8125") 97 | metrics.NewGlobal(metrics.DefaultConfig("service-name"), sink) 98 | 99 | // Emit a Key/Value pair 100 | metrics.EmitKey([]string{"questions", "meaning of life"}, 42) 101 | ``` 102 | 103 | Here is an example of setting up a signal handler: 104 | 105 | ```go 106 | // Setup the inmem sink and signal handler 107 | inm := metrics.NewInmemSink(10*time.Second, time.Minute) 108 | sig := metrics.DefaultInmemSignal(inm) 109 | metrics.NewGlobal(metrics.DefaultConfig("service-name"), inm) 110 | 111 | // Run some code 112 | inm.SetGauge([]string{"foo"}, 42) 113 | inm.EmitKey([]string{"bar"}, 30) 114 | 115 | inm.IncrCounter([]string{"baz"}, 42) 116 | inm.IncrCounter([]string{"baz"}, 1) 117 | inm.IncrCounter([]string{"baz"}, 80) 118 | 119 | inm.AddSample([]string{"method", "wow"}, 42) 120 | inm.AddSample([]string{"method", "wow"}, 100) 121 | inm.AddSample([]string{"method", "wow"}, 22) 122 | 123 | .... 124 | ``` 125 | 126 | When a signal comes in, output like the following will be dumped to stderr: 127 | 128 | [2014-01-28 14:57:33.04 -0800 PST][G] 'foo': 42.000 129 | [2014-01-28 14:57:33.04 -0800 PST][P] 'bar': 30.000 130 | [2014-01-28 14:57:33.04 -0800 PST][C] 'baz': Count: 3 Min: 1.000 Mean: 41.000 Max: 80.000 Stddev: 39.509 131 | [2014-01-28 14:57:33.04 -0800 PST][S] 'method.wow': Count: 3 Min: 22.000 Mean: 54.667 Max: 100.000 Stddev: 40.513 132 | -------------------------------------------------------------------------------- /circonus/circonus.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | // Circonus Metrics Sink 5 | 6 | package circonus 7 | 8 | import ( 9 | "strings" 10 | 11 | cgm "github.com/circonus-labs/circonus-gometrics" 12 | "github.com/hashicorp/go-metrics" 13 | ) 14 | 15 | // CirconusSink provides an interface to forward metrics to Circonus with 16 | // automatic check creation and metric management 17 | type CirconusSink struct { 18 | metrics *cgm.CirconusMetrics 19 | } 20 | 21 | // Config options for CirconusSink 22 | // See https://github.com/circonus-labs/circonus-gometrics for configuration options 23 | type Config cgm.Config 24 | 25 | // NewCirconusSink - create new metric sink for circonus 26 | // 27 | // one of the following must be supplied: 28 | // - API Token - search for an existing check or create a new check 29 | // - API Token + Check Id - the check identified by check id will be used 30 | // - API Token + Check Submission URL - the check identified by the submission url will be used 31 | // - Check Submission URL - the check identified by the submission url will be used 32 | // metric management will be *disabled* 33 | // 34 | // Note: If submission url is supplied w/o an api token, the public circonus ca cert will be used 35 | // to verify the broker for metrics submission. 36 | func NewCirconusSink(cc *Config) (*CirconusSink, error) { 37 | cfg := cgm.Config{} 38 | if cc != nil { 39 | cfg = cgm.Config(*cc) 40 | } 41 | 42 | metrics, err := cgm.NewCirconusMetrics(&cfg) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | return &CirconusSink{ 48 | metrics: metrics, 49 | }, nil 50 | } 51 | 52 | // Start submitting metrics to Circonus (flush every SubmitInterval) 53 | func (s *CirconusSink) Start() { 54 | s.metrics.Start() 55 | } 56 | 57 | // Flush manually triggers metric submission to Circonus 58 | func (s *CirconusSink) Flush() { 59 | s.metrics.Flush() 60 | } 61 | 62 | // SetGauge sets value for a gauge metric 63 | func (s *CirconusSink) SetGauge(key []string, val float32) { 64 | flatKey := s.flattenKey(key) 65 | s.metrics.SetGauge(flatKey, int64(val)) 66 | } 67 | 68 | // SetGaugeWithLabels sets value for a gauge metric with the given labels 69 | func (s *CirconusSink) SetGaugeWithLabels(key []string, val float32, labels []metrics.Label) { 70 | flatKey := s.flattenKeyLabels(key, labels) 71 | s.metrics.SetGauge(flatKey, int64(val)) 72 | } 73 | 74 | // SetPrecisionGauge sets value for a gauge metric with float64 precision 75 | func (s *CirconusSink) SetPrecisionGauge(key []string, val float64) { 76 | flatKey := s.flattenKey(key) 77 | s.metrics.SetGauge(flatKey, val) 78 | } 79 | 80 | // SetPrecisionGaugeWithLabels sets value for a gauge metric with the given labels with float64 precision 81 | func (s *CirconusSink) SetPrecisionGaugeWithLabels(key []string, val float64, labels []metrics.Label) { 82 | flatKey := s.flattenKeyLabels(key, labels) 83 | s.metrics.SetGauge(flatKey, val) 84 | } 85 | 86 | // EmitKey is not implemented in circonus 87 | func (s *CirconusSink) EmitKey(key []string, val float32) { 88 | // NOP 89 | } 90 | 91 | // IncrCounter increments a counter metric 92 | func (s *CirconusSink) IncrCounter(key []string, val float32) { 93 | flatKey := s.flattenKey(key) 94 | s.metrics.IncrementByValue(flatKey, uint64(val)) 95 | } 96 | 97 | // IncrCounterWithLabels increments a counter metric with the given labels 98 | func (s *CirconusSink) IncrCounterWithLabels(key []string, val float32, labels []metrics.Label) { 99 | flatKey := s.flattenKeyLabels(key, labels) 100 | s.metrics.IncrementByValue(flatKey, uint64(val)) 101 | } 102 | 103 | // AddSample adds a sample to a histogram metric 104 | func (s *CirconusSink) AddSample(key []string, val float32) { 105 | flatKey := s.flattenKey(key) 106 | s.metrics.RecordValue(flatKey, float64(val)) 107 | } 108 | 109 | // AddSampleWithLabels adds a sample to a histogram metric with the given labels 110 | func (s *CirconusSink) AddSampleWithLabels(key []string, val float32, labels []metrics.Label) { 111 | flatKey := s.flattenKeyLabels(key, labels) 112 | s.metrics.RecordValue(flatKey, float64(val)) 113 | } 114 | 115 | // Shutdown blocks while flushing metrics to the backend. 116 | func (s *CirconusSink) Shutdown() { 117 | // The version of circonus metrics in go.mod (v2.3.1), and the current 118 | // version (v3.4.6) do not support a shutdown operation. Instead we call 119 | // Flush which blocks until metrics are submitted to storage, and then exit 120 | // as the README examples do. 121 | s.metrics.Flush() 122 | } 123 | 124 | // Flattens key to Circonus metric name 125 | func (s *CirconusSink) flattenKey(parts []string) string { 126 | joined := strings.Join(parts, "`") 127 | return strings.Map(func(r rune) rune { 128 | switch r { 129 | case ' ': 130 | return '_' 131 | default: 132 | return r 133 | } 134 | }, joined) 135 | } 136 | 137 | // Flattens the key along with labels for formatting, removes spaces 138 | func (s *CirconusSink) flattenKeyLabels(parts []string, labels []metrics.Label) string { 139 | for _, label := range labels { 140 | parts = append(parts, label.Value) 141 | } 142 | return s.flattenKey(parts) 143 | } 144 | -------------------------------------------------------------------------------- /circonus/circonus_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | package circonus 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "io/ioutil" 10 | "net/http" 11 | "net/http/httptest" 12 | "strings" 13 | "testing" 14 | 15 | "github.com/hashicorp/go-metrics" 16 | ) 17 | 18 | func TestNewCirconusSink(t *testing.T) { 19 | 20 | // test with invalid config (nil) 21 | expectedError := errors.New("invalid check manager configuration (no API token AND no submission url)") 22 | _, err := NewCirconusSink(nil) 23 | if err == nil || !strings.Contains(err.Error(), expectedError.Error()) { 24 | t.Errorf("Expected an '%#v' error, got '%#v'", expectedError, err) 25 | } 26 | 27 | // test w/submission url and w/o token 28 | cfg := &Config{} 29 | cfg.CheckManager.Check.SubmissionURL = "http://127.0.0.1:43191/" 30 | _, err = NewCirconusSink(cfg) 31 | if err != nil { 32 | t.Errorf("Expected no error, got '%v'", err) 33 | } 34 | 35 | // note: a test with a valid token is *not* done as it *will* create a 36 | // check resulting in testing the api more than the circonus sink 37 | // see circonus-gometrics/checkmgr/checkmgr_test.go for testing of api token 38 | } 39 | 40 | func TestFlattenKey(t *testing.T) { 41 | var testKeys = []struct { 42 | input []string 43 | expected string 44 | }{ 45 | {[]string{"a", "b", "c"}, "a`b`c"}, 46 | {[]string{"a-a", "b_b", "c/c"}, "a-a`b_b`c/c"}, 47 | {[]string{"spaces must", "flatten", "to", "underscores"}, "spaces_must`flatten`to`underscores"}, 48 | } 49 | 50 | c := &CirconusSink{} 51 | 52 | for _, test := range testKeys { 53 | if actual := c.flattenKey(test.input); actual != test.expected { 54 | t.Fatalf("Flattening %v failed, expected '%s' got '%s'", test.input, test.expected, actual) 55 | } 56 | } 57 | } 58 | 59 | func fakeBroker(q chan string) *httptest.Server { 60 | handler := func(w http.ResponseWriter, r *http.Request) { 61 | w.WriteHeader(200) 62 | w.Header().Set("Content-Type", "application/json") 63 | defer r.Body.Close() 64 | body, err := ioutil.ReadAll(r.Body) 65 | if err != nil { 66 | q <- err.Error() 67 | fmt.Fprintln(w, err.Error()) 68 | } else { 69 | q <- string(body) 70 | fmt.Fprintln(w, `{"stats":1}`) 71 | } 72 | } 73 | 74 | return httptest.NewServer(http.HandlerFunc(handler)) 75 | } 76 | 77 | func TestSetGauge(t *testing.T) { 78 | q := make(chan string) 79 | 80 | server := fakeBroker(q) 81 | defer server.Close() 82 | 83 | cfg := &Config{} 84 | cfg.CheckManager.Check.SubmissionURL = server.URL 85 | 86 | cs, err := NewCirconusSink(cfg) 87 | if err != nil { 88 | t.Errorf("Expected no error, got '%v'", err) 89 | } 90 | 91 | go func() { 92 | cs.SetGauge([]string{"foo", "bar"}, 1) 93 | cs.Flush() 94 | }() 95 | 96 | expect := "{\"foo`bar\":{\"_type\":\"l\",\"_value\":1}}" 97 | actual := <-q 98 | 99 | if actual != expect { 100 | t.Errorf("Expected '%s', got '%s'", expect, actual) 101 | 102 | } 103 | } 104 | 105 | func TestSetPrecisionGauge(t *testing.T) { 106 | q := make(chan string) 107 | 108 | server := fakeBroker(q) 109 | defer server.Close() 110 | 111 | cfg := &Config{} 112 | cfg.CheckManager.Check.SubmissionURL = server.URL 113 | 114 | cs, err := NewCirconusSink(cfg) 115 | if err != nil { 116 | t.Errorf("Expected no error, got '%v'", err) 117 | } 118 | 119 | go func() { 120 | cs.SetPrecisionGauge([]string{"foo", "bar"}, 1) 121 | cs.Flush() 122 | }() 123 | 124 | expect := "{\"foo`bar\":{\"_type\":\"n\",\"_value\":1}}" 125 | actual := <-q 126 | 127 | if actual != expect { 128 | t.Errorf("Expected '%s', got '%s'", expect, actual) 129 | 130 | } 131 | } 132 | 133 | func TestIncrCounter(t *testing.T) { 134 | q := make(chan string) 135 | 136 | server := fakeBroker(q) 137 | defer server.Close() 138 | 139 | cfg := &Config{} 140 | cfg.CheckManager.Check.SubmissionURL = server.URL 141 | 142 | cs, err := NewCirconusSink(cfg) 143 | if err != nil { 144 | t.Errorf("Expected no error, got '%v'", err) 145 | } 146 | 147 | go func() { 148 | cs.IncrCounter([]string{"foo", "bar"}, 1) 149 | cs.Flush() 150 | }() 151 | 152 | expect := "{\"foo`bar\":{\"_type\":\"L\",\"_value\":1}}" 153 | actual := <-q 154 | 155 | if actual != expect { 156 | t.Errorf("Expected '%s', got '%s'", expect, actual) 157 | 158 | } 159 | } 160 | 161 | func TestAddSample(t *testing.T) { 162 | q := make(chan string) 163 | 164 | server := fakeBroker(q) 165 | defer server.Close() 166 | 167 | cfg := &Config{} 168 | cfg.CheckManager.Check.SubmissionURL = server.URL 169 | 170 | cs, err := NewCirconusSink(cfg) 171 | if err != nil { 172 | t.Errorf("Expected no error, got '%v'", err) 173 | } 174 | 175 | go func() { 176 | cs.AddSample([]string{"foo", "bar"}, 1) 177 | cs.Flush() 178 | }() 179 | 180 | expect := "{\"foo`bar\":{\"_type\":\"n\",\"_value\":[\"H[1.0e+00]=1\"]}}" 181 | actual := <-q 182 | 183 | if actual != expect { 184 | t.Errorf("Expected '%s', got '%s'", expect, actual) 185 | 186 | } 187 | } 188 | 189 | func TestMetricSinkInterface(t *testing.T) { 190 | var cs *CirconusSink 191 | _ = metrics.MetricSink(cs) 192 | } 193 | -------------------------------------------------------------------------------- /compat/armon.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | //go:build armonmetrics || ignore || !hashicorpmetrics 5 | // +build armonmetrics ignore !hashicorpmetrics 6 | 7 | package metrics 8 | 9 | import ( 10 | "io" 11 | "net/url" 12 | "syscall" 13 | "time" 14 | 15 | "github.com/armon/go-metrics" 16 | ) 17 | 18 | const ( 19 | // DefaultSignal is used with DefaultInmemSignal 20 | DefaultSignal = metrics.DefaultSignal 21 | ) 22 | 23 | func AddSample(key []string, val float32) { 24 | metrics.AddSample(key, val) 25 | } 26 | func AddSampleWithLabels(key []string, val float32, labels []Label) { 27 | metrics.AddSampleWithLabels(key, val, labels) 28 | } 29 | func EmitKey(key []string, val float32) { 30 | metrics.EmitKey(key, val) 31 | } 32 | func IncrCounter(key []string, val float32) { 33 | metrics.IncrCounter(key, val) 34 | } 35 | func IncrCounterWithLabels(key []string, val float32, labels []Label) { 36 | metrics.IncrCounterWithLabels(key, val, labels) 37 | } 38 | func MeasureSince(key []string, start time.Time) { 39 | metrics.MeasureSince(key, start) 40 | } 41 | func MeasureSinceWithLabels(key []string, start time.Time, labels []Label) { 42 | metrics.MeasureSinceWithLabels(key, start, labels) 43 | } 44 | func SetGauge(key []string, val float32) { 45 | metrics.SetGauge(key, val) 46 | } 47 | func SetGaugeWithLabels(key []string, val float32, labels []Label) { 48 | metrics.SetGaugeWithLabels(key, val, labels) 49 | } 50 | func Shutdown() { 51 | metrics.Shutdown() 52 | } 53 | func UpdateFilter(allow, block []string) { 54 | metrics.UpdateFilter(allow, block) 55 | } 56 | func UpdateFilterAndLabels(allow, block, allowedLabels, blockedLabels []string) { 57 | metrics.UpdateFilterAndLabels(allow, block, allowedLabels, blockedLabels) 58 | } 59 | 60 | type AggregateSample = metrics.AggregateSample 61 | type BlackholeSink = metrics.BlackholeSink 62 | type Config = metrics.Config 63 | type Encoder = metrics.Encoder 64 | type FanoutSink = metrics.FanoutSink 65 | type GaugeValue = metrics.GaugeValue 66 | type InmemSignal = metrics.InmemSignal 67 | type InmemSink = metrics.InmemSink 68 | type IntervalMetrics = metrics.IntervalMetrics 69 | type Label = metrics.Label 70 | type MetricSink = metrics.MetricSink 71 | type Metrics = metrics.Metrics 72 | type MetricsSummary = metrics.MetricsSummary 73 | type PointValue = metrics.PointValue 74 | type SampledValue = metrics.SampledValue 75 | type ShutdownSink = metrics.ShutdownSink 76 | type StatsdSink = metrics.StatsdSink 77 | type StatsiteSink = metrics.StatsiteSink 78 | 79 | func DefaultConfig(serviceName string) *Config { 80 | return metrics.DefaultConfig(serviceName) 81 | } 82 | 83 | func DefaultInmemSignal(inmem *InmemSink) *InmemSignal { 84 | return metrics.DefaultInmemSignal(inmem) 85 | } 86 | func NewInmemSignal(inmem *InmemSink, sig syscall.Signal, w io.Writer) *InmemSignal { 87 | return metrics.NewInmemSignal(inmem, sig, w) 88 | } 89 | 90 | func NewInmemSink(interval, retain time.Duration) *InmemSink { 91 | return metrics.NewInmemSink(interval, retain) 92 | } 93 | 94 | func NewIntervalMetrics(intv time.Time) *IntervalMetrics { 95 | return metrics.NewIntervalMetrics(intv) 96 | } 97 | 98 | func NewInmemSinkFromURL(u *url.URL) (MetricSink, error) { 99 | return metrics.NewInmemSinkFromURL(u) 100 | } 101 | 102 | func NewMetricSinkFromURL(urlStr string) (MetricSink, error) { 103 | return metrics.NewMetricSinkFromURL(urlStr) 104 | } 105 | 106 | func NewStatsdSinkFromURL(u *url.URL) (MetricSink, error) { 107 | return metrics.NewStatsdSinkFromURL(u) 108 | } 109 | 110 | func NewStatsiteSinkFromURL(u *url.URL) (MetricSink, error) { 111 | return metrics.NewStatsiteSinkFromURL(u) 112 | } 113 | 114 | func Default() *Metrics { 115 | return metrics.Default() 116 | } 117 | 118 | func New(conf *Config, sink MetricSink) (*Metrics, error) { 119 | return metrics.New(conf, sink) 120 | } 121 | 122 | func NewGlobal(conf *Config, sink MetricSink) (*Metrics, error) { 123 | return metrics.NewGlobal(conf, sink) 124 | } 125 | 126 | func NewStatsdSink(addr string) (*StatsdSink, error) { 127 | return metrics.NewStatsdSink(addr) 128 | } 129 | 130 | func NewStatsiteSink(addr string) (*StatsiteSink, error) { 131 | return metrics.NewStatsiteSink(addr) 132 | } 133 | -------------------------------------------------------------------------------- /compat/circonus/armon.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | //go:build armonmetrics || ignore || !hashicorpmetrics 5 | // +build armonmetrics ignore !hashicorpmetrics 6 | 7 | package circonus 8 | 9 | import ( 10 | "github.com/armon/go-metrics/circonus" 11 | ) 12 | 13 | type CirconusSink = circonus.CirconusSink 14 | type Config = circonus.Config 15 | 16 | func NewCirconusSink(cc *Config) (*CirconusSink, error) { 17 | return circonus.NewCirconusSink(cc) 18 | } 19 | -------------------------------------------------------------------------------- /compat/circonus/hashicorp.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | //go:build hashicorpmetrics 5 | // +build hashicorpmetrics 6 | 7 | package circonus 8 | 9 | import ( 10 | "github.com/hashicorp/go-metrics/circonus" 11 | ) 12 | 13 | type CirconusSink = circonus.CirconusSink 14 | type Config = circonus.Config 15 | 16 | func NewCirconusSink(cc *Config) (*CirconusSink, error) { 17 | return circonus.NewCirconusSink(cc) 18 | } 19 | -------------------------------------------------------------------------------- /compat/datadog/armon.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | //go:build armonmetrics || ignore || !hashicorpmetrics 5 | // +build armonmetrics ignore !hashicorpmetrics 6 | 7 | package datadog 8 | 9 | import ( 10 | "github.com/armon/go-metrics/datadog" 11 | ) 12 | 13 | type DogStatsdSink = datadog.DogStatsdSink 14 | 15 | func NewDogStatsdSink(addr string, hostName string) (*DogStatsdSink, error) { 16 | return datadog.NewDogStatsdSink(addr, hostName) 17 | } 18 | -------------------------------------------------------------------------------- /compat/datadog/hashicorp.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | //go:build hashicorpmetrics 5 | // +build hashicorpmetrics 6 | 7 | package datadog 8 | 9 | import ( 10 | "github.com/hashicorp/go-metrics/datadog" 11 | ) 12 | 13 | type DogStatsdSink = datadog.DogStatsdSink 14 | 15 | func NewDogStatsdSink(addr string, hostName string) (*DogStatsdSink, error) { 16 | return datadog.NewDogStatsdSink(addr, hostName) 17 | } 18 | -------------------------------------------------------------------------------- /compat/hashicorp.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | //go:build hashicorpmetrics 5 | // +build hashicorpmetrics 6 | 7 | package metrics 8 | 9 | import ( 10 | "io" 11 | "net/url" 12 | "syscall" 13 | "time" 14 | 15 | "github.com/hashicorp/go-metrics" 16 | ) 17 | 18 | const ( 19 | // DefaultSignal is used with DefaultInmemSignal 20 | DefaultSignal = metrics.DefaultSignal 21 | ) 22 | 23 | func AddSample(key []string, val float32) { 24 | metrics.AddSample(key, val) 25 | } 26 | func AddSampleWithLabels(key []string, val float32, labels []Label) { 27 | metrics.AddSampleWithLabels(key, val, labels) 28 | } 29 | func EmitKey(key []string, val float32) { 30 | metrics.EmitKey(key, val) 31 | } 32 | func IncrCounter(key []string, val float32) { 33 | metrics.IncrCounter(key, val) 34 | } 35 | func IncrCounterWithLabels(key []string, val float32, labels []Label) { 36 | metrics.IncrCounterWithLabels(key, val, labels) 37 | } 38 | func MeasureSince(key []string, start time.Time) { 39 | metrics.MeasureSince(key, start) 40 | } 41 | func MeasureSinceWithLabels(key []string, start time.Time, labels []Label) { 42 | metrics.MeasureSinceWithLabels(key, start, labels) 43 | } 44 | func SetGauge(key []string, val float32) { 45 | metrics.SetGauge(key, val) 46 | } 47 | func SetGaugeWithLabels(key []string, val float32, labels []Label) { 48 | metrics.SetGaugeWithLabels(key, val, labels) 49 | } 50 | func Shutdown() { 51 | metrics.Shutdown() 52 | } 53 | func UpdateFilter(allow, block []string) { 54 | metrics.UpdateFilter(allow, block) 55 | } 56 | func UpdateFilterAndLabels(allow, block, allowedLabels, blockedLabels []string) { 57 | metrics.UpdateFilterAndLabels(allow, block, allowedLabels, blockedLabels) 58 | } 59 | 60 | type AggregateSample = metrics.AggregateSample 61 | type BlackholeSink = metrics.BlackholeSink 62 | type Config = metrics.Config 63 | type Encoder = metrics.Encoder 64 | type FanoutSink = metrics.FanoutSink 65 | type GaugeValue = metrics.GaugeValue 66 | type InmemSignal = metrics.InmemSignal 67 | type InmemSink = metrics.InmemSink 68 | type IntervalMetrics = metrics.IntervalMetrics 69 | type Label = metrics.Label 70 | type MetricSink = metrics.MetricSink 71 | type Metrics = metrics.Metrics 72 | type MetricsSummary = metrics.MetricsSummary 73 | type PointValue = metrics.PointValue 74 | type SampledValue = metrics.SampledValue 75 | type ShutdownSink = metrics.ShutdownSink 76 | type StatsdSink = metrics.StatsdSink 77 | type StatsiteSink = metrics.StatsiteSink 78 | 79 | func DefaultConfig(serviceName string) *Config { 80 | return metrics.DefaultConfig(serviceName) 81 | } 82 | 83 | func DefaultInmemSignal(inmem *InmemSink) *InmemSignal { 84 | return metrics.DefaultInmemSignal(inmem) 85 | } 86 | func NewInmemSignal(inmem *InmemSink, sig syscall.Signal, w io.Writer) *InmemSignal { 87 | return metrics.NewInmemSignal(inmem, sig, w) 88 | } 89 | 90 | func NewInmemSink(interval, retain time.Duration) *InmemSink { 91 | return metrics.NewInmemSink(interval, retain) 92 | } 93 | 94 | func NewIntervalMetrics(intv time.Time) *IntervalMetrics { 95 | return metrics.NewIntervalMetrics(intv) 96 | } 97 | 98 | func NewInmemSinkFromURL(u *url.URL) (MetricSink, error) { 99 | return metrics.NewInmemSinkFromURL(u) 100 | } 101 | 102 | func NewMetricSinkFromURL(urlStr string) (MetricSink, error) { 103 | return metrics.NewMetricSinkFromURL(urlStr) 104 | } 105 | 106 | func NewStatsdSinkFromURL(u *url.URL) (MetricSink, error) { 107 | return metrics.NewStatsdSinkFromURL(u) 108 | } 109 | 110 | func NewStatsiteSinkFromURL(u *url.URL) (MetricSink, error) { 111 | return metrics.NewStatsiteSinkFromURL(u) 112 | } 113 | 114 | func Default() *Metrics { 115 | return metrics.Default() 116 | } 117 | 118 | func New(conf *Config, sink MetricSink) (*Metrics, error) { 119 | return metrics.New(conf, sink) 120 | } 121 | 122 | func NewGlobal(conf *Config, sink MetricSink) (*Metrics, error) { 123 | return metrics.NewGlobal(conf, sink) 124 | } 125 | 126 | func NewStatsdSink(addr string) (*StatsdSink, error) { 127 | return metrics.NewStatsdSink(addr) 128 | } 129 | 130 | func NewStatsiteSink(addr string) (*StatsiteSink, error) { 131 | return metrics.NewStatsiteSink(addr) 132 | } 133 | -------------------------------------------------------------------------------- /compat/prometheus/armon.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | //go:build armonmetrics || ignore || !hashicorpmetrics 5 | // +build armonmetrics ignore !hashicorpmetrics 6 | 7 | package prometheus 8 | 9 | import ( 10 | "time" 11 | 12 | "github.com/armon/go-metrics/prometheus" 13 | ) 14 | 15 | var DefaultPrometheusOpts = prometheus.DefaultPrometheusOpts 16 | 17 | type CounterDefinition = prometheus.CounterDefinition 18 | type GaugeDefinition = prometheus.GaugeDefinition 19 | type PrometheusOpts = prometheus.PrometheusOpts 20 | type PrometheusPushSink = prometheus.PrometheusPushSink 21 | type PrometheusSink = prometheus.PrometheusSink 22 | type SummaryDefinition = prometheus.SummaryDefinition 23 | 24 | func NewPrometheusPushSink(address string, pushInterval time.Duration, name string) (*PrometheusPushSink, error) { 25 | return prometheus.NewPrometheusPushSink(address, pushInterval, name) 26 | } 27 | 28 | func NewPrometheusSink() (*PrometheusSink, error) { 29 | return prometheus.NewPrometheusSink() 30 | } 31 | 32 | func NewPrometheusSinkFrom(opts PrometheusOpts) (*PrometheusSink, error) { 33 | return prometheus.NewPrometheusSinkFrom(opts) 34 | } 35 | -------------------------------------------------------------------------------- /compat/prometheus/hashicorp.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | //go:build hashicorpmetrics 5 | // +build hashicorpmetrics 6 | 7 | package prometheus 8 | 9 | import ( 10 | "time" 11 | 12 | "github.com/hashicorp/go-metrics/prometheus" 13 | ) 14 | 15 | var DefaultPrometheusOpts = prometheus.DefaultPrometheusOpts 16 | 17 | type CounterDefinition = prometheus.CounterDefinition 18 | type GaugeDefinition = prometheus.GaugeDefinition 19 | type PrometheusOpts = prometheus.PrometheusOpts 20 | type PrometheusPushSink = prometheus.PrometheusPushSink 21 | type PrometheusSink = prometheus.PrometheusSink 22 | type SummaryDefinition = prometheus.SummaryDefinition 23 | 24 | func NewPrometheusPushSink(address string, pushInterval time.Duration, name string) (*PrometheusPushSink, error) { 25 | return prometheus.NewPrometheusPushSink(address, pushInterval, name) 26 | } 27 | 28 | func NewPrometheusSink() (*PrometheusSink, error) { 29 | return prometheus.NewPrometheusSink() 30 | } 31 | 32 | func NewPrometheusSinkFrom(opts PrometheusOpts) (*PrometheusSink, error) { 33 | return prometheus.NewPrometheusSinkFrom(opts) 34 | } 35 | -------------------------------------------------------------------------------- /const_js.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | package metrics 5 | 6 | const ( 7 | // DefaultSignal is used with DefaultInmemSignal 8 | DefaultSignal = 0x1e 9 | ) 10 | -------------------------------------------------------------------------------- /const_unix.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | //go:build !windows && !js 5 | // +build !windows,!js 6 | 7 | package metrics 8 | 9 | import ( 10 | "syscall" 11 | ) 12 | 13 | const ( 14 | // DefaultSignal is used with DefaultInmemSignal 15 | DefaultSignal = syscall.SIGUSR1 16 | ) 17 | -------------------------------------------------------------------------------- /const_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | // +build windows 5 | 6 | package metrics 7 | 8 | import ( 9 | "syscall" 10 | ) 11 | 12 | const ( 13 | // DefaultSignal is used with DefaultInmemSignal 14 | // Windows has no SIGUSR1, use SIGBREAK 15 | DefaultSignal = syscall.Signal(21) 16 | ) 17 | -------------------------------------------------------------------------------- /datadog/dogstatsd.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | package datadog 5 | 6 | import ( 7 | "fmt" 8 | "strings" 9 | 10 | "github.com/DataDog/datadog-go/statsd" 11 | "github.com/hashicorp/go-metrics" 12 | ) 13 | 14 | // DogStatsdSink provides a MetricSink that can be used 15 | // with a dogstatsd server. It utilizes the Dogstatsd client at github.com/DataDog/datadog-go/statsd 16 | type DogStatsdSink struct { 17 | client *statsd.Client 18 | hostName string 19 | propagateHostname bool 20 | } 21 | 22 | // NewDogStatsdSink is used to create a new DogStatsdSink with sane defaults 23 | func NewDogStatsdSink(addr string, hostName string) (*DogStatsdSink, error) { 24 | client, err := statsd.New(addr) 25 | if err != nil { 26 | return nil, err 27 | } 28 | sink := &DogStatsdSink{ 29 | client: client, 30 | hostName: hostName, 31 | propagateHostname: false, 32 | } 33 | return sink, nil 34 | } 35 | 36 | // SetTags sets common tags on the Dogstatsd Client that will be sent 37 | // along with all dogstatsd packets. 38 | // Ref: http://docs.datadoghq.com/guides/dogstatsd/#tags 39 | func (s *DogStatsdSink) SetTags(tags []string) { 40 | s.client.Tags = tags 41 | } 42 | 43 | // EnableHostnamePropagation forces a Dogstatsd `host` tag with the value specified by `s.HostName` 44 | // Since the go-metrics package has its own mechanism for attaching a hostname to metrics, 45 | // setting the `propagateHostname` flag ensures that `s.HostName` overrides the host tag naively set by the DogStatsd server 46 | func (s *DogStatsdSink) EnableHostNamePropagation() { 47 | s.propagateHostname = true 48 | } 49 | 50 | func (s *DogStatsdSink) flattenKey(parts []string) string { 51 | joined := strings.Join(parts, ".") 52 | return strings.Map(sanitize, joined) 53 | } 54 | 55 | func sanitize(r rune) rune { 56 | switch r { 57 | case ':': 58 | fallthrough 59 | case ' ': 60 | return '_' 61 | default: 62 | return r 63 | } 64 | } 65 | 66 | func (s *DogStatsdSink) parseKey(key []string) ([]string, []metrics.Label) { 67 | // Since DogStatsd supports dimensionality via tags on metric keys, this sink's approach is to splice the hostname out of the key in favor of a `host` tag 68 | // The `host` tag is either forced here, or set downstream by the DogStatsd server 69 | 70 | var labels []metrics.Label 71 | hostName := s.hostName 72 | 73 | // Splice the hostname out of the key 74 | for i, el := range key { 75 | if el == hostName { 76 | // We need an intermediate key to prevent clobbering the 77 | // original backing array that other sinks might be consuming. 78 | tempKey := append([]string{}, key[:i]...) 79 | key = append(tempKey, key[i+1:]...) 80 | break 81 | } 82 | } 83 | 84 | if s.propagateHostname { 85 | labels = append(labels, metrics.Label{"host", hostName}) 86 | } 87 | return key, labels 88 | } 89 | 90 | // Implementation of methods in the MetricSink interface 91 | 92 | func (s *DogStatsdSink) SetGauge(key []string, val float32) { 93 | s.SetGaugeWithLabels(key, val, nil) 94 | } 95 | 96 | func (s *DogStatsdSink) SetPrecisionGauge(key []string, val float64) { 97 | s.SetPrecisionGaugeWithLabels(key, val, nil) 98 | } 99 | 100 | func (s *DogStatsdSink) IncrCounter(key []string, val float32) { 101 | s.IncrCounterWithLabels(key, val, nil) 102 | } 103 | 104 | // EmitKey is not implemented since DogStatsd does not provide a metric type that holds an 105 | // arbitrary number of values 106 | func (s *DogStatsdSink) EmitKey(key []string, val float32) { 107 | } 108 | 109 | func (s *DogStatsdSink) AddSample(key []string, val float32) { 110 | s.AddSampleWithLabels(key, val, nil) 111 | } 112 | 113 | // The following ...WithLabels methods correspond to Datadog's Tag extension to Statsd. 114 | // http://docs.datadoghq.com/guides/dogstatsd/#tags 115 | func (s *DogStatsdSink) SetGaugeWithLabels(key []string, val float32, labels []metrics.Label) { 116 | flatKey, tags := s.getFlatkeyAndCombinedLabels(key, labels) 117 | rate := 1.0 118 | s.client.Gauge(flatKey, float64(val), tags, rate) 119 | } 120 | 121 | // The following ...WithLabels methods correspond to Datadog's Tag extension to Statsd. 122 | // http://docs.datadoghq.com/guides/dogstatsd/#tags 123 | func (s *DogStatsdSink) SetPrecisionGaugeWithLabels(key []string, val float64, labels []metrics.Label) { 124 | flatKey, tags := s.getFlatkeyAndCombinedLabels(key, labels) 125 | rate := 1.0 126 | s.client.Gauge(flatKey, val, tags, rate) 127 | } 128 | 129 | func (s *DogStatsdSink) IncrCounterWithLabels(key []string, val float32, labels []metrics.Label) { 130 | flatKey, tags := s.getFlatkeyAndCombinedLabels(key, labels) 131 | rate := 1.0 132 | s.client.Count(flatKey, int64(val), tags, rate) 133 | } 134 | 135 | func (s *DogStatsdSink) AddSampleWithLabels(key []string, val float32, labels []metrics.Label) { 136 | flatKey, tags := s.getFlatkeyAndCombinedLabels(key, labels) 137 | rate := 1.0 138 | s.client.TimeInMilliseconds(flatKey, float64(val), tags, rate) 139 | } 140 | 141 | // Shutdown disables further metric collection, blocks to flush data, and tears down the sink. 142 | func (s *DogStatsdSink) Shutdown() { 143 | s.client.Close() 144 | } 145 | 146 | func (s *DogStatsdSink) getFlatkeyAndCombinedLabels(key []string, labels []metrics.Label) (string, []string) { 147 | key, parsedLabels := s.parseKey(key) 148 | flatKey := s.flattenKey(key) 149 | labels = append(labels, parsedLabels...) 150 | 151 | var tags []string 152 | for _, label := range labels { 153 | label.Name = strings.Map(sanitize, label.Name) 154 | label.Value = strings.Map(sanitize, label.Value) 155 | if label.Value != "" { 156 | tags = append(tags, fmt.Sprintf("%s:%s", label.Name, label.Value)) 157 | } else { 158 | tags = append(tags, label.Name) 159 | } 160 | } 161 | 162 | return flatKey, tags 163 | } 164 | -------------------------------------------------------------------------------- /datadog/dogstatsd_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | package datadog 5 | 6 | import ( 7 | "net" 8 | "reflect" 9 | "testing" 10 | 11 | "github.com/hashicorp/go-metrics" 12 | ) 13 | 14 | var EmptyTags []metrics.Label 15 | 16 | const ( 17 | DogStatsdAddr = "127.0.0.1:7254" 18 | HostnameEnabled = true 19 | HostnameDisabled = false 20 | TestHostname = "test_hostname" 21 | ) 22 | 23 | func MockGetHostname() string { 24 | return TestHostname 25 | } 26 | 27 | var ParseKeyTests = []struct { 28 | KeyToParse []string 29 | Tags []metrics.Label 30 | PropagateHostname bool 31 | ExpectedKey []string 32 | ExpectedTags []metrics.Label 33 | }{ 34 | {[]string{"a", MockGetHostname(), "b", "c"}, EmptyTags, HostnameDisabled, []string{"a", "b", "c"}, EmptyTags}, 35 | {[]string{"a", "b", "c"}, EmptyTags, HostnameDisabled, []string{"a", "b", "c"}, EmptyTags}, 36 | {[]string{"a", "b", "c"}, EmptyTags, HostnameEnabled, []string{"a", "b", "c"}, []metrics.Label{{"host", MockGetHostname()}}}, 37 | } 38 | 39 | var FlattenKeyTests = []struct { 40 | KeyToFlatten []string 41 | Expected string 42 | }{ 43 | {[]string{"a", "b", "c"}, "a.b.c"}, 44 | {[]string{"spaces must", "flatten", "to", "underscores"}, "spaces_must.flatten.to.underscores"}, 45 | } 46 | 47 | var MetricSinkTests = []struct { 48 | Method string 49 | Metric []string 50 | Value interface{} 51 | Tags []metrics.Label 52 | PropagateHostname bool 53 | Expected string 54 | }{ 55 | {"SetGauge", []string{"foo", "bar"}, float32(42), EmptyTags, HostnameDisabled, "foo.bar:42|g"}, 56 | {"SetGauge", []string{"foo", "bar", "baz"}, float32(42), EmptyTags, HostnameDisabled, "foo.bar.baz:42|g"}, 57 | {"AddSample", []string{"sample", "thing"}, float32(4), EmptyTags, HostnameDisabled, "sample.thing:4.000000|ms"}, 58 | {"IncrCounter", []string{"count", "me"}, float32(3), EmptyTags, HostnameDisabled, "count.me:3|c"}, 59 | 60 | {"SetGauge", []string{"foo", "baz"}, float32(42), []metrics.Label{{"my_tag", ""}}, HostnameDisabled, "foo.baz:42|g|#my_tag"}, 61 | {"SetGauge", []string{"foo", "baz"}, float32(42), []metrics.Label{{"my tag", "my_value"}}, HostnameDisabled, "foo.baz:42|g|#my_tag:my_value"}, 62 | {"SetGauge", []string{"foo", "bar"}, float32(42), []metrics.Label{{"my_tag", "my_value"}, {"other_tag", "other_value"}}, HostnameDisabled, "foo.bar:42|g|#my_tag:my_value,other_tag:other_value"}, 63 | {"SetGauge", []string{"foo", "bar"}, float32(42), []metrics.Label{{"my_tag", "my_value"}, {"other_tag", "other_value"}}, HostnameEnabled, "foo.bar:42|g|#my_tag:my_value,other_tag:other_value,host:test_hostname"}, 64 | } 65 | 66 | func mockNewDogStatsdSink(addr string, labels []metrics.Label, tagWithHostname bool) *DogStatsdSink { 67 | dog, _ := NewDogStatsdSink(addr, MockGetHostname()) 68 | _, tags := dog.getFlatkeyAndCombinedLabels(nil, labels) 69 | dog.SetTags(tags) 70 | if tagWithHostname { 71 | dog.EnableHostNamePropagation() 72 | } 73 | 74 | return dog 75 | } 76 | 77 | func setupTestServerAndBuffer(t *testing.T) (*net.UDPConn, []byte) { 78 | udpAddr, err := net.ResolveUDPAddr("udp", DogStatsdAddr) 79 | if err != nil { 80 | t.Fatal(err) 81 | } 82 | server, err := net.ListenUDP("udp", udpAddr) 83 | if err != nil { 84 | t.Fatal(err) 85 | } 86 | return server, make([]byte, 1024) 87 | } 88 | 89 | func TestParseKey(t *testing.T) { 90 | for _, tt := range ParseKeyTests { 91 | dog := mockNewDogStatsdSink(DogStatsdAddr, tt.Tags, tt.PropagateHostname) 92 | // make a copy of the original key 93 | original := make([]string, len(tt.KeyToParse)) 94 | copy(original, tt.KeyToParse) 95 | 96 | key, tags := dog.parseKey(tt.KeyToParse) 97 | 98 | if !reflect.DeepEqual(key, tt.ExpectedKey) { 99 | t.Fatalf("Key Parsing failed for %v", tt.KeyToParse) 100 | } 101 | 102 | if !reflect.DeepEqual(tags, tt.ExpectedTags) { 103 | t.Fatalf("Tag Parsing Failed for %v, %v != %v", tt.KeyToParse, tags, tt.ExpectedTags) 104 | } 105 | 106 | if !reflect.DeepEqual(original, tt.KeyToParse) { 107 | t.Fatalf("Key parsing modified the original input key:, original: %v, after parse: %v", original, tt.KeyToParse) 108 | } 109 | } 110 | } 111 | 112 | func TestFlattenKey(t *testing.T) { 113 | dog := mockNewDogStatsdSink(DogStatsdAddr, EmptyTags, HostnameDisabled) 114 | for _, tt := range FlattenKeyTests { 115 | if !reflect.DeepEqual(dog.flattenKey(tt.KeyToFlatten), tt.Expected) { 116 | t.Fatalf("Flattening %v failed", tt.KeyToFlatten) 117 | } 118 | } 119 | } 120 | 121 | func TestMetricSink(t *testing.T) { 122 | server, buf := setupTestServerAndBuffer(t) 123 | defer server.Close() 124 | 125 | for _, tt := range MetricSinkTests { 126 | t.Run(tt.Method, func(t *testing.T) { 127 | dog := mockNewDogStatsdSink(DogStatsdAddr, tt.Tags, tt.PropagateHostname) 128 | method := reflect.ValueOf(dog).MethodByName(tt.Method) 129 | method.Call([]reflect.Value{ 130 | reflect.ValueOf(tt.Metric), 131 | reflect.ValueOf(tt.Value)}) 132 | assertServerMatchesExpected(t, server, buf, tt.Expected) 133 | }) 134 | } 135 | } 136 | 137 | func TestTaggableMetrics(t *testing.T) { 138 | server, buf := setupTestServerAndBuffer(t) 139 | defer server.Close() 140 | 141 | dog := mockNewDogStatsdSink(DogStatsdAddr, EmptyTags, HostnameDisabled) 142 | 143 | dog.AddSampleWithLabels([]string{"sample", "thing"}, float32(4), []metrics.Label{{"tagkey", "tagvalue"}}) 144 | assertServerMatchesExpected(t, server, buf, "sample.thing:4.000000|ms|#tagkey:tagvalue") 145 | 146 | dog.SetGaugeWithLabels([]string{"sample", "thing"}, float32(4), []metrics.Label{{"tagkey", "tagvalue"}}) 147 | assertServerMatchesExpected(t, server, buf, "sample.thing:4|g|#tagkey:tagvalue") 148 | 149 | dog.IncrCounterWithLabels([]string{"sample", "thing"}, float32(4), []metrics.Label{{"tagkey", "tagvalue"}}) 150 | assertServerMatchesExpected(t, server, buf, "sample.thing:4|c|#tagkey:tagvalue") 151 | 152 | dog = mockNewDogStatsdSink(DogStatsdAddr, []metrics.Label{{Name: "global"}}, HostnameEnabled) // with hostname, global tags 153 | dog.IncrCounterWithLabels([]string{"sample", "thing"}, float32(4), []metrics.Label{{"tagkey", "tagvalue"}}) 154 | assertServerMatchesExpected(t, server, buf, "sample.thing:4|c|#global,tagkey:tagvalue,host:test_hostname") 155 | } 156 | 157 | func assertServerMatchesExpected(t *testing.T, server *net.UDPConn, buf []byte, expected string) { 158 | t.Helper() 159 | n, _ := server.Read(buf) 160 | msg := buf[:n] 161 | if string(msg) != expected { 162 | t.Fatalf("Line %s does not match expected: %s", string(msg), expected) 163 | } 164 | } 165 | 166 | func TestMetricSinkInterface(t *testing.T) { 167 | var dd *DogStatsdSink 168 | _ = metrics.MetricSink(dd) 169 | } 170 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hashicorp/go-metrics 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/DataDog/datadog-go v3.2.0+incompatible 7 | github.com/armon/go-metrics v0.4.1 8 | github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible 9 | github.com/golang/protobuf v1.5.4 10 | github.com/hashicorp/go-immutable-radix v1.0.0 11 | github.com/pascaldekloe/goe v0.1.0 12 | github.com/prometheus/client_golang v1.11.1 13 | github.com/prometheus/client_model v0.2.0 14 | github.com/prometheus/common v0.26.0 15 | ) 16 | 17 | require ( 18 | github.com/beorn7/perks v1.0.1 // indirect 19 | github.com/cespare/xxhash/v2 v2.1.1 // indirect 20 | github.com/circonus-labs/circonusllhist v0.1.3 // indirect 21 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 22 | github.com/hashicorp/go-retryablehttp v0.7.7 // indirect 23 | github.com/hashicorp/golang-lru v0.5.0 // indirect 24 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 25 | github.com/pkg/errors v0.9.1 // indirect 26 | github.com/prometheus/procfs v0.6.0 // indirect 27 | github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 // indirect 28 | golang.org/x/sys v0.20.0 // indirect 29 | google.golang.org/protobuf v1.33.0 // indirect 30 | ) 31 | 32 | // Introduced undocumented breaking change to metrics sink interface 33 | retract v0.3.11 34 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/DataDog/datadog-go v3.2.0+incompatible h1:qSG2N4FghB1He/r2mFrWKCaL7dXCilEuNEeAn20fdD4= 3 | github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= 4 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 5 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 6 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 7 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 8 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 9 | github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= 10 | github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= 11 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 12 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 13 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 14 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 15 | github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= 16 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 17 | github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible h1:C29Ae4G5GtYyYMm1aztcyj/J5ckgJm2zwdDajFbx1NY= 18 | github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= 19 | github.com/circonus-labs/circonusllhist v0.1.3 h1:TJH+oke8D16535+jHExHj4nQvzlZrj7ug5D7I/orNUA= 20 | github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= 21 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 22 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 23 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 24 | github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= 25 | github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= 26 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 27 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 28 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 29 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 30 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 31 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 32 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 33 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 34 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 35 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 36 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 37 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 38 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 39 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 40 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 41 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 42 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 43 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 44 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 45 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 46 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 47 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 48 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 49 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 50 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 51 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 52 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 53 | github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 54 | github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 55 | github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 56 | github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= 57 | github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 58 | github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= 59 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 60 | github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= 61 | github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= 62 | github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= 63 | github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM= 64 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 65 | github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= 66 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 67 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 68 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 69 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 70 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 71 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 72 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 73 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 74 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 75 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 76 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 77 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 78 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 79 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 80 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 81 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 82 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 83 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 84 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 85 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 86 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 87 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 88 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 89 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 90 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 91 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 92 | github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= 93 | github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 94 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 95 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 96 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 97 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 98 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 99 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 100 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 101 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 102 | github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= 103 | github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 104 | github.com/prometheus/client_golang v1.11.1 h1:+4eQaD7vAZ6DsfsxB15hbE0odUjGI5ARs9yskGu1v4s= 105 | github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= 106 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 107 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 108 | github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= 109 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 110 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 111 | github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= 112 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 113 | github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ= 114 | github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= 115 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 116 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 117 | github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= 118 | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 119 | github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= 120 | github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 121 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 122 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 123 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 124 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 125 | github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= 126 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 127 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 128 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 129 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 130 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 131 | github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 h1:G3dpKMzFDjgEh2q1Z7zUUtKa8ViPtH+ocF0bE0g00O8= 132 | github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= 133 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 134 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 135 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 136 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 137 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 138 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 139 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 140 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 141 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 142 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 143 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 144 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 145 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 146 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 147 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 148 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 149 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 150 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 151 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 152 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 153 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 154 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 155 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 156 | golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 157 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 158 | golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 159 | golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= 160 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 161 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 162 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 163 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 164 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 165 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 166 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 167 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 168 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 169 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 170 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 171 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 172 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 173 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 174 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 175 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 176 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 177 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 178 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 179 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 180 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 181 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 182 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 183 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 184 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 185 | -------------------------------------------------------------------------------- /inmem.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | package metrics 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "math" 10 | "net/url" 11 | "strings" 12 | "sync" 13 | "time" 14 | ) 15 | 16 | var spaceReplacer = strings.NewReplacer(" ", "_") 17 | 18 | // InmemSink provides a MetricSink that does in-memory aggregation 19 | // without sending metrics over a network. It can be embedded within 20 | // an application to provide profiling information. 21 | type InmemSink struct { 22 | // How long is each aggregation interval 23 | interval time.Duration 24 | 25 | // Retain controls how many metrics interval we keep 26 | retain time.Duration 27 | 28 | // maxIntervals is the maximum length of intervals. 29 | // It is retain / interval. 30 | maxIntervals int 31 | 32 | // intervals is a slice of the retained intervals 33 | intervals []*IntervalMetrics 34 | intervalLock sync.RWMutex 35 | 36 | rateDenom float64 37 | } 38 | 39 | // IntervalMetrics stores the aggregated metrics 40 | // for a specific interval 41 | type IntervalMetrics struct { 42 | sync.RWMutex 43 | 44 | // The start time of the interval 45 | Interval time.Time 46 | 47 | // Gauges maps the key to the last set value 48 | Gauges map[string]GaugeValue 49 | 50 | // PrecisionGauges maps the key to the last set value 51 | PrecisionGauges map[string]PrecisionGaugeValue 52 | 53 | // Points maps the string to the list of emitted values 54 | // from EmitKey 55 | Points map[string][]float32 56 | 57 | // Counters maps the string key to a sum of the counter 58 | // values 59 | Counters map[string]SampledValue 60 | 61 | // Samples maps the key to an AggregateSample, 62 | // which has the rolled up view of a sample 63 | Samples map[string]SampledValue 64 | 65 | // done is closed when this interval has ended, and a new IntervalMetrics 66 | // has been created to receive any future metrics. 67 | done chan struct{} 68 | } 69 | 70 | // NewIntervalMetrics creates a new IntervalMetrics for a given interval 71 | func NewIntervalMetrics(intv time.Time) *IntervalMetrics { 72 | return &IntervalMetrics{ 73 | Interval: intv, 74 | Gauges: make(map[string]GaugeValue), 75 | PrecisionGauges: make(map[string]PrecisionGaugeValue), 76 | Points: make(map[string][]float32), 77 | Counters: make(map[string]SampledValue), 78 | Samples: make(map[string]SampledValue), 79 | done: make(chan struct{}), 80 | } 81 | } 82 | 83 | // AggregateSample is used to hold aggregate metrics 84 | // about a sample 85 | type AggregateSample struct { 86 | Count int // The count of emitted pairs 87 | Rate float64 // The values rate per time unit (usually 1 second) 88 | Sum float64 // The sum of values 89 | SumSq float64 `json:"-"` // The sum of squared values 90 | Min float64 // Minimum value 91 | Max float64 // Maximum value 92 | LastUpdated time.Time `json:"-"` // When value was last updated 93 | } 94 | 95 | // Computes a Stddev of the values 96 | func (a *AggregateSample) Stddev() float64 { 97 | num := (float64(a.Count) * a.SumSq) - math.Pow(a.Sum, 2) 98 | div := float64(a.Count * (a.Count - 1)) 99 | if div == 0 { 100 | return 0 101 | } 102 | return math.Sqrt(num / div) 103 | } 104 | 105 | // Computes a mean of the values 106 | func (a *AggregateSample) Mean() float64 { 107 | if a.Count == 0 { 108 | return 0 109 | } 110 | return a.Sum / float64(a.Count) 111 | } 112 | 113 | // Ingest is used to update a sample 114 | func (a *AggregateSample) Ingest(v float64, rateDenom float64) { 115 | a.Count++ 116 | a.Sum += v 117 | a.SumSq += (v * v) 118 | if v < a.Min || a.Count == 1 { 119 | a.Min = v 120 | } 121 | if v > a.Max || a.Count == 1 { 122 | a.Max = v 123 | } 124 | a.Rate = float64(a.Sum) / rateDenom 125 | a.LastUpdated = time.Now() 126 | } 127 | 128 | func (a *AggregateSample) String() string { 129 | if a.Count == 0 { 130 | return "Count: 0" 131 | } else if a.Stddev() == 0 { 132 | return fmt.Sprintf("Count: %d Sum: %0.3f LastUpdated: %s", a.Count, a.Sum, a.LastUpdated) 133 | } else { 134 | return fmt.Sprintf("Count: %d Min: %0.3f Mean: %0.3f Max: %0.3f Stddev: %0.3f Sum: %0.3f LastUpdated: %s", 135 | a.Count, a.Min, a.Mean(), a.Max, a.Stddev(), a.Sum, a.LastUpdated) 136 | } 137 | } 138 | 139 | // NewInmemSinkFromURL creates an InmemSink from a URL. It is used 140 | // (and tested) from NewMetricSinkFromURL. 141 | func NewInmemSinkFromURL(u *url.URL) (MetricSink, error) { 142 | params := u.Query() 143 | 144 | interval, err := time.ParseDuration(params.Get("interval")) 145 | if err != nil { 146 | return nil, fmt.Errorf("Bad 'interval' param: %s", err) 147 | } 148 | 149 | retain, err := time.ParseDuration(params.Get("retain")) 150 | if err != nil { 151 | return nil, fmt.Errorf("Bad 'retain' param: %s", err) 152 | } 153 | 154 | return NewInmemSink(interval, retain), nil 155 | } 156 | 157 | // NewInmemSink is used to construct a new in-memory sink. 158 | // Uses an aggregation interval and maximum retention period. 159 | func NewInmemSink(interval, retain time.Duration) *InmemSink { 160 | rateTimeUnit := time.Second 161 | i := &InmemSink{ 162 | interval: interval, 163 | retain: retain, 164 | maxIntervals: int(retain / interval), 165 | rateDenom: float64(interval.Nanoseconds()) / float64(rateTimeUnit.Nanoseconds()), 166 | } 167 | i.intervals = make([]*IntervalMetrics, 0, i.maxIntervals) 168 | return i 169 | } 170 | 171 | func (i *InmemSink) SetGauge(key []string, val float32) { 172 | i.SetGaugeWithLabels(key, val, nil) 173 | } 174 | 175 | func (i *InmemSink) SetGaugeWithLabels(key []string, val float32, labels []Label) { 176 | k, name := i.flattenKeyLabels(key, labels) 177 | intv := i.getInterval() 178 | 179 | intv.Lock() 180 | defer intv.Unlock() 181 | intv.Gauges[k] = GaugeValue{Name: name, Value: val, Labels: labels} 182 | } 183 | 184 | func (i *InmemSink) SetPrecisionGauge(key []string, val float64) { 185 | i.SetPrecisionGaugeWithLabels(key, val, nil) 186 | } 187 | 188 | func (i *InmemSink) SetPrecisionGaugeWithLabels(key []string, val float64, labels []Label) { 189 | k, name := i.flattenKeyLabels(key, labels) 190 | intv := i.getInterval() 191 | 192 | intv.Lock() 193 | defer intv.Unlock() 194 | intv.PrecisionGauges[k] = PrecisionGaugeValue{Name: name, Value: val, Labels: labels} 195 | } 196 | 197 | func (i *InmemSink) EmitKey(key []string, val float32) { 198 | k := i.flattenKey(key) 199 | intv := i.getInterval() 200 | 201 | intv.Lock() 202 | defer intv.Unlock() 203 | vals := intv.Points[k] 204 | intv.Points[k] = append(vals, val) 205 | } 206 | 207 | func (i *InmemSink) IncrCounter(key []string, val float32) { 208 | i.IncrCounterWithLabels(key, val, nil) 209 | } 210 | 211 | func (i *InmemSink) IncrCounterWithLabels(key []string, val float32, labels []Label) { 212 | k, name := i.flattenKeyLabels(key, labels) 213 | intv := i.getInterval() 214 | 215 | intv.Lock() 216 | defer intv.Unlock() 217 | 218 | agg, ok := intv.Counters[k] 219 | if !ok { 220 | agg = SampledValue{ 221 | Name: name, 222 | AggregateSample: &AggregateSample{}, 223 | Labels: labels, 224 | } 225 | intv.Counters[k] = agg 226 | } 227 | agg.Ingest(float64(val), i.rateDenom) 228 | } 229 | 230 | func (i *InmemSink) AddSample(key []string, val float32) { 231 | i.AddSampleWithLabels(key, val, nil) 232 | } 233 | 234 | func (i *InmemSink) AddSampleWithLabels(key []string, val float32, labels []Label) { 235 | k, name := i.flattenKeyLabels(key, labels) 236 | intv := i.getInterval() 237 | 238 | intv.Lock() 239 | defer intv.Unlock() 240 | 241 | agg, ok := intv.Samples[k] 242 | if !ok { 243 | agg = SampledValue{ 244 | Name: name, 245 | AggregateSample: &AggregateSample{}, 246 | Labels: labels, 247 | } 248 | intv.Samples[k] = agg 249 | } 250 | agg.Ingest(float64(val), i.rateDenom) 251 | } 252 | 253 | // Data is used to retrieve all the aggregated metrics 254 | // Intervals may be in use, and a read lock should be acquired 255 | func (i *InmemSink) Data() []*IntervalMetrics { 256 | // Get the current interval, forces creation 257 | i.getInterval() 258 | 259 | i.intervalLock.RLock() 260 | defer i.intervalLock.RUnlock() 261 | 262 | n := len(i.intervals) 263 | intervals := make([]*IntervalMetrics, n) 264 | 265 | copy(intervals[:n-1], i.intervals[:n-1]) 266 | current := i.intervals[n-1] 267 | 268 | // make its own copy for current interval 269 | intervals[n-1] = &IntervalMetrics{} 270 | copyCurrent := intervals[n-1] 271 | current.RLock() 272 | *copyCurrent = *current 273 | // RWMutex is not safe to copy, so create a new instance on the copy 274 | copyCurrent.RWMutex = sync.RWMutex{} 275 | 276 | copyCurrent.Gauges = make(map[string]GaugeValue, len(current.Gauges)) 277 | for k, v := range current.Gauges { 278 | copyCurrent.Gauges[k] = v 279 | } 280 | copyCurrent.PrecisionGauges = make(map[string]PrecisionGaugeValue, len(current.PrecisionGauges)) 281 | for k, v := range current.PrecisionGauges { 282 | copyCurrent.PrecisionGauges[k] = v 283 | } 284 | // saved values will be not change, just copy its link 285 | copyCurrent.Points = make(map[string][]float32, len(current.Points)) 286 | for k, v := range current.Points { 287 | copyCurrent.Points[k] = v 288 | } 289 | copyCurrent.Counters = make(map[string]SampledValue, len(current.Counters)) 290 | for k, v := range current.Counters { 291 | copyCurrent.Counters[k] = v.deepCopy() 292 | } 293 | copyCurrent.Samples = make(map[string]SampledValue, len(current.Samples)) 294 | for k, v := range current.Samples { 295 | copyCurrent.Samples[k] = v.deepCopy() 296 | } 297 | current.RUnlock() 298 | 299 | return intervals 300 | } 301 | 302 | // getInterval returns the current interval. A new interval is created if no 303 | // previous interval exists, or if the current time is beyond the window for the 304 | // current interval. 305 | func (i *InmemSink) getInterval() *IntervalMetrics { 306 | intv := time.Now().Truncate(i.interval) 307 | 308 | // Attempt to return the existing interval first, because it only requires 309 | // a read lock. 310 | i.intervalLock.RLock() 311 | n := len(i.intervals) 312 | if n > 0 && i.intervals[n-1].Interval == intv { 313 | defer i.intervalLock.RUnlock() 314 | return i.intervals[n-1] 315 | } 316 | i.intervalLock.RUnlock() 317 | 318 | i.intervalLock.Lock() 319 | defer i.intervalLock.Unlock() 320 | 321 | // Re-check for an existing interval now that the lock is re-acquired. 322 | n = len(i.intervals) 323 | if n > 0 && i.intervals[n-1].Interval == intv { 324 | return i.intervals[n-1] 325 | } 326 | 327 | current := NewIntervalMetrics(intv) 328 | i.intervals = append(i.intervals, current) 329 | if n > 0 { 330 | close(i.intervals[n-1].done) 331 | } 332 | 333 | n++ 334 | // Prune old intervals if the count exceeds the max. 335 | if n >= i.maxIntervals { 336 | copy(i.intervals[0:], i.intervals[n-i.maxIntervals:]) 337 | i.intervals = i.intervals[:i.maxIntervals] 338 | } 339 | return current 340 | } 341 | 342 | // Flattens the key for formatting, removes spaces 343 | func (i *InmemSink) flattenKey(parts []string) string { 344 | buf := &bytes.Buffer{} 345 | 346 | joined := strings.Join(parts, ".") 347 | 348 | spaceReplacer.WriteString(buf, joined) 349 | 350 | return buf.String() 351 | } 352 | 353 | // Flattens the key for formatting along with its labels, removes spaces 354 | func (i *InmemSink) flattenKeyLabels(parts []string, labels []Label) (string, string) { 355 | key := i.flattenKey(parts) 356 | buf := bytes.NewBufferString(key) 357 | 358 | for _, label := range labels { 359 | spaceReplacer.WriteString(buf, fmt.Sprintf(";%s=%s", label.Name, label.Value)) 360 | } 361 | 362 | return buf.String(), key 363 | } 364 | -------------------------------------------------------------------------------- /inmem_endpoint.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | package metrics 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "net/http" 10 | "sort" 11 | "time" 12 | ) 13 | 14 | // MetricsSummary holds a roll-up of metrics info for a given interval 15 | type MetricsSummary struct { 16 | Timestamp string 17 | Gauges []GaugeValue 18 | PrecisionGauges []PrecisionGaugeValue 19 | Points []PointValue 20 | Counters []SampledValue 21 | Samples []SampledValue 22 | } 23 | 24 | type GaugeValue struct { 25 | Name string 26 | Hash string `json:"-"` 27 | Value float32 28 | 29 | Labels []Label `json:"-"` 30 | DisplayLabels map[string]string `json:"Labels"` 31 | } 32 | 33 | type PrecisionGaugeValue struct { 34 | Name string 35 | Hash string `json:"-"` 36 | Value float64 37 | 38 | Labels []Label `json:"-"` 39 | DisplayLabels map[string]string `json:"Labels"` 40 | } 41 | 42 | type PointValue struct { 43 | Name string 44 | Points []float32 45 | } 46 | 47 | type SampledValue struct { 48 | Name string 49 | Hash string `json:"-"` 50 | *AggregateSample 51 | Mean float64 52 | Stddev float64 53 | 54 | Labels []Label `json:"-"` 55 | DisplayLabels map[string]string `json:"Labels"` 56 | } 57 | 58 | // deepCopy allocates a new instance of AggregateSample 59 | func (source *SampledValue) deepCopy() SampledValue { 60 | dest := *source 61 | if source.AggregateSample != nil { 62 | dest.AggregateSample = &AggregateSample{} 63 | *dest.AggregateSample = *source.AggregateSample 64 | } 65 | return dest 66 | } 67 | 68 | // DisplayMetrics returns a summary of the metrics from the most recent finished interval. 69 | func (i *InmemSink) DisplayMetrics(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 70 | data := i.Data() 71 | 72 | var interval *IntervalMetrics 73 | n := len(data) 74 | switch { 75 | case n == 0: 76 | return nil, fmt.Errorf("no metric intervals have been initialized yet") 77 | case n == 1: 78 | // Show the current interval if it's all we have 79 | interval = data[0] 80 | default: 81 | // Show the most recent finished interval if we have one 82 | interval = data[n-2] 83 | } 84 | 85 | return newMetricSummaryFromInterval(interval), nil 86 | } 87 | 88 | func newMetricSummaryFromInterval(interval *IntervalMetrics) MetricsSummary { 89 | interval.RLock() 90 | defer interval.RUnlock() 91 | 92 | summary := MetricsSummary{ 93 | Timestamp: interval.Interval.Round(time.Second).UTC().String(), 94 | Gauges: make([]GaugeValue, 0, len(interval.Gauges)), 95 | PrecisionGauges: make([]PrecisionGaugeValue, 0, len(interval.PrecisionGauges)), 96 | Points: make([]PointValue, 0, len(interval.Points)), 97 | } 98 | 99 | // Format and sort the output of each metric type, so it gets displayed in a 100 | // deterministic order. 101 | for name, points := range interval.Points { 102 | summary.Points = append(summary.Points, PointValue{name, points}) 103 | } 104 | sort.Slice(summary.Points, func(i, j int) bool { 105 | return summary.Points[i].Name < summary.Points[j].Name 106 | }) 107 | 108 | for hash, value := range interval.Gauges { 109 | value.Hash = hash 110 | value.DisplayLabels = make(map[string]string) 111 | for _, label := range value.Labels { 112 | value.DisplayLabels[label.Name] = label.Value 113 | } 114 | value.Labels = nil 115 | 116 | summary.Gauges = append(summary.Gauges, value) 117 | } 118 | sort.Slice(summary.Gauges, func(i, j int) bool { 119 | return summary.Gauges[i].Hash < summary.Gauges[j].Hash 120 | }) 121 | 122 | for hash, value := range interval.PrecisionGauges { 123 | value.Hash = hash 124 | value.DisplayLabels = make(map[string]string) 125 | for _, label := range value.Labels { 126 | value.DisplayLabels[label.Name] = label.Value 127 | } 128 | value.Labels = nil 129 | 130 | summary.PrecisionGauges = append(summary.PrecisionGauges, value) 131 | } 132 | sort.Slice(summary.PrecisionGauges, func(i, j int) bool { 133 | return summary.PrecisionGauges[i].Hash < summary.PrecisionGauges[j].Hash 134 | }) 135 | 136 | summary.Counters = formatSamples(interval.Counters) 137 | summary.Samples = formatSamples(interval.Samples) 138 | 139 | return summary 140 | } 141 | 142 | func formatSamples(source map[string]SampledValue) []SampledValue { 143 | output := make([]SampledValue, 0, len(source)) 144 | for hash, sample := range source { 145 | displayLabels := make(map[string]string) 146 | for _, label := range sample.Labels { 147 | displayLabels[label.Name] = label.Value 148 | } 149 | 150 | output = append(output, SampledValue{ 151 | Name: sample.Name, 152 | Hash: hash, 153 | AggregateSample: sample.AggregateSample, 154 | Mean: sample.AggregateSample.Mean(), 155 | Stddev: sample.AggregateSample.Stddev(), 156 | DisplayLabels: displayLabels, 157 | }) 158 | } 159 | sort.Slice(output, func(i, j int) bool { 160 | return output[i].Hash < output[j].Hash 161 | }) 162 | 163 | return output 164 | } 165 | 166 | type Encoder interface { 167 | Encode(interface{}) error 168 | } 169 | 170 | // Stream writes metrics using encoder.Encode each time an interval ends. Runs 171 | // until the request context is cancelled, or the encoder returns an error. 172 | // The caller is responsible for logging any errors from encoder. 173 | func (i *InmemSink) Stream(ctx context.Context, encoder Encoder) { 174 | interval := i.getInterval() 175 | 176 | for { 177 | select { 178 | case <-interval.done: 179 | summary := newMetricSummaryFromInterval(interval) 180 | if err := encoder.Encode(summary); err != nil { 181 | return 182 | } 183 | 184 | // update interval to the next one 185 | interval = i.getInterval() 186 | case <-ctx.Done(): 187 | return 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /inmem_endpoint_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | package metrics 5 | 6 | import ( 7 | "context" 8 | "encoding/json" 9 | "fmt" 10 | "net/http" 11 | "net/http/httptest" 12 | "testing" 13 | "time" 14 | 15 | "github.com/pascaldekloe/goe/verify" 16 | ) 17 | 18 | func TestDisplayMetrics(t *testing.T) { 19 | interval := 10 * time.Millisecond 20 | inm := NewInmemSink(interval, 50*time.Millisecond) 21 | 22 | // Add data points 23 | inm.SetGauge([]string{"foo", "bar"}, 42) 24 | inm.SetGaugeWithLabels([]string{"foo", "bar"}, 23, []Label{{"a", "b"}}) 25 | inm.EmitKey([]string{"foo", "bar"}, 42) 26 | inm.IncrCounter([]string{"foo", "bar"}, 20) 27 | inm.IncrCounter([]string{"foo", "bar"}, 22) 28 | inm.IncrCounterWithLabels([]string{"foo", "bar"}, 20, []Label{{"a", "b"}}) 29 | inm.IncrCounterWithLabels([]string{"foo", "bar"}, 40, []Label{{"a", "b"}}) 30 | inm.AddSample([]string{"foo", "bar"}, 20) 31 | inm.AddSample([]string{"foo", "bar"}, 24) 32 | inm.AddSampleWithLabels([]string{"foo", "bar"}, 23, []Label{{"a", "b"}}) 33 | inm.AddSampleWithLabels([]string{"foo", "bar"}, 33, []Label{{"a", "b"}}) 34 | 35 | data := inm.Data() 36 | if len(data) != 1 { 37 | t.Fatalf("bad: %v", data) 38 | } 39 | 40 | expected := MetricsSummary{ 41 | Timestamp: data[0].Interval.Round(time.Second).UTC().String(), 42 | Gauges: []GaugeValue{ 43 | { 44 | Name: "foo.bar", 45 | Hash: "foo.bar", 46 | Value: float32(42), 47 | DisplayLabels: map[string]string{}, 48 | }, 49 | { 50 | Name: "foo.bar", 51 | Hash: "foo.bar;a=b", 52 | Value: float32(23), 53 | DisplayLabels: map[string]string{"a": "b"}, 54 | }, 55 | }, 56 | Points: []PointValue{ 57 | { 58 | Name: "foo.bar", 59 | Points: []float32{42}, 60 | }, 61 | }, 62 | Counters: []SampledValue{ 63 | { 64 | Name: "foo.bar", 65 | Hash: "foo.bar", 66 | AggregateSample: &AggregateSample{ 67 | Count: 2, 68 | Min: 20, 69 | Max: 22, 70 | Sum: 42, 71 | SumSq: 884, 72 | Rate: 4200, 73 | }, 74 | Mean: 21, 75 | Stddev: 1.4142135623730951, 76 | }, 77 | { 78 | Name: "foo.bar", 79 | Hash: "foo.bar;a=b", 80 | AggregateSample: &AggregateSample{ 81 | Count: 2, 82 | Min: 20, 83 | Max: 40, 84 | Sum: 60, 85 | SumSq: 2000, 86 | Rate: 6000, 87 | }, 88 | Mean: 30, 89 | Stddev: 14.142135623730951, 90 | DisplayLabels: map[string]string{"a": "b"}, 91 | }, 92 | }, 93 | Samples: []SampledValue{ 94 | { 95 | Name: "foo.bar", 96 | Hash: "foo.bar", 97 | AggregateSample: &AggregateSample{ 98 | Count: 2, 99 | Min: 20, 100 | Max: 24, 101 | Sum: 44, 102 | SumSq: 976, 103 | Rate: 4400, 104 | }, 105 | Mean: 22, 106 | Stddev: 2.8284271247461903, 107 | }, 108 | { 109 | Name: "foo.bar", 110 | Hash: "foo.bar;a=b", 111 | AggregateSample: &AggregateSample{ 112 | Count: 2, 113 | Min: 23, 114 | Max: 33, 115 | Sum: 56, 116 | SumSq: 1618, 117 | Rate: 5600, 118 | }, 119 | Mean: 28, 120 | Stddev: 7.0710678118654755, 121 | DisplayLabels: map[string]string{"a": "b"}, 122 | }, 123 | }, 124 | } 125 | 126 | raw, err := inm.DisplayMetrics(nil, nil) 127 | if err != nil { 128 | t.Fatalf("err: %v", err) 129 | } 130 | result := raw.(MetricsSummary) 131 | 132 | // Ignore the LastUpdated field, we don't export that anyway 133 | for i, got := range result.Counters { 134 | expected.Counters[i].LastUpdated = got.LastUpdated 135 | } 136 | for i, got := range result.Samples { 137 | expected.Samples[i].LastUpdated = got.LastUpdated 138 | } 139 | 140 | verify.Values(t, "all", result, expected) 141 | } 142 | 143 | func TestDisplayMetrics_RaceSetGauge(t *testing.T) { 144 | interval := 200 * time.Millisecond 145 | inm := NewInmemSink(interval, 10*interval) 146 | result := make(chan float32) 147 | 148 | go func() { 149 | for { 150 | time.Sleep(150 * time.Millisecond) 151 | inm.SetGauge([]string{"foo", "bar"}, float32(42)) 152 | } 153 | }() 154 | 155 | go func() { 156 | start := time.Now() 157 | var summary MetricsSummary 158 | // test for twenty intervals 159 | for time.Now().Sub(start) < 20*interval { 160 | time.Sleep(100 * time.Millisecond) 161 | raw, _ := inm.DisplayMetrics(nil, nil) 162 | summary = raw.(MetricsSummary) 163 | } 164 | // save result 165 | for _, g := range summary.Gauges { 166 | if g.Name == "foo.bar" { 167 | result <- g.Value 168 | } 169 | } 170 | close(result) 171 | }() 172 | 173 | got := <-result 174 | verify.Values(t, "all", got, float32(42)) 175 | } 176 | 177 | func TestDisplayMetrics_RaceAddSample(t *testing.T) { 178 | interval := 200 * time.Millisecond 179 | inm := NewInmemSink(interval, 10*interval) 180 | result := make(chan float32) 181 | 182 | go func() { 183 | for { 184 | time.Sleep(75 * time.Millisecond) 185 | inm.AddSample([]string{"foo", "bar"}, float32(0.0)) 186 | } 187 | }() 188 | 189 | go func() { 190 | start := time.Now() 191 | var summary MetricsSummary 192 | // test for twenty intervals 193 | for time.Now().Sub(start) < 20*interval { 194 | time.Sleep(100 * time.Millisecond) 195 | raw, _ := inm.DisplayMetrics(nil, nil) 196 | summary = raw.(MetricsSummary) 197 | } 198 | // save result 199 | for _, g := range summary.Gauges { 200 | if g.Name == "foo.bar" { 201 | result <- g.Value 202 | } 203 | } 204 | close(result) 205 | }() 206 | 207 | got := <-result 208 | verify.Values(t, "all", got, float32(0.0)) 209 | } 210 | 211 | func TestDisplayMetrics_RaceIncrCounter(t *testing.T) { 212 | interval := 200 * time.Millisecond 213 | inm := NewInmemSink(interval, 10*interval) 214 | result := make(chan float32) 215 | 216 | go func() { 217 | for { 218 | time.Sleep(75 * time.Millisecond) 219 | inm.IncrCounter([]string{"foo", "bar"}, float32(0.0)) 220 | } 221 | }() 222 | 223 | go func() { 224 | start := time.Now() 225 | var summary MetricsSummary 226 | // test for twenty intervals 227 | for time.Now().Sub(start) < 20*interval { 228 | time.Sleep(30 * time.Millisecond) 229 | raw, _ := inm.DisplayMetrics(nil, nil) 230 | summary = raw.(MetricsSummary) 231 | } 232 | // save result for testing 233 | for _, g := range summary.Gauges { 234 | if g.Name == "foo.bar" { 235 | result <- g.Value 236 | } 237 | } 238 | close(result) 239 | }() 240 | 241 | got := <-result 242 | verify.Values(t, "all", got, float32(0.0)) 243 | } 244 | 245 | func TestDisplayMetrics_RaceMetricsSetGauge(t *testing.T) { 246 | interval := 200 * time.Millisecond 247 | inm := NewInmemSink(interval, 10*interval) 248 | met := &Metrics{Config: Config{FilterDefault: true}, sink: inm} 249 | result := make(chan float32) 250 | labels := []Label{ 251 | {"name1", "value1"}, 252 | {"name2", "value2"}, 253 | } 254 | 255 | go func() { 256 | for { 257 | time.Sleep(75 * time.Millisecond) 258 | met.SetGaugeWithLabels([]string{"foo", "bar"}, float32(42), labels) 259 | } 260 | }() 261 | 262 | go func() { 263 | start := time.Now() 264 | var summary MetricsSummary 265 | // test for twenty intervals 266 | for time.Now().Sub(start) < 40*interval { 267 | time.Sleep(150 * time.Millisecond) 268 | raw, _ := inm.DisplayMetrics(nil, nil) 269 | summary = raw.(MetricsSummary) 270 | } 271 | // save result 272 | for _, g := range summary.Gauges { 273 | if g.Name == "foo.bar" { 274 | result <- g.Value 275 | } 276 | } 277 | close(result) 278 | }() 279 | 280 | got := <-result 281 | verify.Values(t, "all", got, float32(42)) 282 | } 283 | 284 | func TestInmemSink_Stream(t *testing.T) { 285 | interval := 10 * time.Millisecond 286 | total := 50 * time.Millisecond 287 | inm := NewInmemSink(interval, total) 288 | 289 | ctx, cancel := context.WithTimeout(context.Background(), total*2) 290 | defer cancel() 291 | 292 | chDone := make(chan struct{}) 293 | 294 | go func() { 295 | for i := float32(0); ctx.Err() == nil; i++ { 296 | inm.SetGaugeWithLabels([]string{"gauge", "foo"}, 20+i, []Label{{"a", "b"}}) 297 | inm.EmitKey([]string{"key", "foo"}, 30+i) 298 | inm.IncrCounterWithLabels([]string{"counter", "bar"}, 40+i, []Label{{"a", "b"}}) 299 | inm.IncrCounterWithLabels([]string{"counter", "bar"}, 50+i, []Label{{"a", "b"}}) 300 | inm.AddSampleWithLabels([]string{"sample", "bar"}, 60+i, []Label{{"a", "b"}}) 301 | inm.AddSampleWithLabels([]string{"sample", "bar"}, 70+i, []Label{{"a", "b"}}) 302 | time.Sleep(interval / 3) 303 | } 304 | close(chDone) 305 | }() 306 | 307 | resp := httptest.NewRecorder() 308 | enc := encoder{ 309 | encoder: json.NewEncoder(resp), 310 | flusher: resp, 311 | } 312 | inm.Stream(ctx, enc) 313 | 314 | <-chDone 315 | 316 | decoder := json.NewDecoder(resp.Body) 317 | var prevGaugeValue float32 318 | for i := 0; i < 8; i++ { 319 | var summary MetricsSummary 320 | if err := decoder.Decode(&summary); err != nil { 321 | t.Fatalf("expected no error while decoding response %d, got %v", i, err) 322 | } 323 | if count := len(summary.Gauges); count != 1 { 324 | t.Fatalf("expected at least one gauge in response %d, got %v", i, count) 325 | } 326 | value := summary.Gauges[0].Value 327 | // The upper bound of the gauge value is not known, but we can expect it 328 | // to be less than 50 because it increments by 3 every interval and we run 329 | // for ~10 intervals. 330 | if value < 20 || value > 50 { 331 | t.Fatalf("expected interval %d guage value between 20 and 50, got %v", i, value) 332 | } 333 | if value <= prevGaugeValue { 334 | t.Fatalf("expected interval %d guage value to be greater than previous, %v == %v", i, value, prevGaugeValue) 335 | } 336 | prevGaugeValue = value 337 | } 338 | } 339 | 340 | type encoder struct { 341 | flusher http.Flusher 342 | encoder *json.Encoder 343 | } 344 | 345 | func (e encoder) Encode(metrics interface{}) error { 346 | if err := e.encoder.Encode(metrics); err != nil { 347 | fmt.Println("failed to encode metrics summary", "error", err) 348 | return err 349 | } 350 | e.flusher.Flush() 351 | return nil 352 | } 353 | -------------------------------------------------------------------------------- /inmem_signal.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | package metrics 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "io" 10 | "os" 11 | "os/signal" 12 | "strings" 13 | "sync" 14 | "syscall" 15 | ) 16 | 17 | // InmemSignal is used to listen for a given signal, and when received, 18 | // to dump the current metrics from the InmemSink to an io.Writer 19 | type InmemSignal struct { 20 | signal syscall.Signal 21 | inm *InmemSink 22 | w io.Writer 23 | sigCh chan os.Signal 24 | 25 | stop bool 26 | stopCh chan struct{} 27 | stopLock sync.Mutex 28 | } 29 | 30 | // NewInmemSignal creates a new InmemSignal which listens for a given signal, 31 | // and dumps the current metrics out to a writer 32 | func NewInmemSignal(inmem *InmemSink, sig syscall.Signal, w io.Writer) *InmemSignal { 33 | i := &InmemSignal{ 34 | signal: sig, 35 | inm: inmem, 36 | w: w, 37 | sigCh: make(chan os.Signal, 1), 38 | stopCh: make(chan struct{}), 39 | } 40 | signal.Notify(i.sigCh, sig) 41 | go i.run() 42 | return i 43 | } 44 | 45 | // DefaultInmemSignal returns a new InmemSignal that responds to SIGUSR1 46 | // and writes output to stderr. Windows uses SIGBREAK 47 | func DefaultInmemSignal(inmem *InmemSink) *InmemSignal { 48 | return NewInmemSignal(inmem, DefaultSignal, os.Stderr) 49 | } 50 | 51 | // Stop is used to stop the InmemSignal from listening 52 | func (i *InmemSignal) Stop() { 53 | i.stopLock.Lock() 54 | defer i.stopLock.Unlock() 55 | 56 | if i.stop { 57 | return 58 | } 59 | i.stop = true 60 | close(i.stopCh) 61 | signal.Stop(i.sigCh) 62 | } 63 | 64 | // run is a long running routine that handles signals 65 | func (i *InmemSignal) run() { 66 | for { 67 | select { 68 | case <-i.sigCh: 69 | i.dumpStats() 70 | case <-i.stopCh: 71 | return 72 | } 73 | } 74 | } 75 | 76 | // dumpStats is used to dump the data to output writer 77 | func (i *InmemSignal) dumpStats() { 78 | buf := bytes.NewBuffer(nil) 79 | 80 | data := i.inm.Data() 81 | // Skip the last period which is still being aggregated 82 | for j := 0; j < len(data)-1; j++ { 83 | intv := data[j] 84 | intv.RLock() 85 | for _, val := range intv.Gauges { 86 | name := i.flattenLabels(val.Name, val.Labels) 87 | fmt.Fprintf(buf, "[%v][G] '%s': %0.3f\n", intv.Interval, name, val.Value) 88 | } 89 | for _, val := range intv.PrecisionGauges { 90 | name := i.flattenLabels(val.Name, val.Labels) 91 | fmt.Fprintf(buf, "[%v][G] '%s': %0.3f\n", intv.Interval, name, val.Value) 92 | } 93 | for name, vals := range intv.Points { 94 | for _, val := range vals { 95 | fmt.Fprintf(buf, "[%v][P] '%s': %0.3f\n", intv.Interval, name, val) 96 | } 97 | } 98 | for _, agg := range intv.Counters { 99 | name := i.flattenLabels(agg.Name, agg.Labels) 100 | fmt.Fprintf(buf, "[%v][C] '%s': %s\n", intv.Interval, name, agg.AggregateSample) 101 | } 102 | for _, agg := range intv.Samples { 103 | name := i.flattenLabels(agg.Name, agg.Labels) 104 | fmt.Fprintf(buf, "[%v][S] '%s': %s\n", intv.Interval, name, agg.AggregateSample) 105 | } 106 | intv.RUnlock() 107 | } 108 | 109 | // Write out the bytes 110 | i.w.Write(buf.Bytes()) 111 | } 112 | 113 | // Flattens the key for formatting along with its labels, removes spaces 114 | func (i *InmemSignal) flattenLabels(name string, labels []Label) string { 115 | buf := bytes.NewBufferString(name) 116 | replacer := strings.NewReplacer(" ", "_", ":", "_") 117 | 118 | for _, label := range labels { 119 | replacer.WriteString(buf, ".") 120 | replacer.WriteString(buf, label.Value) 121 | } 122 | 123 | return buf.String() 124 | } 125 | -------------------------------------------------------------------------------- /inmem_signal_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | package metrics 5 | 6 | import ( 7 | "bytes" 8 | "os" 9 | "strings" 10 | "sync" 11 | "syscall" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | func TestInmemSignal(t *testing.T) { 17 | buf := newBuffer() 18 | inm := NewInmemSink(10*time.Millisecond, 50*time.Millisecond) 19 | sig := NewInmemSignal(inm, syscall.SIGUSR1, buf) 20 | defer sig.Stop() 21 | 22 | inm.SetGauge([]string{"foo"}, 42) 23 | inm.EmitKey([]string{"bar"}, 42) 24 | inm.IncrCounter([]string{"baz"}, 42) 25 | inm.AddSample([]string{"wow"}, 42) 26 | inm.SetGaugeWithLabels([]string{"asdf"}, 42, []Label{{"a", "b"}}) 27 | inm.IncrCounterWithLabels([]string{"qwer"}, 42, []Label{{"a", "b"}}) 28 | inm.AddSampleWithLabels([]string{"zxcv"}, 42, []Label{{"a", "b"}}) 29 | 30 | // Wait for period to end 31 | time.Sleep(15 * time.Millisecond) 32 | 33 | // Send signal! 34 | syscall.Kill(os.Getpid(), syscall.SIGUSR1) 35 | 36 | // Wait for flush 37 | time.Sleep(10 * time.Millisecond) 38 | 39 | // Check the output 40 | out := buf.String() 41 | if !strings.Contains(out, "[G] 'foo': 42") { 42 | t.Fatalf("bad: %v", out) 43 | } 44 | if !strings.Contains(out, "[P] 'bar': 42") { 45 | t.Fatalf("bad: %v", out) 46 | } 47 | if !strings.Contains(out, "[C] 'baz': Count: 1 Sum: 42") { 48 | t.Fatalf("bad: %v", out) 49 | } 50 | if !strings.Contains(out, "[S] 'wow': Count: 1 Sum: 42") { 51 | t.Fatalf("bad: %v", out) 52 | } 53 | if !strings.Contains(out, "[G] 'asdf.b': 42") { 54 | t.Fatalf("bad: %v", out) 55 | } 56 | if !strings.Contains(out, "[C] 'qwer.b': Count: 1 Sum: 42") { 57 | t.Fatalf("bad: %v", out) 58 | } 59 | if !strings.Contains(out, "[S] 'zxcv.b': Count: 1 Sum: 42") { 60 | t.Fatalf("bad: %v", out) 61 | } 62 | } 63 | 64 | func newBuffer() *syncBuffer { 65 | return &syncBuffer{buf: bytes.NewBuffer(nil)} 66 | } 67 | 68 | type syncBuffer struct { 69 | buf *bytes.Buffer 70 | lock sync.Mutex 71 | } 72 | 73 | func (s *syncBuffer) Write(p []byte) (int, error) { 74 | s.lock.Lock() 75 | defer s.lock.Unlock() 76 | 77 | return s.buf.Write(p) 78 | } 79 | 80 | func (s *syncBuffer) String() string { 81 | s.lock.Lock() 82 | defer s.lock.Unlock() 83 | 84 | return s.buf.String() 85 | } 86 | -------------------------------------------------------------------------------- /inmem_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | package metrics 5 | 6 | import ( 7 | "math" 8 | "net/url" 9 | "strings" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func TestInmemSink(t *testing.T) { 15 | inm := NewInmemSink(10*time.Millisecond, 50*time.Millisecond) 16 | 17 | data := inm.Data() 18 | if len(data) != 1 { 19 | t.Fatalf("bad: %v", data) 20 | } 21 | 22 | // Add data points 23 | inm.SetGauge([]string{"foo", "bar"}, 42) 24 | inm.SetGaugeWithLabels([]string{"foo", "bar"}, 23, []Label{{"a", "b"}}) 25 | inm.EmitKey([]string{"foo", "bar"}, 42) 26 | inm.IncrCounter([]string{"foo", "bar"}, 20) 27 | inm.IncrCounter([]string{"foo", "bar"}, 22) 28 | inm.IncrCounterWithLabels([]string{"foo", "bar"}, 20, []Label{{"a", "b"}}) 29 | inm.IncrCounterWithLabels([]string{"foo", "bar"}, 22, []Label{{"a", "b"}}) 30 | inm.AddSample([]string{"foo", "bar"}, 20) 31 | inm.AddSample([]string{"foo", "bar"}, 22) 32 | inm.AddSampleWithLabels([]string{"foo", "bar"}, 23, []Label{{"a", "b"}}) 33 | 34 | data = inm.Data() 35 | if len(data) != 1 { 36 | t.Fatalf("bad: %v", data) 37 | } 38 | 39 | intvM := data[0] 40 | intvM.RLock() 41 | 42 | if time.Now().Sub(intvM.Interval) > 10*time.Millisecond { 43 | t.Fatalf("interval too old") 44 | } 45 | if intvM.Gauges["foo.bar"].Value != 42 { 46 | t.Fatalf("bad val: %v", intvM.Gauges) 47 | } 48 | if intvM.Gauges["foo.bar;a=b"].Value != 23 { 49 | t.Fatalf("bad val: %v", intvM.Gauges) 50 | } 51 | if intvM.Points["foo.bar"][0] != 42 { 52 | t.Fatalf("bad val: %v", intvM.Points) 53 | } 54 | 55 | for _, agg := range []SampledValue{intvM.Counters["foo.bar"], intvM.Counters["foo.bar;a=b"]} { 56 | if agg.Count != 2 { 57 | t.Fatalf("bad val: %v", agg) 58 | } 59 | if agg.Rate != 4200 { 60 | t.Fatalf("bad val: %v", agg.Rate) 61 | } 62 | if agg.Sum != 42 { 63 | t.Fatalf("bad val: %v", agg) 64 | } 65 | if agg.SumSq != 884 { 66 | t.Fatalf("bad val: %v", agg) 67 | } 68 | if agg.Min != 20 { 69 | t.Fatalf("bad val: %v", agg) 70 | } 71 | if agg.Max != 22 { 72 | t.Fatalf("bad val: %v", agg) 73 | } 74 | if agg.AggregateSample.Mean() != 21 { 75 | t.Fatalf("bad val: %v", agg) 76 | } 77 | if agg.AggregateSample.Stddev() != math.Sqrt(2) { 78 | t.Fatalf("bad val: %v", agg) 79 | } 80 | 81 | if agg.LastUpdated.IsZero() { 82 | t.Fatalf("agg.LastUpdated is not set: %v", agg) 83 | } 84 | 85 | diff := time.Now().Sub(agg.LastUpdated).Seconds() 86 | if diff > 1 { 87 | t.Fatalf("time diff too great: %f", diff) 88 | } 89 | } 90 | 91 | if _, ok := intvM.Samples["foo.bar"]; !ok { 92 | t.Fatalf("missing sample") 93 | } 94 | 95 | if _, ok := intvM.Samples["foo.bar;a=b"]; !ok { 96 | t.Fatalf("missing sample") 97 | } 98 | 99 | intvM.RUnlock() 100 | 101 | for i := 1; i < 10; i++ { 102 | time.Sleep(10 * time.Millisecond) 103 | inm.SetGauge([]string{"foo", "bar"}, 42) 104 | data = inm.Data() 105 | if len(data) != min(i+1, 5) { 106 | t.Fatalf("bad: %v", data) 107 | } 108 | } 109 | 110 | // Should not exceed 5 intervals! 111 | time.Sleep(10 * time.Millisecond) 112 | inm.SetGauge([]string{"foo", "bar"}, 42) 113 | data = inm.Data() 114 | if len(data) != 5 { 115 | t.Fatalf("bad: %v", data) 116 | } 117 | } 118 | 119 | func TestNewInmemSinkFromURL(t *testing.T) { 120 | for _, tc := range []struct { 121 | desc string 122 | input string 123 | expectErr string 124 | expectInterval time.Duration 125 | expectRetain time.Duration 126 | }{ 127 | { 128 | desc: "interval and duration are set via query params", 129 | input: "inmem://?interval=11s&retain=22s", 130 | expectInterval: duration(t, "11s"), 131 | expectRetain: duration(t, "22s"), 132 | }, 133 | { 134 | desc: "interval is required", 135 | input: "inmem://?retain=22s", 136 | expectErr: "Bad 'interval' param", 137 | }, 138 | { 139 | desc: "interval must be a duration", 140 | input: "inmem://?retain=30s&interval=HIYA", 141 | expectErr: "Bad 'interval' param", 142 | }, 143 | { 144 | desc: "retain is required", 145 | input: "inmem://?interval=30s", 146 | expectErr: "Bad 'retain' param", 147 | }, 148 | { 149 | desc: "retain must be a valid duration", 150 | input: "inmem://?interval=30s&retain=HELLO", 151 | expectErr: "Bad 'retain' param", 152 | }, 153 | } { 154 | t.Run(tc.desc, func(t *testing.T) { 155 | u, err := url.Parse(tc.input) 156 | if err != nil { 157 | t.Fatalf("error parsing URL: %s", err) 158 | } 159 | ms, err := NewInmemSinkFromURL(u) 160 | if tc.expectErr != "" { 161 | if !strings.Contains(err.Error(), tc.expectErr) { 162 | t.Fatalf("expected err: %q, to contain: %q", err, tc.expectErr) 163 | } 164 | } else { 165 | if err != nil { 166 | t.Fatalf("unexpected err: %s", err) 167 | } 168 | is := ms.(*InmemSink) 169 | if is.interval != tc.expectInterval { 170 | t.Fatalf("expected interval %s, got: %s", tc.expectInterval, is.interval) 171 | } 172 | if is.retain != tc.expectRetain { 173 | t.Fatalf("expected retain %s, got: %s", tc.expectRetain, is.retain) 174 | } 175 | } 176 | }) 177 | } 178 | } 179 | 180 | func min(a, b int) int { 181 | if a < b { 182 | return a 183 | } 184 | return b 185 | } 186 | 187 | func duration(t *testing.T, s string) time.Duration { 188 | dur, err := time.ParseDuration(s) 189 | if err != nil { 190 | t.Fatalf("error parsing duration: %s", err) 191 | } 192 | return dur 193 | } 194 | -------------------------------------------------------------------------------- /metrics.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | package metrics 5 | 6 | import ( 7 | "runtime" 8 | "strings" 9 | "time" 10 | 11 | iradix "github.com/hashicorp/go-immutable-radix" 12 | ) 13 | 14 | type Label struct { 15 | Name string 16 | Value string 17 | } 18 | 19 | func (m *Metrics) SetGauge(key []string, val float32) { 20 | m.SetGaugeWithLabels(key, val, nil) 21 | } 22 | 23 | func (m *Metrics) SetGaugeWithLabels(key []string, val float32, labels []Label) { 24 | if m.HostName != "" { 25 | if m.EnableHostnameLabel { 26 | labels = append(labels, Label{"host", m.HostName}) 27 | } else if m.EnableHostname { 28 | key = insert(0, m.HostName, key) 29 | } 30 | } 31 | if m.EnableTypePrefix { 32 | key = insert(0, "gauge", key) 33 | } 34 | if m.ServiceName != "" { 35 | if m.EnableServiceLabel { 36 | labels = append(labels, Label{"service", m.ServiceName}) 37 | } else { 38 | key = insert(0, m.ServiceName, key) 39 | } 40 | } 41 | allowed, labelsFiltered := m.allowMetric(key, labels) 42 | if !allowed { 43 | return 44 | } 45 | m.sink.SetGaugeWithLabels(key, val, labelsFiltered) 46 | } 47 | 48 | func (m *Metrics) SetPrecisionGauge(key []string, val float64) { 49 | m.SetPrecisionGaugeWithLabels(key, val, nil) 50 | } 51 | 52 | func (m *Metrics) SetPrecisionGaugeWithLabels(key []string, val float64, labels []Label) { 53 | if m.HostName != "" { 54 | if m.EnableHostnameLabel { 55 | labels = append(labels, Label{"host", m.HostName}) 56 | } else if m.EnableHostname { 57 | key = insert(0, m.HostName, key) 58 | } 59 | } 60 | if m.EnableTypePrefix { 61 | key = insert(0, "gauge", key) 62 | } 63 | if m.ServiceName != "" { 64 | if m.EnableServiceLabel { 65 | labels = append(labels, Label{"service", m.ServiceName}) 66 | } else { 67 | key = insert(0, m.ServiceName, key) 68 | } 69 | } 70 | allowed, labelsFiltered := m.allowMetric(key, labels) 71 | if !allowed { 72 | return 73 | } 74 | sink, ok := m.sink.(PrecisionGaugeMetricSink) 75 | if !ok { 76 | // Sink does not implement PrecisionGaugeMetricSink. 77 | } else { 78 | sink.SetPrecisionGaugeWithLabels(key, val, labelsFiltered) 79 | } 80 | } 81 | 82 | func (m *Metrics) EmitKey(key []string, val float32) { 83 | if m.EnableTypePrefix { 84 | key = insert(0, "kv", key) 85 | } 86 | if m.ServiceName != "" { 87 | key = insert(0, m.ServiceName, key) 88 | } 89 | allowed, _ := m.allowMetric(key, nil) 90 | if !allowed { 91 | return 92 | } 93 | m.sink.EmitKey(key, val) 94 | } 95 | 96 | func (m *Metrics) IncrCounter(key []string, val float32) { 97 | m.IncrCounterWithLabels(key, val, nil) 98 | } 99 | 100 | func (m *Metrics) IncrCounterWithLabels(key []string, val float32, labels []Label) { 101 | if m.HostName != "" && m.EnableHostnameLabel { 102 | labels = append(labels, Label{"host", m.HostName}) 103 | } 104 | if m.EnableTypePrefix { 105 | key = insert(0, "counter", key) 106 | } 107 | if m.ServiceName != "" { 108 | if m.EnableServiceLabel { 109 | labels = append(labels, Label{"service", m.ServiceName}) 110 | } else { 111 | key = insert(0, m.ServiceName, key) 112 | } 113 | } 114 | allowed, labelsFiltered := m.allowMetric(key, labels) 115 | if !allowed { 116 | return 117 | } 118 | m.sink.IncrCounterWithLabels(key, val, labelsFiltered) 119 | } 120 | 121 | func (m *Metrics) AddSample(key []string, val float32) { 122 | m.AddSampleWithLabels(key, val, nil) 123 | } 124 | 125 | func (m *Metrics) AddSampleWithLabels(key []string, val float32, labels []Label) { 126 | if m.HostName != "" && m.EnableHostnameLabel { 127 | labels = append(labels, Label{"host", m.HostName}) 128 | } 129 | if m.EnableTypePrefix { 130 | key = insert(0, "sample", key) 131 | } 132 | if m.ServiceName != "" { 133 | if m.EnableServiceLabel { 134 | labels = append(labels, Label{"service", m.ServiceName}) 135 | } else { 136 | key = insert(0, m.ServiceName, key) 137 | } 138 | } 139 | allowed, labelsFiltered := m.allowMetric(key, labels) 140 | if !allowed { 141 | return 142 | } 143 | m.sink.AddSampleWithLabels(key, val, labelsFiltered) 144 | } 145 | 146 | func (m *Metrics) MeasureSince(key []string, start time.Time) { 147 | m.MeasureSinceWithLabels(key, start, nil) 148 | } 149 | 150 | func (m *Metrics) MeasureSinceWithLabels(key []string, start time.Time, labels []Label) { 151 | if m.HostName != "" && m.EnableHostnameLabel { 152 | labels = append(labels, Label{"host", m.HostName}) 153 | } 154 | if m.EnableTypePrefix { 155 | key = insert(0, "timer", key) 156 | } 157 | if m.ServiceName != "" { 158 | if m.EnableServiceLabel { 159 | labels = append(labels, Label{"service", m.ServiceName}) 160 | } else { 161 | key = insert(0, m.ServiceName, key) 162 | } 163 | } 164 | allowed, labelsFiltered := m.allowMetric(key, labels) 165 | if !allowed { 166 | return 167 | } 168 | now := time.Now() 169 | elapsed := now.Sub(start) 170 | msec := float32(elapsed.Nanoseconds()) / float32(m.TimerGranularity) 171 | m.sink.AddSampleWithLabels(key, msec, labelsFiltered) 172 | } 173 | 174 | // UpdateFilter overwrites the existing filter with the given rules. 175 | func (m *Metrics) UpdateFilter(allow, block []string) { 176 | m.UpdateFilterAndLabels(allow, block, m.AllowedLabels, m.BlockedLabels) 177 | } 178 | 179 | // UpdateFilterAndLabels overwrites the existing filter with the given rules. 180 | func (m *Metrics) UpdateFilterAndLabels(allow, block, allowedLabels, blockedLabels []string) { 181 | m.filterLock.Lock() 182 | defer m.filterLock.Unlock() 183 | 184 | m.AllowedPrefixes = allow 185 | m.BlockedPrefixes = block 186 | 187 | if allowedLabels == nil { 188 | // Having a white list means we take only elements from it 189 | m.allowedLabels = nil 190 | } else { 191 | m.allowedLabels = make(map[string]bool) 192 | for _, v := range allowedLabels { 193 | m.allowedLabels[v] = true 194 | } 195 | } 196 | m.blockedLabels = make(map[string]bool) 197 | for _, v := range blockedLabels { 198 | m.blockedLabels[v] = true 199 | } 200 | m.AllowedLabels = allowedLabels 201 | m.BlockedLabels = blockedLabels 202 | 203 | m.filter = iradix.New() 204 | for _, prefix := range m.AllowedPrefixes { 205 | m.filter, _, _ = m.filter.Insert([]byte(prefix), true) 206 | } 207 | for _, prefix := range m.BlockedPrefixes { 208 | m.filter, _, _ = m.filter.Insert([]byte(prefix), false) 209 | } 210 | } 211 | 212 | func (m *Metrics) Shutdown() { 213 | if ss, ok := m.sink.(ShutdownSink); ok { 214 | ss.Shutdown() 215 | } 216 | } 217 | 218 | // labelIsAllowed return true if a should be included in metric 219 | // the caller should lock m.filterLock while calling this method 220 | func (m *Metrics) labelIsAllowed(label *Label) bool { 221 | labelName := (*label).Name 222 | if m.blockedLabels != nil { 223 | _, ok := m.blockedLabels[labelName] 224 | if ok { 225 | // If present, let's remove this label 226 | return false 227 | } 228 | } 229 | if m.allowedLabels != nil { 230 | _, ok := m.allowedLabels[labelName] 231 | return ok 232 | } 233 | // Allow by default 234 | return true 235 | } 236 | 237 | // filterLabels return only allowed labels 238 | // the caller should lock m.filterLock while calling this method 239 | func (m *Metrics) filterLabels(labels []Label) []Label { 240 | if labels == nil { 241 | return nil 242 | } 243 | toReturn := []Label{} 244 | for _, label := range labels { 245 | if m.labelIsAllowed(&label) { 246 | toReturn = append(toReturn, label) 247 | } 248 | } 249 | return toReturn 250 | } 251 | 252 | // Returns whether the metric should be allowed based on configured prefix filters 253 | // Also return the applicable labels 254 | func (m *Metrics) allowMetric(key []string, labels []Label) (bool, []Label) { 255 | m.filterLock.RLock() 256 | defer m.filterLock.RUnlock() 257 | 258 | if m.filter == nil || m.filter.Len() == 0 { 259 | return m.Config.FilterDefault, m.filterLabels(labels) 260 | } 261 | 262 | _, allowed, ok := m.filter.Root().LongestPrefix([]byte(strings.Join(key, "."))) 263 | if !ok { 264 | return m.Config.FilterDefault, m.filterLabels(labels) 265 | } 266 | 267 | return allowed.(bool), m.filterLabels(labels) 268 | } 269 | 270 | // Periodically collects runtime stats to publish 271 | func (m *Metrics) collectStats() { 272 | for { 273 | time.Sleep(m.ProfileInterval) 274 | m.EmitRuntimeStats() 275 | } 276 | } 277 | 278 | // Emits various runtime statsitics 279 | func (m *Metrics) EmitRuntimeStats() { 280 | // Export number of Goroutines 281 | numRoutines := runtime.NumGoroutine() 282 | m.SetGauge([]string{"runtime", "num_goroutines"}, float32(numRoutines)) 283 | 284 | // Export memory stats 285 | var stats runtime.MemStats 286 | runtime.ReadMemStats(&stats) 287 | m.SetGauge([]string{"runtime", "alloc_bytes"}, float32(stats.Alloc)) 288 | m.SetGauge([]string{"runtime", "sys_bytes"}, float32(stats.Sys)) 289 | m.SetGauge([]string{"runtime", "malloc_count"}, float32(stats.Mallocs)) 290 | m.SetGauge([]string{"runtime", "free_count"}, float32(stats.Frees)) 291 | m.SetGauge([]string{"runtime", "heap_objects"}, float32(stats.HeapObjects)) 292 | m.SetGauge([]string{"runtime", "total_gc_pause_ns"}, float32(stats.PauseTotalNs)) 293 | m.SetGauge([]string{"runtime", "total_gc_runs"}, float32(stats.NumGC)) 294 | 295 | // Export info about the last few GC runs 296 | num := stats.NumGC 297 | 298 | // Handle wrap around 299 | if num < m.lastNumGC { 300 | m.lastNumGC = 0 301 | } 302 | 303 | // Ensure we don't scan more than 256 304 | if num-m.lastNumGC >= 256 { 305 | m.lastNumGC = num - 255 306 | } 307 | 308 | for i := m.lastNumGC; i < num; i++ { 309 | pause := stats.PauseNs[i%256] 310 | m.AddSample([]string{"runtime", "gc_pause_ns"}, float32(pause)) 311 | } 312 | m.lastNumGC = num 313 | } 314 | 315 | // Creates a new slice with the provided string value as the first element 316 | // and the provided slice values as the remaining values. 317 | // Ordering of the values in the provided input slice is kept in tact in the output slice. 318 | func insert(i int, v string, s []string) []string { 319 | // Allocate new slice to avoid modifying the input slice 320 | newS := make([]string, len(s)+1) 321 | 322 | // Copy s[0, i-1] into newS 323 | for j := 0; j < i; j++ { 324 | newS[j] = s[j] 325 | } 326 | 327 | // Insert provided element at index i 328 | newS[i] = v 329 | 330 | // Copy s[i, len(s)-1] into newS starting at newS[i+1] 331 | for j := i; j < len(s); j++ { 332 | newS[j+1] = s[j] 333 | } 334 | 335 | return newS 336 | } 337 | -------------------------------------------------------------------------------- /metrics_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | package metrics 5 | 6 | import ( 7 | "reflect" 8 | "runtime" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func mockMetric() (*MockSink, *Metrics) { 14 | m := &MockSink{} 15 | met := &Metrics{Config: Config{FilterDefault: true}, sink: m} 16 | return m, met 17 | } 18 | 19 | func TestMetrics_SetGauge(t *testing.T) { 20 | m, met := mockMetric() 21 | met.SetGauge([]string{"key"}, float32(1)) 22 | if m.getKeys()[0][0] != "key" { 23 | t.Fatalf("") 24 | } 25 | if m.vals[0] != 1 { 26 | t.Fatalf("") 27 | } 28 | 29 | m, met = mockMetric() 30 | labels := []Label{{"a", "b"}} 31 | met.SetGaugeWithLabels([]string{"key"}, float32(1), labels) 32 | if m.getKeys()[0][0] != "key" { 33 | t.Fatalf("") 34 | } 35 | if m.vals[0] != 1 { 36 | t.Fatalf("") 37 | } 38 | if !reflect.DeepEqual(m.labels[0], labels) { 39 | t.Fatalf("") 40 | } 41 | 42 | m, met = mockMetric() 43 | met.HostName = "test" 44 | met.EnableHostname = true 45 | met.SetGauge([]string{"key"}, float32(1)) 46 | if m.getKeys()[0][0] != "test" || m.getKeys()[0][1] != "key" { 47 | t.Fatalf("") 48 | } 49 | if m.vals[0] != 1 { 50 | t.Fatalf("") 51 | } 52 | 53 | m, met = mockMetric() 54 | met.EnableTypePrefix = true 55 | met.SetGauge([]string{"key"}, float32(1)) 56 | if m.getKeys()[0][0] != "gauge" || m.getKeys()[0][1] != "key" { 57 | t.Fatalf("") 58 | } 59 | if m.vals[0] != 1 { 60 | t.Fatalf("") 61 | } 62 | 63 | m, met = mockMetric() 64 | met.ServiceName = "service" 65 | met.SetGauge([]string{"key"}, float32(1)) 66 | if m.getKeys()[0][0] != "service" || m.getKeys()[0][1] != "key" { 67 | t.Fatalf("") 68 | } 69 | if m.vals[0] != 1 { 70 | t.Fatalf("") 71 | } 72 | } 73 | 74 | func TestMetrics_SetPrecisionGauge(t *testing.T) { 75 | m, met := mockMetric() 76 | met.SetPrecisionGauge([]string{"key"}, float64(1)) 77 | if m.getKeys()[0][0] != "key" { 78 | t.Fatalf("") 79 | } 80 | if m.precisionVals[0] != 1 { 81 | t.Fatalf("") 82 | } 83 | 84 | m, met = mockMetric() 85 | labels := []Label{{"a", "b"}} 86 | met.SetPrecisionGaugeWithLabels([]string{"key"}, float64(1), labels) 87 | if m.getKeys()[0][0] != "key" { 88 | t.Fatalf("") 89 | } 90 | if m.precisionVals[0] != 1 { 91 | t.Fatalf("") 92 | } 93 | if !reflect.DeepEqual(m.labels[0], labels) { 94 | t.Fatalf("") 95 | } 96 | 97 | m, met = mockMetric() 98 | met.HostName = "test" 99 | met.EnableHostname = true 100 | met.SetPrecisionGauge([]string{"key"}, float64(1)) 101 | if m.getKeys()[0][0] != "test" || m.getKeys()[0][1] != "key" { 102 | t.Fatalf("") 103 | } 104 | if m.precisionVals[0] != 1 { 105 | t.Fatalf("") 106 | } 107 | 108 | m, met = mockMetric() 109 | met.EnableTypePrefix = true 110 | met.SetPrecisionGauge([]string{"key"}, float64(1)) 111 | if m.getKeys()[0][0] != "gauge" || m.getKeys()[0][1] != "key" { 112 | t.Fatalf("") 113 | } 114 | if m.precisionVals[0] != 1 { 115 | t.Fatalf("") 116 | } 117 | 118 | m, met = mockMetric() 119 | met.ServiceName = "service" 120 | met.SetPrecisionGauge([]string{"key"}, float64(1)) 121 | if m.getKeys()[0][0] != "service" || m.getKeys()[0][1] != "key" { 122 | t.Fatalf("") 123 | } 124 | if m.precisionVals[0] != 1 { 125 | t.Fatalf("") 126 | } 127 | } 128 | 129 | func TestMetrics_EmitKey(t *testing.T) { 130 | m, met := mockMetric() 131 | met.EmitKey([]string{"key"}, float32(1)) 132 | if m.getKeys()[0][0] != "key" { 133 | t.Fatalf("") 134 | } 135 | if m.vals[0] != 1 { 136 | t.Fatalf("") 137 | } 138 | 139 | m, met = mockMetric() 140 | met.EnableTypePrefix = true 141 | met.EmitKey([]string{"key"}, float32(1)) 142 | if m.getKeys()[0][0] != "kv" || m.getKeys()[0][1] != "key" { 143 | t.Fatalf("") 144 | } 145 | if m.vals[0] != 1 { 146 | t.Fatalf("") 147 | } 148 | 149 | m, met = mockMetric() 150 | met.ServiceName = "service" 151 | met.EmitKey([]string{"key"}, float32(1)) 152 | if m.getKeys()[0][0] != "service" || m.getKeys()[0][1] != "key" { 153 | t.Fatalf("") 154 | } 155 | if m.vals[0] != 1 { 156 | t.Fatalf("") 157 | } 158 | } 159 | 160 | func TestMetrics_IncrCounter(t *testing.T) { 161 | m, met := mockMetric() 162 | met.IncrCounter([]string{"key"}, float32(1)) 163 | if m.getKeys()[0][0] != "key" { 164 | t.Fatalf("") 165 | } 166 | if m.vals[0] != 1 { 167 | t.Fatalf("") 168 | } 169 | 170 | m, met = mockMetric() 171 | labels := []Label{{"a", "b"}} 172 | met.IncrCounterWithLabels([]string{"key"}, float32(1), labels) 173 | if m.getKeys()[0][0] != "key" { 174 | t.Fatalf("") 175 | } 176 | if m.vals[0] != 1 { 177 | t.Fatalf("") 178 | } 179 | if !reflect.DeepEqual(m.labels[0], labels) { 180 | t.Fatalf("") 181 | } 182 | 183 | m, met = mockMetric() 184 | met.EnableTypePrefix = true 185 | met.IncrCounter([]string{"key"}, float32(1)) 186 | if m.getKeys()[0][0] != "counter" || m.getKeys()[0][1] != "key" { 187 | t.Fatalf("") 188 | } 189 | if m.vals[0] != 1 { 190 | t.Fatalf("") 191 | } 192 | 193 | m, met = mockMetric() 194 | met.ServiceName = "service" 195 | met.IncrCounter([]string{"key"}, float32(1)) 196 | if m.getKeys()[0][0] != "service" || m.getKeys()[0][1] != "key" { 197 | t.Fatalf("") 198 | } 199 | if m.vals[0] != 1 { 200 | t.Fatalf("") 201 | } 202 | } 203 | 204 | func TestMetrics_AddSample(t *testing.T) { 205 | m, met := mockMetric() 206 | met.AddSample([]string{"key"}, float32(1)) 207 | if m.getKeys()[0][0] != "key" { 208 | t.Fatalf("") 209 | } 210 | if m.vals[0] != 1 { 211 | t.Fatalf("") 212 | } 213 | 214 | m, met = mockMetric() 215 | labels := []Label{{"a", "b"}} 216 | met.AddSampleWithLabels([]string{"key"}, float32(1), labels) 217 | if m.getKeys()[0][0] != "key" { 218 | t.Fatalf("") 219 | } 220 | if m.vals[0] != 1 { 221 | t.Fatalf("") 222 | } 223 | if !reflect.DeepEqual(m.labels[0], labels) { 224 | t.Fatalf("") 225 | } 226 | 227 | m, met = mockMetric() 228 | met.EnableTypePrefix = true 229 | met.AddSample([]string{"key"}, float32(1)) 230 | if m.getKeys()[0][0] != "sample" || m.getKeys()[0][1] != "key" { 231 | t.Fatalf("") 232 | } 233 | if m.vals[0] != 1 { 234 | t.Fatalf("") 235 | } 236 | 237 | m, met = mockMetric() 238 | met.ServiceName = "service" 239 | met.AddSample([]string{"key"}, float32(1)) 240 | if m.getKeys()[0][0] != "service" || m.getKeys()[0][1] != "key" { 241 | t.Fatalf("") 242 | } 243 | if m.vals[0] != 1 { 244 | t.Fatalf("") 245 | } 246 | } 247 | 248 | func TestMetrics_MeasureSince(t *testing.T) { 249 | m, met := mockMetric() 250 | met.TimerGranularity = time.Millisecond 251 | n := time.Now() 252 | met.MeasureSince([]string{"key"}, n) 253 | if m.getKeys()[0][0] != "key" { 254 | t.Fatalf("") 255 | } 256 | if m.vals[0] > 0.1 { 257 | t.Fatalf("") 258 | } 259 | 260 | m, met = mockMetric() 261 | met.TimerGranularity = time.Millisecond 262 | labels := []Label{{"a", "b"}} 263 | met.MeasureSinceWithLabels([]string{"key"}, n, labels) 264 | if m.getKeys()[0][0] != "key" { 265 | t.Fatalf("") 266 | } 267 | if m.vals[0] > 0.1 { 268 | t.Fatalf("") 269 | } 270 | if !reflect.DeepEqual(m.labels[0], labels) { 271 | t.Fatalf("") 272 | } 273 | 274 | m, met = mockMetric() 275 | met.TimerGranularity = time.Millisecond 276 | met.EnableTypePrefix = true 277 | met.MeasureSince([]string{"key"}, n) 278 | if m.getKeys()[0][0] != "timer" || m.getKeys()[0][1] != "key" { 279 | t.Fatalf("") 280 | } 281 | if m.vals[0] > 0.1 { 282 | t.Fatalf("") 283 | } 284 | 285 | m, met = mockMetric() 286 | met.TimerGranularity = time.Millisecond 287 | met.ServiceName = "service" 288 | met.MeasureSince([]string{"key"}, n) 289 | if m.getKeys()[0][0] != "service" || m.getKeys()[0][1] != "key" { 290 | t.Fatalf("") 291 | } 292 | if m.vals[0] > 0.1 { 293 | t.Fatalf("") 294 | } 295 | } 296 | 297 | func TestMetrics_EmitRuntimeStats(t *testing.T) { 298 | runtime.GC() 299 | m, met := mockMetric() 300 | met.EmitRuntimeStats() 301 | 302 | if m.getKeys()[0][0] != "runtime" || m.getKeys()[0][1] != "num_goroutines" { 303 | t.Fatalf("bad key %v", m.getKeys()) 304 | } 305 | if m.vals[0] <= 1 { 306 | t.Fatalf("bad val: %v", m.vals) 307 | } 308 | 309 | if m.getKeys()[1][0] != "runtime" || m.getKeys()[1][1] != "alloc_bytes" { 310 | t.Fatalf("bad key %v", m.getKeys()) 311 | } 312 | if m.vals[1] <= 40000 { 313 | t.Fatalf("bad val: %v", m.vals) 314 | } 315 | 316 | if m.getKeys()[2][0] != "runtime" || m.getKeys()[2][1] != "sys_bytes" { 317 | t.Fatalf("bad key %v", m.getKeys()) 318 | } 319 | if m.vals[2] <= 100000 { 320 | t.Fatalf("bad val: %v", m.vals) 321 | } 322 | 323 | if m.getKeys()[3][0] != "runtime" || m.getKeys()[3][1] != "malloc_count" { 324 | t.Fatalf("bad key %v", m.getKeys()) 325 | } 326 | if m.vals[3] <= 100 { 327 | t.Fatalf("bad val: %v", m.vals) 328 | } 329 | 330 | if m.getKeys()[4][0] != "runtime" || m.getKeys()[4][1] != "free_count" { 331 | t.Fatalf("bad key %v", m.getKeys()) 332 | } 333 | if m.vals[4] <= 100 { 334 | t.Fatalf("bad val: %v", m.vals) 335 | } 336 | 337 | if m.getKeys()[5][0] != "runtime" || m.getKeys()[5][1] != "heap_objects" { 338 | t.Fatalf("bad key %v", m.getKeys()) 339 | } 340 | if m.vals[5] <= 100 { 341 | t.Fatalf("bad val: %v", m.vals) 342 | } 343 | 344 | if m.getKeys()[6][0] != "runtime" || m.getKeys()[6][1] != "total_gc_pause_ns" { 345 | t.Fatalf("bad key %v", m.getKeys()) 346 | } 347 | if m.vals[6] <= 100 { 348 | t.Fatalf("bad val: %v\nkeys: %v", m.vals, m.getKeys()) 349 | } 350 | 351 | if m.getKeys()[7][0] != "runtime" || m.getKeys()[7][1] != "total_gc_runs" { 352 | t.Fatalf("bad key %v", m.getKeys()) 353 | } 354 | if m.vals[7] < 1 { 355 | t.Fatalf("bad val: %v", m.vals) 356 | } 357 | 358 | if m.getKeys()[8][0] != "runtime" || m.getKeys()[8][1] != "gc_pause_ns" { 359 | t.Fatalf("bad key %v", m.getKeys()) 360 | } 361 | if m.vals[8] <= 1000 { 362 | t.Fatalf("bad val: %v", m.vals) 363 | } 364 | } 365 | 366 | func TestInsert(t *testing.T) { 367 | k := []string{"hi", "bob"} 368 | exp := []string{"hi", "there", "bob"} 369 | out := insert(1, "there", k) 370 | if !reflect.DeepEqual(exp, out) { 371 | t.Fatalf("bad insert %v %v", exp, out) 372 | } 373 | } 374 | 375 | func TestMetrics_Filter_Blacklist(t *testing.T) { 376 | m := &MockSink{} 377 | conf := DefaultConfig("") 378 | conf.AllowedPrefixes = []string{"service", "debug.thing"} 379 | conf.BlockedPrefixes = []string{"debug"} 380 | conf.EnableHostname = false 381 | met, err := New(conf, m) 382 | if err != nil { 383 | t.Fatal(err) 384 | } 385 | 386 | // Allowed by default 387 | key := []string{"thing"} 388 | met.SetGauge(key, 1) 389 | if !reflect.DeepEqual(m.getKeys()[0], key) { 390 | t.Fatalf("key doesn't exist %v, %v", m.getKeys()[0], key) 391 | } 392 | if m.vals[0] != 1 { 393 | t.Fatalf("bad val: %v", m.vals[0]) 394 | } 395 | 396 | // Allowed by filter 397 | key = []string{"service", "thing"} 398 | met.SetGauge(key, 2) 399 | if !reflect.DeepEqual(m.getKeys()[1], key) { 400 | t.Fatalf("key doesn't exist") 401 | } 402 | if m.vals[1] != 2 { 403 | t.Fatalf("bad val: %v", m.vals[1]) 404 | } 405 | 406 | // Allowed by filter, subtree of a blocked entry 407 | key = []string{"debug", "thing"} 408 | met.SetGauge(key, 3) 409 | if !reflect.DeepEqual(m.getKeys()[2], key) { 410 | t.Fatalf("key doesn't exist") 411 | } 412 | if m.vals[2] != 3 { 413 | t.Fatalf("bad val: %v", m.vals[2]) 414 | } 415 | 416 | // Blocked by filter 417 | key = []string{"debug", "other-thing"} 418 | met.SetGauge(key, 4) 419 | if len(m.getKeys()) != 3 { 420 | t.Fatalf("key shouldn't exist") 421 | } 422 | } 423 | 424 | func HasElem(s interface{}, elem interface{}) bool { 425 | arrV := reflect.ValueOf(s) 426 | 427 | if arrV.Kind() == reflect.Slice { 428 | for i := 0; i < arrV.Len(); i++ { 429 | if arrV.Index(i).Interface() == elem { 430 | return true 431 | } 432 | } 433 | } 434 | 435 | return false 436 | } 437 | 438 | func TestMetrics_Filter_Whitelist(t *testing.T) { 439 | m := &MockSink{} 440 | conf := DefaultConfig("") 441 | conf.AllowedPrefixes = []string{"service", "debug.thing"} 442 | conf.BlockedPrefixes = []string{"debug"} 443 | conf.FilterDefault = false 444 | conf.EnableHostname = false 445 | conf.BlockedLabels = []string{"bad_label"} 446 | met, err := New(conf, m) 447 | if err != nil { 448 | t.Fatal(err) 449 | } 450 | 451 | // Blocked by default 452 | key := []string{"thing"} 453 | met.SetGauge(key, 1) 454 | if len(m.getKeys()) != 0 { 455 | t.Fatalf("key should not exist") 456 | } 457 | 458 | // Allowed by filter 459 | key = []string{"service", "thing"} 460 | met.SetGauge(key, 2) 461 | if !reflect.DeepEqual(m.getKeys()[0], key) { 462 | t.Fatalf("key doesn't exist") 463 | } 464 | if m.vals[0] != 2 { 465 | t.Fatalf("bad val: %v", m.vals[0]) 466 | } 467 | 468 | // Allowed by filter, subtree of a blocked entry 469 | key = []string{"debug", "thing"} 470 | met.SetGauge(key, 3) 471 | if !reflect.DeepEqual(m.getKeys()[1], key) { 472 | t.Fatalf("key doesn't exist") 473 | } 474 | if m.vals[1] != 3 { 475 | t.Fatalf("bad val: %v", m.vals[1]) 476 | } 477 | 478 | // Blocked by filter 479 | key = []string{"debug", "other-thing"} 480 | met.SetGauge(key, 4) 481 | if len(m.getKeys()) != 2 { 482 | t.Fatalf("key shouldn't exist") 483 | } 484 | // Test blacklisting of labels 485 | key = []string{"debug", "thing"} 486 | goodLabel := Label{Name: "good", Value: "should be present"} 487 | badLabel := Label{Name: "bad_label", Value: "should not be there"} 488 | labels := []Label{badLabel, goodLabel} 489 | met.SetGaugeWithLabels(key, 3, labels) 490 | if !reflect.DeepEqual(m.getKeys()[1], key) { 491 | t.Fatalf("key doesn't exist") 492 | } 493 | if m.vals[2] != 3 { 494 | t.Fatalf("bad val: %v", m.vals[1]) 495 | } 496 | if HasElem(m.labels[2], badLabel) { 497 | t.Fatalf("bad_label should not be present in %v", m.labels[2]) 498 | } 499 | if !HasElem(m.labels[2], goodLabel) { 500 | t.Fatalf("good label is not present in %v", m.labels[2]) 501 | } 502 | } 503 | 504 | func TestMetrics_Filter_Labels_Whitelist(t *testing.T) { 505 | m := &MockSink{} 506 | conf := DefaultConfig("") 507 | conf.AllowedPrefixes = []string{"service", "debug.thing"} 508 | conf.BlockedPrefixes = []string{"debug"} 509 | conf.FilterDefault = false 510 | conf.EnableHostname = false 511 | conf.AllowedLabels = []string{"good_label"} 512 | conf.BlockedLabels = []string{"bad_label"} 513 | met, err := New(conf, m) 514 | if err != nil { 515 | t.Fatal(err) 516 | } 517 | 518 | // Blocked by default 519 | key := []string{"thing"} 520 | key = []string{"debug", "thing"} 521 | goodLabel := Label{Name: "good_label", Value: "should be present"} 522 | notReallyGoodLabel := Label{Name: "not_really_good_label", Value: "not whitelisted, but not blacklisted"} 523 | badLabel := Label{Name: "bad_label", Value: "should not be there"} 524 | labels := []Label{badLabel, notReallyGoodLabel, goodLabel} 525 | met.SetGaugeWithLabels(key, 1, labels) 526 | 527 | if HasElem(m.labels[0], badLabel) { 528 | t.Fatalf("bad_label should not be present in %v", m.labels[0]) 529 | } 530 | if HasElem(m.labels[0], notReallyGoodLabel) { 531 | t.Fatalf("not_really_good_label should not be present in %v", m.labels[0]) 532 | } 533 | if !HasElem(m.labels[0], goodLabel) { 534 | t.Fatalf("good label is not present in %v", m.labels[0]) 535 | } 536 | 537 | conf.AllowedLabels = nil 538 | met.UpdateFilterAndLabels(conf.AllowedPrefixes, conf.BlockedLabels, conf.AllowedLabels, conf.BlockedLabels) 539 | met.SetGaugeWithLabels(key, 1, labels) 540 | 541 | if HasElem(m.labels[1], badLabel) { 542 | t.Fatalf("bad_label should not be present in %v", m.labels[1]) 543 | } 544 | // Since no whitelist, not_really_good_label should be there 545 | if !HasElem(m.labels[1], notReallyGoodLabel) { 546 | t.Fatalf("not_really_good_label is not present in %v", m.labels[1]) 547 | } 548 | if !HasElem(m.labels[1], goodLabel) { 549 | t.Fatalf("good label is not present in %v", m.labels[1]) 550 | } 551 | } 552 | 553 | func TestMetrics_Filter_Labels_ModifyArgs(t *testing.T) { 554 | m := &MockSink{} 555 | conf := DefaultConfig("") 556 | conf.FilterDefault = false 557 | conf.EnableHostname = false 558 | conf.AllowedLabels = []string{"keep"} 559 | conf.BlockedLabels = []string{"delete"} 560 | met, err := New(conf, m) 561 | if err != nil { 562 | t.Fatal(err) 563 | } 564 | 565 | // Blocked by default 566 | key := []string{"thing"} 567 | key = []string{"debug", "thing"} 568 | goodLabel := Label{Name: "keep", Value: "should be kept"} 569 | badLabel := Label{Name: "delete", Value: "should be deleted"} 570 | argLabels := []Label{badLabel, goodLabel, badLabel, goodLabel, badLabel, goodLabel, badLabel} 571 | origLabels := append([]Label{}, argLabels...) 572 | met.SetGaugeWithLabels(key, 1, argLabels) 573 | 574 | if !reflect.DeepEqual(argLabels, origLabels) { 575 | t.Fatalf("SetGaugeWithLabels modified the input argument") 576 | } 577 | } 578 | -------------------------------------------------------------------------------- /prometheus/prometheus.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | //go:build go1.9 5 | // +build go1.9 6 | 7 | package prometheus 8 | 9 | import ( 10 | "fmt" 11 | "log" 12 | "strings" 13 | "sync" 14 | "time" 15 | 16 | "github.com/hashicorp/go-metrics" 17 | "github.com/prometheus/client_golang/prometheus" 18 | "github.com/prometheus/client_golang/prometheus/push" 19 | ) 20 | 21 | var ( 22 | // DefaultPrometheusOpts is the default set of options used when creating a 23 | // PrometheusSink. 24 | DefaultPrometheusOpts = PrometheusOpts{ 25 | Expiration: 60 * time.Second, 26 | Name: "default_prometheus_sink", 27 | } 28 | ) 29 | 30 | // PrometheusOpts is used to configure the Prometheus Sink 31 | type PrometheusOpts struct { 32 | // Expiration is the duration a metric is valid for, after which it will be 33 | // untracked. If the value is zero, a metric is never expired. 34 | Expiration time.Duration 35 | Registerer prometheus.Registerer 36 | 37 | // Gauges, Summaries, and Counters allow us to pre-declare metrics by giving 38 | // their Name, Help, and ConstLabels to the PrometheusSink when it is created. 39 | // Metrics declared in this way will be initialized at zero and will not be 40 | // deleted or altered when their expiry is reached. 41 | // 42 | // Ex: PrometheusOpts{ 43 | // Expiration: 10 * time.Second, 44 | // Gauges: []GaugeDefinition{ 45 | // { 46 | // Name: []string{ "application", "component", "measurement"}, 47 | // Help: "application_component_measurement provides an example of how to declare static metrics", 48 | // ConstLabels: []metrics.Label{ { Name: "my_label", Value: "does_not_change" }, }, 49 | // }, 50 | // }, 51 | // } 52 | GaugeDefinitions []GaugeDefinition 53 | SummaryDefinitions []SummaryDefinition 54 | CounterDefinitions []CounterDefinition 55 | Name string 56 | } 57 | 58 | type PrometheusSink struct { 59 | // If these will ever be copied, they should be converted to *sync.Map values and initialized appropriately 60 | gauges sync.Map 61 | summaries sync.Map 62 | counters sync.Map 63 | expiration time.Duration 64 | help map[string]string 65 | name string 66 | } 67 | 68 | // GaugeDefinition can be provided to PrometheusOpts to declare a constant gauge that is not deleted on expiry. 69 | type GaugeDefinition struct { 70 | Name []string 71 | ConstLabels []metrics.Label 72 | Help string 73 | } 74 | 75 | type gauge struct { 76 | prometheus.Gauge 77 | updatedAt time.Time 78 | // canDelete is set if the metric is created during runtime so we know it's ephemeral and can delete it on expiry. 79 | canDelete bool 80 | } 81 | 82 | // SummaryDefinition can be provided to PrometheusOpts to declare a constant summary that is not deleted on expiry. 83 | type SummaryDefinition struct { 84 | Name []string 85 | ConstLabels []metrics.Label 86 | Help string 87 | } 88 | 89 | type summary struct { 90 | prometheus.Summary 91 | updatedAt time.Time 92 | canDelete bool 93 | } 94 | 95 | // CounterDefinition can be provided to PrometheusOpts to declare a constant counter that is not deleted on expiry. 96 | type CounterDefinition struct { 97 | Name []string 98 | ConstLabels []metrics.Label 99 | Help string 100 | } 101 | 102 | type counter struct { 103 | prometheus.Counter 104 | updatedAt time.Time 105 | canDelete bool 106 | } 107 | 108 | // NewPrometheusSink creates a new PrometheusSink using the default options. 109 | func NewPrometheusSink() (*PrometheusSink, error) { 110 | return NewPrometheusSinkFrom(DefaultPrometheusOpts) 111 | } 112 | 113 | // NewPrometheusSinkFrom creates a new PrometheusSink using the passed options. 114 | func NewPrometheusSinkFrom(opts PrometheusOpts) (*PrometheusSink, error) { 115 | name := opts.Name 116 | if name == "" { 117 | name = "default_prometheus_sink" 118 | } 119 | sink := &PrometheusSink{ 120 | gauges: sync.Map{}, 121 | summaries: sync.Map{}, 122 | counters: sync.Map{}, 123 | expiration: opts.Expiration, 124 | help: make(map[string]string), 125 | name: name, 126 | } 127 | 128 | initGauges(&sink.gauges, opts.GaugeDefinitions, sink.help) 129 | initSummaries(&sink.summaries, opts.SummaryDefinitions, sink.help) 130 | initCounters(&sink.counters, opts.CounterDefinitions, sink.help) 131 | 132 | reg := opts.Registerer 133 | if reg == nil { 134 | reg = prometheus.DefaultRegisterer 135 | } 136 | 137 | return sink, reg.Register(sink) 138 | } 139 | 140 | // Describe sends a Collector.Describe value from the descriptor created around PrometheusSink.Name 141 | // Note that we cannot describe all the metrics (gauges, counters, summaries) in the sink as 142 | // metrics can be added at any point during the lifecycle of the sink, which does not respect 143 | // the idempotency aspect of the Collector.Describe() interface 144 | func (p *PrometheusSink) Describe(c chan<- *prometheus.Desc) { 145 | // dummy value to be able to register and unregister "empty" sinks 146 | // Note this is not actually retained in the PrometheusSink so this has no side effects 147 | // on the caller's sink. So it shouldn't show up to any of its consumers. 148 | prometheus.NewGauge(prometheus.GaugeOpts{Name: p.name, Help: p.name}).Describe(c) 149 | } 150 | 151 | // Collect meets the collection interface and allows us to enforce our expiration 152 | // logic to clean up ephemeral metrics if their value haven't been set for a 153 | // duration exceeding our allowed expiration time. 154 | func (p *PrometheusSink) Collect(c chan<- prometheus.Metric) { 155 | p.collectAtTime(c, time.Now()) 156 | } 157 | 158 | // collectAtTime allows internal testing of the expiry based logic here without 159 | // mocking clocks or making tests timing sensitive. 160 | func (p *PrometheusSink) collectAtTime(c chan<- prometheus.Metric, t time.Time) { 161 | expire := p.expiration != 0 162 | p.gauges.Range(func(k, v interface{}) bool { 163 | if v == nil { 164 | return true 165 | } 166 | g := v.(*gauge) 167 | lastUpdate := g.updatedAt 168 | if expire && lastUpdate.Add(p.expiration).Before(t) { 169 | if g.canDelete { 170 | p.gauges.Delete(k) 171 | return true 172 | } 173 | } 174 | g.Collect(c) 175 | return true 176 | }) 177 | p.summaries.Range(func(k, v interface{}) bool { 178 | if v == nil { 179 | return true 180 | } 181 | s := v.(*summary) 182 | lastUpdate := s.updatedAt 183 | if expire && lastUpdate.Add(p.expiration).Before(t) { 184 | if s.canDelete { 185 | p.summaries.Delete(k) 186 | return true 187 | } 188 | } 189 | s.Collect(c) 190 | return true 191 | }) 192 | p.counters.Range(func(k, v interface{}) bool { 193 | if v == nil { 194 | return true 195 | } 196 | count := v.(*counter) 197 | lastUpdate := count.updatedAt 198 | if expire && lastUpdate.Add(p.expiration).Before(t) { 199 | if count.canDelete { 200 | p.counters.Delete(k) 201 | return true 202 | } 203 | } 204 | count.Collect(c) 205 | return true 206 | }) 207 | } 208 | 209 | func initGauges(m *sync.Map, gauges []GaugeDefinition, help map[string]string) { 210 | for _, g := range gauges { 211 | key, hash := flattenKey(g.Name, g.ConstLabels) 212 | help[fmt.Sprintf("gauge.%s", key)] = g.Help 213 | pG := prometheus.NewGauge(prometheus.GaugeOpts{ 214 | Name: key, 215 | Help: g.Help, 216 | ConstLabels: prometheusLabels(g.ConstLabels), 217 | }) 218 | m.Store(hash, &gauge{Gauge: pG}) 219 | } 220 | return 221 | } 222 | 223 | func initSummaries(m *sync.Map, summaries []SummaryDefinition, help map[string]string) { 224 | for _, s := range summaries { 225 | key, hash := flattenKey(s.Name, s.ConstLabels) 226 | help[fmt.Sprintf("summary.%s", key)] = s.Help 227 | pS := prometheus.NewSummary(prometheus.SummaryOpts{ 228 | Name: key, 229 | Help: s.Help, 230 | MaxAge: 10 * time.Second, 231 | ConstLabels: prometheusLabels(s.ConstLabels), 232 | Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, 233 | }) 234 | m.Store(hash, &summary{Summary: pS}) 235 | } 236 | return 237 | } 238 | 239 | func initCounters(m *sync.Map, counters []CounterDefinition, help map[string]string) { 240 | for _, c := range counters { 241 | key, hash := flattenKey(c.Name, c.ConstLabels) 242 | help[fmt.Sprintf("counter.%s", key)] = c.Help 243 | pC := prometheus.NewCounter(prometheus.CounterOpts{ 244 | Name: key, 245 | Help: c.Help, 246 | ConstLabels: prometheusLabels(c.ConstLabels), 247 | }) 248 | m.Store(hash, &counter{Counter: pC}) 249 | } 250 | return 251 | } 252 | 253 | var forbiddenCharsReplacer = strings.NewReplacer(" ", "_", ".", "_", "=", "_", "-", "_", "/", "_") 254 | 255 | func flattenKey(parts []string, labels []metrics.Label) (string, string) { 256 | key := strings.Join(parts, "_") 257 | key = forbiddenCharsReplacer.Replace(key) 258 | 259 | hash := key 260 | for _, label := range labels { 261 | hash += ";" + label.Name + "=" + label.Value 262 | } 263 | 264 | return key, hash 265 | } 266 | 267 | func prometheusLabels(labels []metrics.Label) prometheus.Labels { 268 | l := make(prometheus.Labels) 269 | for _, label := range labels { 270 | l[label.Name] = label.Value 271 | } 272 | return l 273 | } 274 | 275 | func (p *PrometheusSink) SetGauge(parts []string, val float32) { 276 | p.SetPrecisionGauge(parts, float64(val)) 277 | } 278 | 279 | func (p *PrometheusSink) SetGaugeWithLabels(parts []string, val float32, labels []metrics.Label) { 280 | p.SetPrecisionGaugeWithLabels(parts, float64(val), labels) 281 | } 282 | 283 | func (p *PrometheusSink) SetPrecisionGauge(parts []string, val float64) { 284 | p.SetPrecisionGaugeWithLabels(parts, val, nil) 285 | } 286 | 287 | func (p *PrometheusSink) SetPrecisionGaugeWithLabels(parts []string, val float64, labels []metrics.Label) { 288 | key, hash := flattenKey(parts, labels) 289 | pg, ok := p.gauges.Load(hash) 290 | 291 | // The sync.Map underlying gauges stores pointers to our structs. If we need to make updates, 292 | // rather than modifying the underlying value directly, which would be racy, we make a local 293 | // copy by dereferencing the pointer we get back, making the appropriate changes, and then 294 | // storing a pointer to our local copy. The underlying Prometheus types are threadsafe, 295 | // so there's no issues there. It's possible for racy updates to occur to the updatedAt 296 | // value, but since we're always setting it to time.Now(), it doesn't really matter. 297 | if ok { 298 | localGauge := *pg.(*gauge) 299 | localGauge.Set(val) 300 | localGauge.updatedAt = time.Now() 301 | p.gauges.Store(hash, &localGauge) 302 | 303 | // The gauge does not exist, create the gauge and allow it to be deleted 304 | } else { 305 | help := key 306 | existingHelp, ok := p.help[fmt.Sprintf("gauge.%s", key)] 307 | if ok { 308 | help = existingHelp 309 | } 310 | g := prometheus.NewGauge(prometheus.GaugeOpts{ 311 | Name: key, 312 | Help: help, 313 | ConstLabels: prometheusLabels(labels), 314 | }) 315 | g.Set(val) 316 | pg = &gauge{ 317 | Gauge: g, 318 | updatedAt: time.Now(), 319 | canDelete: true, 320 | } 321 | p.gauges.Store(hash, pg) 322 | } 323 | } 324 | 325 | func (p *PrometheusSink) AddSample(parts []string, val float32) { 326 | p.AddSampleWithLabels(parts, val, nil) 327 | } 328 | 329 | func (p *PrometheusSink) AddSampleWithLabels(parts []string, val float32, labels []metrics.Label) { 330 | key, hash := flattenKey(parts, labels) 331 | ps, ok := p.summaries.Load(hash) 332 | 333 | // Does the summary already exist for this sample type? 334 | if ok { 335 | localSummary := *ps.(*summary) 336 | localSummary.Observe(float64(val)) 337 | localSummary.updatedAt = time.Now() 338 | p.summaries.Store(hash, &localSummary) 339 | 340 | // The summary does not exist, create the Summary and allow it to be deleted 341 | } else { 342 | help := key 343 | existingHelp, ok := p.help[fmt.Sprintf("summary.%s", key)] 344 | if ok { 345 | help = existingHelp 346 | } 347 | s := prometheus.NewSummary(prometheus.SummaryOpts{ 348 | Name: key, 349 | Help: help, 350 | MaxAge: 10 * time.Second, 351 | ConstLabels: prometheusLabels(labels), 352 | Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, 353 | }) 354 | s.Observe(float64(val)) 355 | ps = &summary{ 356 | Summary: s, 357 | updatedAt: time.Now(), 358 | canDelete: true, 359 | } 360 | p.summaries.Store(hash, ps) 361 | } 362 | } 363 | 364 | // EmitKey is not implemented. Prometheus doesn’t offer a type for which an 365 | // arbitrary number of values is retained, as Prometheus works with a pull 366 | // model, rather than a push model. 367 | func (p *PrometheusSink) EmitKey(key []string, val float32) { 368 | } 369 | 370 | func (p *PrometheusSink) IncrCounter(parts []string, val float32) { 371 | p.IncrCounterWithLabels(parts, val, nil) 372 | } 373 | 374 | func (p *PrometheusSink) IncrCounterWithLabels(parts []string, val float32, labels []metrics.Label) { 375 | key, hash := flattenKey(parts, labels) 376 | pc, ok := p.counters.Load(hash) 377 | 378 | // Prometheus Counter.Add() panics if val < 0. We don't want this to 379 | // cause applications to crash, so log an error instead. 380 | if val < 0 { 381 | log.Printf("[ERR] Attempting to increment Prometheus counter %v with value negative value %v", key, val) 382 | return 383 | } 384 | 385 | // Does the counter exist? 386 | if ok { 387 | localCounter := *pc.(*counter) 388 | localCounter.Add(float64(val)) 389 | localCounter.updatedAt = time.Now() 390 | p.counters.Store(hash, &localCounter) 391 | 392 | // The counter does not exist yet, create it and allow it to be deleted 393 | } else { 394 | help := key 395 | existingHelp, ok := p.help[fmt.Sprintf("counter.%s", key)] 396 | if ok { 397 | help = existingHelp 398 | } 399 | c := prometheus.NewCounter(prometheus.CounterOpts{ 400 | Name: key, 401 | Help: help, 402 | ConstLabels: prometheusLabels(labels), 403 | }) 404 | c.Add(float64(val)) 405 | pc = &counter{ 406 | Counter: c, 407 | updatedAt: time.Now(), 408 | canDelete: true, 409 | } 410 | p.counters.Store(hash, pc) 411 | } 412 | } 413 | 414 | // PrometheusPushSink wraps a normal prometheus sink and provides an address and facilities to export it to an address 415 | // on an interval. 416 | type PrometheusPushSink struct { 417 | *PrometheusSink 418 | pusher *push.Pusher 419 | address string 420 | pushInterval time.Duration 421 | stopChan chan struct{} 422 | } 423 | 424 | // NewPrometheusPushSink creates a PrometheusPushSink by taking an address, interval, and destination name. 425 | func NewPrometheusPushSink(address string, pushInterval time.Duration, name string) (*PrometheusPushSink, error) { 426 | promSink := &PrometheusSink{ 427 | gauges: sync.Map{}, 428 | summaries: sync.Map{}, 429 | counters: sync.Map{}, 430 | expiration: 60 * time.Second, 431 | name: "default_prometheus_sink", 432 | } 433 | 434 | pusher := push.New(address, name).Collector(promSink) 435 | 436 | sink := &PrometheusPushSink{ 437 | promSink, 438 | pusher, 439 | address, 440 | pushInterval, 441 | make(chan struct{}), 442 | } 443 | 444 | sink.flushMetrics() 445 | return sink, nil 446 | } 447 | 448 | func (s *PrometheusPushSink) flushMetrics() { 449 | ticker := time.NewTicker(s.pushInterval) 450 | 451 | go func() { 452 | for { 453 | select { 454 | case <-ticker.C: 455 | err := s.pusher.Push() 456 | if err != nil { 457 | log.Printf("[ERR] Error pushing to Prometheus! Err: %s", err) 458 | } 459 | case <-s.stopChan: 460 | ticker.Stop() 461 | return 462 | } 463 | } 464 | }() 465 | } 466 | 467 | // Shutdown tears down the PrometheusPushSink, and blocks while flushing metrics to the backend. 468 | func (s *PrometheusPushSink) Shutdown() { 469 | close(s.stopChan) 470 | // Closing the channel only stops the running goroutine that pushes metrics. 471 | // To minimize the chance of data loss pusher.Push is called one last time. 472 | s.pusher.Push() 473 | } 474 | -------------------------------------------------------------------------------- /prometheus/prometheus_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | package prometheus 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "log" 10 | "net/http" 11 | "net/http/httptest" 12 | "net/url" 13 | "reflect" 14 | "strings" 15 | "testing" 16 | "time" 17 | 18 | "github.com/golang/protobuf/proto" 19 | dto "github.com/prometheus/client_model/go" 20 | 21 | "github.com/hashicorp/go-metrics" 22 | "github.com/prometheus/client_golang/prometheus" 23 | "github.com/prometheus/common/expfmt" 24 | ) 25 | 26 | const ( 27 | TestHostname = "test_hostname" 28 | ) 29 | 30 | func TestNewPrometheusSinkFrom(t *testing.T) { 31 | reg := prometheus.NewRegistry() 32 | 33 | sink, err := NewPrometheusSinkFrom(PrometheusOpts{ 34 | Registerer: reg, 35 | }) 36 | 37 | if err != nil { 38 | t.Fatalf("err = %v, want nil", err) 39 | } 40 | 41 | //check if register has a sink by unregistering it. 42 | ok := reg.Unregister(sink) 43 | if !ok { 44 | t.Fatalf("Unregister(sink) = false, want true") 45 | } 46 | } 47 | 48 | func TestNewPrometheusSink(t *testing.T) { 49 | sink, err := NewPrometheusSink() 50 | if err != nil { 51 | t.Fatalf("err = %v, want nil", err) 52 | } 53 | 54 | //check if register has a sink by unregistering it. 55 | ok := prometheus.Unregister(sink) 56 | if !ok { 57 | t.Fatalf("Unregister(sink) = false, want true") 58 | } 59 | } 60 | 61 | // TestMultiplePrometheusSink tests registering multiple sinks on the same registerer with different descriptors 62 | func TestMultiplePrometheusSink(t *testing.T) { 63 | gaugeDef := GaugeDefinition{ 64 | Name: []string{"my", "test", "gauge"}, 65 | Help: "A gauge for testing? How helpful!", 66 | } 67 | 68 | cfg := PrometheusOpts{ 69 | Expiration: 5 * time.Second, 70 | GaugeDefinitions: append([]GaugeDefinition{}, gaugeDef), 71 | SummaryDefinitions: append([]SummaryDefinition{}), 72 | CounterDefinitions: append([]CounterDefinition{}), 73 | Name: "sink1", 74 | } 75 | 76 | sink1, err := NewPrometheusSinkFrom(cfg) 77 | if err != nil { 78 | t.Fatalf("err = %v, want nil", err) 79 | } 80 | 81 | reg := prometheus.DefaultRegisterer 82 | if reg == nil { 83 | t.Fatalf("Expected default register to be non nil, got nil.") 84 | } 85 | 86 | gaugeDef2 := GaugeDefinition{ 87 | Name: []string{"my2", "test", "gauge"}, 88 | Help: "A gauge for testing? How helpful!", 89 | } 90 | 91 | cfg2 := PrometheusOpts{ 92 | Expiration: 15 * time.Second, 93 | GaugeDefinitions: append([]GaugeDefinition{}, gaugeDef2), 94 | SummaryDefinitions: append([]SummaryDefinition{}), 95 | CounterDefinitions: append([]CounterDefinition{}), 96 | // commenting out the name to point out that the default name will be used here instead 97 | // Name: "sink2", 98 | } 99 | 100 | sink2, err := NewPrometheusSinkFrom(cfg2) 101 | if err != nil { 102 | t.Fatalf("err = %v, want nil", err) 103 | } 104 | //check if register has a sink by unregistering it. 105 | ok := reg.Unregister(sink1) 106 | if !ok { 107 | t.Fatalf("Unregister(sink) = false, want true") 108 | } 109 | 110 | //check if register has a sink by unregistering it. 111 | ok = reg.Unregister(sink2) 112 | if !ok { 113 | t.Fatalf("Unregister(sink) = false, want true") 114 | } 115 | } 116 | 117 | func TestDefinitions(t *testing.T) { 118 | gaugeDef := GaugeDefinition{ 119 | Name: []string{"my", "test", "gauge"}, 120 | Help: "A gauge for testing? How helpful!", 121 | } 122 | summaryDef := SummaryDefinition{ 123 | Name: []string{"my", "test", "summary"}, 124 | Help: "A summary for testing? How helpful!", 125 | } 126 | counterDef := CounterDefinition{ 127 | Name: []string{"my", "test", "counter"}, 128 | Help: "A counter for testing? How helpful!", 129 | } 130 | 131 | // PrometheusSink config w/ definitions for each metric type 132 | cfg := PrometheusOpts{ 133 | Expiration: 5 * time.Second, 134 | GaugeDefinitions: append([]GaugeDefinition{}, gaugeDef), 135 | SummaryDefinitions: append([]SummaryDefinition{}, summaryDef), 136 | CounterDefinitions: append([]CounterDefinition{}, counterDef), 137 | } 138 | sink, err := NewPrometheusSinkFrom(cfg) 139 | if err != nil { 140 | t.Fatalf("err = %v, want nil", err) 141 | } 142 | defer prometheus.Unregister(sink) 143 | 144 | // We can't just len(x) where x is a sync.Map, so we range over the single item and assert the name in our metric 145 | // definition matches the key we have for the map entry. Should fail if any metrics exist that aren't defined, or if 146 | // the defined metrics don't exist. 147 | sink.gauges.Range(func(key, value interface{}) bool { 148 | name, _ := flattenKey(gaugeDef.Name, gaugeDef.ConstLabels) 149 | if name != key { 150 | t.Fatalf("expected my_test_gauge, got #{name}") 151 | } 152 | return true 153 | }) 154 | sink.summaries.Range(func(key, value interface{}) bool { 155 | name, _ := flattenKey(summaryDef.Name, summaryDef.ConstLabels) 156 | if name != key { 157 | t.Fatalf("expected my_test_summary, got #{name}") 158 | } 159 | return true 160 | }) 161 | sink.counters.Range(func(key, value interface{}) bool { 162 | name, _ := flattenKey(counterDef.Name, counterDef.ConstLabels) 163 | if name != key { 164 | t.Fatalf("expected my_test_counter, got #{name}") 165 | } 166 | return true 167 | }) 168 | 169 | // Set a bunch of values 170 | sink.SetGauge(gaugeDef.Name, 42) 171 | sink.AddSample(summaryDef.Name, 42) 172 | sink.IncrCounter(counterDef.Name, 1) 173 | 174 | // Prometheus panic should not be propagated 175 | sink.IncrCounter(counterDef.Name, -1) 176 | 177 | // Test that the expiry behavior works as expected. First pick a time which 178 | // is after all the actual updates above. 179 | timeAfterUpdates := time.Now() 180 | // Buffer the chan to make sure it doesn't block. We expect only 3 metrics to 181 | // be produced but give some extra room as this will hang the test if we don't 182 | // have a big enough buffer. 183 | ch := make(chan prometheus.Metric, 10) 184 | 185 | // Collect the metrics as if it's some time in the future, way beyond the 5 186 | // second expiry. 187 | sink.collectAtTime(ch, timeAfterUpdates.Add(10*time.Second)) 188 | 189 | // We should see all the metrics desired Expiry behavior 190 | expectedNum := 3 191 | for i := 0; i < expectedNum; i++ { 192 | select { 193 | case m := <-ch: 194 | // m is a prometheus.Metric the only thing we can do is Write it to a 195 | // protobuf type and read from there. 196 | var pb dto.Metric 197 | if err := m.Write(&pb); err != nil { 198 | t.Fatalf("unexpected error reading metric: %s", err) 199 | } 200 | desc := m.Desc().String() 201 | switch { 202 | case pb.Counter != nil: 203 | if !strings.Contains(desc, counterDef.Help) { 204 | t.Fatalf("expected counter to include correct help=%s, but was %s", counterDef.Help, m.Desc().String()) 205 | } 206 | // Counters should _not_ reset. We could assert not nil too but that 207 | // would be a bug in prometheus client code so assume it's never nil... 208 | if *pb.Counter.Value != float64(1) { 209 | t.Fatalf("expected defined counter to have value 42 after expiring, got %f", *pb.Counter.Value) 210 | } 211 | case pb.Gauge != nil: 212 | if !strings.Contains(desc, gaugeDef.Help) { 213 | t.Fatalf("expected gauge to include correct help=%s, but was %s", gaugeDef.Help, m.Desc().String()) 214 | } 215 | // Gauges should _not_ reset. We could assert not nil too but that 216 | // would be a bug in prometheus client code so assume it's never nil... 217 | if *pb.Gauge.Value != float64(42) { 218 | t.Fatalf("expected defined gauge to have value 42 after expiring, got %f", *pb.Gauge.Value) 219 | } 220 | case pb.Summary != nil: 221 | if !strings.Contains(desc, summaryDef.Help) { 222 | t.Fatalf("expected summary to include correct help=%s, but was %s", summaryDef.Help, m.Desc().String()) 223 | } 224 | // Summaries should not be reset. Previous behavior here did attempt to 225 | // reset them by calling Observe(NaN) which results in all values being 226 | // set to NaN but doesn't actually clear the time window of data 227 | // predictably so future observations could also end up as NaN until the 228 | // NaN sample has aged out of the window. Since the summary is already 229 | // aging out a fixed time window (we fix it a 10 seconds currently for 230 | // all summaries and it's not affected by Expiration option), there's no 231 | // point in trying to reset it after "expiry". 232 | if *pb.Summary.SampleSum != float64(42) { 233 | t.Fatalf("expected defined summary sum to have value 42 after expiring, got %f", *pb.Summary.SampleSum) 234 | } 235 | default: 236 | t.Fatalf("unexpected metric type %v", pb) 237 | } 238 | case <-time.After(100 * time.Millisecond): 239 | t.Fatalf("Timed out waiting to collect expected metric. Got %d, want %d", i, expectedNum) 240 | } 241 | } 242 | } 243 | 244 | func MockGetHostname() string { 245 | return TestHostname 246 | } 247 | 248 | func fakeServer(q chan string) *httptest.Server { 249 | handler := func(w http.ResponseWriter, r *http.Request) { 250 | w.WriteHeader(202) 251 | w.Header().Set("Content-Type", "application/json") 252 | defer r.Body.Close() 253 | dec := expfmt.NewDecoder(r.Body, expfmt.FmtProtoDelim) 254 | m := &dto.MetricFamily{} 255 | dec.Decode(m) 256 | expectedm := &dto.MetricFamily{ 257 | Name: proto.String("default_one_two"), 258 | Help: proto.String("default_one_two"), 259 | Type: dto.MetricType_GAUGE.Enum(), 260 | Metric: []*dto.Metric{ 261 | &dto.Metric{ 262 | Label: []*dto.LabelPair{ 263 | &dto.LabelPair{ 264 | Name: proto.String("host"), 265 | Value: proto.String(MockGetHostname()), 266 | }, 267 | }, 268 | Gauge: &dto.Gauge{ 269 | Value: proto.Float64(42), 270 | }, 271 | }, 272 | }, 273 | } 274 | if !reflect.DeepEqual(m, expectedm) { 275 | msg := fmt.Sprintf("Unexpected samples extracted, got: %+v, want: %+v", m, expectedm) 276 | q <- errors.New(msg).Error() 277 | } else { 278 | q <- "ok" 279 | } 280 | } 281 | 282 | return httptest.NewServer(http.HandlerFunc(handler)) 283 | } 284 | 285 | func TestSetGauge(t *testing.T) { 286 | q := make(chan string) 287 | server := fakeServer(q) 288 | defer server.Close() 289 | u, err := url.Parse(server.URL) 290 | if err != nil { 291 | log.Fatal(err) 292 | } 293 | host := u.Hostname() + ":" + u.Port() 294 | sink, err := NewPrometheusPushSink(host, time.Second, "pushtest") 295 | metricsConf := metrics.DefaultConfig("default") 296 | metricsConf.HostName = MockGetHostname() 297 | metricsConf.EnableHostnameLabel = true 298 | metrics.NewGlobal(metricsConf, sink) 299 | metrics.SetGauge([]string{"one", "two"}, 42) 300 | response := <-q 301 | if response != "ok" { 302 | t.Fatal(response) 303 | } 304 | } 305 | 306 | func TestSetPrecisionGauge(t *testing.T) { 307 | q := make(chan string) 308 | server := fakeServer(q) 309 | defer server.Close() 310 | u, err := url.Parse(server.URL) 311 | if err != nil { 312 | log.Fatal(err) 313 | } 314 | host := u.Hostname() + ":" + u.Port() 315 | sink, err := NewPrometheusPushSink(host, time.Second, "pushtest") 316 | metricsConf := metrics.DefaultConfig("default") 317 | metricsConf.HostName = MockGetHostname() 318 | metricsConf.EnableHostnameLabel = true 319 | metrics.NewGlobal(metricsConf, sink) 320 | metrics.SetPrecisionGauge([]string{"one", "two"}, 42) 321 | response := <-q 322 | if response != "ok" { 323 | t.Fatal(response) 324 | } 325 | } 326 | 327 | func TestDefinitionsWithLabels(t *testing.T) { 328 | gaugeDef := GaugeDefinition{ 329 | Name: []string{"my", "test", "gauge"}, 330 | Help: "A gauge for testing? How helpful!", 331 | } 332 | summaryDef := SummaryDefinition{ 333 | Name: []string{"my", "test", "summary"}, 334 | Help: "A summary for testing? How helpful!", 335 | } 336 | counterDef := CounterDefinition{ 337 | Name: []string{"my", "test", "counter"}, 338 | Help: "A counter for testing? How helpful!", 339 | } 340 | 341 | // PrometheusSink config w/ definitions for each metric type 342 | cfg := PrometheusOpts{ 343 | Expiration: 5 * time.Second, 344 | GaugeDefinitions: append([]GaugeDefinition{}, gaugeDef), 345 | SummaryDefinitions: append([]SummaryDefinition{}, summaryDef), 346 | CounterDefinitions: append([]CounterDefinition{}, counterDef), 347 | } 348 | sink, err := NewPrometheusSinkFrom(cfg) 349 | if err != nil { 350 | t.Fatalf("err =%#v, want nil", err) 351 | } 352 | defer prometheus.Unregister(sink) 353 | if len(sink.help) != 3 { 354 | t.Fatalf("Expected len(sink.help) to be 3, was %d: %#v", len(sink.help), sink.help) 355 | } 356 | 357 | sink.SetGaugeWithLabels(gaugeDef.Name, 42.0, []metrics.Label{ 358 | {Name: "version", Value: "some info"}, 359 | }) 360 | sink.gauges.Range(func(key, value interface{}) bool { 361 | localGauge := *value.(*gauge) 362 | if !strings.Contains(localGauge.Desc().String(), gaugeDef.Help) { 363 | t.Fatalf("expected gauge to include correct help=%s, but was %s", gaugeDef.Help, localGauge.Desc().String()) 364 | } 365 | return true 366 | }) 367 | 368 | sink.AddSampleWithLabels(summaryDef.Name, 42.0, []metrics.Label{ 369 | {Name: "version", Value: "some info"}, 370 | }) 371 | sink.summaries.Range(func(key, value interface{}) bool { 372 | metric := *value.(*summary) 373 | if !strings.Contains(metric.Desc().String(), summaryDef.Help) { 374 | t.Fatalf("expected gauge to include correct help=%s, but was %s", summaryDef.Help, metric.Desc().String()) 375 | } 376 | return true 377 | }) 378 | 379 | sink.IncrCounterWithLabels(counterDef.Name, 42.0, []metrics.Label{ 380 | {Name: "version", Value: "some info"}, 381 | }) 382 | sink.counters.Range(func(key, value interface{}) bool { 383 | metric := *value.(*counter) 384 | if !strings.Contains(metric.Desc().String(), counterDef.Help) { 385 | t.Fatalf("expected gauge to include correct help=%s, but was %s", counterDef.Help, metric.Desc().String()) 386 | } 387 | return true 388 | }) 389 | 390 | // Prometheus panic should not be propagated 391 | sink.IncrCounterWithLabels(counterDef.Name, -1, []metrics.Label{ 392 | {Name: "version", Value: "some info"}, 393 | }) 394 | } 395 | 396 | func TestMetricSinkInterface(t *testing.T) { 397 | var ps *PrometheusSink 398 | _ = metrics.MetricSink(ps) 399 | var pps *PrometheusPushSink 400 | _ = metrics.MetricSink(pps) 401 | } 402 | 403 | func Test_flattenKey(t *testing.T) { 404 | testCases := []struct { 405 | name string 406 | inputParts []string 407 | inputLabels []metrics.Label 408 | expectedOutputKey string 409 | expectedOutputHash string 410 | }{ 411 | { 412 | name: "no replacement needed", 413 | inputParts: []string{"my", "example", "metric"}, 414 | inputLabels: []metrics.Label{ 415 | {Name: "foo", Value: "bar"}, 416 | {Name: "baz", Value: "buz"}, 417 | }, 418 | expectedOutputKey: "my_example_metric", 419 | expectedOutputHash: "my_example_metric;foo=bar;baz=buz", 420 | }, 421 | { 422 | name: "key with whitespace", 423 | inputParts: []string{" my ", " example ", " metric "}, 424 | inputLabels: []metrics.Label{ 425 | {Name: "foo", Value: "bar"}, 426 | {Name: "baz", Value: "buz"}, 427 | }, 428 | expectedOutputKey: "_my___example___metric_", 429 | expectedOutputHash: "_my___example___metric_;foo=bar;baz=buz", 430 | }, 431 | { 432 | name: "key with dot", 433 | inputParts: []string{".my.", ".example.", ".metric."}, 434 | inputLabels: []metrics.Label{ 435 | {Name: "foo", Value: "bar"}, 436 | {Name: "baz", Value: "buz"}, 437 | }, 438 | expectedOutputKey: "_my___example___metric_", 439 | expectedOutputHash: "_my___example___metric_;foo=bar;baz=buz", 440 | }, 441 | { 442 | name: "key with dash", 443 | inputParts: []string{"-my-", "-example-", "-metric-"}, 444 | inputLabels: []metrics.Label{ 445 | {Name: "foo", Value: "bar"}, 446 | {Name: "baz", Value: "buz"}, 447 | }, 448 | expectedOutputKey: "_my___example___metric_", 449 | expectedOutputHash: "_my___example___metric_;foo=bar;baz=buz", 450 | }, 451 | { 452 | name: "key with forward slash", 453 | inputParts: []string{"/my/", "/example/", "/metric/"}, 454 | inputLabels: []metrics.Label{ 455 | {Name: "foo", Value: "bar"}, 456 | {Name: "baz", Value: "buz"}, 457 | }, 458 | expectedOutputKey: "_my___example___metric_", 459 | expectedOutputHash: "_my___example___metric_;foo=bar;baz=buz", 460 | }, 461 | { 462 | name: "key with all restricted", 463 | inputParts: []string{"/my-", ".example ", "metric"}, 464 | inputLabels: []metrics.Label{ 465 | {Name: "foo", Value: "bar"}, 466 | {Name: "baz", Value: "buz"}, 467 | }, 468 | expectedOutputKey: "_my___example__metric", 469 | expectedOutputHash: "_my___example__metric;foo=bar;baz=buz", 470 | }, 471 | } 472 | 473 | for _, tc := range testCases { 474 | t.Run(tc.name, func(b *testing.T) { 475 | actualKey, actualHash := flattenKey(tc.inputParts, tc.inputLabels) 476 | if actualKey != tc.expectedOutputKey { 477 | t.Fatalf("expected key %s, got %s", tc.expectedOutputKey, actualKey) 478 | } 479 | if actualHash != tc.expectedOutputHash { 480 | t.Fatalf("expected hash %s, got %s", tc.expectedOutputHash, actualHash) 481 | } 482 | }) 483 | } 484 | } 485 | -------------------------------------------------------------------------------- /sink.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | package metrics 5 | 6 | import ( 7 | "fmt" 8 | "net/url" 9 | ) 10 | 11 | // The MetricSink interface is used to transmit metrics information 12 | // to an external system 13 | type MetricSink interface { 14 | // A Gauge should retain the last value it is set to 15 | SetGauge(key []string, val float32) 16 | SetGaugeWithLabels(key []string, val float32, labels []Label) 17 | 18 | // Should emit a Key/Value pair for each call 19 | EmitKey(key []string, val float32) 20 | 21 | // Counters should accumulate values 22 | IncrCounter(key []string, val float32) 23 | IncrCounterWithLabels(key []string, val float32, labels []Label) 24 | 25 | // Samples are for timing information, where quantiles are used 26 | AddSample(key []string, val float32) 27 | AddSampleWithLabels(key []string, val float32, labels []Label) 28 | } 29 | 30 | // PrecisionGaugeMetricSink interfae is used to support 64 bit precisions for Sinks, if needed. 31 | type PrecisionGaugeMetricSink interface { 32 | SetPrecisionGauge(key []string, val float64) 33 | SetPrecisionGaugeWithLabels(key []string, val float64, labels []Label) 34 | } 35 | 36 | type ShutdownSink interface { 37 | MetricSink 38 | 39 | // Shutdown the metric sink, flush metrics to storage, and cleanup resources. 40 | // Called immediately prior to application exit. Implementations must block 41 | // until metrics are flushed to storage. 42 | Shutdown() 43 | } 44 | 45 | // BlackholeSink is used to just blackhole messages 46 | type BlackholeSink struct{} 47 | 48 | func (*BlackholeSink) SetGauge(key []string, val float32) {} 49 | func (*BlackholeSink) SetGaugeWithLabels(key []string, val float32, labels []Label) {} 50 | func (*BlackholeSink) SetPrecisionGauge(key []string, val float64) {} 51 | func (*BlackholeSink) SetPrecisionGaugeWithLabels(key []string, val float64, labels []Label) {} 52 | func (*BlackholeSink) EmitKey(key []string, val float32) {} 53 | func (*BlackholeSink) IncrCounter(key []string, val float32) {} 54 | func (*BlackholeSink) IncrCounterWithLabels(key []string, val float32, labels []Label) {} 55 | func (*BlackholeSink) AddSample(key []string, val float32) {} 56 | func (*BlackholeSink) AddSampleWithLabels(key []string, val float32, labels []Label) {} 57 | 58 | // FanoutSink is used to sink to fanout values to multiple sinks 59 | type FanoutSink []MetricSink 60 | 61 | func (fh FanoutSink) SetGauge(key []string, val float32) { 62 | fh.SetGaugeWithLabels(key, val, nil) 63 | } 64 | 65 | func (fh FanoutSink) SetGaugeWithLabels(key []string, val float32, labels []Label) { 66 | for _, s := range fh { 67 | s.SetGaugeWithLabels(key, val, labels) 68 | } 69 | } 70 | 71 | func (fh FanoutSink) SetPrecisionGauge(key []string, val float64) { 72 | fh.SetPrecisionGaugeWithLabels(key, val, nil) 73 | } 74 | 75 | func (fh FanoutSink) SetPrecisionGaugeWithLabels(key []string, val float64, labels []Label) { 76 | for _, s := range fh { 77 | // The Sink needs to implement PrecisionGaugeMetricSink, in case it doesn't, the metric value won't be set and ingored instead 78 | if s64, ok := s.(PrecisionGaugeMetricSink); ok { 79 | s64.SetPrecisionGaugeWithLabels(key, val, labels) 80 | } 81 | } 82 | } 83 | 84 | func (fh FanoutSink) EmitKey(key []string, val float32) { 85 | for _, s := range fh { 86 | s.EmitKey(key, val) 87 | } 88 | } 89 | 90 | func (fh FanoutSink) IncrCounter(key []string, val float32) { 91 | fh.IncrCounterWithLabels(key, val, nil) 92 | } 93 | 94 | func (fh FanoutSink) IncrCounterWithLabels(key []string, val float32, labels []Label) { 95 | for _, s := range fh { 96 | s.IncrCounterWithLabels(key, val, labels) 97 | } 98 | } 99 | 100 | func (fh FanoutSink) AddSample(key []string, val float32) { 101 | fh.AddSampleWithLabels(key, val, nil) 102 | } 103 | 104 | func (fh FanoutSink) AddSampleWithLabels(key []string, val float32, labels []Label) { 105 | for _, s := range fh { 106 | s.AddSampleWithLabels(key, val, labels) 107 | } 108 | } 109 | 110 | func (fh FanoutSink) Shutdown() { 111 | for _, s := range fh { 112 | if ss, ok := s.(ShutdownSink); ok { 113 | ss.Shutdown() 114 | } 115 | } 116 | } 117 | 118 | // sinkURLFactoryFunc is an generic interface around the *SinkFromURL() function provided 119 | // by each sink type 120 | type sinkURLFactoryFunc func(*url.URL) (MetricSink, error) 121 | 122 | // sinkRegistry supports the generic NewMetricSink function by mapping URL 123 | // schemes to metric sink factory functions 124 | var sinkRegistry = map[string]sinkURLFactoryFunc{ 125 | "statsd": NewStatsdSinkFromURL, 126 | "statsite": NewStatsiteSinkFromURL, 127 | "inmem": NewInmemSinkFromURL, 128 | } 129 | 130 | // NewMetricSinkFromURL allows a generic URL input to configure any of the 131 | // supported sinks. The scheme of the URL identifies the type of the sink, the 132 | // and query parameters are used to set options. 133 | // 134 | // "statsd://" - Initializes a StatsdSink. The host and port are passed through 135 | // as the "addr" of the sink 136 | // 137 | // "statsite://" - Initializes a StatsiteSink. The host and port become the 138 | // "addr" of the sink 139 | // 140 | // "inmem://" - Initializes an InmemSink. The host and port are ignored. The 141 | // "interval" and "duration" query parameters must be specified with valid 142 | // durations, see NewInmemSink for details. 143 | func NewMetricSinkFromURL(urlStr string) (MetricSink, error) { 144 | u, err := url.Parse(urlStr) 145 | if err != nil { 146 | return nil, err 147 | } 148 | 149 | sinkURLFactoryFunc := sinkRegistry[u.Scheme] 150 | if sinkURLFactoryFunc == nil { 151 | return nil, fmt.Errorf( 152 | "cannot create metric sink, unrecognized sink name: %q", u.Scheme) 153 | } 154 | 155 | return sinkURLFactoryFunc(u) 156 | } 157 | -------------------------------------------------------------------------------- /sink_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | package metrics 5 | 6 | import ( 7 | "reflect" 8 | "strings" 9 | "sync" 10 | "testing" 11 | ) 12 | 13 | type MockSink struct { 14 | lock sync.Mutex 15 | 16 | shutdown bool 17 | keys [][]string 18 | vals []float32 19 | precisionVals []float64 20 | labels [][]Label 21 | } 22 | 23 | func (m *MockSink) getKeys() [][]string { 24 | m.lock.Lock() 25 | defer m.lock.Unlock() 26 | 27 | return m.keys 28 | } 29 | 30 | func (m *MockSink) SetGauge(key []string, val float32) { 31 | m.SetGaugeWithLabels(key, val, nil) 32 | } 33 | func (m *MockSink) SetGaugeWithLabels(key []string, val float32, labels []Label) { 34 | m.lock.Lock() 35 | defer m.lock.Unlock() 36 | 37 | m.keys = append(m.keys, key) 38 | m.vals = append(m.vals, val) 39 | m.labels = append(m.labels, labels) 40 | } 41 | func (m *MockSink) SetPrecisionGauge(key []string, val float64) { 42 | m.SetPrecisionGaugeWithLabels(key, val, nil) 43 | } 44 | func (m *MockSink) SetPrecisionGaugeWithLabels(key []string, val float64, labels []Label) { 45 | m.lock.Lock() 46 | defer m.lock.Unlock() 47 | 48 | m.keys = append(m.keys, key) 49 | m.precisionVals = append(m.precisionVals, val) 50 | m.labels = append(m.labels, labels) 51 | } 52 | func (m *MockSink) EmitKey(key []string, val float32) { 53 | m.lock.Lock() 54 | defer m.lock.Unlock() 55 | 56 | m.keys = append(m.keys, key) 57 | m.vals = append(m.vals, val) 58 | m.labels = append(m.labels, nil) 59 | } 60 | func (m *MockSink) IncrCounter(key []string, val float32) { 61 | m.IncrCounterWithLabels(key, val, nil) 62 | } 63 | func (m *MockSink) IncrCounterWithLabels(key []string, val float32, labels []Label) { 64 | m.lock.Lock() 65 | defer m.lock.Unlock() 66 | 67 | m.keys = append(m.keys, key) 68 | m.vals = append(m.vals, val) 69 | m.labels = append(m.labels, labels) 70 | } 71 | func (m *MockSink) AddSample(key []string, val float32) { 72 | m.AddSampleWithLabels(key, val, nil) 73 | } 74 | func (m *MockSink) AddSampleWithLabels(key []string, val float32, labels []Label) { 75 | m.lock.Lock() 76 | defer m.lock.Unlock() 77 | 78 | m.keys = append(m.keys, key) 79 | m.vals = append(m.vals, val) 80 | m.labels = append(m.labels, labels) 81 | } 82 | func (m *MockSink) Shutdown() { 83 | m.lock.Lock() 84 | defer m.lock.Unlock() 85 | 86 | m.shutdown = true 87 | } 88 | 89 | func TestFanoutSink_Gauge(t *testing.T) { 90 | m1 := &MockSink{} 91 | m2 := &MockSink{} 92 | fh := &FanoutSink{m1, m2} 93 | 94 | k := []string{"test"} 95 | v := float32(42.0) 96 | fh.SetGauge(k, v) 97 | 98 | if !reflect.DeepEqual(m1.keys[0], k) { 99 | t.Fatalf("key not equal") 100 | } 101 | if !reflect.DeepEqual(m2.keys[0], k) { 102 | t.Fatalf("key not equal") 103 | } 104 | if !reflect.DeepEqual(m1.vals[0], v) { 105 | t.Fatalf("val not equal") 106 | } 107 | if !reflect.DeepEqual(m2.vals[0], v) { 108 | t.Fatalf("val not equal") 109 | } 110 | } 111 | 112 | func TestFanoutSink_Gauge_Labels(t *testing.T) { 113 | m1 := &MockSink{} 114 | m2 := &MockSink{} 115 | fh := &FanoutSink{m1, m2} 116 | 117 | k := []string{"test"} 118 | v := float32(42.0) 119 | l := []Label{{"a", "b"}} 120 | fh.SetGaugeWithLabels(k, v, l) 121 | 122 | if !reflect.DeepEqual(m1.keys[0], k) { 123 | t.Fatalf("key not equal") 124 | } 125 | if !reflect.DeepEqual(m2.keys[0], k) { 126 | t.Fatalf("key not equal") 127 | } 128 | if !reflect.DeepEqual(m1.vals[0], v) { 129 | t.Fatalf("val not equal") 130 | } 131 | if !reflect.DeepEqual(m2.vals[0], v) { 132 | t.Fatalf("val not equal") 133 | } 134 | if !reflect.DeepEqual(m1.labels[0], l) { 135 | t.Fatalf("labels not equal") 136 | } 137 | if !reflect.DeepEqual(m2.labels[0], l) { 138 | t.Fatalf("labels not equal") 139 | } 140 | } 141 | 142 | func TestFanoutSink_Key(t *testing.T) { 143 | m1 := &MockSink{} 144 | m2 := &MockSink{} 145 | fh := &FanoutSink{m1, m2} 146 | 147 | k := []string{"test"} 148 | v := float32(42.0) 149 | fh.EmitKey(k, v) 150 | 151 | if !reflect.DeepEqual(m1.keys[0], k) { 152 | t.Fatalf("key not equal") 153 | } 154 | if !reflect.DeepEqual(m2.keys[0], k) { 155 | t.Fatalf("key not equal") 156 | } 157 | if !reflect.DeepEqual(m1.vals[0], v) { 158 | t.Fatalf("val not equal") 159 | } 160 | if !reflect.DeepEqual(m2.vals[0], v) { 161 | t.Fatalf("val not equal") 162 | } 163 | } 164 | 165 | func TestFanoutSink_Counter(t *testing.T) { 166 | m1 := &MockSink{} 167 | m2 := &MockSink{} 168 | fh := &FanoutSink{m1, m2} 169 | 170 | k := []string{"test"} 171 | v := float32(42.0) 172 | fh.IncrCounter(k, v) 173 | 174 | if !reflect.DeepEqual(m1.keys[0], k) { 175 | t.Fatalf("key not equal") 176 | } 177 | if !reflect.DeepEqual(m2.keys[0], k) { 178 | t.Fatalf("key not equal") 179 | } 180 | if !reflect.DeepEqual(m1.vals[0], v) { 181 | t.Fatalf("val not equal") 182 | } 183 | if !reflect.DeepEqual(m2.vals[0], v) { 184 | t.Fatalf("val not equal") 185 | } 186 | } 187 | 188 | func TestFanoutSink_Counter_Labels(t *testing.T) { 189 | m1 := &MockSink{} 190 | m2 := &MockSink{} 191 | fh := &FanoutSink{m1, m2} 192 | 193 | k := []string{"test"} 194 | v := float32(42.0) 195 | l := []Label{{"a", "b"}} 196 | fh.IncrCounterWithLabels(k, v, l) 197 | 198 | if !reflect.DeepEqual(m1.keys[0], k) { 199 | t.Fatalf("key not equal") 200 | } 201 | if !reflect.DeepEqual(m2.keys[0], k) { 202 | t.Fatalf("key not equal") 203 | } 204 | if !reflect.DeepEqual(m1.vals[0], v) { 205 | t.Fatalf("val not equal") 206 | } 207 | if !reflect.DeepEqual(m2.vals[0], v) { 208 | t.Fatalf("val not equal") 209 | } 210 | if !reflect.DeepEqual(m1.labels[0], l) { 211 | t.Fatalf("labels not equal") 212 | } 213 | if !reflect.DeepEqual(m2.labels[0], l) { 214 | t.Fatalf("labels not equal") 215 | } 216 | } 217 | 218 | func TestFanoutSink_Sample(t *testing.T) { 219 | m1 := &MockSink{} 220 | m2 := &MockSink{} 221 | fh := &FanoutSink{m1, m2} 222 | 223 | k := []string{"test"} 224 | v := float32(42.0) 225 | fh.AddSample(k, v) 226 | 227 | if !reflect.DeepEqual(m1.keys[0], k) { 228 | t.Fatalf("key not equal") 229 | } 230 | if !reflect.DeepEqual(m2.keys[0], k) { 231 | t.Fatalf("key not equal") 232 | } 233 | if !reflect.DeepEqual(m1.vals[0], v) { 234 | t.Fatalf("val not equal") 235 | } 236 | if !reflect.DeepEqual(m2.vals[0], v) { 237 | t.Fatalf("val not equal") 238 | } 239 | } 240 | 241 | func TestFanoutSink_Sample_Labels(t *testing.T) { 242 | m1 := &MockSink{} 243 | m2 := &MockSink{} 244 | fh := &FanoutSink{m1, m2} 245 | 246 | k := []string{"test"} 247 | v := float32(42.0) 248 | l := []Label{{"a", "b"}} 249 | fh.AddSampleWithLabels(k, v, l) 250 | 251 | if !reflect.DeepEqual(m1.keys[0], k) { 252 | t.Fatalf("key not equal") 253 | } 254 | if !reflect.DeepEqual(m2.keys[0], k) { 255 | t.Fatalf("key not equal") 256 | } 257 | if !reflect.DeepEqual(m1.vals[0], v) { 258 | t.Fatalf("val not equal") 259 | } 260 | if !reflect.DeepEqual(m2.vals[0], v) { 261 | t.Fatalf("val not equal") 262 | } 263 | if !reflect.DeepEqual(m1.labels[0], l) { 264 | t.Fatalf("labels not equal") 265 | } 266 | if !reflect.DeepEqual(m2.labels[0], l) { 267 | t.Fatalf("labels not equal") 268 | } 269 | } 270 | 271 | func TestNewMetricSinkFromURL(t *testing.T) { 272 | for _, tc := range []struct { 273 | desc string 274 | input string 275 | expect reflect.Type 276 | expectErr string 277 | }{ 278 | { 279 | desc: "statsd scheme yields a StatsdSink", 280 | input: "statsd://someserver:123", 281 | expect: reflect.TypeOf(&StatsdSink{}), 282 | }, 283 | { 284 | desc: "statsite scheme yields a StatsiteSink", 285 | input: "statsite://someserver:123", 286 | expect: reflect.TypeOf(&StatsiteSink{}), 287 | }, 288 | { 289 | desc: "inmem scheme yields an InmemSink", 290 | input: "inmem://?interval=30s&retain=30s", 291 | expect: reflect.TypeOf(&InmemSink{}), 292 | }, 293 | { 294 | desc: "unknown scheme yields an error", 295 | input: "notasink://whatever", 296 | expectErr: "unrecognized sink name: \"notasink\"", 297 | }, 298 | } { 299 | t.Run(tc.desc, func(t *testing.T) { 300 | ms, err := NewMetricSinkFromURL(tc.input) 301 | if tc.expectErr != "" { 302 | if !strings.Contains(err.Error(), tc.expectErr) { 303 | t.Fatalf("expected err: %q to contain: %q", err, tc.expectErr) 304 | } 305 | } else { 306 | if err != nil { 307 | t.Fatalf("unexpected err: %s", err) 308 | } 309 | got := reflect.TypeOf(ms) 310 | if got != tc.expect { 311 | t.Fatalf("expected return type to be %v, got: %v", tc.expect, got) 312 | } 313 | } 314 | }) 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /start.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | package metrics 5 | 6 | import ( 7 | "os" 8 | "sync" 9 | "sync/atomic" 10 | "time" 11 | 12 | iradix "github.com/hashicorp/go-immutable-radix" 13 | ) 14 | 15 | // Config is used to configure metrics settings 16 | type Config struct { 17 | ServiceName string // Prefixed with keys to separate services 18 | HostName string // Hostname to use. If not provided and EnableHostname, it will be os.Hostname 19 | EnableHostname bool // Enable prefixing gauge values with hostname 20 | EnableHostnameLabel bool // Enable adding hostname to labels 21 | EnableServiceLabel bool // Enable adding service to labels 22 | EnableRuntimeMetrics bool // Enables profiling of runtime metrics (GC, Goroutines, Memory) 23 | EnableTypePrefix bool // Prefixes key with a type ("counter", "gauge", "timer") 24 | TimerGranularity time.Duration // Granularity of timers. 25 | ProfileInterval time.Duration // Interval to profile runtime metrics 26 | 27 | AllowedPrefixes []string // A list of metric prefixes to allow, with '.' as the separator 28 | BlockedPrefixes []string // A list of metric prefixes to block, with '.' as the separator 29 | AllowedLabels []string // A list of metric labels to allow, with '.' as the separator 30 | BlockedLabels []string // A list of metric labels to block, with '.' as the separator 31 | FilterDefault bool // Whether to allow metrics by default 32 | } 33 | 34 | // Metrics represents an instance of a metrics sink that can 35 | // be used to emit 36 | type Metrics struct { 37 | Config 38 | lastNumGC uint32 39 | sink MetricSink 40 | filter *iradix.Tree 41 | allowedLabels map[string]bool 42 | blockedLabels map[string]bool 43 | filterLock sync.RWMutex // Lock filters and allowedLabels/blockedLabels access 44 | } 45 | 46 | // Shared global metrics instance 47 | var globalMetrics atomic.Value // *Metrics 48 | 49 | func init() { 50 | // Initialize to a blackhole sink to avoid errors 51 | globalMetrics.Store(&Metrics{sink: &BlackholeSink{}}) 52 | } 53 | 54 | // Default returns the shared global metrics instance. 55 | func Default() *Metrics { 56 | return globalMetrics.Load().(*Metrics) 57 | } 58 | 59 | // DefaultConfig provides a sane default configuration 60 | func DefaultConfig(serviceName string) *Config { 61 | c := &Config{ 62 | ServiceName: serviceName, // Use client provided service 63 | HostName: "", 64 | EnableHostname: true, // Enable hostname prefix 65 | EnableRuntimeMetrics: true, // Enable runtime profiling 66 | EnableTypePrefix: false, // Disable type prefix 67 | TimerGranularity: time.Millisecond, // Timers are in milliseconds 68 | ProfileInterval: time.Second, // Poll runtime every second 69 | FilterDefault: true, // Don't filter metrics by default 70 | } 71 | 72 | // Try to get the hostname 73 | name, _ := os.Hostname() 74 | c.HostName = name 75 | return c 76 | } 77 | 78 | // New is used to create a new instance of Metrics 79 | func New(conf *Config, sink MetricSink) (*Metrics, error) { 80 | met := &Metrics{} 81 | met.Config = *conf 82 | met.sink = sink 83 | met.UpdateFilterAndLabels(conf.AllowedPrefixes, conf.BlockedPrefixes, conf.AllowedLabels, conf.BlockedLabels) 84 | 85 | // Start the runtime collector 86 | if conf.EnableRuntimeMetrics { 87 | go met.collectStats() 88 | } 89 | return met, nil 90 | } 91 | 92 | // NewGlobal is the same as New, but it assigns the metrics object to be 93 | // used globally as well as returning it. 94 | func NewGlobal(conf *Config, sink MetricSink) (*Metrics, error) { 95 | metrics, err := New(conf, sink) 96 | if err == nil { 97 | globalMetrics.Store(metrics) 98 | } 99 | return metrics, err 100 | } 101 | 102 | // Proxy all the methods to the globalMetrics instance 103 | 104 | // Set gauge key and value with 32 bit precision 105 | func SetGauge(key []string, val float32) { 106 | globalMetrics.Load().(*Metrics).SetGauge(key, val) 107 | } 108 | 109 | // Set gauge key and value with 32 bit precision 110 | func SetGaugeWithLabels(key []string, val float32, labels []Label) { 111 | globalMetrics.Load().(*Metrics).SetGaugeWithLabels(key, val, labels) 112 | } 113 | 114 | // Set gauge key and value with 64 bit precision 115 | // The Sink needs to implement PrecisionGaugeMetricSink, in case it doesn't, the metric value won't be set and ingored instead 116 | func SetPrecisionGauge(key []string, val float64) { 117 | globalMetrics.Load().(*Metrics).SetPrecisionGauge(key, val) 118 | } 119 | 120 | // Set gauge key, value with 64 bit precision, and labels 121 | // The Sink needs to implement PrecisionGaugeMetricSink, in case it doesn't, the metric value won't be set and ingored instead 122 | func SetPrecisionGaugeWithLabels(key []string, val float64, labels []Label) { 123 | globalMetrics.Load().(*Metrics).SetPrecisionGaugeWithLabels(key, val, labels) 124 | } 125 | 126 | func EmitKey(key []string, val float32) { 127 | globalMetrics.Load().(*Metrics).EmitKey(key, val) 128 | } 129 | 130 | func IncrCounter(key []string, val float32) { 131 | globalMetrics.Load().(*Metrics).IncrCounter(key, val) 132 | } 133 | 134 | func IncrCounterWithLabels(key []string, val float32, labels []Label) { 135 | globalMetrics.Load().(*Metrics).IncrCounterWithLabels(key, val, labels) 136 | } 137 | 138 | func AddSample(key []string, val float32) { 139 | globalMetrics.Load().(*Metrics).AddSample(key, val) 140 | } 141 | 142 | func AddSampleWithLabels(key []string, val float32, labels []Label) { 143 | globalMetrics.Load().(*Metrics).AddSampleWithLabels(key, val, labels) 144 | } 145 | 146 | func MeasureSince(key []string, start time.Time) { 147 | globalMetrics.Load().(*Metrics).MeasureSince(key, start) 148 | } 149 | 150 | func MeasureSinceWithLabels(key []string, start time.Time, labels []Label) { 151 | globalMetrics.Load().(*Metrics).MeasureSinceWithLabels(key, start, labels) 152 | } 153 | 154 | func UpdateFilter(allow, block []string) { 155 | globalMetrics.Load().(*Metrics).UpdateFilter(allow, block) 156 | } 157 | 158 | // UpdateFilterAndLabels set allow/block prefixes of metrics while allowedLabels 159 | // and blockedLabels - when not nil - allow filtering of labels in order to 160 | // block/allow globally labels (especially useful when having large number of 161 | // values for a given label). See README.md for more information about usage. 162 | func UpdateFilterAndLabels(allow, block, allowedLabels, blockedLabels []string) { 163 | globalMetrics.Load().(*Metrics).UpdateFilterAndLabels(allow, block, allowedLabels, blockedLabels) 164 | } 165 | 166 | // Shutdown disables metric collection, then blocks while attempting to flush metrics to storage. 167 | // WARNING: Not all MetricSink backends support this functionality, and calling this will cause them to leak resources. 168 | // This is intended for use immediately prior to application exit. 169 | func Shutdown() { 170 | m := globalMetrics.Load().(*Metrics) 171 | // Swap whatever MetricSink is currently active with a BlackholeSink. Callers must not have a 172 | // reason to expect that calls to the library will successfully collect metrics after Shutdown 173 | // has been called. 174 | globalMetrics.Store(&Metrics{sink: &BlackholeSink{}}) 175 | m.Shutdown() 176 | } 177 | -------------------------------------------------------------------------------- /start_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | package metrics 5 | 6 | import ( 7 | "io/ioutil" 8 | "log" 9 | "reflect" 10 | "sync/atomic" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | func TestDefaultConfig(t *testing.T) { 16 | conf := DefaultConfig("service") 17 | if conf.ServiceName != "service" { 18 | t.Fatalf("Bad name") 19 | } 20 | if conf.HostName == "" { 21 | t.Fatalf("missing hostname") 22 | } 23 | if !conf.EnableHostname || !conf.EnableRuntimeMetrics { 24 | t.Fatalf("expect true") 25 | } 26 | if conf.EnableTypePrefix { 27 | t.Fatalf("expect false") 28 | } 29 | if conf.TimerGranularity != time.Millisecond { 30 | t.Fatalf("bad granularity") 31 | } 32 | if conf.ProfileInterval != time.Second { 33 | t.Fatalf("bad interval") 34 | } 35 | } 36 | 37 | func Test_GlobalMetrics(t *testing.T) { 38 | var tests = []struct { 39 | desc string 40 | key []string 41 | val float32 42 | fn func([]string, float32) 43 | }{ 44 | {"SetGauge", []string{"test"}, 42, SetGauge}, 45 | {"EmitKey", []string{"test"}, 42, EmitKey}, 46 | {"IncrCounter", []string{"test"}, 42, IncrCounter}, 47 | {"AddSample", []string{"test"}, 42, AddSample}, 48 | } 49 | 50 | for _, tt := range tests { 51 | t.Run(tt.desc, func(t *testing.T) { 52 | s := &MockSink{} 53 | globalMetrics.Store(&Metrics{Config: Config{FilterDefault: true}, sink: s}) 54 | tt.fn(tt.key, tt.val) 55 | if got, want := s.keys[0], tt.key; !reflect.DeepEqual(got, want) { 56 | t.Fatalf("got key %s want %s", got, want) 57 | } 58 | if got, want := s.vals[0], tt.val; !reflect.DeepEqual(got, want) { 59 | t.Fatalf("got val %v want %v", got, want) 60 | } 61 | }) 62 | } 63 | } 64 | 65 | func Test_GlobalMetrics_Labels(t *testing.T) { 66 | labels := []Label{{"a", "b"}} 67 | var tests = []struct { 68 | desc string 69 | key []string 70 | val float32 71 | fn func([]string, float32, []Label) 72 | labels []Label 73 | }{ 74 | {"SetGaugeWithLabels", []string{"test"}, 42, SetGaugeWithLabels, labels}, 75 | {"IncrCounterWithLabels", []string{"test"}, 42, IncrCounterWithLabels, labels}, 76 | {"AddSampleWithLabels", []string{"test"}, 42, AddSampleWithLabels, labels}, 77 | } 78 | 79 | for _, tt := range tests { 80 | t.Run(tt.desc, func(t *testing.T) { 81 | s := &MockSink{} 82 | globalMetrics.Store(&Metrics{Config: Config{FilterDefault: true}, sink: s}) 83 | tt.fn(tt.key, tt.val, tt.labels) 84 | if got, want := s.keys[0], tt.key; !reflect.DeepEqual(got, want) { 85 | t.Fatalf("got key %s want %s", got, want) 86 | } 87 | if got, want := s.vals[0], tt.val; !reflect.DeepEqual(got, want) { 88 | t.Fatalf("got val %v want %v", got, want) 89 | } 90 | if got, want := s.labels[0], tt.labels; !reflect.DeepEqual(got, want) { 91 | t.Fatalf("got val %s want %s", got, want) 92 | } 93 | }) 94 | } 95 | } 96 | 97 | func Test_GlobalPrecisionMetrics_Labels(t *testing.T) { 98 | labels := []Label{{"a", "b"}} 99 | var tests = []struct { 100 | desc string 101 | key []string 102 | val float64 103 | fn func([]string, float64, []Label) 104 | labels []Label 105 | }{ 106 | {"SetPrecisionGaugeWithLabels", []string{"test"}, 42, SetPrecisionGaugeWithLabels, labels}, 107 | } 108 | 109 | for _, tt := range tests { 110 | t.Run(tt.desc, func(t *testing.T) { 111 | s := &MockSink{} 112 | globalMetrics.Store(&Metrics{Config: Config{FilterDefault: true}, sink: s}) 113 | tt.fn(tt.key, tt.val, tt.labels) 114 | if got, want := s.keys[0], tt.key; !reflect.DeepEqual(got, want) { 115 | t.Fatalf("got key %s want %s", got, want) 116 | } 117 | if got, want := s.precisionVals[0], tt.val; !reflect.DeepEqual(got, want) { 118 | t.Fatalf("got val %v want %v", got, want) 119 | } 120 | if got, want := s.labels[0], tt.labels; !reflect.DeepEqual(got, want) { 121 | t.Fatalf("got val %s want %s", got, want) 122 | } 123 | }) 124 | } 125 | } 126 | 127 | func Test_GlobalMetrics_DefaultLabels(t *testing.T) { 128 | config := Config{ 129 | HostName: "host1", 130 | ServiceName: "redis", 131 | EnableHostnameLabel: true, 132 | EnableServiceLabel: true, 133 | FilterDefault: true, 134 | } 135 | labels := []Label{ 136 | {"host", config.HostName}, 137 | {"service", config.ServiceName}, 138 | } 139 | var tests = []struct { 140 | desc string 141 | key []string 142 | val float32 143 | fn func([]string, float32, []Label) 144 | labels []Label 145 | }{ 146 | {"SetGaugeWithLabels", []string{"test"}, 42, SetGaugeWithLabels, labels}, 147 | {"IncrCounterWithLabels", []string{"test"}, 42, IncrCounterWithLabels, labels}, 148 | {"AddSampleWithLabels", []string{"test"}, 42, AddSampleWithLabels, labels}, 149 | } 150 | 151 | for _, tt := range tests { 152 | t.Run(tt.desc, func(t *testing.T) { 153 | s := &MockSink{} 154 | globalMetrics.Store(&Metrics{Config: config, sink: s}) 155 | tt.fn(tt.key, tt.val, nil) 156 | if got, want := s.keys[0], tt.key; !reflect.DeepEqual(got, want) { 157 | t.Fatalf("got key %s want %s", got, want) 158 | } 159 | if got, want := s.vals[0], tt.val; !reflect.DeepEqual(got, want) { 160 | t.Fatalf("got val %v want %v", got, want) 161 | } 162 | if got, want := s.labels[0], tt.labels; !reflect.DeepEqual(got, want) { 163 | t.Fatalf("got val %s want %s", got, want) 164 | } 165 | }) 166 | } 167 | } 168 | 169 | func Test_GlobalPrecisionMetrics_DefaultLabels(t *testing.T) { 170 | config := Config{ 171 | HostName: "host1", 172 | ServiceName: "redis", 173 | EnableHostnameLabel: true, 174 | EnableServiceLabel: true, 175 | FilterDefault: true, 176 | } 177 | labels := []Label{ 178 | {"host", config.HostName}, 179 | {"service", config.ServiceName}, 180 | } 181 | var tests = []struct { 182 | desc string 183 | key []string 184 | val float64 185 | fn func([]string, float64, []Label) 186 | labels []Label 187 | }{ 188 | {"SetGaugeWithLabels", []string{"test"}, 42, SetPrecisionGaugeWithLabels, labels}, 189 | } 190 | 191 | for _, tt := range tests { 192 | t.Run(tt.desc, func(t *testing.T) { 193 | s := &MockSink{} 194 | globalMetrics.Store(&Metrics{Config: config, sink: s}) 195 | tt.fn(tt.key, tt.val, nil) 196 | if got, want := s.keys[0], tt.key; !reflect.DeepEqual(got, want) { 197 | t.Fatalf("got key %s want %s", got, want) 198 | } 199 | if got, want := s.precisionVals[0], tt.val; !reflect.DeepEqual(got, want) { 200 | t.Fatalf("got val %v want %v", got, want) 201 | } 202 | if got, want := s.labels[0], tt.labels; !reflect.DeepEqual(got, want) { 203 | t.Fatalf("got val %s want %s", got, want) 204 | } 205 | }) 206 | } 207 | } 208 | 209 | func Test_GlobalMetrics_MeasureSince(t *testing.T) { 210 | s := &MockSink{} 211 | m := &Metrics{sink: s, Config: Config{TimerGranularity: time.Millisecond, FilterDefault: true}} 212 | globalMetrics.Store(m) 213 | 214 | k := []string{"test"} 215 | now := time.Now() 216 | MeasureSince(k, now) 217 | 218 | if !reflect.DeepEqual(s.keys[0], k) { 219 | t.Fatalf("key not equal") 220 | } 221 | if s.vals[0] > 0.1 { 222 | t.Fatalf("val too large %v", s.vals[0]) 223 | } 224 | 225 | labels := []Label{{"a", "b"}} 226 | MeasureSinceWithLabels(k, now, labels) 227 | if got, want := s.keys[1], k; !reflect.DeepEqual(got, want) { 228 | t.Fatalf("got key %s want %s", got, want) 229 | } 230 | if s.vals[1] > 0.1 { 231 | t.Fatalf("val too large %v", s.vals[0]) 232 | } 233 | if got, want := s.labels[1], labels; !reflect.DeepEqual(got, want) { 234 | t.Fatalf("got val %s want %s", got, want) 235 | } 236 | } 237 | 238 | func Test_GlobalMetrics_UpdateFilter(t *testing.T) { 239 | globalMetrics.Store(&Metrics{Config: Config{ 240 | AllowedPrefixes: []string{"a"}, 241 | BlockedPrefixes: []string{"b"}, 242 | AllowedLabels: []string{"1"}, 243 | BlockedLabels: []string{"2"}, 244 | }}) 245 | UpdateFilterAndLabels([]string{"c"}, []string{"d"}, []string{"3"}, []string{"4"}) 246 | 247 | m := globalMetrics.Load().(*Metrics) 248 | if m.AllowedPrefixes[0] != "c" { 249 | t.Fatalf("bad: %v", m.AllowedPrefixes) 250 | } 251 | if m.BlockedPrefixes[0] != "d" { 252 | t.Fatalf("bad: %v", m.BlockedPrefixes) 253 | } 254 | if m.AllowedLabels[0] != "3" { 255 | t.Fatalf("bad: %v", m.AllowedPrefixes) 256 | } 257 | if m.BlockedLabels[0] != "4" { 258 | t.Fatalf("bad: %v", m.AllowedPrefixes) 259 | } 260 | if _, ok := m.allowedLabels["3"]; !ok { 261 | t.Fatalf("bad: %v", m.allowedLabels) 262 | } 263 | if _, ok := m.blockedLabels["4"]; !ok { 264 | t.Fatalf("bad: %v", m.blockedLabels) 265 | } 266 | } 267 | 268 | func Test_GlobalMetrics_Shutdown(t *testing.T) { 269 | s := &MockSink{} 270 | m := &Metrics{sink: s} 271 | globalMetrics.Store(m) 272 | 273 | Shutdown() 274 | 275 | loaded := globalMetrics.Load() 276 | metrics, ok := loaded.(*Metrics) 277 | if !ok { 278 | t.Fatalf("Expected globalMetrics to contain a Metrics pointer, but found: %v", loaded) 279 | } 280 | if metrics == m { 281 | t.Errorf("Calling shutdown should have replaced the Metrics struct stored in globalMetrics") 282 | } 283 | if !s.shutdown { 284 | t.Errorf("Expected Shutdown to have been called on MockSink") 285 | } 286 | } 287 | 288 | // Benchmark_GlobalMetrics_Direct/direct-8 5000000 278 ns/op 289 | // Benchmark_GlobalMetrics_Direct/atomic.Value-8 5000000 235 ns/op 290 | func Benchmark_GlobalMetrics_Direct(b *testing.B) { 291 | log.SetOutput(ioutil.Discard) 292 | s := &MockSink{} 293 | m := &Metrics{sink: s} 294 | var v atomic.Value 295 | v.Store(m) 296 | k := []string{"test"} 297 | b.Run("direct", func(b *testing.B) { 298 | for i := 0; i < b.N; i++ { 299 | m.IncrCounter(k, 1) 300 | } 301 | }) 302 | b.Run("atomic.Value", func(b *testing.B) { 303 | for i := 0; i < b.N; i++ { 304 | v.Load().(*Metrics).IncrCounter(k, 1) 305 | } 306 | }) 307 | // do something with m so that the compiler does not optimize this away 308 | b.Logf("%d", m.lastNumGC) 309 | } 310 | -------------------------------------------------------------------------------- /statsd.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | package metrics 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "log" 10 | "net" 11 | "net/url" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | const ( 17 | // statsdMaxLen is the maximum size of a packet 18 | // to send to statsd 19 | statsdMaxLen = 1400 20 | ) 21 | 22 | // StatsdSink provides a MetricSink that can be used 23 | // with a statsite or statsd metrics server. It uses 24 | // only UDP packets, while StatsiteSink uses TCP. 25 | type StatsdSink struct { 26 | addr string 27 | metricQueue chan string 28 | } 29 | 30 | // NewStatsdSinkFromURL creates an StatsdSink from a URL. It is used 31 | // (and tested) from NewMetricSinkFromURL. 32 | func NewStatsdSinkFromURL(u *url.URL) (MetricSink, error) { 33 | return NewStatsdSink(u.Host) 34 | } 35 | 36 | // NewStatsdSink is used to create a new StatsdSink 37 | func NewStatsdSink(addr string) (*StatsdSink, error) { 38 | s := &StatsdSink{ 39 | addr: addr, 40 | metricQueue: make(chan string, 4096), 41 | } 42 | go s.flushMetrics() 43 | return s, nil 44 | } 45 | 46 | // Close is used to stop flushing to statsd 47 | func (s *StatsdSink) Shutdown() { 48 | close(s.metricQueue) 49 | } 50 | 51 | func (s *StatsdSink) SetGauge(key []string, val float32) { 52 | flatKey := s.flattenKey(key) 53 | s.pushMetric(fmt.Sprintf("%s:%f|g\n", flatKey, val)) 54 | } 55 | 56 | func (s *StatsdSink) SetGaugeWithLabels(key []string, val float32, labels []Label) { 57 | flatKey := s.flattenKeyLabels(key, labels) 58 | s.pushMetric(fmt.Sprintf("%s:%f|g\n", flatKey, val)) 59 | } 60 | 61 | func (s *StatsdSink) SetPrecisionGauge(key []string, val float64) { 62 | flatKey := s.flattenKey(key) 63 | s.pushMetric(fmt.Sprintf("%s:%f|g\n", flatKey, val)) 64 | } 65 | 66 | func (s *StatsdSink) SetPrecisionGaugeWithLabels(key []string, val float64, labels []Label) { 67 | flatKey := s.flattenKeyLabels(key, labels) 68 | s.pushMetric(fmt.Sprintf("%s:%f|g\n", flatKey, val)) 69 | } 70 | 71 | func (s *StatsdSink) EmitKey(key []string, val float32) { 72 | flatKey := s.flattenKey(key) 73 | s.pushMetric(fmt.Sprintf("%s:%f|kv\n", flatKey, val)) 74 | } 75 | 76 | func (s *StatsdSink) IncrCounter(key []string, val float32) { 77 | flatKey := s.flattenKey(key) 78 | s.pushMetric(fmt.Sprintf("%s:%f|c\n", flatKey, val)) 79 | } 80 | 81 | func (s *StatsdSink) IncrCounterWithLabels(key []string, val float32, labels []Label) { 82 | flatKey := s.flattenKeyLabels(key, labels) 83 | s.pushMetric(fmt.Sprintf("%s:%f|c\n", flatKey, val)) 84 | } 85 | 86 | func (s *StatsdSink) AddSample(key []string, val float32) { 87 | flatKey := s.flattenKey(key) 88 | s.pushMetric(fmt.Sprintf("%s:%f|ms\n", flatKey, val)) 89 | } 90 | 91 | func (s *StatsdSink) AddSampleWithLabels(key []string, val float32, labels []Label) { 92 | flatKey := s.flattenKeyLabels(key, labels) 93 | s.pushMetric(fmt.Sprintf("%s:%f|ms\n", flatKey, val)) 94 | } 95 | 96 | // Flattens the key for formatting, removes spaces 97 | func (s *StatsdSink) flattenKey(parts []string) string { 98 | joined := strings.Join(parts, ".") 99 | return strings.Map(func(r rune) rune { 100 | switch r { 101 | case ':': 102 | fallthrough 103 | case ' ': 104 | return '_' 105 | default: 106 | return r 107 | } 108 | }, joined) 109 | } 110 | 111 | // Flattens the key along with labels for formatting, removes spaces 112 | func (s *StatsdSink) flattenKeyLabels(parts []string, labels []Label) string { 113 | for _, label := range labels { 114 | parts = append(parts, label.Value) 115 | } 116 | return s.flattenKey(parts) 117 | } 118 | 119 | // Does a non-blocking push to the metrics queue 120 | func (s *StatsdSink) pushMetric(m string) { 121 | select { 122 | case s.metricQueue <- m: 123 | default: 124 | } 125 | } 126 | 127 | // Flushes metrics 128 | func (s *StatsdSink) flushMetrics() { 129 | var sock net.Conn 130 | var err error 131 | var wait <-chan time.Time 132 | ticker := time.NewTicker(flushInterval) 133 | defer ticker.Stop() 134 | 135 | CONNECT: 136 | // Create a buffer 137 | buf := bytes.NewBuffer(nil) 138 | 139 | // Attempt to connect 140 | sock, err = net.Dial("udp", s.addr) 141 | if err != nil { 142 | log.Printf("[ERR] Error connecting to statsd! Err: %s", err) 143 | goto WAIT 144 | } 145 | 146 | for { 147 | select { 148 | case metric, ok := <-s.metricQueue: 149 | // Get a metric from the queue 150 | if !ok { 151 | goto QUIT 152 | } 153 | 154 | // Check if this would overflow the packet size 155 | if len(metric)+buf.Len() > statsdMaxLen { 156 | _, err := sock.Write(buf.Bytes()) 157 | buf.Reset() 158 | if err != nil { 159 | log.Printf("[ERR] Error writing to statsd! Err: %s", err) 160 | goto WAIT 161 | } 162 | } 163 | 164 | // Append to the buffer 165 | buf.WriteString(metric) 166 | 167 | case <-ticker.C: 168 | if buf.Len() == 0 { 169 | continue 170 | } 171 | 172 | _, err := sock.Write(buf.Bytes()) 173 | buf.Reset() 174 | if err != nil { 175 | log.Printf("[ERR] Error flushing to statsd! Err: %s", err) 176 | goto WAIT 177 | } 178 | } 179 | } 180 | 181 | WAIT: 182 | // Wait for a while 183 | wait = time.After(time.Duration(5) * time.Second) 184 | for { 185 | select { 186 | // Dequeue the messages to avoid backlog 187 | case _, ok := <-s.metricQueue: 188 | if !ok { 189 | goto QUIT 190 | } 191 | case <-wait: 192 | goto CONNECT 193 | } 194 | } 195 | QUIT: 196 | s.metricQueue = nil 197 | } 198 | -------------------------------------------------------------------------------- /statsd_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | package metrics 5 | 6 | import ( 7 | "bufio" 8 | "bytes" 9 | "net" 10 | "net/url" 11 | "strings" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | func TestStatsd_Flatten(t *testing.T) { 17 | s := &StatsdSink{} 18 | flat := s.flattenKey([]string{"a", "b", "c", "d"}) 19 | if flat != "a.b.c.d" { 20 | t.Fatalf("Bad flat") 21 | } 22 | } 23 | 24 | func TestStatsd_PushFullQueue(t *testing.T) { 25 | q := make(chan string, 1) 26 | q <- "full" 27 | 28 | s := &StatsdSink{metricQueue: q} 29 | s.pushMetric("omit") 30 | 31 | out := <-q 32 | if out != "full" { 33 | t.Fatalf("bad val %v", out) 34 | } 35 | 36 | select { 37 | case v := <-q: 38 | t.Fatalf("bad val %v", v) 39 | default: 40 | } 41 | } 42 | 43 | func TestStatsd_Conn(t *testing.T) { 44 | addr := "127.0.0.1:7524" 45 | done := make(chan bool) 46 | go func() { 47 | list, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 7524}) 48 | if err != nil { 49 | panic(err) 50 | } 51 | defer list.Close() 52 | buf := make([]byte, 1500) 53 | n, err := list.Read(buf) 54 | if err != nil { 55 | panic(err) 56 | } 57 | buf = buf[:n] 58 | reader := bufio.NewReader(bytes.NewReader(buf)) 59 | 60 | line, err := reader.ReadString('\n') 61 | if err != nil { 62 | t.Fatalf("unexpected err %s", err) 63 | } 64 | if line != "gauge.val:1.000000|g\n" { 65 | t.Fatalf("bad line %s", line) 66 | } 67 | 68 | line, err = reader.ReadString('\n') 69 | if err != nil { 70 | t.Fatalf("unexpected err %s", err) 71 | } 72 | if line != "gauge_labels.val.label:2.000000|g\n" { 73 | t.Fatalf("bad line %s", line) 74 | } 75 | 76 | line, err = reader.ReadString('\n') 77 | if err != nil { 78 | t.Fatalf("unexpected err %s", err) 79 | } 80 | if line != "gauge.val:1.000000|g\n" { 81 | t.Fatalf("bad line %s", line) 82 | } 83 | 84 | line, err = reader.ReadString('\n') 85 | if err != nil { 86 | t.Fatalf("unexpected err %s", err) 87 | } 88 | if line != "gauge_labels.val.label:2.000000|g\n" { 89 | t.Fatalf("bad line %s", line) 90 | } 91 | 92 | line, err = reader.ReadString('\n') 93 | if err != nil { 94 | t.Fatalf("unexpected err %s", err) 95 | } 96 | if line != "key.other:3.000000|kv\n" { 97 | t.Fatalf("bad line %s", line) 98 | } 99 | 100 | line, err = reader.ReadString('\n') 101 | if err != nil { 102 | t.Fatalf("unexpected err %s", err) 103 | } 104 | if line != "counter.me:4.000000|c\n" { 105 | t.Fatalf("bad line %s", line) 106 | } 107 | 108 | line, err = reader.ReadString('\n') 109 | if err != nil { 110 | t.Fatalf("unexpected err %s", err) 111 | } 112 | if line != "counter_labels.me.label:5.000000|c\n" { 113 | t.Fatalf("bad line %s", line) 114 | } 115 | 116 | line, err = reader.ReadString('\n') 117 | if err != nil { 118 | t.Fatalf("unexpected err %s", err) 119 | } 120 | if line != "sample.slow_thingy:6.000000|ms\n" { 121 | t.Fatalf("bad line %s", line) 122 | } 123 | 124 | line, err = reader.ReadString('\n') 125 | if err != nil { 126 | t.Fatalf("unexpected err %s", err) 127 | } 128 | if line != "sample_labels.slow_thingy.label:7.000000|ms\n" { 129 | t.Fatalf("bad line %s", line) 130 | } 131 | 132 | done <- true 133 | }() 134 | s, err := NewStatsdSink(addr) 135 | if err != nil { 136 | t.Fatalf("bad error") 137 | } 138 | 139 | s.SetGauge([]string{"gauge", "val"}, float32(1)) 140 | s.SetGaugeWithLabels([]string{"gauge_labels", "val"}, float32(2), []Label{{"a", "label"}}) 141 | s.SetPrecisionGauge([]string{"gauge", "val"}, float64(1)) 142 | s.SetPrecisionGaugeWithLabels([]string{"gauge_labels", "val"}, float64(2), []Label{{"a", "label"}}) 143 | s.EmitKey([]string{"key", "other"}, float32(3)) 144 | s.IncrCounter([]string{"counter", "me"}, float32(4)) 145 | s.IncrCounterWithLabels([]string{"counter_labels", "me"}, float32(5), []Label{{"a", "label"}}) 146 | s.AddSample([]string{"sample", "slow thingy"}, float32(6)) 147 | s.AddSampleWithLabels([]string{"sample_labels", "slow thingy"}, float32(7), []Label{{"a", "label"}}) 148 | 149 | select { 150 | case <-done: 151 | s.Shutdown() 152 | case <-time.After(3 * time.Second): 153 | t.Fatalf("timeout") 154 | } 155 | } 156 | 157 | func TestNewStatsdSinkFromURL(t *testing.T) { 158 | for _, tc := range []struct { 159 | desc string 160 | input string 161 | expectErr string 162 | expectAddr string 163 | }{ 164 | { 165 | desc: "address is populated", 166 | input: "statsd://statsd.service.consul", 167 | expectAddr: "statsd.service.consul", 168 | }, 169 | { 170 | desc: "address includes port", 171 | input: "statsd://statsd.service.consul:1234", 172 | expectAddr: "statsd.service.consul:1234", 173 | }, 174 | } { 175 | t.Run(tc.desc, func(t *testing.T) { 176 | u, err := url.Parse(tc.input) 177 | if err != nil { 178 | t.Fatalf("error parsing URL: %s", err) 179 | } 180 | ms, err := NewStatsdSinkFromURL(u) 181 | if tc.expectErr != "" { 182 | if !strings.Contains(err.Error(), tc.expectErr) { 183 | t.Fatalf("expected err: %q, to contain: %q", err, tc.expectErr) 184 | } 185 | } else { 186 | if err != nil { 187 | t.Fatalf("unexpected err: %s", err) 188 | } 189 | is := ms.(*StatsdSink) 190 | if is.addr != tc.expectAddr { 191 | t.Fatalf("expected addr %s, got: %s", tc.expectAddr, is.addr) 192 | } 193 | } 194 | }) 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /statsite.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | package metrics 5 | 6 | import ( 7 | "bufio" 8 | "fmt" 9 | "log" 10 | "net" 11 | "net/url" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | const ( 17 | // We force flush the statsite metrics after this period of 18 | // inactivity. Prevents stats from getting stuck in a buffer 19 | // forever. 20 | flushInterval = 100 * time.Millisecond 21 | ) 22 | 23 | // NewStatsiteSinkFromURL creates an StatsiteSink from a URL. It is used 24 | // (and tested) from NewMetricSinkFromURL. 25 | func NewStatsiteSinkFromURL(u *url.URL) (MetricSink, error) { 26 | return NewStatsiteSink(u.Host) 27 | } 28 | 29 | // StatsiteSink provides a MetricSink that can be used with a 30 | // statsite metrics server 31 | type StatsiteSink struct { 32 | addr string 33 | metricQueue chan string 34 | } 35 | 36 | // NewStatsiteSink is used to create a new StatsiteSink 37 | func NewStatsiteSink(addr string) (*StatsiteSink, error) { 38 | s := &StatsiteSink{ 39 | addr: addr, 40 | metricQueue: make(chan string, 4096), 41 | } 42 | go s.flushMetrics() 43 | return s, nil 44 | } 45 | 46 | // Close is used to stop flushing to statsite 47 | func (s *StatsiteSink) Shutdown() { 48 | close(s.metricQueue) 49 | } 50 | 51 | func (s *StatsiteSink) SetGauge(key []string, val float32) { 52 | flatKey := s.flattenKey(key) 53 | s.pushMetric(fmt.Sprintf("%s:%f|g\n", flatKey, val)) 54 | } 55 | 56 | func (s *StatsiteSink) SetGaugeWithLabels(key []string, val float32, labels []Label) { 57 | flatKey := s.flattenKeyLabels(key, labels) 58 | s.pushMetric(fmt.Sprintf("%s:%f|g\n", flatKey, val)) 59 | } 60 | 61 | func (s *StatsiteSink) SetPrecisionGauge(key []string, val float64) { 62 | flatKey := s.flattenKey(key) 63 | s.pushMetric(fmt.Sprintf("%s:%f|g\n", flatKey, val)) 64 | } 65 | 66 | func (s *StatsiteSink) SetPrecisionGaugeWithLabels(key []string, val float64, labels []Label) { 67 | flatKey := s.flattenKeyLabels(key, labels) 68 | s.pushMetric(fmt.Sprintf("%s:%f|g\n", flatKey, val)) 69 | } 70 | 71 | func (s *StatsiteSink) EmitKey(key []string, val float32) { 72 | flatKey := s.flattenKey(key) 73 | s.pushMetric(fmt.Sprintf("%s:%f|kv\n", flatKey, val)) 74 | } 75 | 76 | func (s *StatsiteSink) IncrCounter(key []string, val float32) { 77 | flatKey := s.flattenKey(key) 78 | s.pushMetric(fmt.Sprintf("%s:%f|c\n", flatKey, val)) 79 | } 80 | 81 | func (s *StatsiteSink) IncrCounterWithLabels(key []string, val float32, labels []Label) { 82 | flatKey := s.flattenKeyLabels(key, labels) 83 | s.pushMetric(fmt.Sprintf("%s:%f|c\n", flatKey, val)) 84 | } 85 | 86 | func (s *StatsiteSink) AddSample(key []string, val float32) { 87 | flatKey := s.flattenKey(key) 88 | s.pushMetric(fmt.Sprintf("%s:%f|ms\n", flatKey, val)) 89 | } 90 | 91 | func (s *StatsiteSink) AddSampleWithLabels(key []string, val float32, labels []Label) { 92 | flatKey := s.flattenKeyLabels(key, labels) 93 | s.pushMetric(fmt.Sprintf("%s:%f|ms\n", flatKey, val)) 94 | } 95 | 96 | // Flattens the key for formatting, removes spaces 97 | func (s *StatsiteSink) flattenKey(parts []string) string { 98 | joined := strings.Join(parts, ".") 99 | return strings.Map(func(r rune) rune { 100 | switch r { 101 | case ':': 102 | fallthrough 103 | case ' ': 104 | return '_' 105 | default: 106 | return r 107 | } 108 | }, joined) 109 | } 110 | 111 | // Flattens the key along with labels for formatting, removes spaces 112 | func (s *StatsiteSink) flattenKeyLabels(parts []string, labels []Label) string { 113 | for _, label := range labels { 114 | parts = append(parts, label.Value) 115 | } 116 | return s.flattenKey(parts) 117 | } 118 | 119 | // Does a non-blocking push to the metrics queue 120 | func (s *StatsiteSink) pushMetric(m string) { 121 | select { 122 | case s.metricQueue <- m: 123 | default: 124 | } 125 | } 126 | 127 | // Flushes metrics 128 | func (s *StatsiteSink) flushMetrics() { 129 | var sock net.Conn 130 | var err error 131 | var wait <-chan time.Time 132 | var buffered *bufio.Writer 133 | ticker := time.NewTicker(flushInterval) 134 | defer ticker.Stop() 135 | 136 | CONNECT: 137 | // Attempt to connect 138 | sock, err = net.Dial("tcp", s.addr) 139 | if err != nil { 140 | log.Printf("[ERR] Error connecting to statsite! Err: %s", err) 141 | goto WAIT 142 | } 143 | 144 | // Create a buffered writer 145 | buffered = bufio.NewWriter(sock) 146 | 147 | for { 148 | select { 149 | case metric, ok := <-s.metricQueue: 150 | // Get a metric from the queue 151 | if !ok { 152 | goto QUIT 153 | } 154 | 155 | // Try to send to statsite 156 | _, err := buffered.Write([]byte(metric)) 157 | if err != nil { 158 | log.Printf("[ERR] Error writing to statsite! Err: %s", err) 159 | goto WAIT 160 | } 161 | case <-ticker.C: 162 | if err := buffered.Flush(); err != nil { 163 | log.Printf("[ERR] Error flushing to statsite! Err: %s", err) 164 | goto WAIT 165 | } 166 | } 167 | } 168 | 169 | WAIT: 170 | // Wait for a while 171 | wait = time.After(time.Duration(5) * time.Second) 172 | for { 173 | select { 174 | // Dequeue the messages to avoid backlog 175 | case _, ok := <-s.metricQueue: 176 | if !ok { 177 | goto QUIT 178 | } 179 | case <-wait: 180 | goto CONNECT 181 | } 182 | } 183 | QUIT: 184 | s.metricQueue = nil 185 | } 186 | -------------------------------------------------------------------------------- /statsite_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MIT 3 | 4 | package metrics 5 | 6 | import ( 7 | "bufio" 8 | "net" 9 | "net/url" 10 | "strings" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | func TestStatsite_Flatten(t *testing.T) { 16 | s := &StatsiteSink{} 17 | flat := s.flattenKey([]string{"a", "b", "c", "d"}) 18 | if flat != "a.b.c.d" { 19 | t.Fatalf("Bad flat") 20 | } 21 | } 22 | 23 | func TestStatsite_PushFullQueue(t *testing.T) { 24 | q := make(chan string, 1) 25 | q <- "full" 26 | 27 | s := &StatsiteSink{metricQueue: q} 28 | s.pushMetric("omit") 29 | 30 | out := <-q 31 | if out != "full" { 32 | t.Fatalf("bad val %v", out) 33 | } 34 | 35 | select { 36 | case v := <-q: 37 | t.Fatalf("bad val %v", v) 38 | default: 39 | } 40 | } 41 | 42 | func TestStatsite_Conn(t *testing.T) { 43 | addr := "localhost:7523" 44 | 45 | ln, _ := net.Listen("tcp", addr) 46 | 47 | done := make(chan bool) 48 | go func() { 49 | conn, err := ln.Accept() 50 | if err != nil { 51 | t.Fatalf("unexpected err %s", err) 52 | } 53 | 54 | reader := bufio.NewReader(conn) 55 | 56 | line, err := reader.ReadString('\n') 57 | if err != nil { 58 | t.Fatalf("unexpected err %s", err) 59 | } 60 | if line != "gauge.val:1.000000|g\n" { 61 | t.Fatalf("bad line %s", line) 62 | } 63 | 64 | line, err = reader.ReadString('\n') 65 | if err != nil { 66 | t.Fatalf("unexpected err %s", err) 67 | } 68 | if line != "gauge_labels.val.label:2.000000|g\n" { 69 | t.Fatalf("bad line %s", line) 70 | } 71 | 72 | line, err = reader.ReadString('\n') 73 | if err != nil { 74 | t.Fatalf("unexpected err %s", err) 75 | } 76 | if line != "gauge.val:1.000000|g\n" { 77 | t.Fatalf("bad line %s", line) 78 | } 79 | 80 | line, err = reader.ReadString('\n') 81 | if err != nil { 82 | t.Fatalf("unexpected err %s", err) 83 | } 84 | if line != "gauge_labels.val.label:2.000000|g\n" { 85 | t.Fatalf("bad line %s", line) 86 | } 87 | 88 | line, err = reader.ReadString('\n') 89 | if err != nil { 90 | t.Fatalf("unexpected err %s", err) 91 | } 92 | if line != "key.other:3.000000|kv\n" { 93 | t.Fatalf("bad line %s", line) 94 | } 95 | 96 | line, err = reader.ReadString('\n') 97 | if err != nil { 98 | t.Fatalf("unexpected err %s", err) 99 | } 100 | if line != "counter.me:4.000000|c\n" { 101 | t.Fatalf("bad line %s", line) 102 | } 103 | 104 | line, err = reader.ReadString('\n') 105 | if err != nil { 106 | t.Fatalf("unexpected err %s", err) 107 | } 108 | if line != "counter_labels.me.label:5.000000|c\n" { 109 | t.Fatalf("bad line %s", line) 110 | } 111 | 112 | line, err = reader.ReadString('\n') 113 | if err != nil { 114 | t.Fatalf("unexpected err %s", err) 115 | } 116 | if line != "sample.slow_thingy:6.000000|ms\n" { 117 | t.Fatalf("bad line %s", line) 118 | } 119 | 120 | line, err = reader.ReadString('\n') 121 | if err != nil { 122 | t.Fatalf("unexpected err %s", err) 123 | } 124 | if line != "sample_labels.slow_thingy.label:7.000000|ms\n" { 125 | t.Fatalf("bad line %s", line) 126 | } 127 | 128 | conn.Close() 129 | done <- true 130 | }() 131 | s, err := NewStatsiteSink(addr) 132 | if err != nil { 133 | t.Fatalf("bad error") 134 | } 135 | 136 | s.SetGauge([]string{"gauge", "val"}, float32(1)) 137 | s.SetGaugeWithLabels([]string{"gauge_labels", "val"}, float32(2), []Label{{"a", "label"}}) 138 | s.SetPrecisionGauge([]string{"gauge", "val"}, float64(1)) 139 | s.SetPrecisionGaugeWithLabels([]string{"gauge_labels", "val"}, float64(2), []Label{{"a", "label"}}) 140 | s.EmitKey([]string{"key", "other"}, float32(3)) 141 | s.IncrCounter([]string{"counter", "me"}, float32(4)) 142 | s.IncrCounterWithLabels([]string{"counter_labels", "me"}, float32(5), []Label{{"a", "label"}}) 143 | s.AddSample([]string{"sample", "slow thingy"}, float32(6)) 144 | s.AddSampleWithLabels([]string{"sample_labels", "slow thingy"}, float32(7), []Label{{"a", "label"}}) 145 | 146 | select { 147 | case <-done: 148 | s.Shutdown() 149 | case <-time.After(3 * time.Second): 150 | t.Fatalf("timeout") 151 | } 152 | } 153 | 154 | func TestNewStatsiteSinkFromURL(t *testing.T) { 155 | for _, tc := range []struct { 156 | desc string 157 | input string 158 | expectErr string 159 | expectAddr string 160 | }{ 161 | { 162 | desc: "address is populated", 163 | input: "statsd://statsd.service.consul", 164 | expectAddr: "statsd.service.consul", 165 | }, 166 | { 167 | desc: "address includes port", 168 | input: "statsd://statsd.service.consul:1234", 169 | expectAddr: "statsd.service.consul:1234", 170 | }, 171 | } { 172 | t.Run(tc.desc, func(t *testing.T) { 173 | u, err := url.Parse(tc.input) 174 | if err != nil { 175 | t.Fatalf("error parsing URL: %s", err) 176 | } 177 | ms, err := NewStatsiteSinkFromURL(u) 178 | if tc.expectErr != "" { 179 | if !strings.Contains(err.Error(), tc.expectErr) { 180 | t.Fatalf("expected err: %q, to contain: %q", err, tc.expectErr) 181 | } 182 | } else { 183 | if err != nil { 184 | t.Fatalf("unexpected err: %s", err) 185 | } 186 | is := ms.(*StatsiteSink) 187 | if is.addr != tc.expectAddr { 188 | t.Fatalf("expected addr %s, got: %s", tc.expectAddr, is.addr) 189 | } 190 | } 191 | }) 192 | } 193 | } 194 | --------------------------------------------------------------------------------