├── .gitignore ├── go.mod ├── .github └── workflows │ ├── golangci-lint.yml │ └── codecov.yml ├── tools.go ├── LICENSE ├── submit_test.go ├── text.go ├── checkmgr ├── cert_test.go ├── cert.go ├── metrics.go ├── broker.go ├── metrics_test.go ├── checkmgr_test.go ├── check.go └── broker_test.go ├── counter.go ├── tags_test.go ├── go.sum ├── gauge.go ├── CHANGELOG.md ├── tags.go ├── histogram.go ├── text_test.go ├── README.md ├── submit.go ├── circonus-gometrics_test.go ├── OPTIONS.md ├── circonus-gometrics.go ├── counter_test.go ├── .golangci.yml ├── metric_output.go ├── histogram_test.go └── gauge_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | env.sh 3 | NOTES.md 4 | 5 | # codecov.io 6 | .codecov 7 | coverage.txt 8 | coverage.xml 9 | coverage.html 10 | 11 | vendor/ 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/circonus-labs/circonus-gometrics/v3 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/circonus-labs/go-apiclient v0.7.15 7 | github.com/hashicorp/go-retryablehttp v0.7.7 8 | github.com/openhistogram/circonusllhist v0.3.0 9 | github.com/pkg/errors v0.9.1 10 | github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c 11 | ) 12 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - v3 8 | pull_request: 9 | jobs: 10 | golangci: 11 | name: lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: golangci-lint 16 | uses: golangci/golangci-lint-action@v2 17 | with: 18 | version: latest 19 | args: --timeout=5m 20 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: Test and coverage 2 | 3 | on: 4 | pull_request: 5 | branches: ['v3'] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Run coverage 13 | run: go test ./... -race -coverprofile=coverage.out -covermode=atomic 14 | - name: Upload coverage to Codecov 15 | if: success() || failure() 16 | uses: codecov/codecov-action@v3 17 | with: 18 | token: ${{ secrets.CODECOV_TOKEN }} 19 | fail_ci_if_error: false 20 | files: ./coverage.out 21 | move_coverage_to_trash: true 22 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Circonus, Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package circonusgometrics 6 | 7 | import ( 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | // TrackHTTPLatency wraps Handler functions registered with an http.ServerMux tracking latencies. 13 | // Metrics are of the for go`HTTP```latency and are tracked in a histogram in units 14 | // of seconds (as a float64) providing nanosecond ganularity. 15 | func (m *CirconusMetrics) TrackHTTPLatency(name string, handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { 16 | return func(rw http.ResponseWriter, req *http.Request) { 17 | start := time.Now().UnixNano() 18 | handler(rw, req) 19 | elapsed := time.Now().UnixNano() - start 20 | m.RecordValue("go`HTTP`"+req.Method+"`"+name+"`latency", float64(elapsed)/float64(time.Second)) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Circonus, Inc. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following 11 | disclaimer in the documentation and/or other materials provided 12 | with the distribution. 13 | * Neither the name Circonus, Inc. nor the names 14 | of its contributors may be used to endorse or promote products 15 | derived from this software without specific prior written 16 | permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /submit_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Circonus, Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package circonusgometrics 6 | 7 | import ( 8 | "encoding/json" 9 | "fmt" 10 | "net/http" 11 | "net/http/httptest" 12 | "testing" 13 | "time" 14 | 15 | apiclient "github.com/circonus-labs/go-apiclient" 16 | ) 17 | 18 | func fakeBroker() *httptest.Server { 19 | handler := func(w http.ResponseWriter, r *http.Request) { 20 | w.WriteHeader(200) 21 | w.Header().Set("Content-Type", "application/json") 22 | fmt.Fprintln(w, `{"stats":1}`) 23 | } 24 | 25 | return httptest.NewServer(http.HandlerFunc(handler)) 26 | } 27 | 28 | func TestSubmit(t *testing.T) { 29 | t.Log("Testing submit.submit") 30 | 31 | server := fakeBroker() 32 | defer server.Close() 33 | 34 | cfg := &Config{} 35 | cfg.CheckManager.Check.SubmissionURL = server.URL 36 | 37 | cm, err := NewCirconusMetrics(cfg) 38 | if err != nil { 39 | t.Fatalf("unexpected error (%s)", err) 40 | } 41 | 42 | newMetrics := make(map[string]*apiclient.CheckBundleMetric) 43 | output := Metrics{"foo": Metric{Type: "n", Value: 1}} 44 | // output["foo"] = map[string]interface{}{ 45 | // "_type": "n", 46 | // "_value": 1, 47 | // } 48 | cm.submit(output, newMetrics) 49 | } 50 | 51 | func TestTrapCall(t *testing.T) { 52 | t.Log("Testing submit.trapCall") 53 | 54 | server := fakeBroker() 55 | defer server.Close() 56 | 57 | cfg := &Config{} 58 | cfg.CheckManager.Check.SubmissionURL = server.URL 59 | 60 | cm, err := NewCirconusMetrics(cfg) 61 | if err != nil { 62 | t.Fatalf("unexpected error (%s)", err) 63 | } 64 | 65 | for !cm.check.IsReady() { 66 | t.Log("\twaiting for cm to init") 67 | time.Sleep(1 * time.Second) 68 | } 69 | 70 | output := make(map[string]interface{}) 71 | output["foo"] = map[string]interface{}{ 72 | "_type": "n", 73 | "_value": 1, 74 | } 75 | 76 | str, err := json.Marshal(output) 77 | if err != nil { 78 | t.Fatalf("unexpected error (%s)", err) 79 | } 80 | 81 | result, err := cm.trapCall(str) 82 | if err != nil { 83 | t.Fatalf("unexpected error (%s)", err) 84 | } 85 | 86 | if result.Stats != 1 { 87 | t.Fatalf("Expected 1, got %#v", result) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /text.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Circonus, Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package circonusgometrics 6 | 7 | // A Text metric is an arbitrary string 8 | // 9 | 10 | // SetTextWithTags sets a text metric with tags 11 | func (m *CirconusMetrics) SetTextWithTags(metric string, tags Tags, val string) { 12 | m.SetTextValueWithTags(metric, tags, val) 13 | } 14 | 15 | // SetText sets a text metric 16 | func (m *CirconusMetrics) SetText(metric string, val string) { 17 | m.SetTextValue(metric, val) 18 | } 19 | 20 | // SetTextValueWithTags sets a text metric with tags 21 | func (m *CirconusMetrics) SetTextValueWithTags(metric string, tags Tags, val string) { 22 | m.SetTextValue(m.MetricNameWithStreamTags(metric, tags), val) 23 | } 24 | 25 | // SetTextValue sets a text metric 26 | func (m *CirconusMetrics) SetTextValue(metric string, val string) { 27 | m.tm.Lock() 28 | defer m.tm.Unlock() 29 | m.text[metric] = val 30 | } 31 | 32 | // RemoveTextWithTags removes a text metric with tags 33 | func (m *CirconusMetrics) RemoveTextWithTags(metric string, tags Tags) { 34 | m.RemoveText(m.MetricNameWithStreamTags(metric, tags)) 35 | } 36 | 37 | // RemoveText removes a text metric 38 | func (m *CirconusMetrics) RemoveText(metric string) { 39 | m.tm.Lock() 40 | defer m.tm.Unlock() 41 | delete(m.text, metric) 42 | } 43 | 44 | // SetTextFuncWithTags sets a text metric with tags to a function [called at flush interval] 45 | func (m *CirconusMetrics) SetTextFuncWithTags(metric string, tags Tags, fn func() string) { 46 | m.SetTextFunc(m.MetricNameWithStreamTags(metric, tags), fn) 47 | } 48 | 49 | // SetTextFunc sets a text metric to a function [called at flush interval] 50 | func (m *CirconusMetrics) SetTextFunc(metric string, fn func() string) { 51 | m.tfm.Lock() 52 | defer m.tfm.Unlock() 53 | m.textFuncs[metric] = fn 54 | } 55 | 56 | // RemoveTextFuncWithTags removes a text metric with tags function 57 | func (m *CirconusMetrics) RemoveTextFuncWithTags(metric string, tags Tags) { 58 | m.RemoveTextFunc(m.MetricNameWithStreamTags(metric, tags)) 59 | } 60 | 61 | // RemoveTextFunc a text metric function 62 | func (m *CirconusMetrics) RemoveTextFunc(metric string) { 63 | m.tfm.Lock() 64 | defer m.tfm.Unlock() 65 | delete(m.textFuncs, metric) 66 | } 67 | -------------------------------------------------------------------------------- /checkmgr/cert_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Circonus, Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package checkmgr 6 | 7 | import ( 8 | "encoding/json" 9 | "fmt" 10 | "io/ioutil" 11 | "log" 12 | "net/http" 13 | "net/http/httptest" 14 | "testing" 15 | 16 | apiclient "github.com/circonus-labs/go-apiclient" 17 | ) 18 | 19 | var ( 20 | apiCert = CACert{ 21 | Contents: string(circonusCA), 22 | } 23 | ) 24 | 25 | func testCertServer() *httptest.Server { 26 | f := func(w http.ResponseWriter, r *http.Request) { 27 | switch r.URL.Path { 28 | case "/pki/ca.crt": 29 | ret, err := json.Marshal(apiCert) 30 | if err != nil { 31 | panic(err) 32 | } 33 | w.WriteHeader(200) 34 | w.Header().Set("Content-Type", "application/json") 35 | fmt.Fprintln(w, string(ret)) 36 | default: 37 | w.WriteHeader(500) 38 | fmt.Fprintln(w, "unsupported") 39 | } 40 | } 41 | 42 | return httptest.NewServer(http.HandlerFunc(f)) 43 | } 44 | 45 | func TestLoadCACert(t *testing.T) { 46 | t.Log("default cert, no fetch") 47 | 48 | cm := &CheckManager{ 49 | enabled: false, 50 | } 51 | 52 | if err := cm.loadCACert(); err != nil { 53 | t.Fatalf("expected no error got (%v)", err) 54 | } 55 | 56 | if cm.certPool == nil { 57 | t.Errorf("Expected cert pool to be initialized, still nil.") 58 | } 59 | 60 | subjs := cm.certPool.Subjects() 61 | if len(subjs) == 0 { 62 | t.Errorf("Expected > 0 certs in pool") 63 | } 64 | } 65 | 66 | func TestFetchCert(t *testing.T) { 67 | server := testCertServer() 68 | defer server.Close() 69 | 70 | cm := &CheckManager{ 71 | enabled: true, 72 | Log: log.New(ioutil.Discard, "", log.LstdFlags), 73 | } 74 | ac := &apiclient.Config{ 75 | TokenApp: "abcd", 76 | TokenKey: "1234", 77 | URL: server.URL, 78 | } 79 | apih, err := apiclient.NewAPI(ac) 80 | if err != nil { 81 | t.Errorf("Expected no error, got '%v'", err) 82 | } 83 | cm.apih = apih 84 | 85 | _, err = cm.fetchCert() 86 | if err != nil { 87 | t.Fatalf("Expected no error, got %v", err) 88 | } 89 | 90 | t.Log("load cert w/fetch") 91 | 92 | if err := cm.loadCACert(); err != nil { 93 | t.Fatalf("expexted no error, got (%v)", err) 94 | } 95 | 96 | if cm.certPool == nil { 97 | t.Errorf("Expected cert pool to be initialized, still nil.") 98 | } 99 | 100 | subjs := cm.certPool.Subjects() 101 | if len(subjs) == 0 { 102 | t.Errorf("Expected > 0 certs in pool") 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /checkmgr/cert.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Circonus, Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package checkmgr 6 | 7 | import ( 8 | "crypto/x509" 9 | "encoding/json" 10 | 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | // Default Circonus CA certificate 15 | var circonusCA = []byte(`-----BEGIN CERTIFICATE----- 16 | MIIE6zCCA9OgAwIBAgIJALY0C6uznIh+MA0GCSqGSIb3DQEBCwUAMIGpMQswCQYD 17 | VQQGEwJVUzERMA8GA1UECBMITWFyeWxhbmQxDzANBgNVBAcTBkZ1bHRvbjEXMBUG 18 | A1UEChMOQ2lyY29udXMsIEluYy4xETAPBgNVBAsTCENpcmNvbnVzMSowKAYDVQQD 19 | EyFDaXJjb251cyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgRzIxHjAcBgkqhkiG9w0B 20 | CQEWD2NhQGNpcmNvbnVzLm5ldDAeFw0xOTEyMDYyMDAzMzdaFw0zOTEyMDYyMDAz 21 | MzdaMIGpMQswCQYDVQQGEwJVUzERMA8GA1UECBMITWFyeWxhbmQxDzANBgNVBAcT 22 | BkZ1bHRvbjEXMBUGA1UEChMOQ2lyY29udXMsIEluYy4xETAPBgNVBAsTCENpcmNv 23 | bnVzMSowKAYDVQQDEyFDaXJjb251cyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgRzIx 24 | HjAcBgkqhkiG9w0BCQEWD2NhQGNpcmNvbnVzLm5ldDCCASIwDQYJKoZIhvcNAQEB 25 | BQADggEPADCCAQoCggEBAK9oN6wBfBgjRYKBbL0Hllcr9TR2e0wIDGhk15Ltym32 26 | zkndEcNKoz61BBJZGalPYDQ8khGQEJAHF6jE/q+qPFHA7vMoIll0frD/C8MM09PK 27 | wvvw+HfnRLjnAWwmefDsE+zhdXlOMnsRPPmMHOCYw0RYe4z8Zna3Jl57zZt8zlKh 28 | FnWRsZg8zc5dFQsAteu2vV+ZSYXUZyj2IgmqaeKgjyUL09ByBKH+weS0ICXiIS51 29 | 8lEmofj87ceBMRJHjIwnFr9dRvj3YU/DZVL8NVy91jBHPw9PhLV8XQRh6oQXkrSr 30 | vlcs3NN2FNqWIfZmL6g8/OCCXr3oFgotumGUc7H/cS0CAwEAAaOCARIwggEOMB0G 31 | A1UdDgQWBBRk0xgZQ17grBWWZbRRTzZfqlAd4zCB3gYDVR0jBIHWMIHTgBRk0xgZ 32 | Q17grBWWZbRRTzZfqlAd46GBr6SBrDCBqTELMAkGA1UEBhMCVVMxETAPBgNVBAgT 33 | CE1hcnlsYW5kMQ8wDQYDVQQHEwZGdWx0b24xFzAVBgNVBAoTDkNpcmNvbnVzLCBJ 34 | bmMuMREwDwYDVQQLEwhDaXJjb251czEqMCgGA1UEAxMhQ2lyY29udXMgQ2VydGlm 35 | aWNhdGUgQXV0aG9yaXR5IEcyMR4wHAYJKoZIhvcNAQkBFg9jYUBjaXJjb251cy5u 36 | ZXSCCQC2NAurs5yIfjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCq 37 | 9yqOHBWeP65jUnr+pn5nf9+dJhIQ/zgEiIygUwJoSo0+OG1fwfXEeQMQdrYJlTfT 38 | LLgAlK/lJ0fXfS4ruMwyOnH5/2UTrh2eE1u8xToKg7afbaIoO/sg002f3qod1MRx 39 | JYPppNW16wG4kaBKOXJY6LzqXeaStCFotrer5Wt4tl/xOaVav1lmdXC8V3vUtoMJ 40 | FasyBc3tBlgKRJ0f2ijD+P6vEie4w8gJMSurqqKskiY+2zuNzClki0bqCi06m0lt 41 | TESkwBQfV80GJXyz4kTQIZgGnwLcNE9GOlihWX2axTpW7RwpX25lOaMtu+vZtao/ 42 | yQRBN07uOh4gEhJIngzr 43 | -----END CERTIFICATE-----`) 44 | 45 | // CACert contains cert returned from Circonus API 46 | type CACert struct { 47 | Contents string `json:"contents"` 48 | } 49 | 50 | // loadCACert loads the CA cert for the broker designated by the submission url 51 | func (cm *CheckManager) loadCACert() error { 52 | if cm.certPool != nil { 53 | return nil 54 | } 55 | 56 | if cm.brokerTLS != nil { 57 | cm.certPool = cm.brokerTLS.RootCAs 58 | return nil 59 | } 60 | 61 | cm.certPool = x509.NewCertPool() 62 | 63 | var cert []byte 64 | var err error 65 | 66 | if cm.enabled { 67 | // only attempt to retrieve broker CA cert if 68 | // the check is being managed. 69 | cert, err = cm.fetchCert() 70 | if err != nil { 71 | return err 72 | } 73 | } 74 | 75 | if cert == nil { 76 | cert = circonusCA 77 | } 78 | 79 | cm.certPool.AppendCertsFromPEM(cert) 80 | 81 | return nil 82 | } 83 | 84 | // fetchCert fetches CA certificate using Circonus API 85 | func (cm *CheckManager) fetchCert() ([]byte, error) { 86 | if !cm.enabled { 87 | return nil, errors.New("check manager is not enabled") 88 | } 89 | 90 | cm.Log.Printf("fetching broker cert from api") 91 | 92 | response, err := cm.apih.Get("/pki/ca.crt") 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | cadata := new(CACert) 98 | if err := json.Unmarshal(response, cadata); err != nil { 99 | return nil, err 100 | } 101 | 102 | if cadata.Contents == "" { 103 | return nil, errors.Errorf("error, unable to find ca cert %+v", cadata) 104 | } 105 | 106 | return []byte(cadata.Contents), nil 107 | } 108 | -------------------------------------------------------------------------------- /counter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Circonus, Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package circonusgometrics 6 | 7 | import "github.com/pkg/errors" 8 | 9 | // A Counter is a monotonically increasing unsigned integer. 10 | // 11 | // Use a counter to derive rates (e.g., record total number of requests, derive 12 | // requests per second). 13 | 14 | // IncrementWithTags counter by 1, with tags 15 | func (m *CirconusMetrics) IncrementWithTags(metric string, tags Tags) { 16 | m.AddWithTags(metric, tags, 1) 17 | } 18 | 19 | // Increment counter by 1 20 | func (m *CirconusMetrics) Increment(metric string) { 21 | m.Add(metric, 1) 22 | } 23 | 24 | // IncrementByValueWithTags updates counter metric with tags by supplied value 25 | func (m *CirconusMetrics) IncrementByValueWithTags(metric string, tags Tags, val uint64) { 26 | m.AddWithTags(metric, tags, val) 27 | } 28 | 29 | // IncrementByValue updates counter by supplied value 30 | func (m *CirconusMetrics) IncrementByValue(metric string, val uint64) { 31 | m.Add(metric, val) 32 | } 33 | 34 | // SetWithTags sets a counter metric with tags to specific value 35 | func (m *CirconusMetrics) SetWithTags(metric string, tags Tags, val uint64) { 36 | m.Set(m.MetricNameWithStreamTags(metric, tags), val) 37 | } 38 | 39 | // Set a counter to specific value 40 | func (m *CirconusMetrics) Set(metric string, val uint64) { 41 | m.cm.Lock() 42 | defer m.cm.Unlock() 43 | m.counters[metric] = val 44 | } 45 | 46 | // AddWithTags updates counter metric with tags by supplied value 47 | func (m *CirconusMetrics) AddWithTags(metric string, tags Tags, val uint64) { 48 | m.Add(m.MetricNameWithStreamTags(metric, tags), val) 49 | } 50 | 51 | // Add updates counter by supplied value 52 | func (m *CirconusMetrics) Add(metric string, val uint64) { 53 | m.cm.Lock() 54 | defer m.cm.Unlock() 55 | m.counters[metric] += val 56 | } 57 | 58 | // RemoveCounterWithTags removes the named counter metric with tags 59 | func (m *CirconusMetrics) RemoveCounterWithTags(metric string, tags Tags) { 60 | m.RemoveCounter(m.MetricNameWithStreamTags(metric, tags)) 61 | } 62 | 63 | // RemoveCounter removes the named counter 64 | func (m *CirconusMetrics) RemoveCounter(metric string) { 65 | m.cm.Lock() 66 | defer m.cm.Unlock() 67 | delete(m.counters, metric) 68 | } 69 | 70 | // GetCounterTest returns the current value for a counter. (note: it is a function specifically for "testing", disable automatic submission during testing.) 71 | func (m *CirconusMetrics) GetCounterTest(metric string) (uint64, error) { 72 | m.cm.Lock() 73 | defer m.cm.Unlock() 74 | 75 | if val, ok := m.counters[metric]; ok { 76 | return val, nil 77 | } 78 | 79 | return 0, errors.Errorf("counter metric '%s' not found", metric) 80 | 81 | } 82 | 83 | // SetCounterFuncWithTags set counter metric with tags to a function [called at flush interval] 84 | func (m *CirconusMetrics) SetCounterFuncWithTags(metric string, tags Tags, fn func() uint64) { 85 | m.SetCounterFunc(m.MetricNameWithStreamTags(metric, tags), fn) 86 | } 87 | 88 | // SetCounterFunc set counter to a function [called at flush interval] 89 | func (m *CirconusMetrics) SetCounterFunc(metric string, fn func() uint64) { 90 | m.cfm.Lock() 91 | defer m.cfm.Unlock() 92 | m.counterFuncs[metric] = fn 93 | } 94 | 95 | // RemoveCounterFuncWithTags removes the named counter metric function with tags 96 | func (m *CirconusMetrics) RemoveCounterFuncWithTags(metric string, tags Tags) { 97 | m.RemoveCounterFunc(m.MetricNameWithStreamTags(metric, tags)) 98 | } 99 | 100 | // RemoveCounterFunc removes the named counter function 101 | func (m *CirconusMetrics) RemoveCounterFunc(metric string) { 102 | m.cfm.Lock() 103 | defer m.cfm.Unlock() 104 | delete(m.counterFuncs, metric) 105 | } 106 | -------------------------------------------------------------------------------- /tags_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Circonus, Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Tags helper functions 6 | 7 | package circonusgometrics 8 | 9 | import ( 10 | "encoding/base64" 11 | "fmt" 12 | "regexp" 13 | "strings" 14 | "testing" 15 | ) 16 | 17 | func TestEncodeMetricTags(t *testing.T) { 18 | inputTags := Tags{ 19 | {"cat1", "val1"}, 20 | {"cat2", "val2"}, 21 | {"cat2", "val1"}, 22 | {"cat3", "compound:val"}, 23 | } 24 | expectTags := Tags{ 25 | {"cat1", "val1"}, 26 | {"cat2", "val1"}, 27 | {"cat2", "val2"}, 28 | {"cat3", "compound:val"}, 29 | } 30 | 31 | cm := CirconusMetrics{} 32 | tl := cm.EncodeMetricTags("test", inputTags) 33 | if len(tl) != len(expectTags) { 34 | t.Fatalf("expected %d tags, got %d", len(expectTags), len(tl)) 35 | } 36 | for idx, tag := range tl { 37 | otag := fmt.Sprintf("%s:%s", expectTags[idx].Category, expectTags[idx].Value) 38 | if tag != otag { 39 | t.Fatalf("expected '%s' got '%s'", otag, tag) 40 | } 41 | } 42 | } 43 | 44 | func TestEncodeMetricStreamTags(t *testing.T) { 45 | inputTags := Tags{ 46 | {"cat1", "val1"}, 47 | {"cat1", "val2"}, 48 | {"cat2", "val2"}, 49 | {"cat2", "val1"}, // should be sorted above previous one 50 | {"cat3", fmt.Sprintf(`b"%s"`, base64.StdEncoding.EncodeToString([]byte("bar")))}, // manually base64 encoded and formatted (e.g. `b"base64encodedstr"`), do not double encode 51 | {"cat3", "compound:val"}, 52 | {"cat4", ""}, 53 | {"cat 1", "val2"}, // should have space removed and then be deduplicated 54 | {"cat2", "val1"}, // duplicate should be omitted 55 | } 56 | expectTags := Tags{ 57 | {"cat1", "val1"}, 58 | {"cat1", "val2"}, 59 | {"cat2", "val1"}, 60 | {"cat2", "val2"}, 61 | {"cat3", "bar"}, 62 | {"cat3", "compound:val"}, 63 | {"cat4", ""}, 64 | } 65 | 66 | t.Logf("tags: %v\n", inputTags) 67 | // expect ts to be in format b"b64cat":b"b64val",... 68 | cm := CirconusMetrics{} 69 | ts := cm.EncodeMetricStreamTags("test", inputTags) 70 | tl := strings.Split(ts, ",") 71 | if len(tl) != len(expectTags) { 72 | t.Fatalf("expected %d tags, got %d", len(expectTags), len(tl)) 73 | } 74 | rx := regexp.MustCompile(`^b"(?P[^"]+)":(b"(?P[^"]+)")?$`) 75 | for id, tag := range tl { 76 | // t.Logf("%d = %s -- %s", id, tag, inputTags[id]) 77 | matches := rx.FindStringSubmatch(string(tag)) 78 | if len(matches) < 2 { 79 | t.Fatalf("tag did not match (%s)", tag) 80 | } 81 | result := make(map[string]string) 82 | for i, name := range rx.SubexpNames() { 83 | if i != 0 && name != "" { 84 | result[name] = matches[i] 85 | } 86 | } 87 | if cat, found := result["cat"]; !found { //nolint:gocritic 88 | t.Fatalf("category: named match not found '%s'", cat) 89 | } else if cat == "" { 90 | t.Fatalf("category: invalid (empty) '%s'", cat) 91 | } else { 92 | if dcat, err := base64.StdEncoding.DecodeString(cat); err != nil { 93 | t.Fatalf("category: error decoding base64 '%s' (%s)", cat, err) 94 | } else if string(dcat) != expectTags[id].Category { 95 | t.Fatalf("category: expected '%s' got '%s'->'%s'", expectTags[id].Category, cat, string(dcat)) 96 | } 97 | } 98 | 99 | if val, found := result["val"]; !found { 100 | t.Fatalf("value: named match not found '%s'", val) 101 | // category only tags are acceptable with streamtags 102 | // } else if val == "" && t.cat != "emptyok" { 103 | // t.Fatalf("value: invalid (empty) '%s'", val) 104 | } else if val != "" { 105 | if dval, err := base64.StdEncoding.DecodeString(val); err != nil { 106 | t.Fatalf("value: error decoding base64 '%s' (%s)", val, err) 107 | } else if string(dval) != expectTags[id].Value { 108 | t.Fatalf("value: expected '%s' got '%s'->'%s'", expectTags[id].Value, val, string(dval)) 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/circonus-labs/go-apiclient v0.7.15 h1:r9sUdc+EDM0tL6Z6u03dac8fxYvlz1kPhxlNwkoIoqM= 2 | github.com/circonus-labs/go-apiclient v0.7.15/go.mod h1:RFgkvdYEkimzgu3V2vVYlS1bitjOz1SF6uw109ieNeY= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 7 | github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= 8 | github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= 9 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 10 | github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 11 | github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 12 | github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= 13 | github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= 14 | github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 15 | github.com/hashicorp/go-retryablehttp v0.6.8/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= 16 | github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= 17 | github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= 18 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 19 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 20 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 21 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 22 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 23 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 24 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 25 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 26 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 27 | github.com/openhistogram/circonusllhist v0.3.0 h1:CuEawy94hKEzjhSABdqkGirl6o67QrqtRoZg3CXBn6k= 28 | github.com/openhistogram/circonusllhist v0.3.0/go.mod h1:PfeYJ/RW2+Jfv3wTz0upbY2TRour/LLqIm2K2Kw5zg0= 29 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 30 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 31 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 32 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 33 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 34 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 35 | github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= 36 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 37 | github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c h1:u6SKchux2yDvFQnDHS3lPnIRmfVJ5Sxy3ao2SIdysLQ= 38 | github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM= 39 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 40 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 41 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 42 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 43 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 44 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 45 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 46 | golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 47 | golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= 48 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 49 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 50 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 51 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 52 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 53 | -------------------------------------------------------------------------------- /gauge.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Circonus, Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package circonusgometrics 6 | 7 | import "github.com/pkg/errors" 8 | 9 | // A Gauge is an instantaneous measurement of a value. 10 | // 11 | // Use a gauge to track metrics which increase and decrease (e.g., amount of 12 | // free memory). 13 | 14 | // GaugeWithTags sets a gauge metric with tags to a value 15 | func (m *CirconusMetrics) GaugeWithTags(metric string, tags Tags, val interface{}) { 16 | m.SetGaugeWithTags(metric, tags, val) 17 | } 18 | 19 | // Gauge sets a gauge to a value 20 | func (m *CirconusMetrics) Gauge(metric string, val interface{}) { 21 | m.SetGauge(metric, val) 22 | } 23 | 24 | // SetGaugeWithTags sets a gauge metric with tags to a value 25 | func (m *CirconusMetrics) SetGaugeWithTags(metric string, tags Tags, val interface{}) { 26 | m.SetGauge(m.MetricNameWithStreamTags(metric, tags), val) 27 | } 28 | 29 | // SetGauge sets a gauge to a value 30 | func (m *CirconusMetrics) SetGauge(metric string, val interface{}) { 31 | m.gm.Lock() 32 | defer m.gm.Unlock() 33 | m.gauges[metric] = val 34 | } 35 | 36 | // AddGaugeWithTags adds value to existing gauge metric with tags 37 | func (m *CirconusMetrics) AddGaugeWithTags(metric string, tags Tags, val interface{}) { 38 | m.AddGauge(m.MetricNameWithStreamTags(metric, tags), val) 39 | } 40 | 41 | // AddGauge adds value to existing gauge 42 | func (m *CirconusMetrics) AddGauge(metric string, val interface{}) { 43 | m.gm.Lock() 44 | defer m.gm.Unlock() 45 | 46 | v, ok := m.gauges[metric] 47 | if !ok { 48 | m.gauges[metric] = val 49 | return 50 | } 51 | 52 | switch vnew := val.(type) { 53 | default: 54 | // ignore it, unsupported type 55 | case int: 56 | m.gauges[metric] = v.(int) + vnew 57 | case int8: 58 | m.gauges[metric] = v.(int8) + vnew 59 | case int16: 60 | m.gauges[metric] = v.(int16) + vnew 61 | case int32: 62 | m.gauges[metric] = v.(int32) + vnew 63 | case int64: 64 | m.gauges[metric] = v.(int64) + vnew 65 | case uint: 66 | m.gauges[metric] = v.(uint) + vnew 67 | case uint8: 68 | m.gauges[metric] = v.(uint8) + vnew 69 | case uint16: 70 | m.gauges[metric] = v.(uint16) + vnew 71 | case uint32: 72 | m.gauges[metric] = v.(uint32) + vnew 73 | case uint64: 74 | m.gauges[metric] = v.(uint64) + vnew 75 | case float32: 76 | m.gauges[metric] = v.(float32) + vnew 77 | case float64: 78 | m.gauges[metric] = v.(float64) + vnew 79 | } 80 | } 81 | 82 | // RemoveGaugeWithTags removes a gauge metric with tags 83 | func (m *CirconusMetrics) RemoveGaugeWithTags(metric string, tags Tags) { 84 | m.RemoveGauge(m.MetricNameWithStreamTags(metric, tags)) 85 | } 86 | 87 | // RemoveGauge removes a gauge 88 | func (m *CirconusMetrics) RemoveGauge(metric string) { 89 | m.gm.Lock() 90 | defer m.gm.Unlock() 91 | delete(m.gauges, metric) 92 | } 93 | 94 | // GetGaugeTest returns the current value for a gauge. (note: it is a function specifically for "testing", disable automatic submission during testing.) 95 | func (m *CirconusMetrics) GetGaugeTest(metric string) (interface{}, error) { 96 | m.gm.Lock() 97 | defer m.gm.Unlock() 98 | 99 | if val, ok := m.gauges[metric]; ok { 100 | return val, nil 101 | } 102 | 103 | return nil, errors.Errorf("Gauge metric '%s' not found", metric) 104 | } 105 | 106 | // SetGaugeFuncWithTags sets a gauge metric with tags to a function [called at flush interval] 107 | func (m *CirconusMetrics) SetGaugeFuncWithTags(metric string, tags Tags, fn func() int64) { 108 | m.SetGaugeFunc(m.MetricNameWithStreamTags(metric, tags), fn) 109 | } 110 | 111 | // SetGaugeFunc sets a gauge to a function [called at flush interval] 112 | func (m *CirconusMetrics) SetGaugeFunc(metric string, fn func() int64) { 113 | m.gfm.Lock() 114 | defer m.gfm.Unlock() 115 | m.gaugeFuncs[metric] = fn 116 | } 117 | 118 | // RemoveGaugeFuncWithTags removes a gauge metric with tags function 119 | func (m *CirconusMetrics) RemoveGaugeFuncWithTags(metric string, tags Tags) { 120 | m.RemoveGaugeFunc(m.MetricNameWithStreamTags(metric, tags)) 121 | } 122 | 123 | // RemoveGaugeFunc removes a gauge function 124 | func (m *CirconusMetrics) RemoveGaugeFunc(metric string) { 125 | m.gfm.Lock() 126 | defer m.gfm.Unlock() 127 | delete(m.gaugeFuncs, metric) 128 | } 129 | 130 | // getGaugeType returns accurate resmon type for underlying type of gauge value 131 | func (m *CirconusMetrics) getGaugeType(v interface{}) string { 132 | mt := "n" 133 | switch v.(type) { 134 | case int: 135 | mt = "i" 136 | case int8: 137 | mt = "i" 138 | case int16: 139 | mt = "i" 140 | case int32: 141 | mt = "i" 142 | case uint: 143 | mt = "I" 144 | case uint8: 145 | mt = "I" 146 | case uint16: 147 | mt = "I" 148 | case uint32: 149 | mt = "I" 150 | case int64: 151 | mt = "l" 152 | case uint64: 153 | mt = "L" 154 | } 155 | 156 | return mt 157 | } 158 | -------------------------------------------------------------------------------- /checkmgr/metrics.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Circonus, Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package checkmgr 6 | 7 | import apiclient "github.com/circonus-labs/go-apiclient" 8 | 9 | // IsMetricActive checks whether a given metric name is currently active(enabled) 10 | func (cm *CheckManager) IsMetricActive(name string) bool { 11 | if !cm.manageMetrics { // short circuit for metric filters 12 | return true 13 | } 14 | 15 | cm.availableMetricsmu.Lock() 16 | defer cm.availableMetricsmu.Unlock() 17 | 18 | return cm.availableMetrics[name] 19 | } 20 | 21 | // ActivateMetric determines if a given metric should be activated 22 | func (cm *CheckManager) ActivateMetric(name string) bool { 23 | if !cm.manageMetrics { // short circuit for metric filters 24 | return false 25 | } 26 | 27 | cm.availableMetricsmu.Lock() 28 | defer cm.availableMetricsmu.Unlock() 29 | 30 | active, exists := cm.availableMetrics[name] 31 | 32 | if !exists { 33 | return true 34 | } 35 | 36 | if !active && cm.forceMetricActivation { 37 | return true 38 | } 39 | 40 | return false 41 | } 42 | 43 | // AddMetricTags updates check bundle metrics with tags 44 | func (cm *CheckManager) AddMetricTags(metricName string, tags []string, appendTags bool) bool { 45 | tagsUpdated := false 46 | 47 | if !cm.manageMetrics { 48 | return tagsUpdated 49 | } 50 | 51 | if appendTags && len(tags) == 0 { 52 | return tagsUpdated 53 | } 54 | 55 | currentTags, exists := cm.metricTags[metricName] 56 | if !exists { 57 | foundMetric := false 58 | 59 | if cm.checkBundle != nil { 60 | for _, metric := range cm.checkBundle.Metrics { 61 | if metric.Name == metricName { 62 | foundMetric = true 63 | currentTags = metric.Tags 64 | break 65 | } 66 | } 67 | } 68 | 69 | if !foundMetric { 70 | currentTags = []string{} 71 | } 72 | } 73 | 74 | action := "" 75 | if appendTags { 76 | numNewTags := countNewTags(currentTags, tags) 77 | if numNewTags > 0 { 78 | action = "Added" 79 | currentTags = append(currentTags, tags...) 80 | tagsUpdated = true 81 | } 82 | } else { 83 | if len(tags) != len(currentTags) { 84 | action = "Set" 85 | currentTags = tags 86 | tagsUpdated = true 87 | } else { 88 | numNewTags := countNewTags(currentTags, tags) 89 | if numNewTags > 0 { 90 | action = "Set" 91 | currentTags = tags 92 | tagsUpdated = true 93 | } 94 | } 95 | } 96 | 97 | if tagsUpdated { 98 | cm.metricTags[metricName] = currentTags 99 | } 100 | 101 | if cm.Debug && action != "" { 102 | cm.Log.Printf("%s metric tag(s) %s %v\n", action, metricName, tags) 103 | } 104 | 105 | return tagsUpdated 106 | } 107 | 108 | // addNewMetrics updates a check bundle with new metrics 109 | func (cm *CheckManager) addNewMetrics(newMetrics map[string]*apiclient.CheckBundleMetric) bool { 110 | updatedCheckBundle := false 111 | 112 | if len(newMetrics) == 0 { 113 | return updatedCheckBundle 114 | } 115 | 116 | if cm.checkBundle == nil { 117 | return updatedCheckBundle 118 | } 119 | 120 | cm.cbmu.Lock() 121 | defer cm.cbmu.Unlock() 122 | 123 | numCurrMetrics := len(cm.checkBundle.Metrics) 124 | numNewMetrics := len(newMetrics) 125 | 126 | if numCurrMetrics+numNewMetrics >= cap(cm.checkBundle.Metrics) { 127 | nm := make([]apiclient.CheckBundleMetric, numCurrMetrics+numNewMetrics) 128 | copy(nm, cm.checkBundle.Metrics) 129 | cm.checkBundle.Metrics = nm 130 | } 131 | 132 | cm.checkBundle.Metrics = cm.checkBundle.Metrics[0 : numCurrMetrics+numNewMetrics] 133 | 134 | i := 0 135 | for _, metric := range newMetrics { 136 | cm.checkBundle.Metrics[numCurrMetrics+i] = *metric 137 | i++ 138 | updatedCheckBundle = true 139 | } 140 | 141 | if updatedCheckBundle { 142 | cm.forceCheckUpdate = true 143 | } 144 | 145 | return updatedCheckBundle 146 | } 147 | 148 | // inventoryMetrics creates list of active metrics in check bundle 149 | func (cm *CheckManager) inventoryMetrics() { 150 | availableMetrics := make(map[string]bool) 151 | for _, metric := range cm.checkBundle.Metrics { 152 | availableMetrics[metric.Name] = metric.Status == "active" 153 | } 154 | cm.availableMetricsmu.Lock() 155 | cm.availableMetrics = availableMetrics 156 | cm.availableMetricsmu.Unlock() 157 | } 158 | 159 | // countNewTags returns a count of new tags which do not exist in the current list of tags 160 | func countNewTags(currTags []string, newTags []string) int { 161 | if len(newTags) == 0 { 162 | return 0 163 | } 164 | 165 | if len(currTags) == 0 { 166 | return len(newTags) 167 | } 168 | 169 | newTagCount := 0 170 | 171 | for _, newTag := range newTags { 172 | found := false 173 | for _, currTag := range currTags { 174 | if newTag == currTag { 175 | found = true 176 | break 177 | } 178 | } 179 | if !found { 180 | newTagCount++ 181 | } 182 | } 183 | 184 | return newTagCount 185 | } 186 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v3.4.6 2 | 3 | * upd: more stringent tag limit enforcement 4 | 5 | # v3.4.5 6 | 7 | * upd: set metric ts only when submit timestamp set by caller 8 | * upd: dependencies 9 | * fix: lint struct field alignment 10 | 11 | # v3.4.4 12 | 13 | * add: setting submission timestamp 14 | * add: support timestamps on histograms with SerializeB64 15 | 16 | # v3.4.3 17 | 18 | * add: submit duration to results 19 | * add: more per req logging on submit failures 20 | * upd: optimize struct layout/size chkmgr 21 | 22 | # v3.4.2 23 | 24 | * upd: dependency (go-apiclient) 25 | * upd: clean up/normalize http client for httptrap requests 26 | * add: output full response from broker stats, filtered, and error if applicable in debug message 27 | * add: ability to send to clustered brokers behind LB which are not using a cert for a single CN 28 | * add: `SerialInit` option to initialize check serially 29 | * add: `GetBrokerTLSConfig` to retrieve broker tls configuration 30 | * add: `GetCheckBundle` for caching 31 | 32 | # v3.4.1 33 | 34 | * add: `DumpMetrics` config option, log payload sent to broker for debugging 35 | * upd: dependencies 36 | 37 | # v3.4.0 38 | 39 | * fix: service tag file path 40 | * fix: lint issues 41 | * add: lint workflow 42 | * upd: dependencies (go-apiclient,circonusllhst) 43 | 44 | # v3.3.4 45 | 46 | * upd: dependencies (cgm,circonusllhst,retryablehttp) 47 | 48 | # v3.3.3 49 | 50 | * add: custom metric support 51 | 52 | # v3.3.2 53 | 54 | * fix: put locking in snapshot handlers 55 | 56 | # v3.3.1 57 | 58 | * add: concurrent snapshot 59 | 60 | # v3.3.0 61 | 62 | * upd: add timestamps to metric output 63 | 64 | # v3.2.2 65 | 66 | * upd: add debug log when fetching cert from api 67 | * fix: update internal cert copy for fallback 68 | * upd: use manageMetrics to get check metric management 69 | * add: manageMetrics state for non-metric filter checks 70 | 71 | # v3.2.1 72 | 73 | * fix: internal log interface satisfies rh log interface no need to cast to log.Logger any longer 74 | 75 | # v3.2.0 76 | 77 | * add: accept category only tags 78 | * fix: tls config for self-signed certs (go1.15) 79 | 80 | # v3.1.2 81 | 82 | * fix: identify httptrap with subtype for submission url 83 | 84 | # v3.1.1 85 | 86 | * fix: quoting on error message in test 87 | 88 | # v3.1.0 89 | 90 | * upd: do not force tag values to lowercase 91 | 92 | # v3.0.2 93 | 94 | * add: method to flush metrics without resetting (`FlushMetricsNoReset()`) 95 | 96 | # v3.0.1 97 | 98 | * upd: dependencies 99 | * fix: send empty array for `check_bundle.metrics`, api errors on null now 100 | 101 | # v3.0.0 102 | 103 | * upd: stricter linting 104 | * upd: dependencies 105 | * upd: api submodule is deprecated (use github.com/circonus-labs/go-apiclient or older v2 branch of circonus-gometrics) 106 | 107 | # v3.0.0-beta.4 108 | 109 | * fix: verify at least one active check found when searching for checks 110 | * upd: broker test IP and external host for match against submission url host 111 | 112 | # v3.0.0-beta.3 113 | 114 | * upd: go-apiclient for graph overlay attribute type fixes 115 | 116 | # v3.0.0-beta.2 117 | 118 | * fix: submit for breaking change in dependency patch release 119 | * upd: dependencies 120 | 121 | # v3.0.0-beta.1 122 | 123 | * upd: merge tag helper methods, support logging invalid tags 124 | * upd: allow manually formatted and base64 encoded tags 125 | * upd: allow tag values to have embedded colons 126 | 127 | # v3.0.0-beta 128 | 129 | * add: log deprecation notice on api calls 130 | * upd: dependency circonusllhist v0.1.2, go-apiclient v0.5.3 131 | * upd: `snapHistograms()` method to use the histogram `Copy()` if `resetHistograms` is false, otherwise uses `CopyAndReset()` 132 | 133 | # v3.0.0-alpha.5 134 | 135 | * add: allow any log package with a `Printf` to be used 136 | * upd: circonus-labs/go-apiclient v0.5.2 (for generic log support) 137 | * upd: ensure only `Printf` is used for logging 138 | * upd: migrate to errors package (`errors.Wrap` et al.) 139 | * upd: error and log messages, remove explicit log level classifications from logging messages 140 | * upd: OBSOLETE github.com/circonus-labs/v3/circonus-gometrics/api will be REMOVED --- USE **github.com/circonus-labs/go-apiclient** 141 | 142 | # v3.0.0-alpha.4 143 | 144 | * add: missing SetHistogramDurationWithTags 145 | * upd: go-apiclient v0.5.1 146 | * fix: remove cgm v2 dependency from DEPRECATED api package 147 | * upd: retryablehttp v0.5.0 148 | 149 | # v3.0.0-alpha.3 150 | 151 | * add: RecordDuration, RecordDurationWithTags, SetHistogramDuration 152 | 153 | # v3.0.0-alpha.2 154 | 155 | * upd: circllhist v0.1.2 156 | 157 | # v3.0.0-alpha.1 158 | 159 | * fix: enable check management for add tags test 160 | * fix: api.circonus.com hostname (accidentally changed during switch to apiclient) 161 | 162 | # v3.0.0-alpha 163 | 164 | * add: helper functions for metrics `*WithTags` e.g. `TimingWithTags(metricName,tagList,val)` 165 | * upd: default new checks to use metric_filters 166 | * add: metric_filters support 167 | * upd: dependencies (circonusllhist v0.1.0) 168 | * upd: change histograms from type 'n' to type 'h' in submissions 169 | * upd: DEPRECATED github.com/circonus-labs/v3/circonus-gometrics/api 170 | * upd: switch to using github.com/circonus-labs/go-apiclient 171 | * upd: merge other metric tag functions into tags 172 | * add: helper methods for handling tags (for new stream tags syntax and old check_bundle.metrics.metric.tags) 173 | * upd: merge other metric output functions into metric_output 174 | * upd: merge util into metric_output (methods in util are specifically for working with metric outputs) 175 | -------------------------------------------------------------------------------- /tags.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Circonus, Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Tags helper functions 6 | 7 | package circonusgometrics 8 | 9 | import ( 10 | "encoding/base64" 11 | "fmt" 12 | "sort" 13 | "strings" 14 | "unicode" 15 | ) 16 | 17 | const ( 18 | // NOTE: max tags and metric name len are enforced here so that 19 | // details on which metric(s) can be logged. Otherwise, any 20 | // metric(s) exceeding the limits are rejected by the broker 21 | // without details on exactly which metric(s) caused the error. 22 | // All metrics sent with the offending metric(s) are also rejected. 23 | 24 | MaxTagLen = 256 // sync w/NOIT_TAG_MAX_PAIR_LEN https://github.com/circonus-labs/reconnoiter/blob/master/src/noit_metric.h#L102 25 | MaxTagCat = 254 // sync w/NOIT_TAG_MAX_CAT_LEN https://github.com/circonus-labs/reconnoiter/blob/master/src/noit_metric.h#L104 26 | 27 | // MaxTags reconnoiter will accept in stream tagged metric name 28 | MaxTags = 256 // sync w/MAX_TAGS https://github.com/circonus-labs/reconnoiter/blob/master/src/noit_metric.h#L46 29 | 30 | // MaxMetricNameLen reconnoiter will accept (name+stream tags) 31 | MaxMetricNameLen = 4096 // sync w/MAX_METRIC_TAGGED_NAME https://github.com/circonus-labs/reconnoiter/blob/master/src/noit_metric.h#L45 32 | ) 33 | 34 | // Tag defines an individual tag 35 | type Tag struct { 36 | Category string 37 | Value string 38 | } 39 | 40 | // Tags defines a list of tags 41 | type Tags []Tag 42 | 43 | // SetMetricTags sets the tags for the named metric and flags a check update is needed 44 | // Note: does not work with checks using metric_filters (the default) use metric 45 | // `*WithTags` helper methods or manual manage stream tags in metric names. 46 | func (m *CirconusMetrics) SetMetricTags(name string, tags []string) bool { 47 | return m.check.AddMetricTags(name, tags, false) 48 | } 49 | 50 | // AddMetricTags appends tags to any existing tags for the named metric and flags a check update is needed 51 | // Note: does not work with checks using metric_filters (the default) use metric 52 | // `*WithTags` helper methods or manual manage stream tags in metric names. 53 | func (m *CirconusMetrics) AddMetricTags(name string, tags []string) bool { 54 | return m.check.AddMetricTags(name, tags, true) 55 | } 56 | 57 | // MetricNameWithStreamTags will encode tags as stream tags into supplied metric name. 58 | // Note: if metric name already has stream tags it is assumed the metric name and 59 | // embedded stream tags are being managed manually and calling this method will nave no effect. 60 | func (m *CirconusMetrics) MetricNameWithStreamTags(metric string, tags Tags) string { 61 | if len(tags) == 0 { 62 | return metric 63 | } 64 | 65 | if strings.Contains(metric, "|ST[") { 66 | return metric 67 | } 68 | 69 | taglist := m.EncodeMetricStreamTags(metric, tags) 70 | if taglist != "" { 71 | return metric + "|ST[" + taglist + "]" 72 | } 73 | 74 | return metric 75 | } 76 | 77 | // EncodeMetricStreamTags encodes Tags into a string suitable for use with 78 | // stream tags. Tags directly embedded into metric names using the 79 | // `metric_name|ST[]` syntax. 80 | func (m *CirconusMetrics) EncodeMetricStreamTags(metricName string, tags Tags) string { 81 | if len(tags) == 0 { 82 | return "" 83 | } 84 | 85 | tmpTags := m.EncodeMetricTags(metricName, tags) 86 | if len(tmpTags) == 0 { 87 | return "" 88 | } 89 | 90 | tagList := make([]string, len(tmpTags)) 91 | for i, tag := range tmpTags { 92 | tagParts := strings.SplitN(tag, ":", 2) 93 | if len(tagParts) != 2 { 94 | m.Log.Printf("%s has invalid tag (%s)", metricName, tag) 95 | continue // invalid tag, skip it 96 | } 97 | encodeFmt := `b"%s"` 98 | encodedSig := `b"` // has cat or val been previously (or manually) base64 encoded and formatted 99 | tc := tagParts[0] 100 | tv := tagParts[1] 101 | if !strings.HasPrefix(tc, encodedSig) { 102 | tc = fmt.Sprintf(encodeFmt, base64.StdEncoding.EncodeToString([]byte(tc))) 103 | } 104 | if !strings.HasPrefix(tv, encodedSig) && tv != "" { 105 | tv = fmt.Sprintf(encodeFmt, base64.StdEncoding.EncodeToString([]byte(tv))) 106 | } 107 | tagList[i] = tc + ":" + tv 108 | } 109 | 110 | return strings.Join(tagList, ",") 111 | } 112 | 113 | // EncodeMetricTags encodes Tags into an array of strings. The format 114 | // check_bundle.metircs.metric.tags needs. This helper is intended to work 115 | // with legacy check bundle metrics. Tags directly on named metrics are being 116 | // removed in favor of stream tags. 117 | func (m *CirconusMetrics) EncodeMetricTags(metricName string, tags Tags) []string { 118 | if len(tags) == 0 { 119 | return []string{} 120 | } 121 | 122 | uniqueTags := make(map[string]bool) 123 | for _, t := range tags { 124 | tc := strings.Map(removeSpaces, strings.ToLower(t.Category)) 125 | tv := strings.TrimSpace(t.Value) 126 | if tc == "" { 127 | m.Log.Printf("%s has invalid tag (%#v)", metricName, t) 128 | continue 129 | } 130 | if len(tc) > MaxTagCat { 131 | m.Log.Printf("%s has tag cat (%s) >= max len (%d)", metricName, tc, MaxTagCat) 132 | continue 133 | } 134 | tag := tc + ":" 135 | if tv != "" { 136 | tag += tv 137 | } 138 | if len(tag) >= MaxTagLen { 139 | m.Log.Printf("%s has tag (%s) >= max len (%d)", metricName, tag, MaxTagLen) 140 | continue 141 | } 142 | uniqueTags[tag] = true 143 | } 144 | if len(uniqueTags) >= MaxTags { 145 | m.Log.Printf("%s has more tags (%d) >= max tags (%d) - dropping excess tags", metricName, len(uniqueTags), MaxTags) 146 | } 147 | tagList := make([]string, 0, len(uniqueTags)) 148 | idx := 0 149 | for tag := range uniqueTags { 150 | tagList = append(tagList, tag) 151 | idx++ 152 | if idx >= MaxTags { 153 | break 154 | } 155 | } 156 | sort.Strings(tagList) 157 | return tagList 158 | } 159 | 160 | func removeSpaces(r rune) rune { 161 | if unicode.IsSpace(r) { 162 | return -1 163 | } 164 | return r 165 | } 166 | -------------------------------------------------------------------------------- /histogram.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Circonus, Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package circonusgometrics 6 | 7 | import ( 8 | "sync" 9 | "time" 10 | 11 | "github.com/openhistogram/circonusllhist" 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | // Histogram measures the distribution of a stream of values. 16 | type Histogram struct { 17 | rw sync.RWMutex 18 | hist *circonusllhist.Histogram 19 | name string 20 | } 21 | 22 | // TimingWithTags adds a value to a histogram metric with tags 23 | func (m *CirconusMetrics) TimingWithTags(metric string, tags Tags, val float64) { 24 | m.SetHistogramValueWithTags(metric, tags, val) 25 | } 26 | 27 | // Timing adds a value to a histogram 28 | func (m *CirconusMetrics) Timing(metric string, val float64) { 29 | m.SetHistogramValue(metric, val) 30 | } 31 | 32 | // RecordValueWithTags adds a value to a histogram metric with tags 33 | func (m *CirconusMetrics) RecordValueWithTags(metric string, tags Tags, val float64) { 34 | m.SetHistogramValueWithTags(metric, tags, val) 35 | } 36 | 37 | // RecordValue adds a value to a histogram 38 | func (m *CirconusMetrics) RecordValue(metric string, val float64) { 39 | m.SetHistogramValue(metric, val) 40 | } 41 | 42 | // RecordDurationWithTags adds a time.Duration to a histogram metric with tags 43 | // (duration is normalized to time.Second, but supports nanosecond granularity). 44 | func (m *CirconusMetrics) RecordDurationWithTags(metric string, tags Tags, val time.Duration) { 45 | m.SetHistogramDurationWithTags(metric, tags, val) 46 | } 47 | 48 | // RecordDuration adds a time.Duration to a histogram metric (duration is 49 | // normalized to time.Second, but supports nanosecond granularity). 50 | func (m *CirconusMetrics) RecordDuration(metric string, val time.Duration) { 51 | m.SetHistogramDuration(metric, val) 52 | } 53 | 54 | // RecordCountForValueWithTags adds count n for value to a histogram metric with tags 55 | func (m *CirconusMetrics) RecordCountForValueWithTags(metric string, tags Tags, val float64, n int64) { 56 | m.RecordCountForValue(m.MetricNameWithStreamTags(metric, tags), val, n) 57 | } 58 | 59 | // RecordCountForValue adds count n for value to a histogram 60 | func (m *CirconusMetrics) RecordCountForValue(metric string, val float64, n int64) { 61 | hist := m.NewHistogram(metric) 62 | 63 | m.hm.Lock() 64 | hist.rw.Lock() 65 | err := hist.hist.RecordValues(val, n) 66 | if err != nil { 67 | m.Log.Printf("error recording histogram values (%v)\n", err) 68 | } 69 | hist.rw.Unlock() 70 | m.hm.Unlock() 71 | } 72 | 73 | // SetHistogramValueWithTags adds a value to a histogram metric with tags 74 | func (m *CirconusMetrics) SetHistogramValueWithTags(metric string, tags Tags, val float64) { 75 | m.SetHistogramValue(m.MetricNameWithStreamTags(metric, tags), val) 76 | } 77 | 78 | // SetHistogramValue adds a value to a histogram 79 | func (m *CirconusMetrics) SetHistogramValue(metric string, val float64) { 80 | hist := m.NewHistogram(metric) 81 | 82 | m.hm.Lock() 83 | hist.rw.Lock() 84 | err := hist.hist.RecordValue(val) 85 | if err != nil { 86 | m.Log.Printf("error recording histogram value (%v)\n", err) 87 | } 88 | hist.rw.Unlock() 89 | m.hm.Unlock() 90 | } 91 | 92 | // SetHistogramDurationWithTags adds a value to a histogram with tags 93 | func (m *CirconusMetrics) SetHistogramDurationWithTags(metric string, tags Tags, val time.Duration) { 94 | m.SetHistogramDuration(m.MetricNameWithStreamTags(metric, tags), val) 95 | } 96 | 97 | // SetHistogramDuration adds a value to a histogram 98 | func (m *CirconusMetrics) SetHistogramDuration(metric string, val time.Duration) { 99 | hist := m.NewHistogram(metric) 100 | 101 | m.hm.Lock() 102 | hist.rw.Lock() 103 | err := hist.hist.RecordDuration(val) 104 | if err != nil { 105 | m.Log.Printf("error recording histogram duration (%v)\n", err) 106 | } 107 | hist.rw.Unlock() 108 | m.hm.Unlock() 109 | } 110 | 111 | // RemoveHistogramWithTags removes a histogram metric with tags 112 | func (m *CirconusMetrics) RemoveHistogramWithTags(metric string, tags Tags) { 113 | m.RemoveHistogram(m.MetricNameWithStreamTags(metric, tags)) 114 | } 115 | 116 | // RemoveHistogram removes a histogram 117 | func (m *CirconusMetrics) RemoveHistogram(metric string) { 118 | m.hm.Lock() 119 | defer m.hm.Unlock() 120 | delete(m.histograms, metric) 121 | } 122 | 123 | // NewHistogramWithTags returns a histogram metric with tags instance 124 | func (m *CirconusMetrics) NewHistogramWithTags(metric string, tags Tags) *Histogram { 125 | return m.NewHistogram(m.MetricNameWithStreamTags(metric, tags)) 126 | } 127 | 128 | // NewHistogram returns a histogram instance. 129 | func (m *CirconusMetrics) NewHistogram(metric string) *Histogram { 130 | m.hm.Lock() 131 | defer m.hm.Unlock() 132 | 133 | if hist, ok := m.histograms[metric]; ok { 134 | return hist 135 | } 136 | 137 | hist := &Histogram{ 138 | name: metric, 139 | hist: circonusllhist.New(), 140 | } 141 | 142 | m.histograms[metric] = hist 143 | 144 | return hist 145 | } 146 | 147 | // GetHistogramTest returns the current value for a histogram. (note: it is a function specifically for "testing", disable automatic submission during testing.) 148 | func (m *CirconusMetrics) GetHistogramTest(metric string) ([]string, error) { 149 | m.hm.Lock() 150 | defer m.hm.Unlock() 151 | 152 | if hist, ok := m.histograms[metric]; ok { 153 | hist.rw.Lock() 154 | defer hist.rw.Unlock() 155 | return hist.hist.DecStrings(), nil 156 | } 157 | 158 | return []string{""}, errors.Errorf("Histogram metric '%s' not found", metric) 159 | } 160 | 161 | // Name returns the name from a histogram instance 162 | func (h *Histogram) Name() string { 163 | h.rw.Lock() 164 | defer h.rw.Unlock() 165 | return h.name 166 | } 167 | 168 | // RecordValue records the given value to a histogram instance 169 | func (h *Histogram) RecordValue(v float64) { 170 | h.rw.Lock() 171 | defer h.rw.Unlock() 172 | _ = h.hist.RecordValue(v) 173 | } 174 | 175 | // RecordDuration records the given time.Duration to a histogram instance. 176 | // RecordDuration normalizes the value to seconds. 177 | func (h *Histogram) RecordDuration(v time.Duration) { 178 | h.rw.Lock() 179 | defer h.rw.Unlock() 180 | _ = h.hist.RecordDuration(v) 181 | } 182 | -------------------------------------------------------------------------------- /text_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Circonus, Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package circonusgometrics 6 | 7 | import ( 8 | "testing" 9 | ) 10 | 11 | func TestSetText(t *testing.T) { 12 | t.Log("Testing gauge.SetText") 13 | 14 | cm := &CirconusMetrics{text: make(map[string]string)} 15 | 16 | cm.SetText("foo", "bar") 17 | 18 | val, ok := cm.text["foo"] 19 | if !ok { 20 | t.Errorf("Expected to find foo") 21 | } 22 | 23 | if val != "bar" { 24 | t.Errorf("Expected 'bar', found '%s'", val) 25 | } 26 | } 27 | 28 | func TestSetTextWithTags(t *testing.T) { 29 | t.Log("Testing gauge.SetTextWithTags") 30 | 31 | cm := &CirconusMetrics{text: make(map[string]string)} 32 | 33 | metricName := "foo" 34 | tags := Tags{{"foo", "bar"}, {"baz", "qux"}} 35 | streamTagMetricName := cm.MetricNameWithStreamTags("foo", tags) 36 | 37 | cm.SetTextWithTags(metricName, tags, "bar") 38 | 39 | val, ok := cm.text[streamTagMetricName] 40 | if !ok { 41 | t.Fatalf("%s with %v tags not found (%s) (%#v)", metricName, tags, streamTagMetricName, cm.text) 42 | } 43 | 44 | if val != "bar" { 45 | t.Fatalf("Expected 'bar', found '%s'", val) 46 | } 47 | } 48 | 49 | func TestSetTextValue(t *testing.T) { 50 | t.Log("Testing gauge.SetTextValue") 51 | 52 | cm := &CirconusMetrics{} 53 | cm.text = make(map[string]string) 54 | cm.SetTextValue("foo", "bar") 55 | 56 | val, ok := cm.text["foo"] 57 | if !ok { 58 | t.Errorf("Expected to find foo") 59 | } 60 | 61 | if val != "bar" { 62 | t.Errorf("Expected 'bar', found '%s'", val) 63 | } 64 | } 65 | 66 | func TestSetTextValueWithTags(t *testing.T) { 67 | t.Log("Testing gauge.SetTextValueWithTags") 68 | 69 | cm := &CirconusMetrics{text: make(map[string]string)} 70 | 71 | metricName := "foo" 72 | tags := Tags{{"foo", "bar"}, {"baz", "qux"}} 73 | streamTagMetricName := cm.MetricNameWithStreamTags("foo", tags) 74 | 75 | cm.SetTextValueWithTags(metricName, tags, "bar") 76 | 77 | val, ok := cm.text[streamTagMetricName] 78 | if !ok { 79 | t.Fatalf("%s with %v tags not found (%s) (%#v)", metricName, tags, streamTagMetricName, cm.text) 80 | } 81 | 82 | if val != "bar" { 83 | t.Fatalf("Expected 'bar', found '%s'", val) 84 | } 85 | } 86 | 87 | func TestRemoveText(t *testing.T) { 88 | t.Log("Testing text.RemoveText") 89 | 90 | cm := &CirconusMetrics{text: make(map[string]string)} 91 | 92 | cm.SetText("foo", "bar") 93 | 94 | val, ok := cm.text["foo"] 95 | if !ok { 96 | t.Errorf("Expected to find foo") 97 | } 98 | 99 | if val != "bar" { 100 | t.Errorf("Expected 'bar', found '%s'", val) 101 | } 102 | 103 | cm.RemoveText("foo") 104 | 105 | val, ok = cm.text["foo"] 106 | if ok { 107 | t.Errorf("Expected NOT to find foo") 108 | } 109 | 110 | if val != "" { 111 | t.Errorf("Expected '', found '%s'", val) 112 | } 113 | } 114 | 115 | func TestRemoveTextWithTags(t *testing.T) { 116 | t.Log("Testing gauge.RemoveTextWithTags") 117 | 118 | cm := &CirconusMetrics{text: make(map[string]string)} 119 | 120 | metricName := "foo" 121 | tags := Tags{{"foo", "bar"}, {"baz", "qux"}} 122 | streamTagMetricName := cm.MetricNameWithStreamTags("foo", tags) 123 | 124 | cm.SetTextWithTags(metricName, tags, "bar") 125 | 126 | val, ok := cm.text[streamTagMetricName] 127 | if !ok { 128 | t.Fatalf("%s with %v tags not found (%s) (%#v)", metricName, tags, streamTagMetricName, cm.text) 129 | } 130 | 131 | if val != "bar" { 132 | t.Fatalf("Expected 'bar', found '%s'", val) 133 | } 134 | 135 | cm.RemoveTextWithTags(metricName, tags) 136 | 137 | val, ok = cm.text[streamTagMetricName] 138 | if ok { 139 | t.Fatalf("expected NOT to find %s", streamTagMetricName) 140 | } 141 | if val != "" { 142 | t.Fatalf("expected '' got (%s)", val) 143 | } 144 | } 145 | 146 | func TestSetTextFunc(t *testing.T) { 147 | t.Log("Testing text.SetTextFunc") 148 | 149 | tf := func() string { 150 | return "bar" 151 | } 152 | 153 | cm := &CirconusMetrics{textFuncs: make(map[string]func() string)} 154 | 155 | cm.SetTextFunc("foo", tf) 156 | 157 | val, ok := cm.textFuncs["foo"] 158 | if !ok { 159 | t.Errorf("Expected to find foo") 160 | } 161 | 162 | if val() != "bar" { 163 | t.Errorf("Expected 'bar', found '%s'", val()) 164 | } 165 | } 166 | 167 | func TestSetTextFuncWithTags(t *testing.T) { 168 | t.Log("Testing text.SetTextFuncWithTags") 169 | 170 | tf := func() string { 171 | return "bar" 172 | } 173 | 174 | cm := &CirconusMetrics{textFuncs: make(map[string]func() string)} 175 | 176 | metricName := "foo" 177 | tags := Tags{{"foo", "bar"}, {"baz", "qux"}} 178 | streamTagMetricName := cm.MetricNameWithStreamTags("foo", tags) 179 | 180 | cm.SetTextFuncWithTags(metricName, tags, tf) 181 | 182 | val, ok := cm.textFuncs[streamTagMetricName] 183 | if !ok { 184 | t.Fatalf("%s with %v tags not found (%s) (%#v)", metricName, tags, streamTagMetricName, cm.text) 185 | } 186 | 187 | if val() != "bar" { 188 | t.Fatalf("expected 'bar', got (%s)", val()) 189 | } 190 | } 191 | 192 | func TestRemoveTextFunc(t *testing.T) { 193 | t.Log("Testing text.RemoveTextFunc") 194 | 195 | tf := func() string { 196 | return "bar" 197 | } 198 | 199 | cm := &CirconusMetrics{textFuncs: make(map[string]func() string)} 200 | 201 | cm.SetTextFunc("foo", tf) 202 | 203 | val, ok := cm.textFuncs["foo"] 204 | if !ok { 205 | t.Errorf("Expected to find foo") 206 | } 207 | 208 | if val() != "bar" { 209 | t.Errorf("Expected 'bar', found '%s'", val()) 210 | } 211 | 212 | cm.RemoveTextFunc("foo") 213 | 214 | val, ok = cm.textFuncs["foo"] 215 | if ok { 216 | t.Errorf("Expected NOT to find foo") 217 | } 218 | 219 | if val != nil { 220 | t.Errorf("Expected nil, found %s", val()) 221 | } 222 | 223 | } 224 | 225 | func TestRemoveTextFuncWithTags(t *testing.T) { 226 | t.Log("Testing text.RemoveTextFuncWithTags") 227 | 228 | tf := func() string { 229 | return "bar" 230 | } 231 | 232 | cm := &CirconusMetrics{textFuncs: make(map[string]func() string)} 233 | 234 | metricName := "foo" 235 | tags := Tags{{"foo", "bar"}, {"baz", "qux"}} 236 | streamTagMetricName := cm.MetricNameWithStreamTags("foo", tags) 237 | 238 | cm.SetTextFuncWithTags(metricName, tags, tf) 239 | 240 | val, ok := cm.textFuncs[streamTagMetricName] 241 | if !ok { 242 | t.Fatalf("%s with %v tags not found (%s) (%#v)", metricName, tags, streamTagMetricName, cm.text) 243 | } 244 | 245 | if val() != "bar" { 246 | t.Fatalf("expected 'bar', got (%s)", val()) 247 | } 248 | 249 | cm.RemoveTextFuncWithTags(metricName, tags) 250 | 251 | val, ok = cm.textFuncs[streamTagMetricName] 252 | if ok { 253 | t.Fatalf("expected NOT to find %s", streamTagMetricName) 254 | } 255 | if val != nil { 256 | t.Fatalf("expected nil got (%v)", val()) 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Circonus metrics tracking for Go applications 2 | 3 | This library supports named counters, gauges and histograms. It also provides convenience wrappers for registering latency instrumented functions with Go's builtin http server. 4 | 5 | Initializing only requires setting an [API Token](https://login.circonus.com/user/tokens) at a minimum. 6 | 7 | ## Options 8 | 9 | See [OPTIONS.md](OPTIONS.md) for information on all of the available cgm options. 10 | 11 | ## Example 12 | 13 | ### Bare bones minimum 14 | 15 | A working cut-n-past example. Simply set the required environment variable `CIRCONUS_API_TOKEN` and run. 16 | 17 | ```go 18 | package main 19 | 20 | import ( 21 | "log" 22 | "math/rand" 23 | "os" 24 | "os/signal" 25 | "syscall" 26 | "time" 27 | 28 | cgm "github.com/circonus-labs/circonus-gometrics/v3" 29 | ) 30 | 31 | func main() { 32 | 33 | logger := log.New(os.Stdout, "", log.LstdFlags) 34 | 35 | logger.Println("Configuring cgm") 36 | 37 | cmc := &cgm.Config{} 38 | cmc.Debug = false // set to true for debug messages 39 | cmc.Log = logger 40 | 41 | // Circonus API Token key (https://login.circonus.com/user/tokens) 42 | cmc.CheckManager.API.TokenKey = os.Getenv("CIRCONUS_API_TOKEN") 43 | 44 | logger.Println("Creating new cgm instance") 45 | 46 | metrics, err := cgm.NewCirconusMetrics(cmc) 47 | if err != nil { 48 | logger.Println(err) 49 | os.Exit(1) 50 | } 51 | 52 | src := rand.NewSource(time.Now().UnixNano()) 53 | rnd := rand.New(src) 54 | 55 | logger.Println("Adding ctrl-c trap") 56 | c := make(chan os.Signal, 2) 57 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 58 | go func() { 59 | <-c 60 | logger.Println("Received CTRL-C, flushing outstanding metrics before exit") 61 | metrics.Flush() 62 | os.Exit(0) 63 | }() 64 | 65 | logger.Println("Starting to send metrics") 66 | 67 | // number of "sets" of metrics to send 68 | max := 60 69 | 70 | for i := 1; i < max; i++ { 71 | logger.Printf("\tmetric set %d of %d", i, 60) 72 | metrics.Timing("foo", rnd.Float64()*10) 73 | metrics.Increment("bar") 74 | metrics.Gauge("baz", 10) 75 | time.Sleep(time.Second) 76 | } 77 | 78 | metrics.SetText("fini", "complete") 79 | 80 | logger.Println("Flushing any outstanding metrics manually") 81 | metrics.Flush() 82 | } 83 | ``` 84 | 85 | ### A more complete example 86 | 87 | A working, cut-n-paste example with all options available for modification. Also, demonstrates metric tagging. 88 | 89 | ```go 90 | package main 91 | 92 | import ( 93 | "log" 94 | "math/rand" 95 | "os" 96 | "os/signal" 97 | "syscall" 98 | "time" 99 | 100 | cgm "github.com/circonus-labs/circonus-gometrics/v3" 101 | ) 102 | 103 | func main() { 104 | 105 | logger := log.New(os.Stdout, "", log.LstdFlags) 106 | 107 | logger.Println("Configuring cgm") 108 | 109 | cmc := &cgm.Config{} 110 | 111 | // General 112 | 113 | cmc.Interval = "10s" 114 | cmc.Log = logger 115 | cmc.Debug = false 116 | cmc.ResetCounters = "true" 117 | cmc.ResetGauges = "true" 118 | cmc.ResetHistograms = "true" 119 | cmc.ResetText = "true" 120 | 121 | // Circonus API configuration options 122 | cmc.CheckManager.API.TokenKey = os.Getenv("CIRCONUS_API_TOKEN") 123 | cmc.CheckManager.API.TokenApp = os.Getenv("CIRCONUS_API_APP") 124 | cmc.CheckManager.API.URL = os.Getenv("CIRCONUS_API_URL") 125 | cmc.CheckManager.API.TLSConfig = nil 126 | 127 | // Check configuration options 128 | cmc.CheckManager.Check.SubmissionURL = os.Getenv("CIRCONUS_SUBMISSION_URL") 129 | cmc.CheckManager.Check.ID = os.Getenv("CIRCONUS_CHECK_ID") 130 | cmc.CheckManager.Check.InstanceID = "" 131 | cmc.CheckManager.Check.DisplayName = "" 132 | cmc.CheckManager.Check.TargetHost = "" 133 | // if hn, err := os.Hostname(); err == nil { 134 | // cmc.CheckManager.Check.TargetHost = hn 135 | // } 136 | cmc.CheckManager.Check.SearchTag = "" 137 | cmc.CheckManager.Check.Secret = "" 138 | cmc.CheckManager.Check.Tags = "" 139 | cmc.CheckManager.Check.MaxURLAge = "5m" 140 | cmc.CheckManager.Check.ForceMetricActivation = "false" 141 | 142 | // Broker configuration options 143 | cmc.CheckManager.Broker.ID = "" 144 | cmc.CheckManager.Broker.SelectTag = "" 145 | cmc.CheckManager.Broker.MaxResponseTime = "500ms" 146 | cmc.CheckManager.Broker.TLSConfig = nil 147 | 148 | logger.Println("Creating new cgm instance") 149 | 150 | metrics, err := cgm.NewCirconusMetrics(cmc) 151 | if err != nil { 152 | logger.Println(err) 153 | os.Exit(1) 154 | } 155 | 156 | src := rand.NewSource(time.Now().UnixNano()) 157 | rnd := rand.New(src) 158 | 159 | logger.Println("Adding ctrl-c trap") 160 | c := make(chan os.Signal, 2) 161 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 162 | go func() { 163 | <-c 164 | logger.Println("Received CTRL-C, flushing outstanding metrics before exit") 165 | metrics.Flush() 166 | os.Exit(0) 167 | }() 168 | 169 | // Add metric tags (append to any existing tags on specified metric) 170 | metrics.AddMetricTags("foo", []string{"cgm:test"}) 171 | metrics.AddMetricTags("baz", []string{"cgm:test"}) 172 | 173 | logger.Println("Starting to send metrics") 174 | 175 | // number of "sets" of metrics to send 176 | max := 60 177 | 178 | for i := 1; i < max; i++ { 179 | logger.Printf("\tmetric set %d of %d", i, 60) 180 | 181 | metrics.Timing("foo", rnd.Float64()*10) 182 | metrics.Increment("bar") 183 | metrics.Gauge("baz", 10) 184 | 185 | if i == 35 { 186 | // Set metric tags (overwrite current tags on specified metric) 187 | metrics.SetMetricTags("baz", []string{"cgm:reset_test", "cgm:test2"}) 188 | } 189 | 190 | time.Sleep(time.Second) 191 | } 192 | 193 | logger.Println("Flushing any outstanding metrics manually") 194 | metrics.Flush() 195 | 196 | } 197 | ``` 198 | 199 | ### HTTP Handler wrapping 200 | 201 | ```go 202 | http.HandleFunc("/", metrics.TrackHTTPLatency("/", handler_func)) 203 | ``` 204 | 205 | ### HTTP latency example 206 | 207 | ```go 208 | package main 209 | 210 | import ( 211 | "os" 212 | "fmt" 213 | "net/http" 214 | cgm "github.com/circonus-labs/circonus-gometrics/v3" 215 | ) 216 | 217 | func main() { 218 | cmc := &cgm.Config{} 219 | cmc.CheckManager.API.TokenKey = os.Getenv("CIRCONUS_API_TOKEN") 220 | 221 | metrics, err := cgm.NewCirconusMetrics(cmc) 222 | if err != nil { 223 | panic(err) 224 | } 225 | 226 | http.HandleFunc("/", metrics.TrackHTTPLatency("/", func(w http.ResponseWriter, r *http.Request) { 227 | fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:]) 228 | })) 229 | http.ListenAndServe(":8080", http.DefaultServeMux) 230 | } 231 | 232 | ``` 233 | 234 | Unless otherwise noted, the source files are distributed under the BSD-style license found in the [LICENSE](LICENSE) file. 235 | -------------------------------------------------------------------------------- /checkmgr/broker.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Circonus, Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package checkmgr 6 | 7 | import ( 8 | "crypto/rand" 9 | "fmt" 10 | "math/big" 11 | "net" 12 | "net/url" 13 | "reflect" 14 | "strconv" 15 | "strings" 16 | "time" 17 | 18 | apiclient "github.com/circonus-labs/go-apiclient" 19 | "github.com/pkg/errors" 20 | ) 21 | 22 | // Get Broker to use when creating a check 23 | func (cm *CheckManager) getBroker() (*apiclient.Broker, error) { 24 | if cm.brokerID != 0 { 25 | cid := fmt.Sprintf("/broker/%d", cm.brokerID) 26 | broker, err := cm.apih.FetchBroker(apiclient.CIDType(&cid)) 27 | if err != nil { 28 | return nil, err 29 | } 30 | if !cm.isValidBroker(broker) { 31 | return nil, errors.Errorf( 32 | "error, designated broker %d [%s] is invalid (not active, does not support required check type, or connectivity issue)", 33 | cm.brokerID, 34 | broker.Name) 35 | } 36 | return broker, nil 37 | } 38 | broker, err := cm.selectBroker() 39 | if err != nil { 40 | return nil, errors.Errorf("error, unable to fetch suitable broker %s", err) 41 | } 42 | return broker, nil 43 | } 44 | 45 | // Get CN of Broker associated with submission_url to satisfy no IP SANS in certs 46 | func (cm *CheckManager) getBrokerCN(broker *apiclient.Broker, submissionURL apiclient.URLType) (string, string, error) { 47 | u, err := url.Parse(string(submissionURL)) 48 | if err != nil { 49 | return "", "", err 50 | } 51 | 52 | hostParts := strings.Split(u.Host, ":") 53 | host := hostParts[0] 54 | 55 | if net.ParseIP(host) == nil { // it's a non-ip string 56 | return u.Host, u.Host, nil 57 | } 58 | 59 | cn := "" 60 | cnList := make([]string, 0, len(broker.Details)) 61 | 62 | for _, detail := range broker.Details { 63 | // broker must be active 64 | if detail.Status != statusActive { 65 | continue 66 | } 67 | 68 | // certs are generated against the CN (in theory) 69 | // 1. find the right broker instance with matching IP or external hostname 70 | // 2. set the tls.Config.ServerName to whatever that instance's CN is currently 71 | // 3. cert will be valid for TLS conns (in theory) 72 | if detail.IP != nil && *detail.IP == host { 73 | if cn == "" { 74 | cn = detail.CN 75 | } 76 | cnList = append(cnList, detail.CN) 77 | } else if detail.ExternalHost != nil && *detail.ExternalHost == host { 78 | if cn == "" { 79 | cn = detail.CN 80 | } 81 | cnList = append(cnList, detail.CN) 82 | } 83 | } 84 | 85 | if cn == "" { 86 | return "", "", errors.Errorf("error, unable to match URL host (%s) to Broker", u.Host) 87 | } 88 | 89 | return cn, strings.Join(cnList, ","), nil 90 | 91 | } 92 | 93 | // Select a broker for use when creating a check, if a specific broker 94 | // was not specified. 95 | func (cm *CheckManager) selectBroker() (*apiclient.Broker, error) { 96 | var brokerList *[]apiclient.Broker 97 | var err error 98 | enterpriseType := "enterprise" 99 | 100 | if len(cm.brokerSelectTag) > 0 { 101 | filter := apiclient.SearchFilterType{ 102 | "f__tags_has": cm.brokerSelectTag, 103 | } 104 | brokerList, err = cm.apih.SearchBrokers(nil, &filter) 105 | if err != nil { 106 | return nil, err 107 | } 108 | } else { 109 | brokerList, err = cm.apih.FetchBrokers() 110 | if err != nil { 111 | return nil, err 112 | } 113 | } 114 | 115 | if len(*brokerList) == 0 { 116 | return nil, errors.New("zero brokers found") 117 | } 118 | 119 | validBrokers := make(map[string]apiclient.Broker) 120 | haveEnterprise := false 121 | 122 | for _, broker := range *brokerList { 123 | broker := broker 124 | if cm.isValidBroker(&broker) { 125 | validBrokers[broker.CID] = broker 126 | if broker.Type == enterpriseType { 127 | haveEnterprise = true 128 | } 129 | } 130 | } 131 | 132 | if haveEnterprise { // eliminate non-enterprise brokers from valid brokers 133 | for k, v := range validBrokers { 134 | if v.Type != enterpriseType { 135 | delete(validBrokers, k) 136 | } 137 | } 138 | } 139 | 140 | if len(validBrokers) == 0 { 141 | return nil, errors.Errorf("found %d broker(s), zero are valid", len(*brokerList)) 142 | } 143 | 144 | validBrokerKeys := reflect.ValueOf(validBrokers).MapKeys() 145 | maxBrokers := big.NewInt(int64(len(validBrokerKeys))) 146 | bidx, err := rand.Int(rand.Reader, maxBrokers) 147 | if err != nil { 148 | return nil, err 149 | } 150 | selectedBroker := validBrokers[validBrokerKeys[bidx.Uint64()].String()] 151 | 152 | if cm.Debug { 153 | cm.Log.Printf("selected broker '%s'\n", selectedBroker.Name) 154 | } 155 | 156 | return &selectedBroker, nil 157 | 158 | } 159 | 160 | // Verify broker supports the check type to be used 161 | func (cm *CheckManager) brokerSupportsCheckType(checkType CheckTypeType, details *apiclient.BrokerDetail) bool { 162 | 163 | baseType := string(checkType) 164 | 165 | for _, module := range details.Modules { 166 | if module == baseType { 167 | return true 168 | } 169 | } 170 | 171 | if idx := strings.Index(baseType, ":"); idx > 0 { 172 | baseType = baseType[0:idx] 173 | } 174 | 175 | for _, module := range details.Modules { 176 | if module == baseType { 177 | return true 178 | } 179 | } 180 | 181 | return false 182 | 183 | } 184 | 185 | // Is the broker valid (active, supports check type, and reachable) 186 | func (cm *CheckManager) isValidBroker(broker *apiclient.Broker) bool { 187 | var brokerHost string 188 | var brokerPort string 189 | 190 | if broker.Type != "circonus" && broker.Type != "enterprise" { 191 | return false 192 | } 193 | 194 | valid := false 195 | 196 | for _, detail := range broker.Details { 197 | detail := detail 198 | 199 | // broker must be active 200 | if detail.Status != statusActive { 201 | if cm.Debug { 202 | cm.Log.Printf("broker '%s' is not active\n", broker.Name) 203 | } 204 | continue 205 | } 206 | 207 | // broker must have module loaded for the check type to be used 208 | if !cm.brokerSupportsCheckType(cm.checkType, &detail) { 209 | if cm.Debug { 210 | cm.Log.Printf("broker '%s' does not support '%s' checks\n", broker.Name, cm.checkType) 211 | } 212 | continue 213 | } 214 | 215 | if detail.ExternalPort != 0 { 216 | brokerPort = strconv.Itoa(int(detail.ExternalPort)) 217 | } else { 218 | if detail.Port != nil && *detail.Port != 0 { 219 | brokerPort = strconv.Itoa(int(*detail.Port)) 220 | } else { 221 | brokerPort = "43191" 222 | } 223 | } 224 | 225 | if detail.ExternalHost != nil && *detail.ExternalHost != "" { 226 | brokerHost = *detail.ExternalHost 227 | } else if detail.IP != nil && *detail.IP != "" { 228 | brokerHost = *detail.IP 229 | } 230 | 231 | if brokerHost == "" { 232 | cm.Log.Printf("broker '%s' instance %s has no IP or external host set", broker.Name, detail.CN) 233 | continue 234 | } 235 | 236 | if brokerHost == "trap.noit.circonus.net" && brokerPort != "443" { 237 | brokerPort = "443" 238 | } 239 | 240 | retries := 5 241 | for attempt := 1; attempt <= retries; attempt++ { 242 | // broker must be reachable and respond within designated time 243 | conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%s", brokerHost, brokerPort), cm.brokerMaxResponseTime) 244 | if err == nil { 245 | conn.Close() 246 | valid = true 247 | break 248 | } 249 | 250 | cm.Log.Printf("broker '%s' unable to connect, %v. Retrying in 2 seconds, attempt %d of %d\n", broker.Name, err, attempt, retries) 251 | time.Sleep(2 * time.Second) 252 | } 253 | 254 | if valid { 255 | if cm.Debug { 256 | cm.Log.Printf("broker '%s' is valid\n", broker.Name) 257 | } 258 | break 259 | } 260 | } 261 | return valid 262 | } 263 | -------------------------------------------------------------------------------- /submit.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Circonus, Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package circonusgometrics 6 | 7 | import ( 8 | "bytes" 9 | "context" 10 | "encoding/json" 11 | "fmt" 12 | "io" 13 | "io/ioutil" 14 | "log" 15 | "net" 16 | "net/http" 17 | "strconv" 18 | "time" 19 | 20 | "github.com/circonus-labs/go-apiclient" 21 | "github.com/hashicorp/go-retryablehttp" 22 | "github.com/pkg/errors" 23 | ) 24 | 25 | type trapResult struct { 26 | Error string `json:"error,omitempty"` 27 | Duration time.Duration // doesn't come from broker 28 | Filtered uint64 `json:"filtered,omitempty"` 29 | Stats uint64 `json:"stats"` 30 | } 31 | 32 | func (tr *trapResult) String() string { 33 | ret := fmt.Sprintf("stats: %d, filtered: %d", tr.Stats, tr.Filtered) 34 | if tr.Error != "" { 35 | ret += ", error: " + tr.Error 36 | } 37 | return ret 38 | } 39 | func (m *CirconusMetrics) submit(output Metrics, newMetrics map[string]*apiclient.CheckBundleMetric) { 40 | 41 | // if there is nowhere to send metrics to, just return. 42 | if !m.check.IsReady() { 43 | m.Log.Printf("check not ready, skipping metric submission") 44 | return 45 | } 46 | 47 | // update check if there are any new metrics or, if metric tags have been added since last submit 48 | m.check.UpdateCheck(newMetrics) 49 | 50 | str, err := json.Marshal(output) 51 | if err != nil { 52 | m.Log.Printf("error preparing metrics %s", err) 53 | return 54 | } 55 | 56 | result, err := m.trapCall(str) 57 | if err != nil { 58 | m.Log.Printf("error sending metrics - %s\n", err) 59 | return 60 | } 61 | 62 | // OK response from circonus-agent does not 63 | // indicate how many metrics were received 64 | if result.Error == "agent" { 65 | result.Stats = uint64(len(output)) 66 | result.Error = "" 67 | } 68 | 69 | if m.Debug || m.DumpMetrics { 70 | if m.DumpMetrics { 71 | m.Log.Printf("payload: %s", string(str)) 72 | } 73 | if m.Debug { 74 | msg := "broker result --" 75 | if result.Error != "" { 76 | if result.Error == "agent" { 77 | msg += " agent does not provide response metrics" 78 | } else { 79 | msg += fmt.Sprintf(" error: %s", result.Error) 80 | } 81 | } else { 82 | msg += fmt.Sprintf(" stats: %d, filtered: %d", result.Stats, result.Filtered) 83 | } 84 | m.Log.Printf(msg+" duration: %s", result.Duration.String()) 85 | } 86 | } 87 | } 88 | 89 | func (m *CirconusMetrics) trapCall(payload []byte) (*trapResult, error) { 90 | trap, err := m.check.GetSubmissionURL() 91 | if err != nil { 92 | return nil, errors.Wrap(err, "trap call") 93 | } 94 | 95 | dataReader := bytes.NewReader(payload) 96 | 97 | reqStart := time.Now() 98 | 99 | req, err := retryablehttp.NewRequest("PUT", trap.URL.String(), dataReader) 100 | if err != nil { 101 | return nil, err 102 | } 103 | req.Header.Add("Content-Type", "application/json") 104 | req.Header.Add("Accept", "application/json") 105 | req.Header.Set("Connection", "close") 106 | req.Header.Set("User-Agent", "cgm") 107 | req.Header.Set("Content-Length", strconv.Itoa(len(payload))) 108 | req.Close = true 109 | 110 | // keep last HTTP error in the event of retry failure 111 | var lastHTTPError error 112 | retryPolicy := func(ctx context.Context, resp *http.Response, err error) (bool, error) { 113 | if ctxErr := ctx.Err(); ctxErr != nil { 114 | return false, ctxErr 115 | } 116 | 117 | if err != nil { 118 | lastHTTPError = err 119 | return true, errors.Wrap(err, "retry policy") 120 | } 121 | // Check the response code. We retry on 500-range responses to allow 122 | // the server time to recover, as 500's are typically not permanent 123 | // errors and may relate to outages on the server side. This will catch 124 | // invalid response codes as well, like 0 and 999. 125 | if resp.StatusCode == 0 || resp.StatusCode >= 500 { 126 | body, readErr := ioutil.ReadAll(resp.Body) 127 | if readErr != nil { 128 | lastHTTPError = fmt.Errorf("- last HTTP error: %d %w", resp.StatusCode, readErr) 129 | } else { 130 | lastHTTPError = fmt.Errorf("- last HTTP error: %d %s", resp.StatusCode, string(body)) 131 | } 132 | return true, nil 133 | } 134 | return false, nil 135 | } 136 | 137 | client := retryablehttp.NewClient() 138 | switch { 139 | case trap.URL.Scheme == "https": 140 | client.HTTPClient.Transport = &http.Transport{ 141 | Proxy: http.ProxyFromEnvironment, 142 | DialContext: (&net.Dialer{ 143 | Timeout: 10 * time.Second, 144 | KeepAlive: 3 * time.Second, 145 | FallbackDelay: -1 * time.Millisecond, 146 | }).DialContext, 147 | TLSHandshakeTimeout: 10 * time.Second, 148 | TLSClientConfig: trap.TLS, 149 | DisableKeepAlives: true, 150 | MaxIdleConns: 1, 151 | MaxIdleConnsPerHost: -1, 152 | DisableCompression: false, 153 | } 154 | case trap.URL.Scheme == "http": 155 | client.HTTPClient.Transport = &http.Transport{ 156 | Proxy: http.ProxyFromEnvironment, 157 | DialContext: (&net.Dialer{ 158 | Timeout: 10 * time.Second, 159 | KeepAlive: 3 * time.Second, 160 | FallbackDelay: -1 * time.Millisecond, 161 | }).DialContext, 162 | DisableKeepAlives: true, 163 | MaxIdleConns: 1, 164 | MaxIdleConnsPerHost: -1, 165 | DisableCompression: false, 166 | } 167 | case trap.IsSocket: 168 | m.Log.Printf("using socket transport\n") 169 | client.HTTPClient.Transport = trap.SockTransport 170 | default: 171 | return nil, fmt.Errorf("unknown scheme (%s), skipping submission", trap.URL.Scheme) 172 | } 173 | client.RetryWaitMin = 1 * time.Second 174 | client.RetryWaitMax = 5 * time.Second 175 | client.RetryMax = 3 176 | // retryablehttp only groks log or no log 177 | // but, outputs everything as [DEBUG] messages 178 | if m.Debug { 179 | client.Logger = m.Log 180 | } else { 181 | client.Logger = log.New(ioutil.Discard, "", log.LstdFlags) 182 | } 183 | client.CheckRetry = retryPolicy 184 | 185 | client.ResponseLogHook = func(logger retryablehttp.Logger, r *http.Response) { 186 | if r.StatusCode != http.StatusOK { 187 | logger.Printf("non-200 response (%s): %s", r.Request.URL.String(), r.Status) 188 | } 189 | } 190 | 191 | attempts := -1 192 | client.RequestLogHook = func(logger retryablehttp.Logger, req *http.Request, retryNumber int) { 193 | if retryNumber > 0 { 194 | reqStart = time.Now() 195 | logger.Printf("retrying (%s), retry %d", req.URL.String(), retryNumber) 196 | } 197 | attempts = retryNumber 198 | } 199 | 200 | resp, err := client.Do(req) 201 | if resp != nil { 202 | defer resp.Body.Close() 203 | } 204 | if err != nil { 205 | if lastHTTPError != nil { 206 | return nil, fmt.Errorf("submitting: %w previous: %s attempts: %d", err, lastHTTPError, attempts) 207 | } 208 | if attempts == client.RetryMax { 209 | if err = m.check.RefreshTrap(); err != nil { 210 | return nil, fmt.Errorf("refreshing trap: %w", err) 211 | } 212 | } 213 | return nil, fmt.Errorf("trap call: %w", err) 214 | } 215 | 216 | dur := time.Since(reqStart) 217 | 218 | // no content - expected result from 219 | // circonus-agent when metrics accepted 220 | if resp.StatusCode == http.StatusNoContent { 221 | _, _ = io.Copy(ioutil.Discard, resp.Body) 222 | return &trapResult{Stats: 0, Filtered: 0, Error: "agent", Duration: dur}, nil 223 | } 224 | 225 | body, err := ioutil.ReadAll(resp.Body) 226 | if err != nil { 227 | return nil, fmt.Errorf("error reading body: %w", err) 228 | } 229 | 230 | if resp.StatusCode != http.StatusOK { 231 | return nil, fmt.Errorf("bad response code: %d (%s)", resp.StatusCode, string(body)) 232 | } 233 | 234 | var result trapResult 235 | if err := json.Unmarshal(body, &result); err != nil { 236 | return nil, fmt.Errorf("error parsing body: %w (%s)", err, body) 237 | } 238 | 239 | result.Duration = dur 240 | return &result, nil 241 | } 242 | -------------------------------------------------------------------------------- /circonus-gometrics_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Circonus, Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package circonusgometrics 6 | 7 | import ( 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "io/ioutil" 12 | "net/http" 13 | "net/http/httptest" 14 | "testing" 15 | "time" 16 | ) 17 | 18 | func testServer() *httptest.Server { 19 | f := func(w http.ResponseWriter, r *http.Request) { 20 | // fmt.Printf("%s %s\n", r.Method, r.URL.String()) 21 | switch r.URL.Path { 22 | case "/metrics_endpoint": // submit metrics 23 | switch r.Method { 24 | case "POST": 25 | fallthrough 26 | case "PUT": 27 | defer r.Body.Close() 28 | b, err := ioutil.ReadAll(r.Body) 29 | if err != nil { 30 | panic(err) 31 | } 32 | var ret []byte 33 | var r interface{} 34 | err = json.Unmarshal(b, &r) 35 | if err != nil { 36 | ret, err = json.Marshal(err) 37 | if err != nil { 38 | panic(err) 39 | } 40 | } else { 41 | ret, err = json.Marshal(r) 42 | if err != nil { 43 | panic(err) 44 | } 45 | } 46 | w.WriteHeader(200) 47 | w.Header().Set("Content-Type", "application/json") 48 | fmt.Fprintln(w, string(ret)) 49 | default: 50 | w.WriteHeader(500) 51 | fmt.Fprintln(w, "unsupported method") 52 | } 53 | default: 54 | msg := fmt.Sprintf("not found %s", r.URL.Path) 55 | w.WriteHeader(404) 56 | fmt.Fprintln(w, msg) 57 | } 58 | } 59 | 60 | return httptest.NewServer(http.HandlerFunc(f)) 61 | } 62 | 63 | func TestNew(t *testing.T) { 64 | 65 | t.Log("invalid config (none)") 66 | { 67 | expectedError := errors.New("invalid configuration (nil)") 68 | _, err := New(nil) 69 | if err == nil || err.Error() != expectedError.Error() { 70 | t.Fatalf("Expected an '%#v' error, got '%#v'", expectedError, err) 71 | } 72 | } 73 | 74 | t.Log("no API token, no submission URL") 75 | { 76 | cfg := &Config{} 77 | expectedError := errors.New("creating new check manager: invalid check manager configuration (no API token AND no submission url)") 78 | _, err := New(cfg) 79 | if err == nil || err.Error() != expectedError.Error() { 80 | t.Fatalf("Expected an '%#v' error, got '%#v'", expectedError, err) 81 | } 82 | } 83 | 84 | t.Log("no API token, submission URL only") 85 | { 86 | cfg := &Config{} 87 | cfg.CheckManager.Check.SubmissionURL = "http://127.0.0.1:56104/blah/blah" 88 | 89 | cm, err := New(cfg) 90 | if err != nil { 91 | t.Fatalf("Expected no error, got '%v'", err) 92 | } 93 | 94 | for !cm.check.IsReady() { 95 | t.Log("\twaiting for cm to init") 96 | time.Sleep(1 * time.Second) 97 | } 98 | 99 | trap, err := cm.check.GetSubmissionURL() 100 | if err != nil { 101 | t.Fatalf("Expected no error, got '%v'", err) 102 | } 103 | 104 | if trap.URL.String() != cfg.CheckManager.Check.SubmissionURL { 105 | t.Fatalf("Expected '%s' == '%s'", trap.URL.String(), cfg.CheckManager.Check.SubmissionURL) 106 | } 107 | } 108 | 109 | t.Log("no Log, Debug = true") 110 | { 111 | cfg := &Config{ 112 | Debug: true, 113 | } 114 | cfg.CheckManager.Check.SubmissionURL = "http://127.0.0.1:56104/blah/blah" 115 | _, err := New(cfg) 116 | if err != nil { 117 | t.Fatalf("Expected no error, got '%v'", err) 118 | } 119 | } 120 | 121 | t.Log("flush interval [good]") 122 | { 123 | cfg := &Config{ 124 | Interval: "30s", 125 | } 126 | cfg.CheckManager.Check.SubmissionURL = "http://127.0.0.1:56104/blah/blah" 127 | _, err := New(cfg) 128 | if err != nil { 129 | t.Errorf("Expected no error, got '%v'", err) 130 | } 131 | } 132 | t.Log("flush interval [bad]") 133 | { 134 | cfg := &Config{ 135 | Interval: "thirty seconds", 136 | } 137 | expectedError := errors.New(`parsing flush interval: time: invalid duration "thirty seconds"`) 138 | _, err := New(cfg) 139 | if err == nil { 140 | t.Fatal("expected error") 141 | } 142 | if err.Error() != expectedError.Error() { 143 | t.Fatalf("Expected %v got '%v'", expectedError, err) 144 | } 145 | } 146 | 147 | t.Log("reset counters [good(true)]") 148 | { 149 | cfg := &Config{ 150 | ResetCounters: "true", 151 | } 152 | cfg.CheckManager.Check.SubmissionURL = "http://127.0.0.1:56104/blah/blah" 153 | _, err := New(cfg) 154 | if err != nil { 155 | t.Errorf("Expected no error, got '%v'", err) 156 | } 157 | } 158 | t.Log("reset counters [good(1)]") 159 | { 160 | cfg := &Config{ 161 | ResetCounters: "1", 162 | } 163 | cfg.CheckManager.Check.SubmissionURL = "http://127.0.0.1:56104/blah/blah" 164 | _, err := New(cfg) 165 | if err != nil { 166 | t.Errorf("Expected no error, got '%v'", err) 167 | } 168 | } 169 | t.Log("reset counters [bad(yes)]") 170 | { 171 | cfg := &Config{ 172 | ResetCounters: "yes", 173 | } 174 | expectedError := errors.New("parsing reset counters: strconv.ParseBool: parsing \"yes\": invalid syntax") 175 | _, err := New(cfg) 176 | if err == nil { 177 | t.Fatal("expected error") 178 | } 179 | if err.Error() != expectedError.Error() { 180 | t.Fatalf("Expected %v got '%v'", expectedError, err) 181 | } 182 | } 183 | 184 | t.Log("reset gauges [good(true)]") 185 | { 186 | cfg := &Config{ 187 | ResetGauges: "true", 188 | } 189 | cfg.CheckManager.Check.SubmissionURL = "http://127.0.0.1:56104/blah/blah" 190 | _, err := New(cfg) 191 | if err != nil { 192 | t.Errorf("Expected no error, got '%v'", err) 193 | } 194 | } 195 | t.Log("reset gauges [good(1)]") 196 | { 197 | cfg := &Config{ 198 | ResetGauges: "1", 199 | } 200 | cfg.CheckManager.Check.SubmissionURL = "http://127.0.0.1:56104/blah/blah" 201 | _, err := New(cfg) 202 | if err != nil { 203 | t.Errorf("Expected no error, got '%v'", err) 204 | } 205 | } 206 | t.Log("reset gauges [bad(yes)]") 207 | { 208 | cfg := &Config{ 209 | ResetGauges: "yes", 210 | } 211 | expectedError := errors.New("parsing reset gauges: strconv.ParseBool: parsing \"yes\": invalid syntax") 212 | _, err := New(cfg) 213 | if err == nil { 214 | t.Fatal("expected error") 215 | } 216 | if err.Error() != expectedError.Error() { 217 | t.Fatalf("Expected %v got '%v'", expectedError, err) 218 | } 219 | } 220 | 221 | t.Log("reset histograms [good(true)]") 222 | { 223 | cfg := &Config{ 224 | ResetHistograms: "true", 225 | } 226 | cfg.CheckManager.Check.SubmissionURL = "http://127.0.0.1:56104/blah/blah" 227 | _, err := New(cfg) 228 | if err != nil { 229 | t.Errorf("Expected no error, got '%v'", err) 230 | } 231 | } 232 | t.Log("reset histograms [good(1)]") 233 | { 234 | cfg := &Config{ 235 | ResetHistograms: "1", 236 | } 237 | cfg.CheckManager.Check.SubmissionURL = "http://127.0.0.1:56104/blah/blah" 238 | _, err := New(cfg) 239 | if err != nil { 240 | t.Errorf("Expected no error, got '%v'", err) 241 | } 242 | } 243 | t.Log("reset histograms [bad(yes)]") 244 | { 245 | cfg := &Config{ 246 | ResetHistograms: "yes", 247 | } 248 | expectedError := errors.New("parsing reset histograms: strconv.ParseBool: parsing \"yes\": invalid syntax") 249 | _, err := New(cfg) 250 | if err == nil { 251 | t.Fatal("expected error") 252 | } 253 | if err.Error() != expectedError.Error() { 254 | t.Fatalf("Expected %v got '%v'", expectedError, err) 255 | } 256 | } 257 | 258 | t.Log("reset text metrics [good(true)]") 259 | { 260 | cfg := &Config{ 261 | ResetText: "true", 262 | } 263 | cfg.CheckManager.Check.SubmissionURL = "http://127.0.0.1:56104/blah/blah" 264 | _, err := New(cfg) 265 | if err != nil { 266 | t.Errorf("Expected no error, got '%v'", err) 267 | } 268 | } 269 | t.Log("reset text metrics [good(1)]") 270 | { 271 | cfg := &Config{ 272 | ResetText: "1", 273 | } 274 | cfg.CheckManager.Check.SubmissionURL = "http://127.0.0.1:56104/blah/blah" 275 | _, err := New(cfg) 276 | if err != nil { 277 | t.Errorf("Expected no error, got '%v'", err) 278 | } 279 | } 280 | t.Log("reset text metrics [bad(yes)]") 281 | { 282 | cfg := &Config{ 283 | ResetText: "yes", 284 | } 285 | expectedError := errors.New("parsing reset text: strconv.ParseBool: parsing \"yes\": invalid syntax") 286 | _, err := New(cfg) 287 | if err == nil { 288 | t.Fatal("expected error") 289 | } 290 | if err.Error() != expectedError.Error() { 291 | t.Fatalf("Expected %v got '%v'", expectedError, err) 292 | } 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /OPTIONS.md: -------------------------------------------------------------------------------- 1 | # Circonus gometrics options 2 | 3 | ## Example defaults 4 | 5 | ```go 6 | package main 7 | 8 | import ( 9 | "fmt" 10 | "io/ioutil" 11 | "log" 12 | "os" 13 | "path" 14 | 15 | cgm "github.com/circonus-labs/circonus-gometrics/v3" 16 | ) 17 | 18 | func main() { 19 | cfg := &cgm.Config{} 20 | 21 | // Defaults 22 | 23 | // General 24 | cfg.Debug = false 25 | cfg.Log = log.New(ioutil.Discard, "", log.LstdFlags) 26 | cfg.Interval = "10s" 27 | cfg.ResetCounters = "true" 28 | cfg.ResetGauges = "true" 29 | cfg.ResetHistograms = "true" 30 | cfg.ResetText = "true" 31 | 32 | // API 33 | cfg.CheckManager.API.TokenKey = "" 34 | cfg.CheckManager.API.TokenApp = "circonus-gometrics" 35 | cfg.CheckManager.API.TokenURL = "https://api.circonus.com/v2" 36 | cfg.CheckManager.API.CACert = nil 37 | cfg.CheckManager.API.TLSConfig = nil 38 | 39 | // Check 40 | _, an := path.Split(os.Args[0]) 41 | hn, _ := os.Hostname() 42 | cfg.CheckManager.Check.ID = "" 43 | cfg.CheckManager.Check.SubmissionURL = "" 44 | cfg.CheckManager.Check.InstanceID = fmt.Sprintf("%s:%s", hn, an) 45 | cfg.CheckManager.Check.TargetHost = cfg.CheckManager.Check.InstanceID 46 | cfg.CheckManager.Check.DisplayName = cfg.CheckManager.Check.InstanceID 47 | cfg.CheckManager.Check.SearchTag = fmt.Sprintf("service:%s", an) 48 | cfg.CheckManager.Check.Tags = "" 49 | cfg.CheckManager.Check.Secret = "" // randomly generated sha256 hash 50 | cfg.CheckManager.Check.MaxURLAge = "5m" 51 | cfg.CheckManager.Check.ForceMetricActivation = "false" 52 | 53 | // Broker 54 | cfg.CheckManager.Broker.ID = "" 55 | cfg.CheckManager.Broker.SelectTag = "" 56 | cfg.CheckManager.Broker.MaxResponseTime = "500ms" 57 | cfg.CheckManager.Broker.TLSConfig = nil 58 | 59 | // create a new cgm instance and start sending metrics... 60 | // see the complete example in the main README. 61 | } 62 | ``` 63 | 64 | ## Options 65 | 66 | | Option | Default | Description | 67 | | ------ | ------- | ----------- | 68 | | General || 69 | | `cfg.Log` | none | log.Logger instance to send logging messages. Default is to discard messages. If Debug is turned on and no instance is specified, messages will go to stderr. | 70 | | `cfg.Debug` | false | Turn on debugging messages. | 71 | | `cfg.Interval` | "10s" | Interval at which metrics are flushed and sent to Circonus. Set to "0s" to disable automatic flush (note, if disabled, `cgm.Flush()` must be called manually to send metrics to Circonus).| 72 | | `cfg.ResetCounters` | "true" | Reset counter metrics after each submission. Change to "false" to retain (and continue submitting) the last value.| 73 | | `cfg.ResetGauges` | "true" | Reset gauge metrics after each submission. Change to "false" to retain (and continue submitting) the last value.| 74 | | `cfg.ResetHistograms` | "true" | Reset histogram metrics after each submission. Change to "false" to retain (and continue submitting) the last value.| 75 | | `cfg.ResetText` | "true" | Reset text metrics after each submission. Change to "false" to retain (and continue submitting) the last value.| 76 | |API|| 77 | | `cfg.CheckManager.API.TokenKey` | "" | [Circonus API Token key](https://login.circonus.com/user/tokens) | 78 | | `cfg.CheckManager.API.TokenApp` | "circonus-gometrics" | App associated with API token | 79 | | `cfg.CheckManager.API.URL` | "https://api.circonus.com/v2" | Circonus API URL | 80 | | `cfg.CheckManager.API.TLSConfig` | nil | Custom tls.Config to use when communicating with Circonus API | 81 | | `cfg.CheckManager.API.CACert` | nil | DEPRECATED - use TLSConfig ~~[*x509.CertPool](https://golang.org/pkg/crypto/x509/#CertPool) with CA Cert to validate API endpoint using internal CA or self-signed certificates~~ | 82 | |Check|| 83 | | `cfg.CheckManager.Check.ID` | "" | Check ID of previously created check. (*Note: **check id** not **check bundle id**.*) | 84 | | `cfg.CheckManager.Check.SubmissionURL` | "" | Submission URL of previously created check. Metrics can also be sent to a local [circonus-agent](https://github.com/circonus-labs/circonus-agent) by using the agent's URL (e.g. `http://127.0.0.1:2609/write/appid` where `appid` is a unique identifier for the application which will prefix all metrics. Additionally, the circonus-agent can optionally listen for requests to `/write` on a unix socket - to leverage this feature, use a URL such as `http+unix:///path/to/socket_file/write/appid`). | 85 | | `cfg.CheckManager.Check.InstanceID` | hostname:program name | An identifier for the 'group of metrics emitted by this process or service'. | 86 | | `cfg.CheckManager.Check.TargetHost` | InstanceID | Explicit setting of `check.target`. | 87 | | `cfg.CheckManager.Check.DisplayName` | InstanceID | Custom `check.display_name`. Shows in UI check list. | 88 | | `cfg.CheckManager.Check.SearchTag` | service:program name | Specific tag used to search for an existing check when neither SubmissionURL nor ID are provided. | 89 | | `cfg.CheckManager.Check.Tags` | "" | List (comma separated) of tags to add to check when it is being created. The SearchTag will be added to the list. | 90 | | `cfg.CheckManager.Check.Secret` | random generated | A secret to use for when creating an httptrap check. | 91 | | `cfg.CheckManager.Check.MaxURLAge` | "5m" | Maximum amount of time to retry a [failing] submission URL before refreshing it. | 92 | | `cfg.CheckManager.Check.ForceMetricActivation` | "false" | If a metric has been disabled via the UI the default behavior is to *not* re-activate the metric; this setting overrides the behavior and will re-activate the metric when it is encountered. | 93 | |Broker|| 94 | | `cfg.CheckManager.Broker.ID` | "" | ID of a specific broker to use when creating a check. Default is to use a random enterprise broker or the public Circonus default broker. | 95 | | `cfg.CheckManager.Broker.SelectTag` | "" | Used to select a broker with the same tag(s). If more than one broker has the tag(s), one will be selected randomly from the resulting list. (e.g. could be used to select one from a list of brokers serving a specific colo/region. "dc:sfo", "loc:nyc,dc:nyc01", "zone:us-west") | 96 | | `cfg.CheckManager.Broker.MaxResponseTime` | "500ms" | Maximum amount time to wait for a broker connection test to be considered valid. (if latency is > the broker will be considered invalid and not available for selection.) | 97 | | `cfg.CheckManager.Broker.TLSConfig` | nil | Custom tls.Config to use when communicating with Circonus Broker | 98 | 99 | ## Notes 100 | 101 | * All options are *strings* with the following exceptions: 102 | * `cfg.Log` - an instance of [`log.Logger`](https://golang.org/pkg/log/#Logger) or something else (e.g. [logrus](https://github.com/Sirupsen/logrus)) which can be used to satisfy the interface requirements. 103 | * `cfg.Debug` - a boolean true|false. 104 | * At a minimum, one of either `API.TokenKey` or `Check.SubmissionURL` is **required** for cgm to function. 105 | * Check management can be disabled by providing a `Check.SubmissionURL` without an `API.TokenKey`. Note: the supplied URL needs to be http or the broker needs to be running with a cert which can be verified. Otherwise, the `API.TokenKey` will be required to retrieve the correct CA certificate to validate the broker's cert for the SSL connection. 106 | * A note on `Check.InstanceID`, the instance id is used to consistently identify a check. The display name can be changed in the UI. The hostname may be ephemeral. For metric continuity, the instance id is used to locate existing checks. Since the check.target is never actually used by an httptrap check it is more decorative than functional, a valid FQDN is not required for an httptrap check.target. But, using instance id as the target can pollute the Host list in the UI with host:application specific entries. 107 | * Check identification precedence 108 | 1. Check SubmissionURL 109 | 2. Check ID 110 | 3. Search 111 | 1. Search for an active httptrap check for TargetHost which has the SearchTag 112 | 2. Search for an active httptrap check which has the SearchTag and the InstanceID in the notes field 113 | 3. Create a new check 114 | * Broker selection 115 | 1. If Broker.ID or Broker.SelectTag are not specified, a broker will be selected randomly from the list of brokers available to the API token. Enterprise brokers take precedence. A viable broker is "active", has the "httptrap" module enabled, and responds within Broker.MaxResponseTime. 116 | -------------------------------------------------------------------------------- /checkmgr/metrics_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Circonus, Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package checkmgr 6 | 7 | import ( 8 | "reflect" 9 | "testing" 10 | 11 | apiclient "github.com/circonus-labs/go-apiclient" 12 | ) 13 | 14 | func TestIsMetricActive(t *testing.T) { 15 | 16 | cm := &CheckManager{manageMetrics: true} 17 | 18 | cm.availableMetrics = map[string]bool{ 19 | "foo": true, 20 | "baz": false, 21 | } 22 | 23 | t.Log("'foo' in active metric list") 24 | { 25 | if !cm.IsMetricActive("foo") { 26 | t.Error("Expected true") 27 | } 28 | } 29 | 30 | t.Log("'bar' not in active metric list") 31 | { 32 | if cm.IsMetricActive("bar") { 33 | t.Error("Expected false") 34 | } 35 | } 36 | 37 | t.Log("'baz' in active metric list, not active") 38 | { 39 | if cm.IsMetricActive("baz") { 40 | t.Error("Expected false") 41 | } 42 | } 43 | } 44 | 45 | func TestActivateMetric(t *testing.T) { 46 | cm := &CheckManager{manageMetrics: true} 47 | cm.checkBundle = &apiclient.CheckBundle{} 48 | cm.checkBundle.Metrics = []apiclient.CheckBundleMetric{ 49 | { 50 | Name: "foo", 51 | Type: "numeric", 52 | Status: "active", 53 | }, 54 | { 55 | Name: "bar", 56 | Type: "numeric", 57 | Status: "available", 58 | }, 59 | } 60 | 61 | cm.availableMetrics = make(map[string]bool) 62 | cm.forceMetricActivation = false 63 | 64 | cm.inventoryMetrics() 65 | 66 | t.Log("'foo' already active") 67 | { 68 | if cm.ActivateMetric("foo") { 69 | t.Error("Expected false") 70 | } 71 | } 72 | 73 | t.Log("'bar' in list but not active [force=false]") 74 | { 75 | if cm.ActivateMetric("bar") { 76 | t.Error("Expected false") 77 | } 78 | } 79 | 80 | t.Log("'baz' not in list") 81 | { 82 | if !cm.ActivateMetric("baz") { 83 | t.Error("Expected true") 84 | } 85 | } 86 | 87 | cm.forceMetricActivation = true 88 | 89 | t.Log("'bar' in list but not active [force=true]") 90 | { 91 | if !cm.ActivateMetric("bar") { 92 | t.Error("Expected true") 93 | } 94 | } 95 | } 96 | 97 | func TestInventoryMetrics(t *testing.T) { 98 | cm := &CheckManager{} 99 | cm.checkBundle = &apiclient.CheckBundle{} 100 | cm.checkBundle.Metrics = []apiclient.CheckBundleMetric{ 101 | { 102 | Name: "foo", 103 | Type: "numeric", 104 | Status: "active", 105 | }, 106 | { 107 | Name: "bar", 108 | Type: "numeric", 109 | Status: "available", 110 | }, 111 | } 112 | 113 | cm.availableMetrics = make(map[string]bool) 114 | cm.inventoryMetrics() 115 | 116 | expectedMetrics := make(map[string]bool) 117 | expectedMetrics["foo"] = true 118 | expectedMetrics["bar"] = false 119 | 120 | t.Log("'foo', in inventory and active") 121 | { 122 | active, exists := cm.availableMetrics["foo"] 123 | if !active { 124 | t.Fatalf("Expected active") 125 | } 126 | if !exists { 127 | t.Fatalf("Expected exists") 128 | } 129 | } 130 | 131 | t.Log("'bar', in inventory and not active") 132 | { 133 | active, exists := cm.availableMetrics["bar"] 134 | if active { 135 | t.Fatalf("Expected not active") 136 | } 137 | if !exists { 138 | t.Fatalf("Expected exists") 139 | } 140 | } 141 | 142 | t.Log("'baz', not in inventory and not active") 143 | { 144 | active, exists := cm.availableMetrics["baz"] 145 | if active { 146 | t.Fatalf("Expected not active") 147 | } 148 | if exists { 149 | t.Fatalf("Expected not exists") 150 | } 151 | } 152 | } 153 | 154 | func TestAddMetricTags(t *testing.T) { 155 | cm := &CheckManager{manageMetrics: true} 156 | cm.checkBundle = &apiclient.CheckBundle{} 157 | cm.metricTags = make(map[string][]string) 158 | 159 | t.Log("no tags") 160 | { 161 | if cm.AddMetricTags("foo", []string{}, false) { 162 | t.Fatalf("Expected false") 163 | } 164 | } 165 | 166 | t.Log("no metric named 'foo'") 167 | { 168 | if !cm.AddMetricTags("foo", []string{"cat:tag"}, false) { 169 | t.Fatalf("Expected true") 170 | } 171 | } 172 | 173 | cm.checkBundle.Metrics = []apiclient.CheckBundleMetric{ 174 | { 175 | Name: "bar", 176 | Type: "numeric", 177 | Status: "active", 178 | }, 179 | { 180 | Name: "foo", 181 | Type: "numeric", 182 | Status: "active", 183 | }, 184 | { 185 | Name: "baz", 186 | Type: "numeric", 187 | Status: "active", 188 | Tags: []string{"cat1:tag1"}, 189 | }, 190 | } 191 | 192 | t.Log("metric named 'bar', add tag") 193 | { 194 | 195 | cm.metricTags = make(map[string][]string) 196 | // append, zero current 197 | if !cm.AddMetricTags("bar", []string{"cat:tag"}, true) { 198 | t.Fatalf("Expected true") 199 | } 200 | expected := make(map[string][]string) 201 | expected["bar"] = []string{"cat:tag"} 202 | 203 | if !reflect.DeepEqual(cm.metricTags, expected) { 204 | t.Fatalf("expected %v got %+v", expected, cm.metricTags) 205 | } 206 | 207 | // tag already exists, no need to add 208 | if cm.AddMetricTags("bar", []string{"cat:tag"}, true) { 209 | t.Fatalf("Expected false") 210 | } 211 | 212 | if !reflect.DeepEqual(cm.metricTags, expected) { 213 | t.Fatalf("expected %v got %+v", expected, cm.metricTags) 214 | } 215 | 216 | // append, zero tags 217 | if cm.AddMetricTags("bar", []string{}, true) { 218 | t.Fatalf("Expected false") 219 | } 220 | 221 | if !reflect.DeepEqual(cm.metricTags, expected) { 222 | t.Fatalf("expected %v got %+v", expected, cm.metricTags) 223 | } 224 | } 225 | 226 | t.Log("metric named 'baz', add tag") 227 | { 228 | cm.metricTags = make(map[string][]string) 229 | 230 | // append, current tag, should be noupdate 231 | if cm.AddMetricTags("baz", []string{"cat1:tag1"}, true) { 232 | t.Fatalf("Expected false") 233 | } 234 | 235 | if _, found := cm.metricTags["baz"]; found { 236 | t.Fatalf("expected not found") 237 | } 238 | 239 | // append, one current 240 | if !cm.AddMetricTags("baz", []string{"cat2:tag2"}, true) { 241 | t.Fatalf("Expected true") 242 | } 243 | expected := make(map[string][]string) 244 | expected["baz"] = []string{"cat1:tag1", "cat2:tag2"} 245 | 246 | if !reflect.DeepEqual(cm.metricTags, expected) { 247 | t.Fatalf("expected %v got %+v", expected, cm.metricTags) 248 | } 249 | 250 | // append, tag already exists (should be noupdate) 251 | if cm.AddMetricTags("baz", []string{"cat2:tag2"}, true) { 252 | t.Fatalf("Expected false") 253 | } 254 | } 255 | 256 | t.Log("metric named 'foo', set tag") 257 | { 258 | expected := make(map[string][]string) 259 | cm.metricTags = make(map[string][]string) 260 | 261 | // set tag 262 | if !cm.AddMetricTags("foo", []string{"cat:tag"}, false) { 263 | t.Fatalf("Expected true") 264 | } 265 | expected["foo"] = []string{"cat:tag"} 266 | if !reflect.DeepEqual(cm.metricTags, expected) { 267 | t.Fatalf("expected %v got %+v", expected, cm.metricTags) 268 | } 269 | 270 | // set, reset (pass 0 tags) 271 | if !cm.AddMetricTags("foo", []string{}, false) { 272 | t.Fatalf("Expected true") 273 | } 274 | 275 | if _, found := cm.metricTags["foo"]; !found { 276 | t.Fatal("expected found") 277 | } 278 | 279 | expected["foo"] = []string{} 280 | if !reflect.DeepEqual(cm.metricTags, expected) { 281 | t.Fatalf("expected %v got %+v", expected, cm.metricTags) 282 | } 283 | 284 | // set tag 285 | if !cm.AddMetricTags("foo", []string{"cat:tag"}, false) { 286 | t.Fatalf("Expected true") 287 | } 288 | 289 | expected["foo"] = []string{"cat:tag"} 290 | if !reflect.DeepEqual(cm.metricTags, expected) { 291 | t.Fatalf("expected %v got %+v", expected, cm.metricTags) 292 | } 293 | 294 | // set, no update (same tag) 295 | if cm.AddMetricTags("foo", []string{"cat:tag"}, false) { 296 | t.Fatalf("Expected false") 297 | } 298 | 299 | expected["foo"] = []string{"cat:tag"} 300 | if !reflect.DeepEqual(cm.metricTags, expected) { 301 | t.Fatalf("expected %v got %+v", expected, cm.metricTags) 302 | } 303 | 304 | // replace any existing 305 | if !cm.AddMetricTags("foo", []string{"cat:newtag"}, false) { 306 | t.Fatalf("Expected true") 307 | } 308 | 309 | expected["foo"] = []string{"cat:newtag"} 310 | if !reflect.DeepEqual(cm.metricTags, expected) { 311 | t.Fatalf("expected %v got %+v", expected, cm.metricTags) 312 | } 313 | 314 | } 315 | } 316 | 317 | func TestAddNewMetrics(t *testing.T) { 318 | cm := &CheckManager{} 319 | 320 | newMetrics := make(map[string]*apiclient.CheckBundleMetric) 321 | 322 | newMetrics["foo"] = &apiclient.CheckBundleMetric{ 323 | Name: "foo", 324 | Type: "numeric", 325 | Status: "active", 326 | } 327 | 328 | t.Log("no check bundle") 329 | { 330 | if cm.addNewMetrics(newMetrics) { 331 | t.Fatalf("Expected false") 332 | } 333 | } 334 | 335 | cm.checkBundle = &apiclient.CheckBundle{} 336 | t.Log("no check bundle metrics") 337 | { 338 | if !cm.addNewMetrics(newMetrics) { 339 | t.Fatalf("Expected true") 340 | } 341 | if !cm.forceCheckUpdate { 342 | t.Fatal("Expected forceCheckUpdate to be true") 343 | } 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /circonus-gometrics.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Circonus, Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package circonusgometrics provides instrumentation for your applications in the form 6 | // of counters, gauges and histograms and allows you to publish them to 7 | // Circonus 8 | // 9 | // Counters 10 | // 11 | // A counter is a monotonically-increasing, unsigned, 64-bit integer used to 12 | // represent the number of times an event has occurred. By tracking the deltas 13 | // between measurements of a counter over intervals of time, an aggregation 14 | // layer can derive rates, acceleration, etc. 15 | // 16 | // Gauges 17 | // 18 | // A gauge returns instantaneous measurements of something using signed, 64-bit 19 | // integers. This value does not need to be monotonic. 20 | // 21 | // Histograms 22 | // 23 | // A histogram tracks the distribution of a stream of values (e.g. the number of 24 | // seconds it takes to handle requests). Circonus can calculate complex 25 | // analytics on these. 26 | // 27 | // Reporting 28 | // 29 | // A period push to a Circonus httptrap is confgurable. 30 | package circonusgometrics 31 | 32 | import ( 33 | "crypto/tls" 34 | "fmt" 35 | "io/ioutil" 36 | "log" 37 | "os" 38 | "regexp" 39 | "strconv" 40 | "strings" 41 | "sync" 42 | "time" 43 | 44 | "github.com/circonus-labs/circonus-gometrics/v3/checkmgr" 45 | "github.com/circonus-labs/go-apiclient" 46 | "github.com/pkg/errors" 47 | ) 48 | 49 | const ( 50 | defaultFlushInterval = "10s" // 10 * time.Second 51 | 52 | // MetricTypeInt32 reconnoiter 53 | MetricTypeInt32 = "i" 54 | 55 | // MetricTypeUint32 reconnoiter 56 | MetricTypeUint32 = "I" 57 | 58 | // MetricTypeInt64 reconnoiter 59 | MetricTypeInt64 = "l" 60 | 61 | // MetricTypeUint64 reconnoiter 62 | MetricTypeUint64 = "L" 63 | 64 | // MetricTypeFloat64 reconnoiter 65 | MetricTypeFloat64 = "n" 66 | 67 | // MetricTypeString reconnoiter 68 | MetricTypeString = "s" 69 | 70 | // MetricTypeHistogram reconnoiter 71 | MetricTypeHistogram = "h" 72 | 73 | // MetricTypeCumulativeHistogram reconnoiter 74 | MetricTypeCumulativeHistogram = "H" 75 | ) 76 | 77 | var ( 78 | metricTypeRx = regexp.MustCompile(`^[` + strings.Join([]string{ 79 | MetricTypeInt32, 80 | MetricTypeUint32, 81 | MetricTypeInt64, 82 | MetricTypeUint64, 83 | MetricTypeFloat64, 84 | MetricTypeString, 85 | MetricTypeHistogram, 86 | MetricTypeCumulativeHistogram, 87 | }, "") + `]$`) 88 | ) 89 | 90 | // Logger facilitates use of any logger supporting the required methods 91 | // rather than just standard log package log.Logger 92 | type Logger interface { 93 | Printf(string, ...interface{}) 94 | } 95 | 96 | // Metric defines an individual metric 97 | type Metric struct { 98 | Value interface{} `json:"_value"` 99 | Type string `json:"_type"` 100 | Timestamp uint64 `json:"_ts,omitempty"` 101 | } 102 | 103 | // Metrics holds host metrics 104 | type Metrics map[string]Metric 105 | 106 | // Config options for circonus-gometrics 107 | type Config struct { 108 | Log Logger 109 | ResetCounters string // reset/delete counters on flush (default true) 110 | ResetGauges string // reset/delete gauges on flush (default true) 111 | ResetHistograms string // reset/delete histograms on flush (default true) 112 | ResetText string // reset/delete text on flush (default true) 113 | // how frequenly to submit metrics to Circonus, default 10 seconds. 114 | // Set to 0 to disable automatic flushes and call Flush manually. 115 | Interval string 116 | 117 | // API, Check and Broker configuration options 118 | CheckManager checkmgr.Config 119 | 120 | Debug bool 121 | DumpMetrics bool 122 | } 123 | 124 | type prevMetrics struct { 125 | ts time.Time 126 | metricsmu sync.Mutex 127 | metrics *Metrics 128 | } 129 | 130 | // CirconusMetrics state 131 | type CirconusMetrics struct { 132 | Log Logger 133 | lastMetrics *prevMetrics 134 | check *checkmgr.CheckManager 135 | gauges map[string]interface{} 136 | histograms map[string]*Histogram 137 | custom map[string]Metric 138 | text map[string]string 139 | textFuncs map[string]func() string 140 | counterFuncs map[string]func() uint64 141 | gaugeFuncs map[string]func() int64 142 | counters map[string]uint64 143 | submitTimestamp *time.Time 144 | flushInterval time.Duration 145 | flushmu sync.Mutex 146 | packagingmu sync.Mutex 147 | cm sync.Mutex 148 | cfm sync.Mutex 149 | gm sync.Mutex 150 | gfm sync.Mutex 151 | hm sync.Mutex 152 | tm sync.Mutex 153 | tfm sync.Mutex 154 | custm sync.Mutex 155 | flushing bool 156 | Debug bool 157 | DumpMetrics bool 158 | resetCounters bool 159 | resetGauges bool 160 | resetHistograms bool 161 | resetText bool 162 | } 163 | 164 | // NewCirconusMetrics returns a CirconusMetrics instance 165 | func NewCirconusMetrics(cfg *Config) (*CirconusMetrics, error) { 166 | return New(cfg) 167 | } 168 | 169 | // New returns a CirconusMetrics instance 170 | func New(cfg *Config) (*CirconusMetrics, error) { 171 | 172 | if cfg == nil { 173 | return nil, errors.New("invalid configuration (nil)") 174 | } 175 | 176 | cm := &CirconusMetrics{ 177 | counters: make(map[string]uint64), 178 | counterFuncs: make(map[string]func() uint64), 179 | gauges: make(map[string]interface{}), 180 | gaugeFuncs: make(map[string]func() int64), 181 | histograms: make(map[string]*Histogram), 182 | text: make(map[string]string), 183 | textFuncs: make(map[string]func() string), 184 | custom: make(map[string]Metric), 185 | lastMetrics: &prevMetrics{}, 186 | } 187 | 188 | // Logging 189 | { 190 | cm.Debug = cfg.Debug 191 | cm.DumpMetrics = cfg.DumpMetrics 192 | cm.Log = cfg.Log 193 | 194 | if (cm.Debug || cm.DumpMetrics) && cm.Log == nil { 195 | cm.Log = log.New(os.Stderr, "", log.LstdFlags) 196 | } 197 | if cm.Log == nil { 198 | cm.Log = log.New(ioutil.Discard, "", log.LstdFlags) 199 | } 200 | } 201 | 202 | // Flush Interval 203 | { 204 | fi := defaultFlushInterval 205 | if cfg.Interval != "" { 206 | fi = cfg.Interval 207 | } 208 | 209 | dur, err := time.ParseDuration(fi) 210 | if err != nil { 211 | return nil, errors.Wrap(err, "parsing flush interval") 212 | } 213 | cm.flushInterval = dur 214 | } 215 | 216 | // metric resets 217 | 218 | cm.resetCounters = true 219 | if cfg.ResetCounters != "" { 220 | setting, err := strconv.ParseBool(cfg.ResetCounters) 221 | if err != nil { 222 | return nil, errors.Wrap(err, "parsing reset counters") 223 | } 224 | cm.resetCounters = setting 225 | } 226 | 227 | cm.resetGauges = true 228 | if cfg.ResetGauges != "" { 229 | setting, err := strconv.ParseBool(cfg.ResetGauges) 230 | if err != nil { 231 | return nil, errors.Wrap(err, "parsing reset gauges") 232 | } 233 | cm.resetGauges = setting 234 | } 235 | 236 | cm.resetHistograms = true 237 | if cfg.ResetHistograms != "" { 238 | setting, err := strconv.ParseBool(cfg.ResetHistograms) 239 | if err != nil { 240 | return nil, errors.Wrap(err, "parsing reset histograms") 241 | } 242 | cm.resetHistograms = setting 243 | } 244 | 245 | cm.resetText = true 246 | if cfg.ResetText != "" { 247 | setting, err := strconv.ParseBool(cfg.ResetText) 248 | if err != nil { 249 | return nil, errors.Wrap(err, "parsing reset text") 250 | } 251 | cm.resetText = setting 252 | } 253 | 254 | // check manager 255 | { 256 | cfg.CheckManager.Debug = cm.Debug 257 | cfg.CheckManager.Log = cm.Log 258 | 259 | check, err := checkmgr.New(&cfg.CheckManager) 260 | if err != nil { 261 | return nil, errors.Wrap(err, "creating new check manager") 262 | } 263 | cm.check = check 264 | } 265 | 266 | // start initialization (serialized or background) 267 | if err := cm.check.Initialize(); err != nil { 268 | return nil, err 269 | } 270 | 271 | // if automatic flush is enabled, start it. 272 | // NOTE: submit will jettison metrics until initialization has completed. 273 | if cm.flushInterval > time.Duration(0) { 274 | go func() { 275 | for range time.NewTicker(cm.flushInterval).C { 276 | cm.Flush() 277 | } 278 | }() 279 | } 280 | 281 | return cm, nil 282 | } 283 | 284 | // Start deprecated NOP, automatic flush is started in New if flush interval > 0. 285 | func (m *CirconusMetrics) Start() { 286 | // nop 287 | } 288 | 289 | // Ready returns true or false indicating if the check is ready to accept metrics 290 | func (m *CirconusMetrics) Ready() bool { 291 | return m.check.IsReady() 292 | } 293 | 294 | // Custom adds a user defined metric 295 | func (m *CirconusMetrics) Custom(metricName string, metric Metric) error { 296 | if !metricTypeRx.MatchString(metric.Type) { 297 | return fmt.Errorf("unrecognized circonus metric type (%s)", metric.Type) 298 | } 299 | 300 | m.custm.Lock() 301 | m.custom[metricName] = metric 302 | m.custm.Unlock() 303 | 304 | return nil 305 | } 306 | 307 | // GetBrokerTLSConfig returns the tls.Config for the broker 308 | func (m *CirconusMetrics) GetBrokerTLSConfig() *tls.Config { 309 | return m.check.BrokerTLSConfig() 310 | } 311 | 312 | func (m *CirconusMetrics) GetCheckBundle() *apiclient.CheckBundle { 313 | return m.check.GetCheckBundle() 314 | } 315 | 316 | func (m *CirconusMetrics) SetSubmitTimestamp(ts time.Time) { 317 | m.packagingmu.Lock() 318 | defer m.packagingmu.Unlock() 319 | m.submitTimestamp = &ts 320 | } 321 | -------------------------------------------------------------------------------- /counter_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Circonus, Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package circonusgometrics 6 | 7 | import ( 8 | "testing" 9 | ) 10 | 11 | func TestSet(t *testing.T) { 12 | t.Log("Testing counter.Set") 13 | 14 | cm := &CirconusMetrics{counters: make(map[string]uint64)} 15 | 16 | cm.Set("foo", 30) 17 | 18 | val, ok := cm.counters["foo"] 19 | if !ok { 20 | t.Errorf("Expected to find foo") 21 | } 22 | 23 | if val != 30 { 24 | t.Errorf("Expected 30, found %d", val) 25 | } 26 | 27 | cm.Set("foo", 10) 28 | 29 | val, ok = cm.counters["foo"] 30 | if !ok { 31 | t.Errorf("Expected to find foo") 32 | } 33 | 34 | if val != 10 { 35 | t.Errorf("Expected 10, found %d", val) 36 | } 37 | } 38 | 39 | func TestSetWithTags(t *testing.T) { 40 | t.Log("Testing counter.SetWithTags") 41 | 42 | cm := &CirconusMetrics{counters: make(map[string]uint64)} 43 | 44 | metricName := "foo" 45 | tags := Tags{{"foo", "bar"}, {"baz", "qux"}} 46 | streamTagMetricName := cm.MetricNameWithStreamTags("foo", tags) 47 | 48 | cm.SetWithTags(metricName, tags, 30) 49 | 50 | val, ok := cm.counters[streamTagMetricName] 51 | if !ok { 52 | t.Fatalf("%s with %v tags not found (%s) (%#v)", metricName, tags, streamTagMetricName, cm.counters) 53 | } 54 | 55 | if val != 30 { 56 | t.Fatalf("expected 30 got (%d)", val) 57 | } 58 | 59 | cm.SetWithTags(metricName, tags, 10) 60 | 61 | val, ok = cm.counters[streamTagMetricName] 62 | if !ok { 63 | t.Fatalf("%s with %v tags not found (%s) (%#v)", metricName, tags, streamTagMetricName, cm.counters) 64 | } 65 | 66 | if val != 10 { 67 | t.Fatalf("expected 10 got (%d)", val) 68 | } 69 | } 70 | 71 | func TestIncrement(t *testing.T) { 72 | t.Log("Testing counter.Increment") 73 | 74 | cm := &CirconusMetrics{counters: make(map[string]uint64)} 75 | 76 | cm.Increment("foo") 77 | 78 | val, ok := cm.counters["foo"] 79 | if !ok { 80 | t.Errorf("Expected to find foo") 81 | } 82 | 83 | if val != 1 { 84 | t.Errorf("Expected 1, found %d", val) 85 | } 86 | } 87 | 88 | func TestIncrementWithTags(t *testing.T) { 89 | t.Log("Testing counter.IncrementWithTags") 90 | 91 | cm := &CirconusMetrics{counters: make(map[string]uint64)} 92 | 93 | metricName := "foo" 94 | tags := Tags{{"foo", "bar"}, {"baz", "qux"}} 95 | streamTagMetricName := cm.MetricNameWithStreamTags("foo", tags) 96 | 97 | cm.IncrementWithTags(metricName, tags) 98 | 99 | val, ok := cm.counters[streamTagMetricName] 100 | if !ok { 101 | t.Fatalf("%s with %v tags not found (%s) (%#v)", metricName, tags, streamTagMetricName, cm.counters) 102 | } 103 | 104 | if val != 1 { 105 | t.Fatalf("expected 1 got (%d)", val) 106 | } 107 | } 108 | 109 | func TestIncrementByValue(t *testing.T) { 110 | t.Log("Testing counter.IncrementByValue") 111 | 112 | cm := &CirconusMetrics{counters: make(map[string]uint64)} 113 | 114 | cm.IncrementByValue("foo", 10) 115 | 116 | val, ok := cm.counters["foo"] 117 | if !ok { 118 | t.Errorf("Expected to find foo") 119 | } 120 | 121 | if val != 10 { 122 | t.Errorf("Expected 1, found %d", val) 123 | } 124 | } 125 | 126 | func TestIncrementByValueWithTags(t *testing.T) { 127 | t.Log("Testing counter.IncrementByValueWithTags") 128 | 129 | cm := &CirconusMetrics{counters: make(map[string]uint64)} 130 | 131 | metricName := "foo" 132 | tags := Tags{{"foo", "bar"}, {"baz", "qux"}} 133 | streamTagMetricName := cm.MetricNameWithStreamTags("foo", tags) 134 | 135 | cm.IncrementByValueWithTags(metricName, tags, 10) 136 | 137 | val, ok := cm.counters[streamTagMetricName] 138 | if !ok { 139 | t.Fatalf("%s with %v tags not found (%s) (%#v)", metricName, tags, streamTagMetricName, cm.counters) 140 | } 141 | 142 | if val != 10 { 143 | t.Fatalf("expected 10 got (%d)", val) 144 | } 145 | } 146 | 147 | func TestAdd(t *testing.T) { 148 | t.Log("Testing counter.Add") 149 | 150 | cm := &CirconusMetrics{counters: make(map[string]uint64)} 151 | 152 | cm.Set("foo", 2) 153 | cm.Add("foo", 3) 154 | 155 | val, ok := cm.counters["foo"] 156 | if !ok { 157 | t.Fatal("Expected to find foo") 158 | } 159 | 160 | if val != 5 { 161 | t.Fatalf("Expected 1, found %d", val) 162 | } 163 | } 164 | 165 | func TestAddWithTags(t *testing.T) { 166 | t.Log("Testing counter.AddWithTags") 167 | 168 | cm := &CirconusMetrics{counters: make(map[string]uint64)} 169 | 170 | metricName := "foo" 171 | tags := Tags{{"foo", "bar"}, {"baz", "qux"}} 172 | streamTagMetricName := cm.MetricNameWithStreamTags("foo", tags) 173 | 174 | cm.SetWithTags(metricName, tags, 30) 175 | 176 | val, ok := cm.counters[streamTagMetricName] 177 | if !ok { 178 | t.Fatalf("%s with %v tags not found (%s) (%#v)", metricName, tags, streamTagMetricName, cm.counters) 179 | } 180 | 181 | if val != 30 { 182 | t.Fatalf("expected 30, got %d", val) 183 | } 184 | 185 | cm.AddWithTags(metricName, tags, 1) 186 | 187 | val, ok = cm.counters[streamTagMetricName] 188 | if !ok { 189 | t.Fatalf("%s with %v tags not found (%s) (%#v)", metricName, tags, streamTagMetricName, cm.counters) 190 | } 191 | 192 | if val != 31 { 193 | t.Fatalf("expected 31, got %d", val) 194 | } 195 | } 196 | 197 | func TestRemoveCounter(t *testing.T) { 198 | t.Log("Testing counter.RemoveCounter") 199 | 200 | cm := &CirconusMetrics{counters: make(map[string]uint64)} 201 | 202 | cm.Increment("foo") 203 | 204 | val, ok := cm.counters["foo"] 205 | if !ok { 206 | t.Errorf("Expected to find foo") 207 | } 208 | 209 | if val != 1 { 210 | t.Errorf("Expected 1, found %d", val) 211 | } 212 | 213 | cm.RemoveCounter("foo") 214 | 215 | val, ok = cm.counters["foo"] 216 | if ok { 217 | t.Errorf("Expected NOT to find foo") 218 | } 219 | 220 | if val != 0 { 221 | t.Errorf("Expected 0, found %d", val) 222 | } 223 | } 224 | 225 | func TestRemoveCounterWithTags(t *testing.T) { 226 | t.Log("Testing counter.RemoveCounterWithTags") 227 | 228 | cm := &CirconusMetrics{counters: make(map[string]uint64)} 229 | 230 | metricName := "foo" 231 | tags := Tags{{"foo", "bar"}, {"baz", "qux"}} 232 | streamTagMetricName := cm.MetricNameWithStreamTags("foo", tags) 233 | 234 | cm.IncrementWithTags(metricName, tags) 235 | 236 | val, ok := cm.counters[streamTagMetricName] 237 | if !ok { 238 | t.Fatalf("%s with %v tags not found (%s) (%#v)", metricName, tags, streamTagMetricName, cm.counters) 239 | } 240 | 241 | if val != 1 { 242 | t.Fatalf("expected 1 got (%d)", val) 243 | } 244 | 245 | cm.RemoveCounterWithTags(metricName, tags) 246 | 247 | val, ok = cm.counters[streamTagMetricName] 248 | if ok { 249 | t.Fatalf("expected NOT to find %s", streamTagMetricName) 250 | } 251 | 252 | if val != 0 { 253 | t.Fatalf("expected 0 got (%d)", val) 254 | } 255 | } 256 | 257 | func TestSetCounterFunc(t *testing.T) { 258 | t.Log("Testing counter.SetCounterFunc") 259 | 260 | cf := func() uint64 { 261 | return 1 262 | } 263 | 264 | cm := &CirconusMetrics{counterFuncs: make(map[string]func() uint64)} 265 | 266 | cm.SetCounterFunc("foo", cf) 267 | 268 | val, ok := cm.counterFuncs["foo"] 269 | if !ok { 270 | t.Errorf("Expected to find foo") 271 | } 272 | 273 | if val() != 1 { 274 | t.Errorf("Expected 1, found %d", val()) 275 | } 276 | } 277 | 278 | func TestSetCounterFuncWithTags(t *testing.T) { 279 | t.Log("Testing counter.SetCounterFuncWithTags") 280 | 281 | cf := func() uint64 { 282 | return 1 283 | } 284 | 285 | cm := &CirconusMetrics{counterFuncs: make(map[string]func() uint64)} 286 | 287 | metricName := "foo" 288 | tags := Tags{{"foo", "bar"}, {"baz", "qux"}} 289 | streamTagMetricName := cm.MetricNameWithStreamTags("foo", tags) 290 | 291 | cm.SetCounterFuncWithTags(metricName, tags, cf) 292 | 293 | val, ok := cm.counterFuncs[streamTagMetricName] 294 | if !ok { 295 | t.Fatalf("%s with %v tags not found (%s) (%#v)", metricName, tags, streamTagMetricName, cm.counterFuncs) 296 | } 297 | 298 | if val() != 1 { 299 | t.Fatalf("expected 1 got (%d)", val()) 300 | } 301 | } 302 | 303 | func TestRemoveCounterFunc(t *testing.T) { 304 | t.Log("Testing counter.RemoveCounterFunc") 305 | 306 | cf := func() uint64 { 307 | return 1 308 | } 309 | 310 | cm := &CirconusMetrics{counterFuncs: make(map[string]func() uint64)} 311 | 312 | cm.SetCounterFunc("foo", cf) 313 | 314 | val, ok := cm.counterFuncs["foo"] 315 | if !ok { 316 | t.Errorf("Expected to find foo") 317 | } 318 | 319 | if val() != 1 { 320 | t.Errorf("Expected 1, found %d", val()) 321 | } 322 | 323 | cm.RemoveCounterFunc("foo") 324 | 325 | val, ok = cm.counterFuncs["foo"] 326 | if ok { 327 | t.Errorf("Expected NOT to find foo") 328 | } 329 | 330 | if val != nil { 331 | t.Errorf("Expected nil, found %v", val()) 332 | } 333 | 334 | } 335 | 336 | func TestRemoveCounterFuncWithTags(t *testing.T) { 337 | t.Log("Testing counter.RemoveCounterFuncWithTags") 338 | 339 | cf := func() uint64 { 340 | return 1 341 | } 342 | 343 | cm := &CirconusMetrics{counterFuncs: make(map[string]func() uint64)} 344 | 345 | metricName := "foo" 346 | tags := Tags{{"foo", "bar"}, {"baz", "qux"}} 347 | streamTagMetricName := cm.MetricNameWithStreamTags("foo", tags) 348 | 349 | cm.SetCounterFuncWithTags(metricName, tags, cf) 350 | 351 | val, ok := cm.counterFuncs[streamTagMetricName] 352 | if !ok { 353 | t.Fatalf("%s with %v tags not found (%s) (%#v)", metricName, tags, streamTagMetricName, cm.counterFuncs) 354 | } 355 | 356 | if val() != 1 { 357 | t.Fatalf("expected 1 got (%d)", val()) 358 | } 359 | 360 | cm.RemoveCounterFuncWithTags(metricName, tags) 361 | 362 | val, ok = cm.counterFuncs[streamTagMetricName] 363 | if ok { 364 | t.Fatalf("expected NOT to find (%s)", streamTagMetricName) 365 | } 366 | 367 | if val != nil { 368 | t.Fatalf("expected nil got (%v)", val()) 369 | } 370 | 371 | } 372 | 373 | func TestGetCounterTest(t *testing.T) { 374 | t.Log("Testing counter.GetCounterTest") 375 | 376 | cm := &CirconusMetrics{counters: make(map[string]uint64)} 377 | 378 | cm.Set("foo", 10) 379 | 380 | val, err := cm.GetCounterTest("foo") 381 | if err != nil { 382 | t.Errorf("Expected no error %v", err) 383 | } 384 | if val != 10 { 385 | t.Errorf("Expected 10 got %v", val) 386 | } 387 | 388 | _, err = cm.GetCounterTest("bar") 389 | if err == nil { 390 | t.Error("Expected error") 391 | } 392 | 393 | } 394 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | # default is true. Enables skipping of directories: 3 | # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ 4 | skip-dirs-use-default: true 5 | skip-files: 6 | - ".*_mock_test.go$" 7 | # by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules": 8 | # If invoked with -mod=readonly, the go command is disallowed from the implicit 9 | # automatic updating of go.mod described above. Instead, it fails when any changes 10 | # to go.mod are needed. This setting is most useful to check that go.mod does 11 | # not need updates, such as in a continuous integration and testing system. 12 | # If invoked with -mod=vendor, the go command assumes that the vendor 13 | # directory holds the correct copies of dependencies and ignores 14 | # the dependency descriptions in go.mod. 15 | #commented out for: https://github.com/golangci/golangci-lint/issues/1502 16 | #modules-download-mode: readonly 17 | 18 | # all available settings of specific linters 19 | linters-settings: 20 | errcheck: 21 | # report about not checking of errors in type assetions: `a := b.(MyStruct)`; 22 | # default is false: such cases aren't reported by default. 23 | check-type-assertions: false 24 | 25 | # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; 26 | # default is false: such cases aren't reported by default. 27 | check-blank: false 28 | 29 | funlen: 30 | lines: 100 31 | statements: 50 32 | 33 | govet: 34 | # report about shadowed variables 35 | check-shadowing: true 36 | 37 | # settings per analyzer 38 | # settings: 39 | # printf: # analyzer name, run `go tool vet help` to see all analyzers 40 | # funcs: # run `go tool vet help printf` to see available settings for `printf` analyzer 41 | # - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof 42 | # - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf 43 | # - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf 44 | # - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf 45 | 46 | # enable or disable analyzers by name 47 | # enable: 48 | # - atomicalign 49 | enable-all: true 50 | disable: 51 | - fieldalignment 52 | # disable: 53 | # - shadow 54 | # disable-all: false 55 | golint: 56 | # minimal confidence for issues, default is 0.8 57 | min-confidence: 0.8 58 | gofmt: 59 | # simplify code: gofmt with `-s` option, true by default 60 | simplify: true 61 | goimports: 62 | # put imports beginning with prefix after 3rd-party packages; 63 | # it's a comma-separated list of prefixes 64 | local-prefixes: github.com/circonus-labs 65 | gocyclo: 66 | # minimal code complexity to report, 30 by default (but we recommend 10-20) 67 | min-complexity: 10 68 | gocognit: 69 | # minimal code complexity to report, 30 by default (but we recommend 10-20) 70 | min-complexity: 10 71 | dupl: 72 | # tokens count to trigger issue, 150 by default 73 | threshold: 100 74 | goconst: 75 | # minimal length of string constant, 3 by default 76 | min-len: 3 77 | # minimal occurrences count to trigger, 3 by default 78 | min-occurrences: 3 79 | depguard: 80 | # list-type: blacklist 81 | # include-go-root: false 82 | # packages: 83 | # - github.com/sirupsen/logrus 84 | # packages-with-error-messages: 85 | # # specify an error message to output when a blacklisted package is used 86 | # github.com/sirupsen/logrus: "logging is allowed only by logutils.Log" 87 | misspell: 88 | # Correct spellings using locale preferences for US or UK. 89 | # Default is to use a neutral variety of English. 90 | # Setting locale to US will correct the British spelling of 'colour' to 'color'. 91 | locale: US 92 | # ignore-words: 93 | # - someword 94 | lll: 95 | # max line length, lines longer will be reported. Default is 120. 96 | # '\t' is counted as 1 character by default, and can be changed with the tab-width option 97 | line-length: 120 98 | # tab width in spaces. Default to 1. 99 | tab-width: 1 100 | unused: 101 | # treat code as a program (not a library) and report unused exported identifiers; default is false. 102 | # XXX: if you enable this setting, unused will report a lot of false-positives in text editors: 103 | # if it's called for subdir of a project it can't find funcs usages. All text editor integrations 104 | # with golangci-lint call it on a directory with the changed file. 105 | check-exported: false 106 | unparam: 107 | # Inspect exported functions, default is false. Set to true if no external program/library imports your code. 108 | # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: 109 | # if it's called for subdir of a project it can't find external interfaces. All text editor integrations 110 | # with golangci-lint call it on a directory with the changed file. 111 | check-exported: false 112 | nakedret: 113 | # make an issue if func has more lines of code than this setting and it has naked returns; default is 30 114 | max-func-lines: 30 115 | prealloc: 116 | # XXX: we don't recommend using this linter before doing performance profiling. 117 | # For most programs usage of prealloc will be a premature optimization. 118 | 119 | # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them. 120 | # True by default. 121 | simple: true 122 | range-loops: true # Report preallocation suggestions on range loops, true by default 123 | for-loops: false # Report preallocation suggestions on for loops, false by default 124 | gocritic: 125 | # # Which checks should be enabled; can't be combined with 'disabled-checks'; 126 | # # See https://go-critic.github.io/overview#checks-overview 127 | # # To check which checks are enabled run `GL_DEBUG=gocritic golangci-lint run` 128 | # # By default list of stable checks is used. 129 | # enabled-checks: 130 | # - rangeValCopy 131 | 132 | # # Which checks should be disabled; can't be combined with 'enabled-checks'; default is empty 133 | # disabled-checks: 134 | # - regexpMust 135 | 136 | # # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks. 137 | # # Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags". 138 | # enabled-tags: 139 | # - performance 140 | 141 | # settings: # settings passed to gocritic 142 | # captLocal: # must be valid enabled check name 143 | # paramsOnly: true 144 | # rangeValCopy: 145 | # sizeThreshold: 32 146 | godox: 147 | # # report any comments starting with keywords, this is useful for TODO or FIXME comments that 148 | # # might be left in the code accidentally and should be resolved before merging 149 | # keywords: # default keywords are TODO, BUG, and FIXME, these can be overwritten by this setting 150 | # - NOTE 151 | # - OPTIMIZE # marks code that should be optimized before merging 152 | # - HACK # marks hack-arounds that should be removed before merging 153 | dogsled: 154 | # checks assignments with too many blank identifiers; default is 2 155 | max-blank-identifiers: 2 156 | 157 | whitespace: 158 | multi-if: false # Enforces newlines (or comments) after every multi-line if statement 159 | multi-func: false # Enforces newlines (or comments) after every multi-line function signature 160 | wsl: 161 | # If true append is only allowed to be cuddled if appending value is 162 | # matching variables, fields or types on line above. Default is true. 163 | strict-append: true 164 | # Allow calls and assignments to be cuddled as long as the lines have any 165 | # matching variables, fields or types. Default is true. 166 | allow-assign-and-call: true 167 | # Allow multiline assignments to be cuddled. Default is true. 168 | allow-multiline-assign: true 169 | # Allow case blocks to end with a whitespace. 170 | allow-case-traling-whitespace: true 171 | # Allow declarations (var) to be cuddled. 172 | allow-cuddle-declarations: false 173 | 174 | linters: 175 | enable: 176 | - deadcode 177 | - errcheck 178 | - gocritic 179 | - gofmt 180 | - golint 181 | - gosec 182 | - gosimple 183 | - govet 184 | - ineffassign 185 | - megacheck 186 | - misspell 187 | - prealloc 188 | - scopelint 189 | - staticcheck 190 | - structcheck 191 | - typecheck 192 | - unparam 193 | - unused 194 | - varcheck 195 | disable: 196 | # - prealloc 197 | disable-all: false 198 | presets: 199 | - bugs 200 | - unused 201 | fast: false 202 | 203 | 204 | issues: 205 | # List of regexps of issue texts to exclude, empty list by default. 206 | # But independently from this option we use default exclude patterns, 207 | # it can be disabled by `exclude-use-default: false`. To list all 208 | # excluded by default patterns execute `golangci-lint run --help` 209 | exclude: 210 | - abcdef 211 | 212 | # Excluding configuration per-path, per-linter, per-text and per-source 213 | exclude-rules: 214 | # Exclude some linters from running on tests files. 215 | - path: _test\.go 216 | linters: 217 | - gocyclo 218 | - errcheck 219 | - dupl 220 | - gosec 221 | 222 | # Exclude known linters from partially hard-vendored code, 223 | # which is impossible to exclude via "nolint" comments. 224 | - path: internal/hmac/ 225 | text: "weak cryptographic primitive" 226 | linters: 227 | - gosec 228 | 229 | # Exclude some staticcheck messages 230 | - linters: 231 | - staticcheck 232 | text: "SA9003:" 233 | 234 | # Exclude lll issues for long lines with go:generate 235 | - linters: 236 | - lll 237 | source: "^//go:generate " 238 | 239 | # # Independently from option `exclude` we use default exclude patterns, 240 | # # it can be disabled by this option. To list all 241 | # # excluded by default patterns execute `golangci-lint run --help`. 242 | # # Default value for this option is true. 243 | # exclude-use-default: false 244 | 245 | # Maximum issues count per one linter. Set to 0 to disable. Default is 50. 246 | max-issues-per-linter: 0 247 | 248 | # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. 249 | max-same-issues: 0 250 | 251 | # # Show only new issues: if there are unstaged changes or untracked files, 252 | # # only those changes are analyzed, else only changes in HEAD~ are analyzed. 253 | # # It's a super-useful option for integration of golangci-lint into existing 254 | # # large codebase. It's not practical to fix all existing issues at the moment 255 | # # of integration: much better don't allow issues in new code. 256 | # # Default is false. 257 | # new: false 258 | 259 | # # Show only new issues created after git revision `REV` 260 | # new-from-rev: REV 261 | 262 | # # Show only new issues created in git patch with set file path. 263 | # new-from-patch: path/to/patch/file 264 | 265 | -------------------------------------------------------------------------------- /metric_output.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Circonus, Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package circonusgometrics 6 | 7 | import ( 8 | "bufio" 9 | "bytes" 10 | "fmt" 11 | "strings" 12 | "sync" 13 | "time" 14 | 15 | "github.com/circonus-labs/go-apiclient" 16 | "github.com/openhistogram/circonusllhist" 17 | "github.com/pkg/errors" 18 | ) 19 | 20 | func (m *CirconusMetrics) packageMetrics() (map[string]*apiclient.CheckBundleMetric, Metrics) { 21 | 22 | m.packagingmu.Lock() 23 | defer m.packagingmu.Unlock() 24 | 25 | // if m.Debug { 26 | // m.Log.Printf("packaging metrics\n") 27 | // } 28 | 29 | var ts uint64 30 | // always submitting a timestamp forces the broker to treat the check as though 31 | // it is "async" which doesn't work well for "group" checks with multiple submitters 32 | // e.g. circonus-agent with a group statsd check 33 | // if m.submitTimestamp == nil { 34 | // ts = makeTimestamp(time.Now()) 35 | // } else { 36 | if m.submitTimestamp != nil { 37 | ts = makeTimestamp(*m.submitTimestamp) 38 | m.Log.Printf("setting custom timestamp %v -> %v (UTC ms)", *m.submitTimestamp, ts) 39 | } 40 | 41 | newMetrics := make(map[string]*apiclient.CheckBundleMetric) 42 | counters, gauges, histograms, text := m.snapshot() 43 | m.custm.Lock() 44 | output := make(Metrics, len(counters)+len(gauges)+len(histograms)+len(text)+len(m.custom)) 45 | if len(m.custom) > 0 { 46 | // add and reset any custom metrics 47 | for mn, mv := range m.custom { 48 | output[mn] = mv 49 | } 50 | m.custom = make(map[string]Metric) 51 | } 52 | m.custm.Unlock() 53 | for name, value := range counters { 54 | send := m.check.IsMetricActive(name) 55 | if !send && m.check.ActivateMetric(name) { 56 | send = true 57 | newMetrics[name] = &apiclient.CheckBundleMetric{ 58 | Name: name, 59 | Type: "numeric", 60 | Status: "active", 61 | } 62 | } 63 | if send { 64 | metric := Metric{Type: "L", Value: value} 65 | if ts > 0 { 66 | metric.Timestamp = ts 67 | } 68 | output[name] = metric 69 | } 70 | } 71 | 72 | for name, value := range gauges { 73 | send := m.check.IsMetricActive(name) 74 | if !send && m.check.ActivateMetric(name) { 75 | send = true 76 | newMetrics[name] = &apiclient.CheckBundleMetric{ 77 | Name: name, 78 | Type: "numeric", 79 | Status: "active", 80 | } 81 | } 82 | if send { 83 | metric := Metric{Type: m.getGaugeType(value), Value: value} 84 | if ts > 0 { 85 | metric.Timestamp = ts 86 | } 87 | output[name] = metric 88 | } 89 | } 90 | 91 | for name, value := range histograms { 92 | send := m.check.IsMetricActive(name) 93 | if !send && m.check.ActivateMetric(name) { 94 | send = true 95 | newMetrics[name] = &apiclient.CheckBundleMetric{ 96 | Name: name, 97 | Type: "histogram", 98 | Status: "active", 99 | } 100 | } 101 | if send { 102 | buf := bytes.NewBuffer([]byte{}) 103 | if err := value.SerializeB64(buf); err != nil { 104 | m.Log.Printf("[ERR] serializing histogram %s: %s", name, err) 105 | } else { 106 | // histograms b64 serialized support timestamps 107 | metric := Metric{Type: "h", Value: buf.String()} 108 | if ts > 0 { 109 | metric.Timestamp = ts 110 | } 111 | output[name] = metric 112 | } 113 | // output[name] = Metric{Type: "h", Value: value.DecStrings()} // histograms do NOT get timestamps 114 | } 115 | } 116 | 117 | for name, value := range text { 118 | send := m.check.IsMetricActive(name) 119 | if !send && m.check.ActivateMetric(name) { 120 | send = true 121 | newMetrics[name] = &apiclient.CheckBundleMetric{ 122 | Name: name, 123 | Type: "text", 124 | Status: "active", 125 | } 126 | } 127 | if send { 128 | metric := Metric{Type: "s", Value: value} 129 | if ts > 0 { 130 | metric.Timestamp = ts 131 | } 132 | output[name] = metric 133 | } 134 | } 135 | 136 | m.lastMetrics.metricsmu.Lock() 137 | defer m.lastMetrics.metricsmu.Unlock() 138 | m.lastMetrics.metrics = &output 139 | m.lastMetrics.ts = time.Now() 140 | // reset the submission timestamp 141 | m.submitTimestamp = nil 142 | 143 | return newMetrics, output 144 | } 145 | 146 | // PromOutput returns lines of metrics in prom format 147 | func (m *CirconusMetrics) PromOutput() (*bytes.Buffer, error) { 148 | m.lastMetrics.metricsmu.Lock() 149 | defer m.lastMetrics.metricsmu.Unlock() 150 | 151 | if m.lastMetrics.metrics == nil { 152 | return nil, errors.New("no metrics available") 153 | } 154 | 155 | var b bytes.Buffer 156 | w := bufio.NewWriter(&b) 157 | 158 | ts := m.lastMetrics.ts.UnixNano() / int64(time.Millisecond) 159 | 160 | for name, metric := range *m.lastMetrics.metrics { 161 | switch metric.Type { 162 | case "n": 163 | if strings.HasPrefix(fmt.Sprintf("%v", metric.Value), "[H[") { 164 | continue // circonus histogram != prom "histogram" (aka percentile) 165 | } 166 | case "h": 167 | continue // circonus histogram != prom "histogram" (aka percentile) 168 | case "s": 169 | continue // text metrics unsupported 170 | } 171 | fmt.Fprintf(w, "%s %v %d\n", name, metric.Value, ts) 172 | } 173 | 174 | err := w.Flush() 175 | if err != nil { 176 | return nil, errors.Wrap(err, "flushing metric buffer") 177 | } 178 | 179 | return &b, err 180 | } 181 | 182 | // FlushMetricsNoReset flushes current metrics to a structure and returns it (does NOT send to Circonus). 183 | func (m *CirconusMetrics) FlushMetricsNoReset() *Metrics { 184 | m.flushmu.Lock() 185 | if m.flushing { 186 | m.flushmu.Unlock() 187 | return &Metrics{} 188 | } 189 | 190 | m.flushing = true 191 | m.flushmu.Unlock() 192 | 193 | // save values configured at startup 194 | resetC := m.resetCounters 195 | resetG := m.resetGauges 196 | resetH := m.resetHistograms 197 | resetT := m.resetText 198 | // override Reset* to false for this call 199 | m.resetCounters = false 200 | m.resetGauges = false 201 | m.resetHistograms = false 202 | m.resetText = false 203 | 204 | _, output := m.packageMetrics() 205 | 206 | // restore previous values 207 | m.resetCounters = resetC 208 | m.resetGauges = resetG 209 | m.resetHistograms = resetH 210 | m.resetText = resetT 211 | 212 | m.flushmu.Lock() 213 | m.flushing = false 214 | m.flushmu.Unlock() 215 | 216 | return &output 217 | } 218 | 219 | // FlushMetrics flushes current metrics to a structure and returns it (does NOT send to Circonus) 220 | func (m *CirconusMetrics) FlushMetrics() *Metrics { 221 | m.flushmu.Lock() 222 | if m.flushing { 223 | m.flushmu.Unlock() 224 | return &Metrics{} 225 | } 226 | 227 | m.flushing = true 228 | m.flushmu.Unlock() 229 | 230 | _, output := m.packageMetrics() 231 | 232 | m.flushmu.Lock() 233 | m.flushing = false 234 | m.flushmu.Unlock() 235 | 236 | return &output 237 | } 238 | 239 | // Flush metrics kicks off the process of sending metrics to Circonus 240 | func (m *CirconusMetrics) Flush() { 241 | m.flushmu.Lock() 242 | if m.flushing { 243 | m.flushmu.Unlock() 244 | return 245 | } 246 | 247 | m.flushing = true 248 | m.flushmu.Unlock() 249 | 250 | newMetrics, output := m.packageMetrics() 251 | 252 | if len(output) > 0 { 253 | m.submit(output, newMetrics) 254 | } /* else if m.Debug { 255 | m.Log.Printf("no metrics to send, skipping\n") 256 | }*/ 257 | 258 | m.flushmu.Lock() 259 | m.flushing = false 260 | m.flushmu.Unlock() 261 | } 262 | 263 | // Reset removes all existing counters and gauges. 264 | func (m *CirconusMetrics) Reset() { 265 | m.cm.Lock() 266 | defer m.cm.Unlock() 267 | 268 | m.cfm.Lock() 269 | defer m.cfm.Unlock() 270 | 271 | m.gm.Lock() 272 | defer m.gm.Unlock() 273 | 274 | m.gfm.Lock() 275 | defer m.gfm.Unlock() 276 | 277 | m.hm.Lock() 278 | defer m.hm.Unlock() 279 | 280 | m.tm.Lock() 281 | defer m.tm.Unlock() 282 | 283 | m.tfm.Lock() 284 | defer m.tfm.Unlock() 285 | 286 | m.counters = make(map[string]uint64) 287 | m.counterFuncs = make(map[string]func() uint64) 288 | m.gauges = make(map[string]interface{}) 289 | m.gaugeFuncs = make(map[string]func() int64) 290 | m.histograms = make(map[string]*Histogram) 291 | m.text = make(map[string]string) 292 | m.textFuncs = make(map[string]func() string) 293 | } 294 | 295 | // snapshot returns a copy of the values of all registered counters and gauges. 296 | func (m *CirconusMetrics) snapshot() ( 297 | map[string]uint64, // counters 298 | map[string]interface{}, // gauges 299 | map[string]*circonusllhist.Histogram, // histograms 300 | map[string]string) { // text 301 | 302 | var h map[string]*circonusllhist.Histogram 303 | var c map[string]uint64 304 | var g map[string]interface{} 305 | var t map[string]string 306 | 307 | var wg sync.WaitGroup 308 | 309 | wg.Add(1) 310 | go func() { 311 | h = m.snapHistograms() 312 | wg.Done() 313 | }() 314 | 315 | wg.Add(1) 316 | go func() { 317 | c = m.snapCounters() 318 | wg.Done() 319 | }() 320 | 321 | wg.Add(1) 322 | go func() { 323 | g = m.snapGauges() 324 | wg.Done() 325 | }() 326 | 327 | wg.Add(1) 328 | go func() { 329 | t = m.snapText() 330 | wg.Done() 331 | }() 332 | 333 | wg.Wait() 334 | 335 | return c, g, h, t 336 | } 337 | 338 | func (m *CirconusMetrics) snapCounters() map[string]uint64 { 339 | m.cm.Lock() 340 | m.cfm.Lock() 341 | 342 | c := make(map[string]uint64, len(m.counters)+len(m.counterFuncs)) 343 | 344 | for n, v := range m.counters { 345 | c[n] = v 346 | } 347 | if m.resetCounters && len(c) > 0 { 348 | m.counters = make(map[string]uint64) 349 | } 350 | 351 | for n, f := range m.counterFuncs { 352 | c[n] = f() 353 | } 354 | 355 | m.cm.Unlock() 356 | m.cfm.Unlock() 357 | 358 | return c 359 | } 360 | 361 | func (m *CirconusMetrics) snapGauges() map[string]interface{} { 362 | m.gm.Lock() 363 | m.gfm.Lock() 364 | 365 | g := make(map[string]interface{}, len(m.gauges)+len(m.gaugeFuncs)) 366 | 367 | for n, v := range m.gauges { 368 | g[n] = v 369 | } 370 | if m.resetGauges && len(g) > 0 { 371 | m.gauges = make(map[string]interface{}) 372 | } 373 | 374 | for n, f := range m.gaugeFuncs { 375 | g[n] = f() 376 | } 377 | 378 | m.gm.Unlock() 379 | m.gfm.Unlock() 380 | 381 | return g 382 | } 383 | 384 | func (m *CirconusMetrics) snapHistograms() map[string]*circonusllhist.Histogram { 385 | m.hm.Lock() 386 | 387 | h := make(map[string]*circonusllhist.Histogram, len(m.histograms)) 388 | 389 | for n, hist := range m.histograms { 390 | hist.rw.Lock() 391 | if m.resetHistograms { 392 | h[n] = hist.hist.CopyAndReset() 393 | } else { 394 | h[n] = hist.hist.Copy() 395 | } 396 | hist.rw.Unlock() 397 | } 398 | 399 | if m.resetHistograms && len(h) > 0 { 400 | m.histograms = make(map[string]*Histogram) 401 | } 402 | 403 | m.hm.Unlock() 404 | 405 | return h 406 | } 407 | 408 | func (m *CirconusMetrics) snapText() map[string]string { 409 | m.tm.Lock() 410 | m.tfm.Lock() 411 | 412 | t := make(map[string]string, len(m.text)+len(m.textFuncs)) 413 | 414 | for n, v := range m.text { 415 | t[n] = v 416 | } 417 | if m.resetText && len(t) > 0 { 418 | m.text = make(map[string]string) 419 | } 420 | 421 | for n, f := range m.textFuncs { 422 | t[n] = f() 423 | } 424 | 425 | m.tm.Unlock() 426 | m.tfm.Unlock() 427 | 428 | return t 429 | } 430 | 431 | // makeTimestamp returns timestamp in ms units for _ts metric value 432 | func makeTimestamp(ts time.Time) uint64 { 433 | return uint64(ts.UTC().UnixNano() / (int64(time.Millisecond) / int64(time.Nanosecond))) 434 | } 435 | -------------------------------------------------------------------------------- /checkmgr/checkmgr_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Circonus, Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package checkmgr 6 | 7 | import ( 8 | "crypto/tls" 9 | "crypto/x509" 10 | "encoding/json" 11 | "errors" 12 | "fmt" 13 | "io/ioutil" 14 | "log" 15 | "net/http" 16 | "net/http/httptest" 17 | "net/url" 18 | "os" 19 | "strconv" 20 | "strings" 21 | "testing" 22 | "time" 23 | 24 | apiclient "github.com/circonus-labs/go-apiclient" 25 | "github.com/circonus-labs/go-apiclient/config" 26 | ) 27 | 28 | func sslBroker() *httptest.Server { 29 | f := func(w http.ResponseWriter, r *http.Request) { 30 | w.WriteHeader(200) 31 | w.Header().Set("Content-Type", "application/json") 32 | fmt.Fprintln(w, r.Method) 33 | } 34 | 35 | return httptest.NewTLSServer(http.HandlerFunc(f)) 36 | } 37 | 38 | var ( 39 | testCMCheck = apiclient.Check{ 40 | CID: "/check/1234", 41 | Active: true, 42 | BrokerCID: "/broker/1234", 43 | CheckBundleCID: "/check_bundle/1234", 44 | CheckUUID: "abc123-a1b2-c3d4-e5f6-123abc", 45 | Details: map[config.Key]string{config.SubmissionURL: "https://127.0.0.1:43191/module/httptrap/abc123-a1b2-c3d4-e5f6-123abc/blah"}, 46 | } 47 | 48 | testCMCheckBundle = apiclient.CheckBundle{ 49 | CheckUUIDs: []string{"abc123-a1b2-c3d4-e5f6-123abc"}, 50 | Checks: []string{"/check/1234"}, 51 | CID: "/check_bundle/1234", 52 | Created: 0, 53 | LastModified: 0, 54 | LastModifedBy: "", 55 | ReverseConnectURLs: []string{ 56 | "mtev_reverse://127.0.0.1:43191/check/abc123-a1b2-c3d4-e5f6-123abc", 57 | }, 58 | Brokers: []string{"/broker/1234"}, 59 | DisplayName: "test check", 60 | Config: map[config.Key]string{ 61 | config.SubmissionURL: "https://127.0.0.1:43191/module/httptrap/abc123-a1b2-c3d4-e5f6-123abc", 62 | config.ReverseSecretKey: "blah", 63 | }, 64 | // Config: apiclient.CheckBundleConfig{ 65 | // SubmissionURL: "https://127.0.0.1:43191/module/httptrap/abc123-a1b2-c3d4-e5f6-123abc/blah", 66 | // ReverseSecret: "blah", 67 | // }, 68 | Metrics: []apiclient.CheckBundleMetric{ 69 | { 70 | Name: "elmo", 71 | Type: "numeric", 72 | Status: "active", 73 | }, 74 | }, 75 | MetricLimit: 0, 76 | Notes: nil, 77 | Period: 60, 78 | Status: "active", 79 | Target: "127.0.0.1", 80 | Timeout: 10, 81 | Type: "httptrap", 82 | Tags: []string{}, 83 | } 84 | 85 | testCMBroker = apiclient.Broker{ 86 | CID: "/broker/1234", 87 | Name: "test broker", 88 | Type: "enterprise", 89 | Details: []apiclient.BrokerDetail{ 90 | { 91 | CN: "testbroker.example.com", 92 | ExternalHost: nil, 93 | ExternalPort: 43191, 94 | IP: &[]string{"127.0.0.1"}[0], 95 | Modules: []string{"httptrap"}, 96 | Port: &[]uint16{43191}[0], 97 | Status: "active", 98 | }, 99 | }, 100 | } 101 | ) 102 | 103 | func testCMServer() *httptest.Server { 104 | f := func(w http.ResponseWriter, r *http.Request) { 105 | // fmt.Printf("%s %s\n", r.Method, r.URL.String()) 106 | switch r.URL.Path { 107 | case "/check_bundle/1234": // handle GET/PUT/DELETE 108 | switch r.Method { 109 | case "PUT": // update 110 | defer r.Body.Close() 111 | b, err := ioutil.ReadAll(r.Body) 112 | if err != nil { 113 | panic(err) 114 | } 115 | w.WriteHeader(200) 116 | w.Header().Set("Content-Type", "application/json") 117 | fmt.Fprintln(w, string(b)) 118 | case "GET": // get by id/cid 119 | ret, err := json.Marshal(testCMCheckBundle) 120 | if err != nil { 121 | panic(err) 122 | } 123 | w.WriteHeader(200) 124 | w.Header().Set("Content-Type", "application/json") 125 | fmt.Fprintln(w, string(ret)) 126 | default: 127 | w.WriteHeader(500) 128 | fmt.Fprintln(w, "unsupported method") 129 | } 130 | case "/check_bundle": 131 | switch r.Method { 132 | case "GET": // search 133 | if strings.HasPrefix(r.URL.String(), "/check_bundle?search=") { 134 | r := []apiclient.CheckBundle{testCMCheckBundle} 135 | ret, err := json.Marshal(r) 136 | if err != nil { 137 | panic(err) 138 | } 139 | w.WriteHeader(200) 140 | w.Header().Set("Content-Type", "application/json") 141 | fmt.Fprintln(w, string(ret)) 142 | } else { 143 | w.WriteHeader(200) 144 | w.Header().Set("Content-Type", "application/json") 145 | fmt.Fprintln(w, "[]") 146 | } 147 | case "POST": // create 148 | cfg, err := ioutil.ReadAll(r.Body) 149 | if err != nil { 150 | panic(err) 151 | } 152 | 153 | var bundle apiclient.CheckBundle 154 | if err = json.Unmarshal(cfg, &bundle); err != nil { 155 | panic(err) 156 | } 157 | bundle.CID = testCheckBundle.CID 158 | bundle.Checks = testCheckBundle.Checks 159 | bundle.CheckUUIDs = testCheckBundle.CheckUUIDs 160 | bundle.ReverseConnectURLs = testCheckBundle.ReverseConnectURLs 161 | bundle.Config[config.SubmissionURL] = testCheckBundle.Config[config.SubmissionURL] 162 | bundle.Config[config.ReverseSecretKey] = testCheckBundle.Config[config.ReverseSecretKey] 163 | ret, err := json.Marshal(bundle) 164 | if err != nil { 165 | panic(err) 166 | } 167 | w.WriteHeader(200) 168 | w.Header().Set("Content-Type", "application/json") 169 | fmt.Fprintln(w, string(ret)) 170 | default: 171 | w.WriteHeader(405) 172 | fmt.Fprintf(w, "method not allowed %s", r.Method) 173 | } 174 | case "/broker": 175 | switch r.Method { 176 | case "GET": 177 | r := []apiclient.Broker{testCMBroker} 178 | ret, err := json.Marshal(r) 179 | if err != nil { 180 | panic(err) 181 | } 182 | w.WriteHeader(200) 183 | w.Header().Set("Content-Type", "application/json") 184 | fmt.Fprintln(w, string(ret)) 185 | default: 186 | w.WriteHeader(405) 187 | fmt.Fprintf(w, "method not allowed %s", r.Method) 188 | } 189 | case "/broker/1234": 190 | switch r.Method { 191 | case "GET": 192 | ret, err := json.Marshal(testCMBroker) 193 | if err != nil { 194 | panic(err) 195 | } 196 | w.WriteHeader(200) 197 | w.Header().Set("Content-Type", "application/json") 198 | fmt.Fprintln(w, string(ret)) 199 | default: 200 | w.WriteHeader(405) 201 | fmt.Fprintf(w, "method not allowed %s", r.Method) 202 | } 203 | case "/check": 204 | switch r.Method { 205 | case "GET": 206 | r := []apiclient.Check{testCMCheck} 207 | ret, err := json.Marshal(r) 208 | if err != nil { 209 | panic(err) 210 | } 211 | w.WriteHeader(200) 212 | w.Header().Set("Content-Type", "application/json") 213 | fmt.Fprintln(w, string(ret)) 214 | default: 215 | w.WriteHeader(405) 216 | fmt.Fprintf(w, "method not allowed %s", r.Method) 217 | } 218 | case "/check/1234": 219 | switch r.Method { 220 | case "GET": 221 | ret, err := json.Marshal(testCMCheck) 222 | if err != nil { 223 | panic(err) 224 | } 225 | w.WriteHeader(200) 226 | w.Header().Set("Content-Type", "application/json") 227 | fmt.Fprintln(w, string(ret)) 228 | default: 229 | w.WriteHeader(405) 230 | fmt.Fprintf(w, "method not allowed %s", r.Method) 231 | } 232 | case "/pki/ca.crt": 233 | w.WriteHeader(200) 234 | w.Header().Set("Content-Type", "application/json") 235 | fmt.Fprintln(w, cert) 236 | default: 237 | msg := fmt.Sprintf("not found %s", r.URL.Path) 238 | w.WriteHeader(404) 239 | fmt.Fprintln(w, msg) 240 | } 241 | } 242 | 243 | return httptest.NewServer(http.HandlerFunc(f)) 244 | } 245 | 246 | func TestNewCheckManager(t *testing.T) { 247 | 248 | t.Log("no config supplied") 249 | { 250 | expectedError := errors.New("invalid Check Manager configuration (nil)") 251 | _, err := NewCheckManager(nil) 252 | if err == nil || err.Error() != expectedError.Error() { 253 | t.Errorf("Expected an '%#v' error, got '%#v'", expectedError, err) 254 | } 255 | } 256 | 257 | t.Log("no API Token and no Submission URL supplied") 258 | { 259 | expectedError := errors.New("invalid check manager configuration (no API token AND no submission url)") 260 | cfg := &Config{} 261 | _, err := NewCheckManager(cfg) 262 | if err == nil || err.Error() != expectedError.Error() { 263 | t.Errorf("Expected an '%#v' error, got '%#v'", expectedError, err) 264 | } 265 | } 266 | 267 | t.Log("no API Token, Submission URL (http) only") 268 | { 269 | cfg := &Config{} 270 | cfg.Check.SubmissionURL = "http://127.0.0.1:56104" 271 | cm, err := NewCheckManager(cfg) 272 | if err != nil { 273 | t.Errorf("Expected no error, got '%v'", err) 274 | } 275 | 276 | cm.Initialize() 277 | 278 | for !cm.IsReady() { 279 | t.Log("\twaiting for cm to init") 280 | time.Sleep(1 * time.Second) 281 | } 282 | 283 | trap, err := cm.GetSubmissionURL() 284 | if err != nil { 285 | t.Errorf("Expected no error, got '%v'", err) 286 | } 287 | 288 | if trap.URL.String() != cfg.Check.SubmissionURL { 289 | t.Errorf("Expected '%s' == '%s'", trap.URL.String(), cfg.Check.SubmissionURL) 290 | } 291 | 292 | if trap.TLS != nil { 293 | t.Errorf("Expected nil found %#v", trap.TLS) 294 | } 295 | } 296 | 297 | t.Log("no API Token, Submission URL (https) only") 298 | { 299 | cfg := &Config{} 300 | cfg.Check.SubmissionURL = "https://127.0.0.1/v2" 301 | 302 | cm, err := NewCheckManager(cfg) 303 | if err != nil { 304 | t.Fatalf("Expected no error, got '%v'", err) 305 | } 306 | 307 | cm.Initialize() 308 | 309 | for !cm.IsReady() { 310 | t.Log("\twaiting for cm to init") 311 | time.Sleep(1 * time.Second) 312 | } 313 | 314 | trap, err := cm.GetSubmissionURL() 315 | if err != nil { 316 | t.Fatalf("Expected no error, got '%v'", err) 317 | } 318 | 319 | if trap.URL.String() != cfg.Check.SubmissionURL { 320 | t.Fatalf("Expected '%s' == '%s'", trap.URL.String(), cfg.Check.SubmissionURL) 321 | } 322 | 323 | if trap.TLS == nil { 324 | t.Fatalf("Expected a x509 cert pool, found nil") 325 | } 326 | } 327 | 328 | t.Log("Defaults") 329 | { 330 | server := testCMServer() 331 | defer server.Close() 332 | 333 | testURL, err := url.Parse(server.URL) 334 | if err != nil { 335 | t.Fatalf("Error parsing temporary url %v", err) 336 | } 337 | 338 | hostParts := strings.Split(testURL.Host, ":") 339 | hostPort, err := strconv.Atoi(hostParts[1]) 340 | if err != nil { 341 | t.Fatalf("Error converting port to numeric %v", err) 342 | } 343 | 344 | testCMBroker.Details[0].ExternalHost = &hostParts[0] 345 | testCMBroker.Details[0].ExternalPort = uint16(hostPort) 346 | cfg := &Config{ 347 | Log: log.New(os.Stderr, "", log.LstdFlags), 348 | API: apiclient.Config{ 349 | TokenKey: "1234", 350 | TokenApp: "abc", 351 | URL: server.URL, 352 | }, 353 | } 354 | 355 | cm, err := NewCheckManager(cfg) 356 | if err != nil { 357 | t.Fatalf("Expected no error, got '%v'", err) 358 | } 359 | 360 | cm.Initialize() 361 | 362 | for !cm.IsReady() { 363 | t.Log("\twaiting for cm to init") 364 | time.Sleep(1 * time.Second) 365 | } 366 | 367 | trap, err := cm.GetSubmissionURL() 368 | if err != nil { 369 | t.Fatalf("Expected no error, got '%v'", err) 370 | } 371 | suburl, found := testCMCheckBundle.Config["submission_url"] 372 | if !found { 373 | t.Fatalf("Exected submission_url in check bundle config %+v", testCMCheckBundle) 374 | } 375 | if trap.URL.String() != suburl { 376 | t.Fatalf("Expected '%s' got '%s'", suburl, trap.URL.String()) 377 | } 378 | } 379 | 380 | t.Log("Custom broker ssl config") 381 | { 382 | server := sslBroker() 383 | defer server.Close() 384 | 385 | cfg := &Config{ 386 | Log: log.New(os.Stderr, "", log.LstdFlags), 387 | } 388 | 389 | c := server.Certificate() 390 | cp := x509.NewCertPool() 391 | cp.AddCert(c) 392 | 393 | cfg.Check.SubmissionURL = server.URL 394 | cfg.Broker.TLSConfig = &tls.Config{RootCAs: cp} 395 | 396 | cm, err := NewCheckManager(cfg) 397 | if err != nil { 398 | t.Fatalf("Expected no error, got '%v'", err) 399 | } 400 | 401 | cm.Initialize() 402 | 403 | for !cm.IsReady() { 404 | t.Log("\twaiting for cm to init") 405 | time.Sleep(1 * time.Second) 406 | } 407 | 408 | trap, err := cm.GetSubmissionURL() 409 | if err != nil { 410 | t.Fatalf("Expected no error, got '%v'", err) 411 | } 412 | if trap.URL.String() != server.URL { 413 | t.Fatalf("Expected '%s' got '%s'", server.URL, trap.URL.String()) 414 | } 415 | 416 | // quick request to make sure the trap.TLS is the one passed and not the default 417 | client := &http.Client{Transport: &http.Transport{TLSClientConfig: trap.TLS}} 418 | resp, err := client.Get(trap.URL.String()) //nolint:noctx 419 | if err != nil { 420 | t.Fatalf("expected no error, got (%s)", err) 421 | } 422 | resp.Body.Close() 423 | 424 | t.Log("test ResetTrap") 425 | { 426 | err := cm.ResetTrap() 427 | if err != nil { 428 | t.Fatalf("expected no error, got (%s)", err) 429 | } 430 | trap, err := cm.GetSubmissionURL() 431 | if err != nil { 432 | t.Fatalf("Expected no error, got '%v'", err) 433 | } 434 | if trap.URL.String() != server.URL { 435 | t.Fatalf("Expected '%s' got '%s'", server.URL, trap.URL.String()) 436 | } 437 | } 438 | 439 | t.Log("test RefreshTrap") 440 | { 441 | cm.trapLastUpdate = time.Now().Add(-(cm.trapMaxURLAge + 2*time.Second)) 442 | if err := cm.RefreshTrap(); err != nil { 443 | t.Fatalf("expected no error got (%v)", err) 444 | } 445 | trap, err := cm.GetSubmissionURL() 446 | if err != nil { 447 | t.Fatalf("Expected no error, got '%v'", err) 448 | } 449 | if trap.URL.String() != server.URL { 450 | t.Fatalf("Expected '%s' got '%s'", server.URL, trap.URL.String()) 451 | } 452 | } 453 | } 454 | } 455 | -------------------------------------------------------------------------------- /histogram_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Circonus, Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package circonusgometrics 6 | 7 | import ( 8 | "fmt" 9 | "reflect" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func TestTiming(t *testing.T) { 15 | t.Log("Testing histogram.Timing") 16 | 17 | cm := &CirconusMetrics{histograms: make(map[string]*Histogram)} 18 | 19 | cm.Timing("foo", 1) 20 | 21 | hist, ok := cm.histograms["foo"] 22 | if !ok { 23 | t.Errorf("Expected to find foo") 24 | } 25 | 26 | if hist == nil { 27 | t.Errorf("Expected *Histogram, found %v", hist) 28 | } 29 | 30 | val := hist.hist.DecStrings() 31 | if len(val) != 1 { 32 | t.Errorf("Expected 1, found '%v'", val) 33 | } 34 | 35 | expectedVal := "H[1.0e+00]=1" 36 | if val[0] != expectedVal { 37 | t.Errorf("Expected '%s', found '%s'", expectedVal, val[0]) 38 | } 39 | } 40 | 41 | func TestTimingWithTags(t *testing.T) { 42 | t.Log("Testing histogram.TimingWithTags") 43 | 44 | metricName := "foo" 45 | tags := Tags{{"foo", "bar"}, {"baz", "qux"}} 46 | 47 | cm := &CirconusMetrics{histograms: make(map[string]*Histogram)} 48 | 49 | streamTagMetricName := cm.MetricNameWithStreamTags("foo", tags) 50 | 51 | cm.TimingWithTags(metricName, tags, 1) 52 | 53 | hist, ok := cm.histograms[streamTagMetricName] 54 | if !ok { 55 | t.Fatalf("%s with %v tags not found (%s) (%#v)", metricName, tags, streamTagMetricName, cm.histograms) 56 | } 57 | 58 | if hist == nil { 59 | t.Errorf("Expected *Histogram, found %v", hist) 60 | } 61 | 62 | val := hist.hist.DecStrings() 63 | if len(val) != 1 { 64 | t.Errorf("Expected 1, found '%v'", val) 65 | } 66 | 67 | expectedVal := "H[1.0e+00]=1" 68 | if val[0] != expectedVal { 69 | t.Errorf("Expected '%s', found '%s'", expectedVal, val[0]) 70 | } 71 | 72 | } 73 | 74 | func TestRecordValue(t *testing.T) { 75 | t.Log("Testing histogram.RecordValue") 76 | 77 | cm := &CirconusMetrics{histograms: make(map[string]*Histogram)} 78 | 79 | cm.RecordValue("foo", 1) 80 | 81 | hist, ok := cm.histograms["foo"] 82 | if !ok { 83 | t.Errorf("Expected to find foo") 84 | } 85 | 86 | if hist == nil { 87 | t.Errorf("Expected *Histogram, found %v", hist) 88 | } 89 | 90 | val := hist.hist.DecStrings() 91 | if len(val) != 1 { 92 | t.Errorf("Expected 1, found '%v'", val) 93 | } 94 | 95 | expectedVal := "H[1.0e+00]=1" 96 | if val[0] != expectedVal { 97 | t.Errorf("Expected '%s', found '%s'", expectedVal, val[0]) 98 | } 99 | } 100 | 101 | func TestRecordValueWithTags(t *testing.T) { 102 | t.Log("Testing histogram.RecordValueWithTags") 103 | 104 | metricName := "foo" 105 | tags := Tags{{"foo", "bar"}, {"baz", "qux"}} 106 | 107 | cm := &CirconusMetrics{histograms: make(map[string]*Histogram)} 108 | streamTagMetricName := cm.MetricNameWithStreamTags("foo", tags) 109 | 110 | cm.RecordValueWithTags(metricName, tags, 1) 111 | 112 | hist, ok := cm.histograms[streamTagMetricName] 113 | if !ok { 114 | t.Fatalf("%s with %v tags not found (%s) (%#v)", metricName, tags, streamTagMetricName, cm.histograms) 115 | } 116 | 117 | if hist == nil { 118 | t.Errorf("Expected *Histogram, found %v", hist) 119 | } 120 | 121 | val := hist.hist.DecStrings() 122 | if len(val) != 1 { 123 | t.Errorf("Expected 1, found '%v'", val) 124 | } 125 | 126 | expectedVal := "H[1.0e+00]=1" 127 | if val[0] != expectedVal { 128 | t.Errorf("Expected '%s', found '%s'", expectedVal, val[0]) 129 | } 130 | } 131 | 132 | func TestRecordDuration(t *testing.T) { 133 | t.Log("Testing histogram.RecordDuration") 134 | 135 | tests := []struct { 136 | metricName string 137 | durs []time.Duration 138 | out string 139 | tags Tags 140 | }{ 141 | { 142 | metricName: "foo", 143 | durs: []time.Duration{1 * time.Second}, 144 | out: "H[1.0e+00]=1", 145 | }, 146 | { 147 | metricName: "foo", 148 | durs: []time.Duration{1 * time.Millisecond}, 149 | out: "H[1.0e-03]=1", 150 | }, 151 | { 152 | metricName: "foo", 153 | durs: []time.Duration{1 * time.Millisecond}, 154 | tags: Tags{Tag{"unit", "ms"}}, 155 | out: "H[1.0e-03]=1", 156 | }, 157 | } 158 | 159 | for n, test := range tests { 160 | test := test 161 | t.Run(fmt.Sprintf("%d", n), func(t *testing.T) { 162 | cm := &CirconusMetrics{histograms: make(map[string]*Histogram)} 163 | 164 | for _, dur := range test.durs { 165 | if len(test.tags) > 0 { 166 | cm.RecordDuration(test.metricName, dur) 167 | } else { 168 | cm.RecordDurationWithTags(test.metricName, test.tags, dur) 169 | } 170 | } 171 | 172 | hist, ok := cm.histograms[test.metricName] 173 | if !ok { 174 | t.Errorf("Expected to find %q", test.metricName) 175 | } 176 | 177 | if hist == nil { 178 | t.Errorf("Expected *Histogram, found %v", hist) 179 | } 180 | 181 | val := hist.hist.DecStrings() 182 | if len(val) != 1 { 183 | t.Errorf("Expected 1, found '%v'", val) 184 | } 185 | 186 | if val[0] != test.out { 187 | t.Errorf("Expected '%s', found '%s'", test.out, val[0]) 188 | } 189 | }) 190 | } 191 | } 192 | 193 | func TestRecordCountForValue(t *testing.T) { 194 | t.Log("Testing histogram.RecordCountForValue") 195 | 196 | cm := &CirconusMetrics{histograms: make(map[string]*Histogram)} 197 | 198 | cm.RecordCountForValue("foo", 1.2, 5) 199 | 200 | hist, ok := cm.histograms["foo"] 201 | if !ok { 202 | t.Errorf("Expected to find foo") 203 | } 204 | 205 | if hist == nil { 206 | t.Errorf("Expected *Histogram, found %v", hist) 207 | } 208 | 209 | val := hist.hist.DecStrings() 210 | if len(val) != 1 { 211 | t.Errorf("Expected 1, found '%v'", val) 212 | } 213 | 214 | expectedVal := "H[1.2e+00]=5" 215 | if val[0] != expectedVal { 216 | t.Errorf("Expected '%s', found '%s'", expectedVal, val[0]) 217 | } 218 | } 219 | 220 | func TestRecordCountForValueWithTags(t *testing.T) { 221 | t.Log("Testing histogram.RecordCountForValueWithTags") 222 | 223 | metricName := "foo" 224 | tags := Tags{{"foo", "bar"}, {"baz", "qux"}} 225 | 226 | cm := &CirconusMetrics{histograms: make(map[string]*Histogram)} 227 | streamTagMetricName := cm.MetricNameWithStreamTags("foo", tags) 228 | 229 | cm.RecordCountForValueWithTags(metricName, tags, 1.2, 5) 230 | 231 | hist, ok := cm.histograms[streamTagMetricName] 232 | if !ok { 233 | t.Fatalf("%s with %v tags not found (%s) (%#v)", metricName, tags, streamTagMetricName, cm.histograms) 234 | } 235 | 236 | if hist == nil { 237 | t.Errorf("Expected *Histogram, found %v", hist) 238 | } 239 | 240 | val := hist.hist.DecStrings() 241 | if len(val) != 1 { 242 | t.Errorf("Expected 1, found '%v'", val) 243 | } 244 | 245 | expectedVal := "H[1.2e+00]=5" 246 | if val[0] != expectedVal { 247 | t.Errorf("Expected '%s', found '%s'", expectedVal, val[0]) 248 | } 249 | } 250 | 251 | func TestSetHistogramValue(t *testing.T) { 252 | t.Log("Testing histogram.SetHistogramValue") 253 | 254 | cm := &CirconusMetrics{histograms: make(map[string]*Histogram)} 255 | 256 | cm.SetHistogramValue("foo", 1) 257 | 258 | hist, ok := cm.histograms["foo"] 259 | if !ok { 260 | t.Errorf("Expected to find foo") 261 | } 262 | 263 | if hist == nil { 264 | t.Errorf("Expected *Histogram, found %v", hist) 265 | } 266 | 267 | val := hist.hist.DecStrings() 268 | if len(val) != 1 { 269 | t.Errorf("Expected 1, found '%v'", val) 270 | } 271 | 272 | expectedVal := "H[1.0e+00]=1" 273 | if val[0] != expectedVal { 274 | t.Errorf("Expected '%s', found '%s'", expectedVal, val[0]) 275 | } 276 | } 277 | 278 | func TestSetHistogramValueWithTags(t *testing.T) { 279 | t.Log("Testing histogram.SetHistogramValueWithTags") 280 | 281 | cm := &CirconusMetrics{histograms: make(map[string]*Histogram)} 282 | 283 | metricName := "foo" 284 | tags := Tags{{"foo", "bar"}, {"baz", "qux"}} 285 | 286 | cm.SetHistogramValueWithTags(metricName, tags, 1) 287 | streamTagMetricName := cm.MetricNameWithStreamTags("foo", tags) 288 | 289 | hist, ok := cm.histograms[streamTagMetricName] 290 | if !ok { 291 | t.Fatalf("%s with %v tags not found (%s) (%#v)", metricName, tags, streamTagMetricName, cm.histograms) 292 | } 293 | 294 | if hist == nil { 295 | t.Errorf("Expected *Histogram, found %v", hist) 296 | } 297 | 298 | val := hist.hist.DecStrings() 299 | if len(val) != 1 { 300 | t.Errorf("Expected 1, found '%v'", val) 301 | } 302 | 303 | expectedVal := "H[1.0e+00]=1" 304 | if val[0] != expectedVal { 305 | t.Errorf("Expected '%s', found '%s'", expectedVal, val[0]) 306 | } 307 | } 308 | 309 | func TestGetHistogramTest(t *testing.T) { 310 | t.Log("Testing histogram.GetHistogramTest") 311 | 312 | cm := &CirconusMetrics{histograms: make(map[string]*Histogram)} 313 | 314 | cm.SetHistogramValue("foo", 10) 315 | expected := "H[1.0e+01]=1" 316 | 317 | val, err := cm.GetHistogramTest("foo") 318 | if err != nil { 319 | t.Errorf("Expected no error %v", err) 320 | } 321 | if len(val) == 0 { 322 | t.Error("Expected 1 value, got 0 values") 323 | } 324 | if val[0] != expected { 325 | t.Errorf("Expected '%s' got '%v'", expected, val[0]) 326 | } 327 | 328 | _, err = cm.GetHistogramTest("bar") 329 | if err == nil { 330 | t.Error("Expected error") 331 | } 332 | 333 | } 334 | 335 | func TestRemoveHistogram(t *testing.T) { 336 | t.Log("Testing histogram.RemoveHistogram") 337 | 338 | cm := &CirconusMetrics{histograms: make(map[string]*Histogram)} 339 | 340 | cm.SetHistogramValue("foo", 1) 341 | 342 | hist, ok := cm.histograms["foo"] 343 | if !ok { 344 | t.Errorf("Expected to find foo") 345 | } 346 | 347 | if hist == nil { 348 | t.Errorf("Expected *Histogram, found %v", hist) 349 | } 350 | 351 | val := hist.hist.DecStrings() 352 | if len(val) != 1 { 353 | t.Errorf("Expected 1, found '%v'", val) 354 | } 355 | 356 | expectedVal := "H[1.0e+00]=1" 357 | if val[0] != expectedVal { 358 | t.Errorf("Expected '%s', found '%s'", expectedVal, val[0]) 359 | } 360 | 361 | cm.RemoveHistogram("foo") 362 | 363 | hist, ok = cm.histograms["foo"] 364 | if ok { 365 | t.Errorf("Expected NOT to find foo") 366 | } 367 | 368 | if hist != nil { 369 | t.Errorf("Expected nil, found %v", hist) 370 | } 371 | } 372 | 373 | func TestRemoveHistogramWithTags(t *testing.T) { 374 | t.Log("Testing histogram.RemoveHistogramWithTags") 375 | 376 | cm := &CirconusMetrics{histograms: make(map[string]*Histogram)} 377 | 378 | metricName := "foo" 379 | tags := Tags{{"foo", "bar"}, {"baz", "qux"}} 380 | 381 | cm.SetHistogramValueWithTags(metricName, tags, 1) 382 | streamTagMetricName := cm.MetricNameWithStreamTags("foo", tags) 383 | 384 | hist, ok := cm.histograms[streamTagMetricName] 385 | if !ok { 386 | t.Fatalf("%s with %v tags not found (%s) (%#v)", metricName, tags, streamTagMetricName, cm.histograms) 387 | } 388 | 389 | val := hist.hist.DecStrings() 390 | if len(val) != 1 { 391 | t.Fatalf("Expected 1, found '%v'", val) 392 | } 393 | 394 | expectedVal := "H[1.0e+00]=1" 395 | if val[0] != expectedVal { 396 | t.Fatalf("Expected '%s', found '%s'", expectedVal, val[0]) 397 | } 398 | 399 | cm.RemoveHistogramWithTags(metricName, tags) 400 | 401 | hist, ok = cm.histograms[streamTagMetricName] 402 | if ok { 403 | t.Fatalf("expected NOT to find (%s)", streamTagMetricName) 404 | } 405 | 406 | if hist != nil { 407 | t.Fatalf("Expected nil, found %v", hist) 408 | } 409 | } 410 | 411 | func TestNewHistogram(t *testing.T) { 412 | t.Log("Testing histogram.NewHistogram") 413 | 414 | cm := &CirconusMetrics{histograms: make(map[string]*Histogram)} 415 | 416 | hist := cm.NewHistogram("foo") 417 | 418 | actualType := reflect.TypeOf(hist) 419 | expectedType := "*circonusgometrics.Histogram" 420 | if actualType.String() != expectedType { 421 | t.Errorf("Expected %s, got %s", expectedType, actualType.String()) 422 | } 423 | } 424 | 425 | func TestNewHistogramWithTags(t *testing.T) { 426 | t.Log("Testing histogram.NewHistogram") 427 | 428 | cm := &CirconusMetrics{histograms: make(map[string]*Histogram)} 429 | 430 | metricName := "foo" 431 | tags := Tags{{"foo", "bar"}, {"baz", "qux"}} 432 | 433 | hist := cm.NewHistogramWithTags(metricName, tags) 434 | streamTagMetricName := cm.MetricNameWithStreamTags("foo", tags) 435 | 436 | if hist == nil { 437 | t.Fatal("expected not nil") 438 | } 439 | if hist.name != streamTagMetricName { 440 | t.Fatalf("expected name (%s) got (%s)", streamTagMetricName, hist.name) 441 | } 442 | 443 | actualType := reflect.TypeOf(hist) 444 | expectedType := "*circonusgometrics.Histogram" 445 | if actualType.String() != expectedType { 446 | t.Errorf("Expected %s, got %s", expectedType, actualType.String()) 447 | } 448 | } 449 | 450 | func TestHistName(t *testing.T) { 451 | t.Log("Testing hist.Name") 452 | 453 | cm := &CirconusMetrics{histograms: make(map[string]*Histogram)} 454 | 455 | hist := cm.NewHistogram("foo") 456 | 457 | actualType := reflect.TypeOf(hist) 458 | expectedType := "*circonusgometrics.Histogram" 459 | if actualType.String() != expectedType { 460 | t.Errorf("Expected %s, got %s", expectedType, actualType.String()) 461 | } 462 | 463 | expectedName := "foo" 464 | actualName := hist.Name() 465 | if actualName != expectedName { 466 | t.Errorf("Expected '%s', found '%s'", expectedName, actualName) 467 | } 468 | } 469 | 470 | func TestHistRecordValue(t *testing.T) { 471 | t.Log("Testing hist.RecordValue") 472 | 473 | cm := &CirconusMetrics{histograms: make(map[string]*Histogram)} 474 | 475 | hist := cm.NewHistogram("foo") 476 | 477 | actualType := reflect.TypeOf(hist) 478 | expectedType := "*circonusgometrics.Histogram" 479 | if actualType.String() != expectedType { 480 | t.Errorf("Expected %s, got %s", expectedType, actualType.String()) 481 | } 482 | 483 | hist.RecordValue(1) 484 | 485 | val := hist.hist.DecStrings() 486 | if len(val) != 1 { 487 | t.Errorf("Expected 1, found '%v'", val) 488 | } 489 | 490 | expectedVal := "H[1.0e+00]=1" 491 | if val[0] != expectedVal { 492 | t.Errorf("Expected '%s', found '%s'", expectedVal, val[0]) 493 | } 494 | 495 | hist = cm.NewHistogram("foo") 496 | if hist == nil { 497 | t.Fatalf("Expected non-nil") 498 | } 499 | } 500 | -------------------------------------------------------------------------------- /checkmgr/check.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Circonus, Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package checkmgr 6 | 7 | import ( 8 | "crypto/rand" 9 | "crypto/sha256" 10 | "encoding/hex" 11 | "fmt" 12 | "net/url" 13 | "strconv" 14 | "strings" 15 | "time" 16 | 17 | "github.com/circonus-labs/go-apiclient" 18 | "github.com/circonus-labs/go-apiclient/config" 19 | "github.com/pkg/errors" 20 | ) 21 | 22 | // UpdateCheck determines if the check needs to be updated (new metrics, tags, etc.) 23 | func (cm *CheckManager) UpdateCheck(newMetrics map[string]*apiclient.CheckBundleMetric) { 24 | // only if check manager is enabled 25 | if !cm.enabled { 26 | return 27 | } 28 | 29 | // only if checkBundle has been populated 30 | if cm.checkBundle == nil { 31 | return 32 | } 33 | 34 | // only if there is *something* to update 35 | if !cm.forceCheckUpdate && len(newMetrics) == 0 && len(cm.metricTags) == 0 { 36 | return 37 | } 38 | 39 | // refresh check bundle (in case there were changes made by other apps or in UI) 40 | cid := cm.checkBundle.CID 41 | checkBundle, err := cm.apih.FetchCheckBundle(apiclient.CIDType(&cid)) 42 | if err != nil { 43 | cm.Log.Printf("error fetching up-to-date check bundle %v", err) 44 | return 45 | } 46 | cm.cbmu.Lock() 47 | cm.checkBundle = checkBundle 48 | cm.cbmu.Unlock() 49 | 50 | // check metric_limit and see if it’s 0, if so, don't even bother to try to update the check. 51 | 52 | cm.addNewMetrics(newMetrics) 53 | 54 | if len(cm.metricTags) > 0 { 55 | // note: if a tag has been added (queued) for a metric which never gets sent 56 | // the tags will be discarded. (setting tags does not *create* metrics.) 57 | for metricName, metricTags := range cm.metricTags { 58 | for metricIdx, metric := range cm.checkBundle.Metrics { 59 | if metric.Name == metricName { 60 | cm.checkBundle.Metrics[metricIdx].Tags = metricTags 61 | break 62 | } 63 | } 64 | cm.mtmu.Lock() 65 | delete(cm.metricTags, metricName) 66 | cm.mtmu.Unlock() 67 | } 68 | cm.forceCheckUpdate = true 69 | } 70 | 71 | if cm.forceCheckUpdate { 72 | newCheckBundle, err := cm.apih.UpdateCheckBundle(cm.checkBundle) 73 | if err != nil { 74 | cm.Log.Printf("error updating check bundle %v", err) 75 | return 76 | } 77 | 78 | cm.forceCheckUpdate = false 79 | cm.cbmu.Lock() 80 | cm.checkBundle = newCheckBundle 81 | cm.cbmu.Unlock() 82 | cm.inventoryMetrics() 83 | } 84 | 85 | } 86 | 87 | // Initialize CirconusMetrics instance. Attempt to find a check otherwise create one. 88 | // use cases: 89 | // 90 | // check [bundle] by submission url 91 | // check [bundle] by *check* id (note, not check_bundle id) 92 | // check [bundle] by search 93 | // create check [bundle] 94 | func (cm *CheckManager) initializeTrapURL() error { 95 | if cm.trapURL != "" { 96 | return nil 97 | } 98 | 99 | cm.trapmu.Lock() 100 | defer cm.trapmu.Unlock() 101 | 102 | // special case short-circuit: just send to a url, no check management 103 | // up to user to ensure that if url is https that it will work (e.g. not self-signed) 104 | if cm.checkSubmissionURL != "" { 105 | if !cm.enabled { 106 | cm.trapURL = cm.checkSubmissionURL 107 | cm.trapLastUpdate = time.Now() 108 | return nil 109 | } 110 | } 111 | 112 | if !cm.enabled { 113 | return errors.New("unable to initialize trap, check manager is disabled") 114 | } 115 | 116 | var err error 117 | var check *apiclient.Check 118 | var checkBundle *apiclient.CheckBundle 119 | var broker *apiclient.Broker 120 | 121 | switch { 122 | case cm.checkSubmissionURL != "": 123 | check, err = cm.fetchCheckBySubmissionURL(cm.checkSubmissionURL) 124 | if err != nil { 125 | return err 126 | } 127 | if !check.Active { 128 | return errors.Errorf("error, check %v is not active", check.CID) 129 | } 130 | // extract check id from check object returned from looking up using submission url 131 | // set m.CheckId to the id 132 | // set m.SubmissionUrl to "" to prevent trying to search on it going forward 133 | // use case: if the broker is changed in the UI metrics would stop flowing 134 | // unless the new submission url can be fetched with the API (which is no 135 | // longer possible using the original submission url) 136 | var id int 137 | id, err = strconv.Atoi(strings.ReplaceAll(check.CID, "/check/", "")) 138 | if err == nil { 139 | cm.checkID = apiclient.IDType(id) 140 | cm.checkSubmissionURL = "" 141 | } else { 142 | cm.Log.Printf("SubmissionUrl check CID to Check ID: unable to convert %s to int %q\n", check.CID, err) 143 | } 144 | case cm.checkID > 0: 145 | cid := fmt.Sprintf("/check/%d", cm.checkID) 146 | check, err = cm.apih.FetchCheck(apiclient.CIDType(&cid)) 147 | if err != nil { 148 | return err 149 | } 150 | if !check.Active { 151 | return errors.Errorf("error, check %v is not active", check.CID) 152 | } 153 | default: 154 | // new search (check.target != instanceid, instanceid encoded in notes field) 155 | searchCriteria := fmt.Sprintf( 156 | "(active:1)(type:\"%s\")(tags:%s)", cm.checkType, strings.Join(cm.checkSearchTag, ",")) 157 | filterCriteria := map[string][]string{"f_notes": {*cm.getNotes()}} 158 | checkBundle, err = cm.checkBundleSearch(searchCriteria, filterCriteria) 159 | if err != nil { 160 | return err 161 | } 162 | 163 | if checkBundle == nil { 164 | // old search (instanceid as check.target) 165 | searchCriteria := fmt.Sprintf( 166 | "(active:1)(type:\"%s\")(host:\"%s\")(tags:%s)", cm.checkType, cm.checkTarget, strings.Join(cm.checkSearchTag, ",")) 167 | checkBundle, err = cm.checkBundleSearch(searchCriteria, map[string][]string{}) 168 | if err != nil { 169 | return err 170 | } 171 | } 172 | 173 | if checkBundle == nil { 174 | // err==nil && checkBundle==nil is "no check bundles matched" 175 | // an error *should* be returned for any other invalid scenario 176 | checkBundle, broker, err = cm.createNewCheck() 177 | if err != nil { 178 | return err 179 | } 180 | } 181 | } 182 | 183 | if checkBundle == nil { 184 | if check != nil { 185 | cid := check.CheckBundleCID 186 | checkBundle, err = cm.apih.FetchCheckBundle(apiclient.CIDType(&cid)) 187 | if err != nil { 188 | return err 189 | } 190 | } else { 191 | return errors.Errorf("error, unable to retrieve, find, or create a check bundle") 192 | } 193 | } 194 | 195 | if broker == nil { 196 | cid := checkBundle.Brokers[0] 197 | broker, err = cm.apih.FetchBroker(apiclient.CIDType(&cid)) 198 | if err != nil { 199 | return err 200 | } 201 | } 202 | 203 | // retain to facilitate metric management (adding new metrics specifically) 204 | cm.checkBundle = checkBundle 205 | 206 | // determine the trap url to which metrics should be PUT 207 | if strings.HasPrefix(checkBundle.Type, "httptrap") { 208 | if turl, found := checkBundle.Config[config.SubmissionURL]; found { 209 | cm.trapURL = apiclient.URLType(turl) 210 | } else { 211 | if cm.Debug { 212 | cm.Log.Printf("missing config.%s %+v", config.SubmissionURL, checkBundle) 213 | } 214 | return errors.Errorf("error, unable to use check, no %s in config", config.SubmissionURL) 215 | } 216 | } else { 217 | // build a submission_url for non-httptrap checks out of mtev_reverse url 218 | if len(checkBundle.ReverseConnectURLs) == 0 { 219 | return errors.Errorf("error, %s is not an HTTPTRAP check and no reverse connection urls found", checkBundle.Checks[0]) 220 | } 221 | mtevURL := checkBundle.ReverseConnectURLs[0] 222 | mtevURL = strings.Replace(mtevURL, "mtev_reverse", "https", 1) 223 | mtevURL = strings.Replace(mtevURL, "check", "module/httptrap", 1) 224 | if rs, found := checkBundle.Config[config.ReverseSecretKey]; found { 225 | cm.trapURL = apiclient.URLType(fmt.Sprintf("%s/%s", mtevURL, rs)) 226 | } else { 227 | if cm.Debug { 228 | cm.Log.Printf("missing config.%s %+v", config.ReverseSecretKey, checkBundle) 229 | } 230 | return errors.Errorf("error, unable to use check, no %s in config", config.ReverseSecretKey) 231 | } 232 | } 233 | 234 | // used when sending as "ServerName" get around certs not having IP SANS 235 | // (cert created with server name as CN but IP used in trap url) 236 | cn, cnList, err := cm.getBrokerCN(broker, cm.trapURL) 237 | if err != nil { 238 | return err 239 | } 240 | cm.trapCN = BrokerCNType(cn) 241 | cm.trapCNList = cnList 242 | 243 | if cm.enabled { 244 | u, err := url.Parse(string(cm.trapURL)) 245 | if err != nil { 246 | return err 247 | } 248 | if u.Scheme == "https" { 249 | if err := cm.loadCACert(); err != nil { 250 | return err 251 | } 252 | } 253 | } 254 | 255 | // check is using metric filters, disable check management 256 | cm.manageMetrics = true 257 | if len(cm.checkBundle.MetricFilters) > 0 { 258 | cm.manageMetrics = false 259 | } 260 | if cm.manageMetrics { 261 | cm.inventoryMetrics() 262 | } 263 | cm.trapLastUpdate = time.Now() 264 | 265 | return nil 266 | } 267 | 268 | // Search for a check bundle given a predetermined set of criteria 269 | func (cm *CheckManager) checkBundleSearch(searchCriteria string, filterCriteria map[string][]string) (*apiclient.CheckBundle, error) { 270 | search := apiclient.SearchQueryType(searchCriteria) 271 | filter := apiclient.SearchFilterType(filterCriteria) 272 | checkBundles, err := cm.apih.SearchCheckBundles(&search, &filter) 273 | if err != nil { 274 | return nil, err 275 | } 276 | 277 | if len(*checkBundles) == 0 { 278 | return nil, nil // trigger creation of a new check 279 | } 280 | 281 | numActive := 0 282 | checkID := -1 283 | 284 | for idx, check := range *checkBundles { 285 | if check.Status == statusActive { 286 | numActive++ 287 | checkID = idx 288 | } 289 | } 290 | 291 | if numActive > 1 { 292 | return nil, errors.Errorf("multiple check bundles match criteria - search(%v) filter(%v)", searchCriteria, filterCriteria) 293 | } 294 | 295 | bundle := (*checkBundles)[checkID] 296 | 297 | return &bundle, nil 298 | } 299 | 300 | // Create a new check to receive metrics 301 | func (cm *CheckManager) createNewCheck() (*apiclient.CheckBundle, *apiclient.Broker, error) { 302 | checkSecret := string(cm.checkSecret) 303 | if checkSecret == "" { 304 | secret, err := cm.makeSecret() 305 | if err != nil { 306 | secret = "myS3cr3t" 307 | } 308 | checkSecret = secret 309 | } 310 | 311 | broker, err := cm.getBroker() 312 | if err != nil { 313 | return nil, nil, err 314 | } 315 | 316 | chkcfg := &apiclient.CheckBundle{ 317 | Brokers: []string{broker.CID}, 318 | Config: make(map[config.Key]string), 319 | DisplayName: string(cm.checkDisplayName), 320 | MetricFilters: [][]string{{"deny", "^$", ""}, {"allow", "^.+$", ""}}, 321 | MetricLimit: config.DefaultCheckBundleMetricLimit, 322 | Metrics: []apiclient.CheckBundleMetric{}, 323 | Notes: cm.getNotes(), 324 | Period: 60, 325 | Status: statusActive, 326 | Tags: append(cm.checkSearchTag, cm.checkTags...), 327 | Target: string(cm.checkTarget), 328 | Timeout: 10, 329 | Type: string(cm.checkType), 330 | } 331 | 332 | if len(cm.customConfigFields) > 0 { 333 | for fld, val := range cm.customConfigFields { 334 | chkcfg.Config[config.Key(fld)] = val 335 | } 336 | } 337 | 338 | // 339 | // use the default config settings if these are NOT set by user configuration 340 | // 341 | if val, ok := chkcfg.Config[config.AsyncMetrics]; !ok || val == "" { 342 | chkcfg.Config[config.AsyncMetrics] = "true" 343 | } 344 | 345 | if val, ok := chkcfg.Config[config.Secret]; !ok || val == "" { 346 | chkcfg.Config[config.Secret] = checkSecret 347 | } 348 | 349 | // set metric filters if provided 350 | if len(cm.checkMetricFilters) > 0 { 351 | mf := make([][]string, len(cm.checkMetricFilters)) 352 | for idx, rule := range cm.checkMetricFilters { 353 | mf[idx] = []string{rule.Type, rule.Filter, rule.Comment} 354 | } 355 | chkcfg.MetricFilters = mf 356 | } 357 | 358 | checkBundle, err := cm.apih.CreateCheckBundle(chkcfg) 359 | if err != nil { 360 | return nil, nil, err 361 | } 362 | 363 | return checkBundle, broker, nil 364 | } 365 | 366 | // Create a dynamic secret to use with a new check 367 | func (cm *CheckManager) makeSecret() (string, error) { 368 | hash := sha256.New() 369 | x := make([]byte, 2048) 370 | if _, err := rand.Read(x); err != nil { 371 | return "", err 372 | } 373 | if _, err := hash.Write(x); err != nil { 374 | return "", err 375 | } 376 | return hex.EncodeToString(hash.Sum(nil))[0:16], nil 377 | } 378 | 379 | func (cm *CheckManager) getNotes() *string { 380 | notes := fmt.Sprintf("cgm_instanceid|%s", cm.checkInstanceID) 381 | return ¬es 382 | } 383 | 384 | // FetchCheckBySubmissionURL fetch a check configuration by submission_url 385 | func (cm *CheckManager) fetchCheckBySubmissionURL(submissionURL apiclient.URLType) (*apiclient.Check, error) { 386 | if string(submissionURL) == "" { 387 | return nil, errors.New("error, invalid submission URL (blank)") 388 | } 389 | 390 | u, err := url.Parse(string(submissionURL)) 391 | if err != nil { 392 | return nil, err 393 | } 394 | 395 | // valid trap url: scheme://host[:port]/module/httptrap/UUID/secret 396 | 397 | // does it smell like a valid trap url path 398 | if !strings.Contains(u.Path, "/module/httptrap/") { 399 | return nil, errors.Errorf("error, invalid submission URL '%s', unrecognized path", submissionURL) 400 | } 401 | 402 | // extract uuid 403 | pathParts := strings.Split(strings.Replace(u.Path, "/module/httptrap/", "", 1), "/") 404 | if len(pathParts) != 2 { 405 | return nil, errors.Errorf("error, invalid submission URL '%s', UUID not where expected", submissionURL) 406 | } 407 | uuid := pathParts[0] 408 | 409 | filter := apiclient.SearchFilterType{"f__check_uuid": []string{uuid}} 410 | 411 | checks, err := cm.apih.SearchChecks(nil, &filter) 412 | if err != nil { 413 | return nil, err 414 | } 415 | 416 | if len(*checks) == 0 { 417 | return nil, errors.Errorf("error, no checks found with UUID %s", uuid) 418 | } 419 | 420 | numActive := 0 421 | checkID := -1 422 | 423 | for idx, check := range *checks { 424 | if check.Active { 425 | numActive++ 426 | if checkID == -1 { 427 | checkID = idx 428 | } 429 | } 430 | } 431 | 432 | if checkID == -1 { 433 | return nil, errors.Errorf("error, no active checks found %v", *checks) 434 | } 435 | 436 | if numActive > 1 { 437 | return nil, errors.Errorf("error, multiple checks with same UUID %s", uuid) 438 | } 439 | 440 | check := (*checks)[checkID] 441 | 442 | return &check, nil 443 | } 444 | -------------------------------------------------------------------------------- /checkmgr/broker_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Circonus, Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package checkmgr 6 | 7 | import ( 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "log" 12 | "net/http" 13 | "net/http/httptest" 14 | "net/url" 15 | "os" 16 | "strconv" 17 | "strings" 18 | "testing" 19 | "time" 20 | 21 | apiclient "github.com/circonus-labs/go-apiclient" 22 | ) 23 | 24 | var ( 25 | invalidBroker = apiclient.Broker{ 26 | CID: "/broker/1", 27 | Longitude: nil, 28 | Latitude: nil, 29 | Name: "test broker", 30 | Tags: []string{}, 31 | Type: "foo", 32 | Details: []apiclient.BrokerDetail{ 33 | { 34 | CN: "testbroker.example.com", 35 | ExternalHost: &[]string{"testbroker.example.com"}[0], 36 | ExternalPort: 43191, 37 | IP: &[]string{"127.0.0.1"}[0], 38 | MinVer: 0, 39 | Modules: []string{"a", "b", "c"}, 40 | Port: &[]uint16{43191}[0], 41 | Skew: nil, 42 | Status: "unprovisioned", 43 | Version: nil, 44 | }, 45 | }, 46 | } 47 | 48 | noIPorHostBroker = apiclient.Broker{ 49 | CID: "/broker/2", 50 | Longitude: nil, 51 | Latitude: nil, 52 | Name: "no ip or external host broker", 53 | Tags: []string{}, 54 | Type: "enterprise", 55 | Details: []apiclient.BrokerDetail{ 56 | { 57 | CN: "foobar", 58 | ExternalHost: nil, 59 | ExternalPort: 43191, 60 | IP: nil, 61 | MinVer: 0, 62 | Modules: []string{"httptrap"}, 63 | Port: &[]uint16{43191}[0], 64 | Skew: nil, 65 | Status: "active", 66 | Version: nil, 67 | }, 68 | }, 69 | } 70 | 71 | validBroker = apiclient.Broker{ 72 | CID: "/broker/2", 73 | Longitude: nil, 74 | Latitude: nil, 75 | Name: "test broker", 76 | Tags: []string{}, 77 | Type: "enterprise", 78 | Details: []apiclient.BrokerDetail{ 79 | { 80 | CN: "testbroker.example.com", 81 | ExternalHost: nil, 82 | ExternalPort: 43191, 83 | IP: &[]string{"127.0.0.1"}[0], 84 | MinVer: 0, 85 | Modules: []string{"httptrap"}, 86 | Port: &[]uint16{43191}[0], 87 | Skew: nil, 88 | Status: "active", 89 | Version: nil, 90 | }, 91 | }, 92 | } 93 | 94 | validBrokerNonEnterprise = apiclient.Broker{ 95 | CID: "/broker/3", 96 | Longitude: nil, 97 | Latitude: nil, 98 | Name: "test broker", 99 | Tags: []string{}, 100 | Type: "foo", 101 | Details: []apiclient.BrokerDetail{ 102 | { 103 | CN: "testbroker.example.com", 104 | ExternalHost: nil, 105 | ExternalPort: 43191, 106 | IP: &[]string{"127.0.0.1"}[0], 107 | MinVer: 0, 108 | Modules: []string{"httptrap"}, 109 | Port: &[]uint16{43191}[0], 110 | Skew: nil, 111 | Status: "active", 112 | Version: nil, 113 | }, 114 | }, 115 | } 116 | ) 117 | 118 | func testBrokerServer() *httptest.Server { 119 | f := func(w http.ResponseWriter, r *http.Request) { 120 | switch r.URL.Path { 121 | case "/broker/1": 122 | switch r.Method { 123 | case "GET": // get by id/cid 124 | ret, err := json.Marshal(invalidBroker) 125 | if err != nil { 126 | panic(err) 127 | } 128 | w.WriteHeader(200) 129 | w.Header().Set("Content-Type", "application/json") 130 | fmt.Fprintln(w, string(ret)) 131 | default: 132 | w.WriteHeader(500) 133 | fmt.Fprintln(w, "unsupported") 134 | } 135 | case "/broker/2": 136 | switch r.Method { 137 | case "GET": // get by id/cid 138 | ret, err := json.Marshal(validBroker) 139 | if err != nil { 140 | panic(err) 141 | } 142 | w.WriteHeader(200) 143 | w.Header().Set("Content-Type", "application/json") 144 | fmt.Fprintln(w, string(ret)) 145 | default: 146 | w.WriteHeader(500) 147 | fmt.Fprintln(w, "unsupported") 148 | } 149 | case "/broker": 150 | switch r.Method { 151 | case "GET": // search or filter 152 | var c []apiclient.Broker 153 | switch { 154 | case strings.Contains(r.URL.String(), "f__tags_has=no%3Abroker"): 155 | c = []apiclient.Broker{} 156 | case strings.Contains(r.URL.String(), "f__tags_has=multi%3Abroker"): 157 | c = []apiclient.Broker{invalidBroker, invalidBroker} 158 | default: 159 | c = []apiclient.Broker{validBroker, validBrokerNonEnterprise} 160 | } 161 | ret, err := json.Marshal(c) 162 | if err != nil { 163 | panic(err) 164 | } 165 | w.WriteHeader(200) 166 | w.Header().Set("Content-Type", "application/json") 167 | fmt.Fprintln(w, string(ret)) 168 | default: 169 | w.WriteHeader(500) 170 | fmt.Fprintln(w, "unsupported") 171 | } 172 | default: 173 | w.WriteHeader(500) 174 | fmt.Fprintln(w, "unsupported") 175 | } 176 | } 177 | 178 | return httptest.NewServer(http.HandlerFunc(f)) 179 | } 180 | 181 | func TestBrokerSupportsCheckType(t *testing.T) { 182 | detail := &apiclient.BrokerDetail{ 183 | Modules: []string{"httptrap"}, 184 | } 185 | 186 | cm := CheckManager{} 187 | 188 | t.Log("supports 'httptrap' check type?") 189 | { 190 | ok := cm.brokerSupportsCheckType("httptrap", detail) 191 | if !ok { 192 | t.Fatal("Expected OK") 193 | } 194 | } 195 | 196 | t.Log("supports 'foo' check type?") 197 | { 198 | ok := cm.brokerSupportsCheckType("foo", detail) 199 | if ok { 200 | t.Fatal("Expected not OK") 201 | } 202 | } 203 | } 204 | 205 | func TestGetBrokerCN(t *testing.T) { 206 | 207 | t.Log("URL with IP") 208 | { 209 | submissionURL := apiclient.URLType("http://127.0.0.1:43191/blah/blah/blah") 210 | cm := CheckManager{} 211 | 212 | _, _, err := cm.getBrokerCN(&validBroker, submissionURL) 213 | if err != nil { 214 | t.Fatalf("Expected no error, got %+v", err) 215 | } 216 | } 217 | 218 | t.Log("URL with FQDN") 219 | { 220 | submissionURL := apiclient.URLType("http://test.example.com:43191/blah/blah/blah") 221 | cm := CheckManager{} 222 | 223 | _, _, err := cm.getBrokerCN(&validBroker, submissionURL) 224 | if err != nil { 225 | t.Fatalf("Expected no error, got %+v", err) 226 | } 227 | } 228 | 229 | t.Log("URL with invalid IP") 230 | { 231 | submissionURL := apiclient.URLType("http://127.0.0.2:43191/blah/blah/blah") 232 | cm := CheckManager{} 233 | 234 | _, _, err := cm.getBrokerCN(&validBroker, submissionURL) 235 | if err == nil { 236 | t.Fatal("expected error") 237 | } 238 | if err.Error() != "error, unable to match URL host (127.0.0.2:43191) to Broker" { 239 | t.Fatalf("unexpected error (%s)", err) 240 | } 241 | } 242 | } 243 | 244 | func TestSelectBroker(t *testing.T) { 245 | server := testBrokerServer() 246 | defer server.Close() 247 | 248 | testURL, err := url.Parse(server.URL) 249 | if err != nil { 250 | t.Fatalf("Error parsing temporary url %v", err) 251 | } 252 | 253 | hostParts := strings.Split(testURL.Host, ":") 254 | hostPort, err := strconv.Atoi(hostParts[1]) 255 | if err != nil { 256 | t.Fatalf("Error converting port to numeric %v", err) 257 | } 258 | 259 | validBroker.Details[0].ExternalHost = &hostParts[0] 260 | validBroker.Details[0].ExternalPort = uint16(hostPort) 261 | validBroker.Details[0].IP = &hostParts[0] 262 | validBroker.Details[0].Port = &[]uint16{uint16(hostPort)}[0] 263 | 264 | validBrokerNonEnterprise.Details[0].ExternalHost = &hostParts[0] 265 | validBrokerNonEnterprise.Details[0].ExternalPort = uint16(hostPort) 266 | validBrokerNonEnterprise.Details[0].IP = &hostParts[0] 267 | validBrokerNonEnterprise.Details[0].Port = &[]uint16{uint16(hostPort)}[0] 268 | 269 | t.Log("default broker selection") 270 | { 271 | cm := &CheckManager{ 272 | checkType: "httptrap", 273 | brokerMaxResponseTime: time.Duration(time.Millisecond * 500), 274 | } 275 | ac := &apiclient.Config{ 276 | TokenApp: "abcd", 277 | TokenKey: "1234", 278 | URL: server.URL, 279 | } 280 | apih, err := apiclient.New(ac) 281 | if err != nil { 282 | t.Errorf("Expected no error, got '%v'", err) 283 | } 284 | cm.apih = apih 285 | 286 | _, err = cm.selectBroker() 287 | if err != nil { 288 | t.Fatal("Expected no error") 289 | } 290 | } 291 | 292 | t.Log("tag, no brokers matching") 293 | { 294 | cm := &CheckManager{ 295 | checkType: "httptrap", 296 | brokerMaxResponseTime: time.Duration(time.Millisecond * 500), 297 | brokerSelectTag: apiclient.TagType([]string{"no:broker"}), 298 | } 299 | ac := &apiclient.Config{ 300 | TokenApp: "abcd", 301 | TokenKey: "1234", 302 | URL: server.URL, 303 | } 304 | apih, err := apiclient.New(ac) 305 | if err != nil { 306 | t.Errorf("Expected no error, got '%v'", err) 307 | } 308 | cm.apih = apih 309 | 310 | expectedError := errors.New("zero brokers found") 311 | 312 | _, err = cm.selectBroker() 313 | if err == nil { 314 | t.Fatal("Expected an error") 315 | } 316 | if expectedError.Error() != err.Error() { 317 | t.Errorf("Expected %v got '%v'", expectedError, err) 318 | } 319 | } 320 | 321 | t.Log("multiple brokers with tag, none valid") 322 | { 323 | cm := &CheckManager{ 324 | checkType: "httptrap", 325 | brokerMaxResponseTime: time.Duration(time.Millisecond * 500), 326 | brokerSelectTag: apiclient.TagType([]string{"multi:broker"}), 327 | } 328 | ac := &apiclient.Config{ 329 | TokenApp: "abcd", 330 | TokenKey: "1234", 331 | URL: server.URL, 332 | } 333 | apih, err := apiclient.NewAPI(ac) 334 | if err != nil { 335 | t.Errorf("Expected no error, got '%v'", err) 336 | } 337 | cm.apih = apih 338 | 339 | expectedError := errors.New("found 2 broker(s), zero are valid") 340 | _, err = cm.selectBroker() 341 | if err == nil { 342 | t.Fatalf("Expected an error") 343 | } 344 | 345 | if expectedError.Error() != err.Error() { 346 | t.Fatalf("Expected %v got '%v'", expectedError, err) 347 | } 348 | } 349 | 350 | } 351 | 352 | func TestIsValidBroker(t *testing.T) { 353 | cm := &CheckManager{ 354 | Log: log.New(os.Stderr, "", log.LstdFlags), 355 | checkType: "httptrap", 356 | brokerMaxResponseTime: time.Duration(time.Millisecond * 50), 357 | } 358 | 359 | broker := apiclient.Broker{ 360 | CID: "/broker/2", 361 | Name: "test broker", 362 | Type: "enterprise", 363 | Details: []apiclient.BrokerDetail{ 364 | { 365 | CN: "testbroker.example.com", 366 | ExternalHost: nil, 367 | ExternalPort: 43191, 368 | IP: &[]string{"127.0.0.1"}[0], 369 | Modules: []string{"httptrap"}, 370 | Port: &[]uint16{43191}[0], 371 | Status: "unprovisioned", 372 | }, 373 | }, 374 | } 375 | 376 | t.Log("status unprovisioned") 377 | { 378 | if cm.isValidBroker(&broker) { 379 | t.Fatal("Expected invalid broker") 380 | } 381 | } 382 | 383 | t.Log("no ip or host") 384 | { 385 | if cm.isValidBroker(&noIPorHostBroker) { 386 | t.Fatal("Expected invalid broker") 387 | } 388 | } 389 | 390 | t.Log("does not have required module") 391 | { 392 | broker.Details[0].Modules = []string{"foo"} 393 | broker.Details[0].Status = "active" 394 | if cm.isValidBroker(&broker) { 395 | t.Fatal("Expected invalid broker") 396 | } 397 | } 398 | } 399 | 400 | func TestIsValidBrokerTimeout(t *testing.T) { 401 | if os.Getenv("CIRCONUS_BROKER_TEST_TIMEOUT") == "" { 402 | t.Skip("not testing timeouts, CIRCONUS_BROKER_TEST_TIMEOUT not set") 403 | } 404 | 405 | cm := &CheckManager{ 406 | Log: log.New(os.Stderr, "", log.LstdFlags), 407 | checkType: "httptrap", 408 | brokerMaxResponseTime: time.Duration(time.Millisecond * 50), 409 | } 410 | 411 | broker := apiclient.Broker{ 412 | CID: "/broker/2", 413 | Name: "test broker", 414 | Type: "enterprise", 415 | Details: []apiclient.BrokerDetail{ 416 | { 417 | CN: "testbroker.example.com", 418 | ExternalHost: nil, 419 | ExternalPort: 43191, 420 | IP: &[]string{"127.0.0.1"}[0], 421 | Modules: []string{"httptrap"}, 422 | Port: &[]uint16{43191}[0], 423 | Status: "unprovisioned", 424 | }, 425 | }, 426 | } 427 | 428 | t.Log("unable to connect, broker.ExternalPort") 429 | { 430 | broker.Name = "test" 431 | broker.Details[0].Modules = []string{"httptrap"} 432 | broker.Details[0].Status = "active" 433 | if cm.isValidBroker(&broker) { 434 | t.Fatal("Expected invalid broker") 435 | } 436 | } 437 | 438 | t.Log("unable to connect, broker.Port") 439 | { 440 | broker.Name = "test" 441 | broker.Details[0].ExternalPort = 0 442 | broker.Details[0].Modules = []string{"httptrap"} 443 | broker.Details[0].Status = "active" 444 | if cm.isValidBroker(&broker) { 445 | t.Fatal("Expected invalid broker") 446 | } 447 | } 448 | 449 | t.Log("unable to connect, default port") 450 | { 451 | broker.Name = "test" 452 | broker.Details[0].ExternalPort = 0 453 | broker.Details[0].Port = &[]uint16{0}[0] 454 | broker.Details[0].Modules = []string{"httptrap"} 455 | broker.Details[0].Status = "active" 456 | if cm.isValidBroker(&broker) { 457 | t.Fatal("Expected invalid broker") 458 | } 459 | } 460 | } 461 | 462 | func TestGetBroker(t *testing.T) { 463 | server := testBrokerServer() 464 | defer server.Close() 465 | 466 | testURL, err := url.Parse(server.URL) 467 | if err != nil { 468 | t.Fatalf("Error parsing temporary url %v", err) 469 | } 470 | 471 | hostParts := strings.Split(testURL.Host, ":") 472 | hostPort, err := strconv.Atoi(hostParts[1]) 473 | if err != nil { 474 | t.Fatalf("Error converting port to numeric %v", err) 475 | } 476 | 477 | validBroker.Details[0].ExternalHost = &hostParts[0] 478 | validBroker.Details[0].ExternalPort = uint16(hostPort) 479 | validBroker.Details[0].IP = &hostParts[0] 480 | validBroker.Details[0].Port = &[]uint16{uint16(hostPort)}[0] 481 | 482 | t.Log("invalid custom broker") 483 | { 484 | cm := &CheckManager{} 485 | ac := &apiclient.Config{ 486 | TokenApp: "abcd", 487 | TokenKey: "1234", 488 | URL: server.URL, 489 | } 490 | apih, err := apiclient.NewAPI(ac) 491 | if err != nil { 492 | t.Fatalf("unexpected error (%s)", err) 493 | } 494 | cm.apih = apih 495 | cm.brokerID = 1 496 | 497 | _, err = cm.getBroker() 498 | if err == nil || err.Error() != "error, designated broker 1 [test broker] is invalid (not active, does not support required check type, or connectivity issue)" { 499 | t.Fatalf("unexpected error (%s)", err) 500 | } 501 | } 502 | 503 | t.Log("valid custom broker") 504 | { 505 | 506 | cm := &CheckManager{ 507 | checkType: "httptrap", 508 | brokerMaxResponseTime: time.Duration(time.Millisecond * 500), 509 | } 510 | ac := &apiclient.Config{ 511 | TokenApp: "abcd", 512 | TokenKey: "1234", 513 | URL: server.URL, 514 | } 515 | 516 | apih, err := apiclient.NewAPI(ac) 517 | if err != nil { 518 | t.Fatalf("unexpected error (%s)", err) 519 | } 520 | cm.apih = apih 521 | cm.brokerID = 2 522 | 523 | _, err = cm.getBroker() 524 | if err != nil { 525 | t.Fatalf("unexpected error (%s)", err) 526 | } 527 | } 528 | 529 | } 530 | -------------------------------------------------------------------------------- /gauge_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Circonus, Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package circonusgometrics 6 | 7 | import ( 8 | "testing" 9 | ) 10 | 11 | func TestGauge(t *testing.T) { 12 | t.Log("Testing gauge.Gauge") 13 | 14 | t.Log("int") 15 | { 16 | cm := &CirconusMetrics{gauges: make(map[string]interface{})} 17 | 18 | v := int(1) 19 | cm.Gauge("foo", v) 20 | val, ok := cm.gauges["foo"] 21 | if !ok { 22 | t.Errorf("Expected to find foo") 23 | } 24 | 25 | if val.(int) != v { 26 | t.Errorf("Expected %d, found %v", v, val) 27 | } 28 | } 29 | 30 | t.Log("int8") 31 | { 32 | cm := &CirconusMetrics{gauges: make(map[string]interface{})} 33 | 34 | v := int8(1) 35 | cm.Gauge("foo", v) 36 | val, ok := cm.gauges["foo"] 37 | if !ok { 38 | t.Errorf("Expected to find foo") 39 | } 40 | 41 | if val.(int8) != v { 42 | t.Errorf("Expected %v, found %v", v, val) 43 | } 44 | } 45 | 46 | t.Log("int16") 47 | { 48 | cm := &CirconusMetrics{gauges: make(map[string]interface{})} 49 | 50 | v := int16(1) 51 | cm.Gauge("foo", v) 52 | val, ok := cm.gauges["foo"] 53 | if !ok { 54 | t.Errorf("Expected to find foo") 55 | } 56 | 57 | if val.(int16) != v { 58 | t.Errorf("Expected %v, found %v", v, val) 59 | } 60 | } 61 | 62 | t.Log("int32") 63 | { 64 | cm := &CirconusMetrics{gauges: make(map[string]interface{})} 65 | 66 | v := int32(1) 67 | cm.Gauge("foo", v) 68 | val, ok := cm.gauges["foo"] 69 | if !ok { 70 | t.Errorf("Expected to find foo") 71 | } 72 | 73 | if val.(int32) != v { 74 | t.Errorf("Expected %v, found %v", v, val) 75 | } 76 | } 77 | 78 | t.Log("int64") 79 | { 80 | cm := &CirconusMetrics{gauges: make(map[string]interface{})} 81 | 82 | v := int64(1) 83 | cm.Gauge("foo", v) 84 | val, ok := cm.gauges["foo"] 85 | if !ok { 86 | t.Errorf("Expected to find foo") 87 | } 88 | 89 | if val.(int64) != v { 90 | t.Errorf("Expected %v, found %v", v, val) 91 | } 92 | } 93 | 94 | t.Log("uint") 95 | { 96 | cm := &CirconusMetrics{gauges: make(map[string]interface{})} 97 | 98 | v := uint(1) 99 | cm.Gauge("foo", v) 100 | val, ok := cm.gauges["foo"] 101 | if !ok { 102 | t.Errorf("Expected to find foo") 103 | } 104 | 105 | if val.(uint) != v { 106 | t.Errorf("Expected %v, found %v", v, val) 107 | } 108 | } 109 | 110 | t.Log("uint8") 111 | { 112 | cm := &CirconusMetrics{gauges: make(map[string]interface{})} 113 | 114 | v := uint8(1) 115 | cm.Gauge("foo", v) 116 | val, ok := cm.gauges["foo"] 117 | if !ok { 118 | t.Errorf("Expected to find foo") 119 | } 120 | 121 | if val.(uint8) != v { 122 | t.Errorf("Expected %v, found %v", v, val) 123 | } 124 | } 125 | 126 | t.Log("uint16") 127 | { 128 | cm := &CirconusMetrics{gauges: make(map[string]interface{})} 129 | 130 | v := uint16(1) 131 | cm.Gauge("foo", v) 132 | val, ok := cm.gauges["foo"] 133 | if !ok { 134 | t.Errorf("Expected to find foo") 135 | } 136 | 137 | if val.(uint16) != v { 138 | t.Errorf("Expected %v, found %v", v, val) 139 | } 140 | } 141 | 142 | t.Log("uint32") 143 | { 144 | cm := &CirconusMetrics{gauges: make(map[string]interface{})} 145 | 146 | v := uint32(1) 147 | cm.Gauge("foo", v) 148 | val, ok := cm.gauges["foo"] 149 | if !ok { 150 | t.Errorf("Expected to find foo") 151 | } 152 | 153 | if val.(uint32) != v { 154 | t.Errorf("Expected %v, found %v", v, val) 155 | } 156 | } 157 | 158 | t.Log("uint64") 159 | { 160 | cm := &CirconusMetrics{gauges: make(map[string]interface{})} 161 | 162 | v := uint64(1) 163 | cm.Gauge("foo", v) 164 | val, ok := cm.gauges["foo"] 165 | if !ok { 166 | t.Errorf("Expected to find foo") 167 | } 168 | 169 | if val.(uint64) != v { 170 | t.Errorf("Expected %v, found %v", v, val) 171 | } 172 | } 173 | 174 | t.Log("float32") 175 | { 176 | cm := &CirconusMetrics{gauges: make(map[string]interface{})} 177 | 178 | v := float32(3.12) 179 | cm.Gauge("foo", v) 180 | val, ok := cm.gauges["foo"] 181 | if !ok { 182 | t.Errorf("Expected to find foo") 183 | } 184 | 185 | if val.(float32) != v { 186 | t.Errorf("Expected %v, found %v", v, val) 187 | } 188 | } 189 | 190 | t.Log("float64") 191 | { 192 | cm := &CirconusMetrics{gauges: make(map[string]interface{})} 193 | 194 | v := float64(3.12) 195 | cm.Gauge("foo", v) 196 | val, ok := cm.gauges["foo"] 197 | if !ok { 198 | t.Errorf("Expected to find foo") 199 | } 200 | 201 | if val.(float64) != v { 202 | t.Errorf("Expected %v, found %v", v, val) 203 | } 204 | } 205 | } 206 | 207 | func TestGaugeWithTags(t *testing.T) { 208 | t.Log("Testing gauge.GaugeWithTags") 209 | 210 | cm := &CirconusMetrics{gauges: make(map[string]interface{})} 211 | 212 | metricName := "foo" 213 | tags := Tags{{"foo", "bar"}, {"baz", "qux"}} 214 | streamTagMetricName := cm.MetricNameWithStreamTags("foo", tags) 215 | 216 | v := int(10) 217 | cm.GaugeWithTags(metricName, tags, v) 218 | 219 | val, ok := cm.gauges[streamTagMetricName] 220 | if !ok { 221 | t.Fatalf("%s with %v tags not found (%s) (%#v)", metricName, tags, streamTagMetricName, cm.gauges) 222 | } 223 | 224 | if val.(int) != v { 225 | t.Fatalf("expected (%d) found (%v)", v, val) 226 | } 227 | } 228 | 229 | func TestAddGauge(t *testing.T) { 230 | t.Log("Testing gauge.AddGauge") 231 | 232 | t.Log("int") 233 | { 234 | cm := &CirconusMetrics{gauges: make(map[string]interface{})} 235 | 236 | v := int(1) 237 | cm.Gauge("foo", v) 238 | cm.AddGauge("foo", v) 239 | v++ 240 | 241 | val, ok := cm.gauges["foo"] 242 | if !ok { 243 | t.Fatalf("Expected to find foo") 244 | } 245 | 246 | if val.(int) != v { 247 | t.Fatalf("Expected %v, found %v", v, val) 248 | } 249 | } 250 | 251 | t.Log("int8") 252 | { 253 | cm := &CirconusMetrics{gauges: make(map[string]interface{})} 254 | 255 | v := int8(1) 256 | cm.Gauge("foo", v) 257 | cm.AddGauge("foo", v) 258 | v++ 259 | 260 | val, ok := cm.gauges["foo"] 261 | if !ok { 262 | t.Errorf("Expected to find foo") 263 | } 264 | 265 | if val.(int8) != v { 266 | t.Errorf("Expected %v, found %v", v, val) 267 | } 268 | } 269 | 270 | t.Log("int16") 271 | { 272 | cm := &CirconusMetrics{gauges: make(map[string]interface{})} 273 | 274 | v := int16(1) 275 | cm.Gauge("foo", v) 276 | cm.AddGauge("foo", v) 277 | v++ 278 | 279 | val, ok := cm.gauges["foo"] 280 | if !ok { 281 | t.Errorf("Expected to find foo") 282 | } 283 | 284 | if val.(int16) != v { 285 | t.Errorf("Expected %v, found %v", v, val) 286 | } 287 | } 288 | 289 | t.Log("int32") 290 | { 291 | cm := &CirconusMetrics{gauges: make(map[string]interface{})} 292 | 293 | v := int32(1) 294 | cm.Gauge("foo", v) 295 | cm.AddGauge("foo", v) 296 | v++ 297 | 298 | val, ok := cm.gauges["foo"] 299 | if !ok { 300 | t.Errorf("Expected to find foo") 301 | } 302 | 303 | if val.(int32) != v { 304 | t.Errorf("Expected %v, found %v", v, val) 305 | } 306 | } 307 | 308 | t.Log("int64") 309 | { 310 | cm := &CirconusMetrics{gauges: make(map[string]interface{})} 311 | 312 | v := int64(1) 313 | cm.Gauge("foo", v) 314 | cm.AddGauge("foo", v) 315 | v++ 316 | 317 | val, ok := cm.gauges["foo"] 318 | if !ok { 319 | t.Errorf("Expected to find foo") 320 | } 321 | 322 | if val.(int64) != v { 323 | t.Errorf("Expected %v, found %v", v, val) 324 | } 325 | } 326 | 327 | t.Log("uint") 328 | { 329 | cm := &CirconusMetrics{gauges: make(map[string]interface{})} 330 | 331 | v := uint(1) 332 | cm.Gauge("foo", v) 333 | cm.AddGauge("foo", v) 334 | v++ 335 | 336 | val, ok := cm.gauges["foo"] 337 | if !ok { 338 | t.Errorf("Expected to find foo") 339 | } 340 | 341 | if val.(uint) != v { 342 | t.Errorf("Expected %v, found %v", v, val) 343 | } 344 | } 345 | 346 | t.Log("uint8") 347 | { 348 | cm := &CirconusMetrics{gauges: make(map[string]interface{})} 349 | 350 | v := uint8(1) 351 | cm.Gauge("foo", v) 352 | cm.AddGauge("foo", v) 353 | v++ 354 | 355 | val, ok := cm.gauges["foo"] 356 | if !ok { 357 | t.Errorf("Expected to find foo") 358 | } 359 | 360 | if val.(uint8) != v { 361 | t.Errorf("Expected %v, found %v", v, val) 362 | } 363 | } 364 | 365 | t.Log("uint16") 366 | { 367 | cm := &CirconusMetrics{gauges: make(map[string]interface{})} 368 | 369 | v := uint16(1) 370 | cm.Gauge("foo", v) 371 | cm.AddGauge("foo", v) 372 | v++ 373 | 374 | val, ok := cm.gauges["foo"] 375 | if !ok { 376 | t.Errorf("Expected to find foo") 377 | } 378 | 379 | if val.(uint16) != v { 380 | t.Errorf("Expected %v, found %v", v, val) 381 | } 382 | } 383 | 384 | t.Log("uint32") 385 | { 386 | cm := &CirconusMetrics{gauges: make(map[string]interface{})} 387 | 388 | v := uint32(1) 389 | cm.Gauge("foo", v) 390 | cm.AddGauge("foo", v) 391 | v++ 392 | 393 | val, ok := cm.gauges["foo"] 394 | if !ok { 395 | t.Errorf("Expected to find foo") 396 | } 397 | 398 | if val.(uint32) != v { 399 | t.Errorf("Expected %v, found %v", v, val) 400 | } 401 | } 402 | 403 | t.Log("uint64") 404 | { 405 | cm := &CirconusMetrics{gauges: make(map[string]interface{})} 406 | 407 | v := uint64(1) 408 | cm.Gauge("foo", v) 409 | cm.AddGauge("foo", v) 410 | v++ 411 | 412 | val, ok := cm.gauges["foo"] 413 | if !ok { 414 | t.Errorf("Expected to find foo") 415 | } 416 | 417 | if val.(uint64) != v { 418 | t.Errorf("Expected %v, found %v", v, val) 419 | } 420 | } 421 | 422 | t.Log("float32") 423 | { 424 | cm := &CirconusMetrics{gauges: make(map[string]interface{})} 425 | 426 | v := float32(3.12) 427 | cm.Gauge("foo", v) 428 | cm.AddGauge("foo", v) 429 | v += v 430 | 431 | val, ok := cm.gauges["foo"] 432 | if !ok { 433 | t.Errorf("Expected to find foo") 434 | } 435 | 436 | if val.(float32) != v { 437 | t.Errorf("Expected %v, found %v", v, val) 438 | } 439 | } 440 | 441 | t.Log("float64") 442 | { 443 | cm := &CirconusMetrics{gauges: make(map[string]interface{})} 444 | 445 | v := float64(3.12) 446 | cm.Gauge("foo", v) 447 | cm.AddGauge("foo", v) 448 | v += v 449 | 450 | val, ok := cm.gauges["foo"] 451 | if !ok { 452 | t.Errorf("Expected to find foo") 453 | } 454 | 455 | if val.(float64) != v { 456 | t.Errorf("Expected %v, found %v", v, val) 457 | } 458 | } 459 | } 460 | 461 | func TestAddGaugeWithTags(t *testing.T) { 462 | t.Log("Testing gauge.AddGaugeWithTags") 463 | 464 | cm := &CirconusMetrics{gauges: make(map[string]interface{})} 465 | 466 | metricName := "foo" 467 | tags := Tags{{"foo", "bar"}, {"baz", "qux"}} 468 | streamTagMetricName := cm.MetricNameWithStreamTags("foo", tags) 469 | v1 := 10 470 | v2 := 5 471 | 472 | // initial 473 | { 474 | cm.GaugeWithTags(metricName, tags, v1) 475 | 476 | val, ok := cm.gauges[streamTagMetricName] 477 | if !ok { 478 | t.Fatalf("%s with %v tags not found (%s) (%#v)", metricName, tags, streamTagMetricName, cm.gauges) 479 | } 480 | 481 | if val.(int) != v1 { 482 | t.Fatalf("expected (%d) found (%v)", v1, val) 483 | } 484 | } 485 | 486 | // add 487 | { 488 | cm.AddGaugeWithTags(metricName, tags, v2) 489 | 490 | val, ok := cm.gauges[streamTagMetricName] 491 | if !ok { 492 | t.Fatalf("%s with %v tags not found (%s) (%#v)", metricName, tags, streamTagMetricName, cm.gauges) 493 | } 494 | 495 | if val.(int) != v1+v2 { 496 | t.Fatalf("expected (%d) found (%v)", v1+v2, val) 497 | } 498 | } 499 | 500 | } 501 | 502 | func TestSetGauge(t *testing.T) { 503 | t.Log("Testing gauge.SetGauge") 504 | 505 | cm := &CirconusMetrics{gauges: make(map[string]interface{})} 506 | 507 | v := int(10) 508 | cm.SetGauge("foo", v) 509 | 510 | val, ok := cm.gauges["foo"] 511 | if !ok { 512 | t.Errorf("Expected to find foo") 513 | } 514 | 515 | if val.(int) != v { 516 | t.Errorf("Expected %d, found %v", v, val) 517 | } 518 | } 519 | 520 | func TestSetGaugeWithTags(t *testing.T) { 521 | t.Log("Testing gauge.SetGaugeWithTags") 522 | 523 | cm := &CirconusMetrics{gauges: make(map[string]interface{})} 524 | 525 | metricName := "foo" 526 | tags := Tags{{"foo", "bar"}, {"baz", "qux"}} 527 | streamTagMetricName := cm.MetricNameWithStreamTags("foo", tags) 528 | 529 | v := int(10) 530 | cm.SetGaugeWithTags(metricName, tags, v) 531 | 532 | val, ok := cm.gauges[streamTagMetricName] 533 | if !ok { 534 | t.Fatalf("%s with %v tags not found (%s) (%#v)", metricName, tags, streamTagMetricName, cm.gauges) 535 | } 536 | 537 | if val.(int) != v { 538 | t.Fatalf("expected (%d) found (%v)", v, val) 539 | } 540 | } 541 | 542 | func TestGetGaugeTest(t *testing.T) { 543 | t.Log("Testing gauge.GetGaugeTest") 544 | 545 | cm := &CirconusMetrics{gauges: make(map[string]interface{})} 546 | 547 | v := int(10) 548 | cm.SetGauge("foo", v) 549 | 550 | val, err := cm.GetGaugeTest("foo") 551 | if err != nil { 552 | t.Errorf("Expected no error %v", err) 553 | } 554 | if val.(int) != v { 555 | t.Errorf("Expected '%d' got '%v'", v, val) 556 | } 557 | 558 | _, err = cm.GetGaugeTest("bar") 559 | if err == nil { 560 | t.Error("Expected error") 561 | } 562 | 563 | } 564 | 565 | func TestRemoveGauge(t *testing.T) { 566 | t.Log("Testing gauge.RemoveGauge") 567 | 568 | cm := &CirconusMetrics{gauges: make(map[string]interface{})} 569 | 570 | v := int(5) 571 | cm.Gauge("foo", v) 572 | val, ok := cm.gauges["foo"] 573 | if !ok { 574 | t.Errorf("Expected to find foo") 575 | } 576 | 577 | if val.(int) != v { 578 | t.Errorf("Expected %d, found %v", v, val) 579 | } 580 | 581 | cm.RemoveGauge("foo") 582 | 583 | val, ok = cm.gauges["foo"] 584 | if ok { 585 | t.Errorf("Expected NOT to find foo") 586 | } 587 | 588 | if val != nil { 589 | t.Errorf("Expected nil, found '%v'", val) 590 | } 591 | } 592 | 593 | func TestRemoveGaugeWithTags(t *testing.T) { 594 | t.Log("Testing gauge.RemoveGaugeWithTags") 595 | 596 | cm := &CirconusMetrics{gauges: make(map[string]interface{})} 597 | 598 | metricName := "foo" 599 | tags := Tags{{"foo", "bar"}, {"baz", "qux"}} 600 | streamTagMetricName := cm.MetricNameWithStreamTags("foo", tags) 601 | 602 | v := int(5) 603 | cm.GaugeWithTags(metricName, tags, v) 604 | val, ok := cm.gauges[streamTagMetricName] 605 | if !ok { 606 | t.Fatalf("%s with %v tags not found (%s) (%#v)", metricName, tags, streamTagMetricName, cm.gauges) 607 | } 608 | 609 | if val.(int) != v { 610 | t.Fatalf("expected (%d) found (%v)", v, val) 611 | } 612 | 613 | cm.RemoveGaugeWithTags(metricName, tags) 614 | 615 | val, ok = cm.gauges[streamTagMetricName] 616 | if ok { 617 | t.Fatalf("expected NOT to find (%s)", streamTagMetricName) 618 | } 619 | 620 | if val != nil { 621 | t.Fatalf("expected nil, found '%v'", val) 622 | } 623 | } 624 | 625 | func TestSetGaugeFunc(t *testing.T) { 626 | t.Log("Testing gauge.SetGaugeFunc") 627 | 628 | gf := func() int64 { 629 | return 1 630 | } 631 | 632 | cm := &CirconusMetrics{gaugeFuncs: make(map[string]func() int64)} 633 | 634 | cm.SetGaugeFunc("foo", gf) 635 | 636 | val, ok := cm.gaugeFuncs["foo"] 637 | if !ok { 638 | t.Errorf("Expected to find foo") 639 | } 640 | 641 | if val() != 1 { 642 | t.Errorf("Expected 1, found %d", val()) 643 | } 644 | } 645 | 646 | func TestSetGaugeFuncWithTags(t *testing.T) { 647 | t.Log("Testing gauge.SetGaugeFuncWithTags") 648 | 649 | gf := func() int64 { 650 | return 1 651 | } 652 | 653 | cm := &CirconusMetrics{gaugeFuncs: make(map[string]func() int64)} 654 | 655 | metricName := "foo" 656 | tags := Tags{{"foo", "bar"}, {"baz", "qux"}} 657 | streamTagMetricName := cm.MetricNameWithStreamTags("foo", tags) 658 | 659 | cm.SetGaugeFuncWithTags(metricName, tags, gf) 660 | 661 | val, ok := cm.gaugeFuncs[streamTagMetricName] 662 | if !ok { 663 | t.Fatalf("%s with %v tags not found (%s) (%#v)", metricName, tags, streamTagMetricName, cm.gauges) 664 | } 665 | 666 | if val() != 1 { 667 | t.Fatalf("expected 1, found (%v)", val()) 668 | } 669 | } 670 | 671 | func TestRemoveGaugeFunc(t *testing.T) { 672 | t.Log("Testing gauge.RemoveGaugeFunc") 673 | 674 | gf := func() int64 { 675 | return 1 676 | } 677 | 678 | cm := &CirconusMetrics{gaugeFuncs: make(map[string]func() int64)} 679 | 680 | cm.SetGaugeFunc("foo", gf) 681 | 682 | val, ok := cm.gaugeFuncs["foo"] 683 | if !ok { 684 | t.Errorf("Expected to find foo") 685 | } 686 | 687 | if val() != 1 { 688 | t.Errorf("Expected 1, found %d", val()) 689 | } 690 | 691 | cm.RemoveGaugeFunc("foo") 692 | 693 | val, ok = cm.gaugeFuncs["foo"] 694 | if ok { 695 | t.Errorf("Expected NOT to find foo") 696 | } 697 | 698 | if val != nil { 699 | t.Errorf("Expected nil, found %v", val()) 700 | } 701 | 702 | } 703 | 704 | func TestRemoveGaugeFuncWithTags(t *testing.T) { 705 | t.Log("Testing gauge.RemoveGaugeFuncWithTags") 706 | 707 | gf := func() int64 { 708 | return 1 709 | } 710 | 711 | cm := &CirconusMetrics{gaugeFuncs: make(map[string]func() int64)} 712 | 713 | metricName := "foo" 714 | tags := Tags{{"foo", "bar"}, {"baz", "qux"}} 715 | streamTagMetricName := cm.MetricNameWithStreamTags("foo", tags) 716 | 717 | cm.SetGaugeFuncWithTags(metricName, tags, gf) 718 | 719 | val, ok := cm.gaugeFuncs[streamTagMetricName] 720 | if !ok { 721 | t.Fatalf("%s with %v tags not found (%s) (%#v)", metricName, tags, streamTagMetricName, cm.gauges) 722 | } 723 | 724 | if val() != 1 { 725 | t.Fatalf("expected 1 got (%v)", val()) 726 | } 727 | 728 | cm.RemoveGaugeFuncWithTags(metricName, tags) 729 | 730 | val, ok = cm.gaugeFuncs[streamTagMetricName] 731 | if ok { 732 | t.Fatalf("expected NOT to find (%s)", streamTagMetricName) 733 | } 734 | 735 | if val != nil { 736 | t.Fatalf("expected nil got (%v)", val()) 737 | } 738 | 739 | } 740 | --------------------------------------------------------------------------------