diff --git a/go.mod b/go.mod index 07d9ae4d..5536814c 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,11 @@ module github.com/newrelic/nri-prometheus go 1.13 require ( - github.com/Bowery/prompt v0.0.0-20190916142128-fa8279994f75 // indirect - github.com/dchest/safefile v0.0.0-20151022103144-855e8d98f185 // indirect github.com/fsnotify/fsnotify v1.4.8-0.20190312181446-1485a34d5d57 // indirect - github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/googleapis/gnostic v0.2.3-0.20181019180348-e2aafd60c944 // indirect github.com/hashicorp/hcl v1.0.1-0.20190611123218-cf7d376da96d // indirect github.com/imdario/mergo v0.3.8 // indirect - github.com/kardianos/govendor v1.0.9 // indirect - github.com/newrelic/newrelic-telemetry-sdk-go v0.2.1-0.20200116224429-790ff853d12b + github.com/newrelic/newrelic-telemetry-sdk-go v0.3.0 github.com/onsi/ginkgo v1.10.1 // indirect github.com/onsi/gomega v1.7.0 // indirect github.com/pelletier/go-toml v1.2.1-0.20181124002727-27c6b39a135b // indirect diff --git a/go.sum b/go.sum index d741fc0f..eec6b535 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/Azure/go-autorest v11.1.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Bowery/prompt v0.0.0-20190916142128-fa8279994f75 h1:xGHheKK44eC6K0u5X+DZW/fRaR1LnDdqPHMZMWx5fv8= -github.com/Bowery/prompt v0.0.0-20190916142128-fa8279994f75/go.mod h1:4/6eNcqZ09BZ9wLK3tZOjBA1nDj+B0728nlX5YRlSmQ= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= @@ -26,8 +24,6 @@ github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dchest/safefile v0.0.0-20151022103144-855e8d98f185 h1:3T8ZyTDp5QxTx3NU48JVb2u+75xc040fofcBaN+6jPA= -github.com/dchest/safefile v0.0.0-20151022103144-855e8d98f185/go.mod h1:cFRxtTwTOJkz2x3rQUNCYKWC93yP1VKjR8NUhqFxZNU= github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= @@ -73,8 +69,6 @@ github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSN github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= @@ -102,8 +96,6 @@ github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBv github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kardianos/govendor v1.0.9 h1:WOH3FcVI9eOgnIZYg96iwUwrL4eOVx+aQ66oyX2R8Yc= -github.com/kardianos/govendor v1.0.9/go.mod h1:yvmR6q9ZZ7nSF5Wvh40v0wfP+3TwwL8zYQp+itoZSVM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -132,8 +124,8 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/newrelic/newrelic-telemetry-sdk-go v0.2.1-0.20200116224429-790ff853d12b h1:SIJT+jppa/rNo2Dl6gTjvYYeVxxiCZ7q5cpu2yNkINw= -github.com/newrelic/newrelic-telemetry-sdk-go v0.2.1-0.20200116224429-790ff853d12b/go.mod h1:G9MqE/cHGv3Hx3qpYhfuyFUsGx2DpVcGi1iJIqTg+JQ= +github.com/newrelic/newrelic-telemetry-sdk-go v0.3.0 h1:KKeKemCT+woxK9GOI1XxiPui/g59ldJSoU9QtfEjXRs= +github.com/newrelic/newrelic-telemetry-sdk-go v0.3.0/go.mod h1:G9MqE/cHGv3Hx3qpYhfuyFUsGx2DpVcGi1iJIqTg+JQ= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= diff --git a/internal/cmd/scraper/scraper.go b/internal/cmd/scraper/scraper.go index 52e6f939..1236abed 100644 --- a/internal/cmd/scraper/scraper.go +++ b/internal/cmd/scraper/scraper.go @@ -40,7 +40,6 @@ type Config struct { BearerTokenFile string `mapstructure:"bearer_token_file"` InsecureSkipVerify bool `mapstructure:"insecure_skip_verify" default:"false"` ProcessingRules []integration.ProcessingRule `mapstructure:"transformations"` - Percentiles []float64 `mapstructure:"percentiles"` DecorateFile bool EmitterProxy string `mapstructure:"emitter_proxy"` // Parsed version of `EmitterProxy` @@ -80,14 +79,6 @@ func validateConfig(cfg *Config) error { if cfg.LicenseKey == "" { return fmt.Errorf(requiredMsg, "license_key") } - for _, p := range cfg.Percentiles { - if p < 0.0 { - return fmt.Errorf("percentiles must be greater than or equal to 0.0, got %f", p) - } - if p > 100.0 { - return fmt.Errorf("percentiles must be less than or equal to 100.0, got %f", p) - } - } if cfg.EmitterProxy != "" { proxyURL, err := url.Parse(cfg.EmitterProxy) @@ -244,7 +235,6 @@ func Run(cfg *Config) error { } c := integration.TelemetryEmitterConfig{ - Percentiles: cfg.Percentiles, HarvesterOpts: harvesterOpts, DeltaExpirationAge: cfg.TelemetryEmitterDeltaExpirationAge, DeltaExpirationCheckInternval: cfg.TelemetryEmitterDeltaExpirationCheckInterval, diff --git a/internal/histogram/bucket.go b/internal/histogram/bucket.go deleted file mode 100644 index f08071d8..00000000 --- a/internal/histogram/bucket.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2015 The Prometheus Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package histogram - -// Bucket represents a single grouping of values from Prometheus histogram metrics. -type Bucket struct { - UpperBound float64 - Count float64 -} - -// Buckets implements sort.Interface. -type Buckets []Bucket - -func (b Buckets) Len() int { return len(b) } -func (b Buckets) Swap(i, j int) { b[i], b[j] = b[j], b[i] } -func (b Buckets) Less(i, j int) bool { return b[i].UpperBound < b[j].UpperBound } diff --git a/internal/histogram/percentile.go b/internal/histogram/percentile.go deleted file mode 100644 index 9ba25e07..00000000 --- a/internal/histogram/percentile.go +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright 2015 The Prometheus Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package histogram - -import ( - "errors" - "fmt" - "math" - "sort" -) - -// Percentile calculates the percentile `p` based on the buckets. The -// buckets will be sorted by this function (i.e. no sorting needed before -// calling this function). The percentile value is interpolated assuming a -// linear distribution within a bucket. However, if the percentile falls -// into the highest bucket, the upper bound of the 2nd highest bucket is -// returned. A natural lower bound of 0 is assumed if the upper bound of the -// lowest bucket is greater 0. In that case, interpolation in the lowest -// bucket happens linearly between 0 and the upper bound of the lowest -// bucket. However, if the lowest bucket has an upper bound less or equal to -// 0, this upper bound is returned if the percentile falls into the lowest -// bucket. -// -// An error is returned if: -// * `buckets` has fewer than 2 elements -// * the highest bucket is not +Inf -// * p<0 -// * p>100 -func Percentile(p float64, buckets Buckets) (float64, error) { - if p < 0.0 { - return 0, fmt.Errorf("invalid percentile: %g (must be greater than 0.0)", p) - } - if p > 100.0 { - return 0, fmt.Errorf("invalid percentile: %g (must be less than 100.0)", p) - } - if len(buckets) < 2 { - return 0, fmt.Errorf("invalid buckets: minimum of 2 buckets required, got %d", len(buckets)) - } - sort.Sort(buckets) - if !math.IsInf(buckets[len(buckets)-1].UpperBound, +1) { - return 0, errors.New("invalid buckets: highest bucket is not +Inf") - } - - buckets = coalesceBuckets(buckets) - ensureMonotonic(buckets) - - rank := (p / 100.0) * buckets[len(buckets)-1].Count - b := sort.Search(len(buckets)-1, func(i int) bool { return buckets[i].Count >= rank }) - - if b == len(buckets)-1 { - return buckets[len(buckets)-2].UpperBound, nil - } - if b == 0 && buckets[0].UpperBound <= 0 { - return buckets[0].UpperBound, nil - } - var ( - bucketStart float64 - bucketEnd = buckets[b].UpperBound - count = buckets[b].Count - ) - if b > 0 { - bucketStart = buckets[b-1].UpperBound - count -= buckets[b-1].Count - rank -= buckets[b-1].Count - } - return bucketStart + (bucketEnd-bucketStart)*(rank/count), nil -} - -// coalesceBuckets merges buckets with the same upper bound. -// -// The input buckets must be sorted. -func coalesceBuckets(buckets Buckets) Buckets { - last := buckets[0] - i := 0 - for _, b := range buckets[1:] { - if b.UpperBound == last.UpperBound { - last.Count += b.Count - } else { - buckets[i] = last - last = b - i++ - } - } - buckets[i] = last - return buckets[:i+1] -} - -// The assumption that bucket counts increase monotonically with increasing -// UpperBound may be violated during: -// -// * Recording rule evaluation of histogram_quantile, especially when rate() -// has been applied to the underlying bucket timeseries. -// * Evaluation of histogram_quantile computed over federated bucket -// timeseries, especially when rate() has been applied. -// -// This is because scraped data is not made available to rule evaluation or -// federation atomically, so some buckets are computed with data from the -// most recent scrapes, but the other buckets are missing data from the most -// recent scrape. -// -// Monotonicity is usually guaranteed because if a bucket with upper bound -// u1 has count c1, then any bucket with a higher upper bound u > u1 must -// have counted all c1 observations and perhaps more, so that c >= c1. -// -// As a somewhat hacky solution until ingestion is atomic per scrape, we -// calculate the "envelope" of the histogram buckets, essentially removing -// any decreases in the count between successive buckets. -func ensureMonotonic(buckets Buckets) { - max := buckets[0].Count - for i := range buckets[1:] { - switch { - case buckets[i].Count > max: - max = buckets[i].Count - case buckets[i].Count < max: - buckets[i].Count = max - } - } -} diff --git a/internal/histogram/percentile_test.go b/internal/histogram/percentile_test.go deleted file mode 100644 index 8e526533..00000000 --- a/internal/histogram/percentile_test.go +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright 2015 The Prometheus Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package histogram - -import ( - "math" - "testing" -) - -func TestEnsureMonotonic(t *testing.T) { - buckets := Buckets{ - {0.5, 2.0}, - {1.0, 1.0}, - {2.0, 3.0}, - } - - ensureMonotonic(buckets) - - expectedCounts := []float64{2.0, 2.0, 3.0} - for i, want := range expectedCounts { - if got := buckets[i].Count; got != want { - t.Errorf("ensureMonotonic failed to ensure monotonicity: buckets[%d] = %f; want %f", i, got, want) - } - } - - // Run again to ensure nothing changes for a valid Buckets. - ensureMonotonic(buckets) - for i, want := range expectedCounts { - if got := buckets[i].Count; got != want { - t.Errorf("ensureMonotonic modified valid monotonicity: buckets[%d] = %f; want %f", i, got, want) - } - } -} - -func TestCoalesceBuckets(t *testing.T) { - tests := []struct { - input Buckets - want Buckets - }{ - { - Buckets{{0.5, 1.0}, {0.5, 2.0}}, - Buckets{{0.5, 3.0}}, - }, - { - Buckets{{0.5, 1.0}, {0.5, 2.0}, {1.0, 3.0}}, - Buckets{{0.5, 3.0}, {1.0, 3.0}}, - }, - { - Buckets{{0.1, 1.0}, {0.5, 1.0}, {0.5, 2.0}, {1.0, 3.0}}, - Buckets{{0.1, 1.0}, {0.5, 3.0}, {1.0, 3.0}}, - }, - } - - for _, test := range tests { - coalesced := coalesceBuckets(test.input) - if len(coalesced) != len(test.want) { - t.Errorf("coalesceBuckets failed to coalesce to desired length %d; got %d", len(test.want), len(coalesced)) - continue - } - for i, got := range coalesced { - want := test.want[i] - if got.UpperBound != want.UpperBound || got.Count != want.Count { - t.Errorf("coalesceBuckets failed to coalesce element %d: got %#v; want %#v", i, got, want) - } - } - } -} - -func TestPercentileValidatesInput(t *testing.T) { - tests := []struct { - p float64 - buckets Buckets - }{ - // p < 0.0 - {-1.0, Buckets{}}, - // p > 100.0 - {101.0, Buckets{}}, - // len(buckets) < 2 - {50.0, Buckets{}}, - // len(buckets) < 2 - {50.0, Buckets{{math.Inf(1), 1.0}}}, - // Last bucket is not +Inf - {50.0, Buckets{{0.5, 1.0}, {1.0, 2.0}}}, - } - - for _, test := range tests { - if _, err := Percentile(test.p, test.buckets); err == nil { - t.Errorf("Percentile(%g, %#v) did not return an error", test.p, test.buckets) - } - } -} - -func TestPercentile(t *testing.T) { - buckets := Buckets{ - {-20.0, 10.0}, - {1.0, 11.0}, - {40.0, 20.0}, - {200.0, 200.0}, - {math.Inf(1), 220.0}, - } - - tests := []struct { - p float64 - want float64 - }{ - {0.0, -20.0}, - {4.0, -20.0}, - {5.0, 1.0}, - {50.0, 120}, - {99.0, 200.0}, - {100.0, 200.0}, - } - - for _, test := range tests { - if got, _ := Percentile(test.p, buckets); got != test.want { - t.Errorf("Percentile(%g, %v) = %g; want %g", test.p, buckets, got, test.want) - } - } -} - -var benchmarkBuckets = Buckets{ - {10.0, 10.0}, - {20.0, 20.0}, - {30.0, 30.0}, - {math.Inf(1), 30.0}, -} - -func BenchmarkPercentile(b *testing.B) { - for i := 0; i < b.N; i++ { - _, _ = Percentile(0.5, benchmarkBuckets) - } -} diff --git a/internal/integration/emitter.go b/internal/integration/emitter.go index c303944e..024afdce 100644 --- a/internal/integration/emitter.go +++ b/internal/integration/emitter.go @@ -14,7 +14,6 @@ import ( "github.com/newrelic/newrelic-telemetry-sdk-go/cumulative" "github.com/newrelic/newrelic-telemetry-sdk-go/telemetry" - "github.com/newrelic/nri-prometheus/internal/histogram" "github.com/pkg/errors" dto "github.com/prometheus/client_model/go" "github.com/sirupsen/logrus" @@ -34,7 +33,6 @@ type Emitter interface { // TelemetryEmitter emits metrics using the go-telemetry-sdk. type TelemetryEmitter struct { name string - percentiles []float64 harvester *telemetry.Harvester deltaCalculator *cumulative.DeltaCalculator } @@ -42,9 +40,6 @@ type TelemetryEmitter struct { // TelemetryEmitterConfig is the configuration required for the // `TelemetryEmitter` type TelemetryEmitterConfig struct { - // Percentile values to calculate for every Prometheus metrics of histogram type. - Percentiles []float64 - // HarvesterOpts configuration functions for the telemetry Harvester. HarvesterOpts []TelemetryHarvesterOpt @@ -174,7 +169,6 @@ func NewTelemetryEmitter(cfg TelemetryEmitterConfig) (*TelemetryEmitter, error) return &TelemetryEmitter{ name: "telemetry", harvester: harvester, - percentiles: cfg.Percentiles, deltaCalculator: dc, }, nil } @@ -240,103 +234,79 @@ func (te *TelemetryEmitter) Emit(metrics []Metric) error { return results } -// emitSummary sends all quantiles included with the summary as percentiles to New Relic. -// -// Related specification: -// https://github.com/newrelic/newrelic-exporter-specs/blob/master/Guidelines.md#percentiles func (te *TelemetryEmitter) emitSummary(metric Metric, timestamp time.Time) error { summary, ok := metric.value.(*dto.Summary) if !ok { return fmt.Errorf("unknown summary metric type for %q: %T", metric.name, metric.value) } - var results error - metricName := metric.name + ".percentiles" + te.harvester.RecordMetric(telemetry.Summary{ + Name: metric.name + "_sum", + Attributes: metric.attributes, + Count: 1, + Sum: summary.GetSampleSum(), + Min: math.NaN(), + Max: math.NaN(), + Timestamp: timestamp, + }) + + if count, ok := te.deltaCalculator.CountMetric(metric.name+"_count", metric.attributes, float64(summary.GetSampleCount()), timestamp); ok { + te.harvester.RecordMetric(count) + } + quantiles := summary.GetQuantile() for _, q := range quantiles { - // translate to percentiles - p := q.GetQuantile() * 100.0 - if p < 0.0 || p > 100.0 { - err := fmt.Errorf("invalid percentile `%g` for %s: must be in range [0.0, 100.0]", p, metric.name) - if results == nil { - results = err - } else { - results = fmt.Errorf("%v: %w", err, results) - } - continue - } - - percentileAttrs := copyAttrs(metric.attributes) - percentileAttrs["percentile"] = p + quantileAttrs := copyAttrs(metric.attributes) + quantileAttrs["quantile"] = fmt.Sprintf("%g", q.GetQuantile()) te.harvester.RecordMetric(telemetry.Gauge{ - Name: metricName, - Attributes: percentileAttrs, + Name: metric.name, + Attributes: quantileAttrs, Value: q.GetValue(), Timestamp: timestamp, }) } - return results + return nil } -// emitHistogram sends histogram data and curated percentiles to New Relic. -// -// Related specification: -// https://github.com/newrelic/newrelic-exporter-specs/blob/master/Guidelines.md#histograms func (te *TelemetryEmitter) emitHistogram(metric Metric, timestamp time.Time) error { hist, ok := metric.value.(*dto.Histogram) if !ok { return fmt.Errorf("unknown histogram metric type for %q: %T", metric.name, metric.value) } - if m, ok := te.deltaCalculator.CountMetric(metric.name+".sum", metric.attributes, hist.GetSampleSum(), timestamp); ok { - te.harvester.RecordMetric(m) + if sumCount, ok := te.deltaCalculator.CountMetric(metric.name+"_sum", metric.attributes, float64(hist.GetSampleSum()), timestamp); ok { + te.harvester.RecordMetric(telemetry.Summary{ + Name: metric.name + "_sum", + Attributes: metric.attributes, + Count: 1, + Sum: sumCount.Value, + Min: math.NaN(), + Max: math.NaN(), + Timestamp: timestamp, + }) } - metricName := metric.name + ".buckets" - buckets := make(histogram.Buckets, 0, len(hist.Bucket)) - for _, b := range hist.GetBucket() { - upperBound := b.GetUpperBound() - count := float64(b.GetCumulativeCount()) - if !math.IsInf(upperBound, 1) { - bucketAttrs := copyAttrs(metric.attributes) - bucketAttrs["histogram.bucket.upperBound"] = upperBound - if m, ok := te.deltaCalculator.CountMetric(metricName, bucketAttrs, count, timestamp); ok { - te.harvester.RecordMetric(m) - } - } - buckets = append( - buckets, - histogram.Bucket{ - UpperBound: upperBound, - Count: count, - }, - ) + if count, ok := te.deltaCalculator.CountMetric(metric.name+"_count", metric.attributes, float64(hist.GetSampleCount()), timestamp); ok { + te.harvester.RecordMetric(count) } - var results error - metricName = metric.name + ".percentiles" - for _, p := range te.percentiles { - v, err := histogram.Percentile(p, buckets) - if err != nil { - if results == nil { - results = err - } else { - results = fmt.Errorf("%v: %w", err, results) - } - continue + metricName := metric.name + "_bucket" + for _, b := range hist.GetBucket() { + bucketAttrs := copyAttrs(metric.attributes) + bucketAttrs["le"] = fmt.Sprintf("%g", b.GetUpperBound()) + + bucketCount, ok := te.deltaCalculator.CountMetric( + metricName, + bucketAttrs, + float64(b.GetCumulativeCount()), + timestamp, + ) + if ok { + te.harvester.RecordMetric(bucketCount) } - - percentileAttrs := copyAttrs(metric.attributes) - percentileAttrs["percentile"] = p - te.harvester.RecordMetric(telemetry.Gauge{ - Name: metricName, - Attributes: percentileAttrs, - Value: v, - Timestamp: timestamp, - }) } - return results + return nil } // copyAttrs returns a (shallow) copy of the passed attrs. diff --git a/internal/integration/emitter_test.go b/internal/integration/emitter_test.go index 5f921afb..4dd42f17 100644 --- a/internal/integration/emitter_test.go +++ b/internal/integration/emitter_test.go @@ -216,7 +216,6 @@ func TestTelemetryEmitterEmit(t *testing.T) { }, telemetry.ConfigBasicErrorLogger(os.Stdout), }, - Percentiles: []float64{50.0}, } e, err := NewTelemetryEmitter(c) @@ -279,31 +278,47 @@ func TestTelemetryEmitterEmit(t *testing.T) { "nrMetricType": "histogram", "promMetricType": "histogram", }, - "name": "histogram-1.sum", + "name": "histogram-1_sum", + "type": "summary", + "value": map[string]interface{}{ + "sum": float64(10), + "count": float64(1), + "min": nil, + "max": nil, + }, + }, + map[string]interface{}{ + "attributes": map[string]interface{}{ + "name": "histogram-1", + "targetName": "target-d", + "nrMetricType": "histogram", + "promMetricType": "histogram", + }, + "name": "histogram-1_count", "type": "count", - "value": float64(10), + "value": float64(0), }, map[string]interface{}{ "attributes": map[string]interface{}{ - "name": "histogram-1", - "targetName": "target-d", - "nrMetricType": "histogram", - "promMetricType": "histogram", - "histogram.bucket.upperBound": float64(0), + "name": "histogram-1", + "targetName": "target-d", + "nrMetricType": "histogram", + "promMetricType": "histogram", + "le": "0", }, - "name": "histogram-1.buckets", + "name": "histogram-1_bucket", "type": "count", "value": float64(1), }, map[string]interface{}{ "attributes": map[string]interface{}{ - "name": "histogram-1", - "targetName": "target-d", - "nrMetricType": "histogram", - "promMetricType": "histogram", - "histogram.bucket.upperBound": float64(1), + "name": "histogram-1", + "targetName": "target-d", + "nrMetricType": "histogram", + "promMetricType": "histogram", + "le": "1", }, - "name": "histogram-1.buckets", + "name": "histogram-1_bucket", "type": "count", "value": float64(2), }, @@ -313,25 +328,42 @@ func TestTelemetryEmitterEmit(t *testing.T) { "targetName": "target-d", "nrMetricType": "histogram", "promMetricType": "histogram", - "percentile": float64(50), + "le": "+Inf", }, - "name": "histogram-1.percentiles", - "type": "gauge", - "value": float64(1), + "name": "histogram-1_bucket", + "type": "count", + "value": float64(10), + }, + map[string]interface{}{ + "attributes": map[string]interface{}{}, + "name": "summary-1_sum", + "type": "summary", + "value": map[string]interface{}{ + "sum": float64(10), + "count": float64(1), + "min": nil, + "max": nil, + }, + }, + map[string]interface{}{ + "attributes": map[string]interface{}{}, + "name": "summary-1_count", + "type": "count", + "value": float64(0), }, map[string]interface{}{ "attributes": map[string]interface{}{ - "percentile": float64(50), + "quantile": "0.5", }, - "name": "summary-1.percentiles", + "name": "summary-1", "type": "gauge", "value": float64(10), }, map[string]interface{}{ "attributes": map[string]interface{}{ - "percentile": float64(99.9), + "quantile": "0.999", }, - "name": "summary-1.percentiles", + "name": "summary-1", "type": "gauge", "value": float64(100), },