You've already forked opentelemetry-go
mirror of
https://github.com/open-telemetry/opentelemetry-go.git
synced 2025-07-15 01:04:25 +02:00
Metrics stdout export pipeline (#265)
* Add MetricAggregator.Merge() implementations * Update from feedback * Type * Ckpt * Ckpt * Add push controller * Ckpt * Add aggregator interfaces, stdout encoder * Modify basic main.go * Main is working * Batch stdout output * Sum udpate * Rename stdout * Add stateless/stateful Batcher options * Undo a for-loop in the example, remove a done TODO * Update imports * Add note * Rename defaultkeys * Support variable label encoder to speed OpenMetrics/Statsd export * Lint * Doc * Precommit/lint * Simplify Aggregator API * Record->Identifier * Remove export.Record a.k.a. Identifier * Checkpoint * Propagate errors to the SDK, remove a bunch of 'TODO warn' * Checkpoint * Introduce export.Labels * Comments in export/metric.go * Comment * More merge * More doc * Complete example * Lint fixes * Add a testable example * Lint * Let Export return an error * add a basic stdout exporter test * Add measure test; fix aggregator APIs * Use JSON numbers, not strings * Test stdout exporter error * Add a test for the call to RangeTest * Add error handler API to improve correctness test; return errors from RecordOne * Undo the previous -- do not expose errors * Add simple selector variations, test * Repair examples * Test push controller error handling * Add SDK label encoder tests * Add a defaultkeys batcher test * Add an ungrouped batcher test * Lint new tests * Respond to krnowak's feedback * Undo comment * Use concrete receivers for export records and labels, since the constructors return structs not pointers * Bug fix for stateful batchers; clone an aggregator for long term storage * Remove TODO addressed in #318 * Add errors to all aggregator interfaces * Handle ErrNoLastValue case in stdout exporter * Move aggregator API into sdk/export/metric/aggregator * Update all aggregator exported-method comments * Document the aggregator APIs * More aggregator comments * Add multiple updates to the ungrouped test * Fixes for feedback from Gustavo and Liz * Producer->CheckpointSet; add FinishedCollection * Process takes an export.Record * ReadCheckpoint->CheckpointSet * EncodeLabels->Encode * Format a better inconsistent type error; add more aggregator API tests * More RangeTest test coverage * Make benbjohnson/clock a test-only dependency * Handle ErrNoLastValue in stress_test
This commit is contained in:
committed by
rghetia
parent
c3d5b7b16d
commit
9878f3b700
@ -18,6 +18,7 @@ package core
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -569,6 +570,21 @@ func (n Number) Emit(kind NumberKind) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AsInterface returns the number as an interface{}, typically used
|
||||||
|
// for NumberKind-correct JSON conversion.
|
||||||
|
func (n Number) AsInterface(kind NumberKind) interface{} {
|
||||||
|
switch kind {
|
||||||
|
case Int64NumberKind:
|
||||||
|
return n.AsInt64()
|
||||||
|
case Float64NumberKind:
|
||||||
|
return n.AsFloat64()
|
||||||
|
case Uint64NumberKind:
|
||||||
|
return n.AsUint64()
|
||||||
|
default:
|
||||||
|
return math.NaN()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// - private stuff
|
// - private stuff
|
||||||
|
|
||||||
func (n Number) compareWithZero(kind NumberKind) int {
|
func (n Number) compareWithZero(kind NumberKind) int {
|
||||||
|
@ -17,6 +17,8 @@ package core
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNumber(t *testing.T) {
|
func TestNumber(t *testing.T) {
|
||||||
@ -157,3 +159,9 @@ func TestNumberZero(t *testing.T) {
|
|||||||
t.Errorf("Invalid zero representations")
|
t.Errorf("Invalid zero representations")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNumberAsInterface(t *testing.T) {
|
||||||
|
require.Equal(t, int64(10), NewInt64Number(10).AsInterface(Int64NumberKind).(int64))
|
||||||
|
require.Equal(t, float64(11.11), NewFloat64Number(11.11).AsInterface(Float64NumberKind).(float64))
|
||||||
|
require.Equal(t, uint64(100), NewUint64Number(100).AsInterface(Uint64NumberKind).(uint64))
|
||||||
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/DataDog/sketches-go v0.0.0-20190923095040-43f19ad77ff7 h1:qELHH0AWCvf98Yf+CNIJx9vOZOfHFDDzgDRYsnNk/vs=
|
||||||
github.com/DataDog/sketches-go v0.0.0-20190923095040-43f19ad77ff7/go.mod h1:Q5DbzQ+3AkgGwymQO7aZFNP7ns2lZKGtvRBzRXfdi60=
|
github.com/DataDog/sketches-go v0.0.0-20190923095040-43f19ad77ff7/go.mod h1:Q5DbzQ+3AkgGwymQO7aZFNP7ns2lZKGtvRBzRXfdi60=
|
||||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
github.com/OpenPeeDeeP/depguard v1.0.1/go.mod h1:xsIw86fROiiwelg+jB2uM9PiKihMMmUx/1V+TNhjQvM=
|
github.com/OpenPeeDeeP/depguard v1.0.1/go.mod h1:xsIw86fROiiwelg+jB2uM9PiKihMMmUx/1V+TNhjQvM=
|
||||||
@ -7,6 +8,8 @@ github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrU
|
|||||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||||
|
github.com/benbjohnson/clock v1.0.0 h1:78Jk/r6m4wCi6sndMpty7A//t4dw/RW5fV4ZgDVfX1w=
|
||||||
|
github.com/benbjohnson/clock v1.0.0/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM=
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
github.com/bombsimon/wsl v1.2.5/go.mod h1:43lEF/i0kpXbLCeDXL9LMT8c92HyBywXb0AsgMHYngM=
|
github.com/bombsimon/wsl v1.2.5/go.mod h1:43lEF/i0kpXbLCeDXL9LMT8c92HyBywXb0AsgMHYngM=
|
||||||
@ -21,6 +24,7 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc
|
|||||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/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=
|
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||||
@ -82,6 +86,7 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ
|
|||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
|
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
@ -107,9 +112,11 @@ github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgo
|
|||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||||
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||||
@ -136,6 +143,7 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9
|
|||||||
github.com/pelletier/go-toml v1.5.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
|
github.com/pelletier/go-toml v1.5.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||||
@ -177,6 +185,7 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
|||||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
|
github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
|
||||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||||
@ -274,6 +283,7 @@ google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRn
|
|||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
@ -282,6 +292,7 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD
|
|||||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
@ -17,13 +17,19 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
"go.opentelemetry.io/otel/api/distributedcontext"
|
"go.opentelemetry.io/otel/api/distributedcontext"
|
||||||
"go.opentelemetry.io/otel/api/key"
|
"go.opentelemetry.io/otel/api/key"
|
||||||
"go.opentelemetry.io/otel/api/metric"
|
"go.opentelemetry.io/otel/api/metric"
|
||||||
"go.opentelemetry.io/otel/api/trace"
|
"go.opentelemetry.io/otel/api/trace"
|
||||||
"go.opentelemetry.io/otel/exporter/trace/stdout"
|
metricstdout "go.opentelemetry.io/otel/exporter/metric/stdout"
|
||||||
|
tracestdout "go.opentelemetry.io/otel/exporter/trace/stdout"
|
||||||
"go.opentelemetry.io/otel/global"
|
"go.opentelemetry.io/otel/global"
|
||||||
|
metricsdk "go.opentelemetry.io/otel/sdk/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/batcher/defaultkeys"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/controller/push"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/selector/simple"
|
||||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -37,23 +43,44 @@ var (
|
|||||||
// initTracer creates and registers trace provider instance.
|
// initTracer creates and registers trace provider instance.
|
||||||
func initTracer() {
|
func initTracer() {
|
||||||
var err error
|
var err error
|
||||||
exp, err := stdout.NewExporter(stdout.Options{PrettyPrint: false})
|
exp, err := tracestdout.NewExporter(tracestdout.Options{PrettyPrint: false})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panicf("failed to initialize stdout exporter %v\n", err)
|
log.Panicf("failed to initialize trace stdout exporter %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tp, err := sdktrace.NewProvider(sdktrace.WithSyncer(exp),
|
tp, err := sdktrace.NewProvider(sdktrace.WithSyncer(exp),
|
||||||
sdktrace.WithConfig(sdktrace.Config{DefaultSampler: sdktrace.AlwaysSample()}))
|
sdktrace.WithConfig(sdktrace.Config{DefaultSampler: sdktrace.AlwaysSample()}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panicf("failed to initialize trace provider %v\n", err)
|
log.Panicf("failed to initialize trace provider %v", err)
|
||||||
}
|
}
|
||||||
global.SetTraceProvider(tp)
|
global.SetTraceProvider(tp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func initMeter() *push.Controller {
|
||||||
|
selector := simple.NewWithExactMeasure()
|
||||||
|
exporter, err := metricstdout.New(metricstdout.Options{
|
||||||
|
Quantiles: []float64{0.5, 0.9, 0.99},
|
||||||
|
PrettyPrint: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("failed to initialize metric stdout exporter %v", err)
|
||||||
|
}
|
||||||
|
batcher := defaultkeys.New(selector, metricsdk.DefaultLabelEncoder(), true)
|
||||||
|
pusher := push.New(batcher, exporter, time.Second)
|
||||||
|
pusher.Start()
|
||||||
|
|
||||||
|
global.SetMeterProvider(pusher)
|
||||||
|
return pusher
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
defer initMeter().Stop()
|
||||||
initTracer()
|
initTracer()
|
||||||
|
|
||||||
|
// Note: Have to get the meter and tracer after the global is
|
||||||
|
// initialized. See OTEP 0005.
|
||||||
|
|
||||||
tracer := global.TraceProvider().GetTracer("ex.com/basic")
|
tracer := global.TraceProvider().GetTracer("ex.com/basic")
|
||||||
// TODO: Meter doesn't work yet, check if resources to be shared afterwards.
|
|
||||||
meter := global.MeterProvider().GetMeter("ex.com/basic")
|
meter := global.MeterProvider().GetMeter("ex.com/basic")
|
||||||
|
|
||||||
oneMetric := meter.NewFloat64Gauge("ex.com.one",
|
oneMetric := meter.NewFloat64Gauge("ex.com.one",
|
||||||
@ -70,7 +97,7 @@ func main() {
|
|||||||
barKey.String("bar1"),
|
barKey.String("bar1"),
|
||||||
)
|
)
|
||||||
|
|
||||||
commonLabels := meter.Labels(lemonsKey.Int(10))
|
commonLabels := meter.Labels(lemonsKey.Int(10), key.String("A", "1"), key.String("B", "2"), key.String("C", "3"))
|
||||||
|
|
||||||
gauge := oneMetric.AcquireHandle(commonLabels)
|
gauge := oneMetric.AcquireHandle(commonLabels)
|
||||||
defer gauge.Release()
|
defer gauge.Release()
|
||||||
|
@ -22,6 +22,7 @@ github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrU
|
|||||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||||
|
github.com/benbjohnson/clock v1.0.0/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM=
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
github.com/bombsimon/wsl v1.2.5/go.mod h1:43lEF/i0kpXbLCeDXL9LMT8c92HyBywXb0AsgMHYngM=
|
github.com/bombsimon/wsl v1.2.5/go.mod h1:43lEF/i0kpXbLCeDXL9LMT8c92HyBywXb0AsgMHYngM=
|
||||||
|
@ -7,6 +7,7 @@ github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrU
|
|||||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||||
|
github.com/benbjohnson/clock v1.0.0/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM=
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
github.com/bombsimon/wsl v1.2.5/go.mod h1:43lEF/i0kpXbLCeDXL9LMT8c92HyBywXb0AsgMHYngM=
|
github.com/bombsimon/wsl v1.2.5/go.mod h1:43lEF/i0kpXbLCeDXL9LMT8c92HyBywXb0AsgMHYngM=
|
||||||
@ -21,6 +22,7 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc
|
|||||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/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=
|
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||||
@ -141,6 +143,7 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9
|
|||||||
github.com/pelletier/go-toml v1.5.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
|
github.com/pelletier/go-toml v1.5.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||||
@ -182,6 +185,7 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
|||||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
|
github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
|
||||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||||
@ -283,6 +287,7 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks
|
|||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||||
@ -294,6 +299,7 @@ gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bl
|
|||||||
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
@ -11,6 +11,7 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
|
|||||||
github.com/apache/thrift v0.13.0 h1:5hryIiq9gtn+MiLVn0wP37kb/uTeRZgN08WoCsAhIhI=
|
github.com/apache/thrift v0.13.0 h1:5hryIiq9gtn+MiLVn0wP37kb/uTeRZgN08WoCsAhIhI=
|
||||||
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||||
|
github.com/benbjohnson/clock v1.0.0/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM=
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
github.com/bombsimon/wsl v1.2.5/go.mod h1:43lEF/i0kpXbLCeDXL9LMT8c92HyBywXb0AsgMHYngM=
|
github.com/bombsimon/wsl v1.2.5/go.mod h1:43lEF/i0kpXbLCeDXL9LMT8c92HyBywXb0AsgMHYngM=
|
||||||
|
@ -7,6 +7,7 @@ github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrU
|
|||||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||||
|
github.com/benbjohnson/clock v1.0.0/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM=
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
github.com/bombsimon/wsl v1.2.5/go.mod h1:43lEF/i0kpXbLCeDXL9LMT8c92HyBywXb0AsgMHYngM=
|
github.com/bombsimon/wsl v1.2.5/go.mod h1:43lEF/i0kpXbLCeDXL9LMT8c92HyBywXb0AsgMHYngM=
|
||||||
@ -21,6 +22,7 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc
|
|||||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/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=
|
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||||
@ -107,9 +109,11 @@ github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgo
|
|||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||||
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||||
@ -136,6 +140,7 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9
|
|||||||
github.com/pelletier/go-toml v1.5.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
|
github.com/pelletier/go-toml v1.5.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||||
@ -177,6 +182,7 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
|||||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
|
github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
|
||||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||||
@ -274,6 +280,7 @@ google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRn
|
|||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
@ -282,6 +289,7 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD
|
|||||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
214
exporter/metric/stdout/stdout.go
Normal file
214
exporter/metric/stdout/stdout.go
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
// Copyright 2019, OpenTelemetry 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 stdout // import "go.opentelemetry.io/otel/exporter/metric/stdout"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
export "go.opentelemetry.io/otel/sdk/export/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/export/metric/aggregator"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Exporter struct {
|
||||||
|
options Options
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ export.Exporter = &Exporter{}
|
||||||
|
|
||||||
|
// Options are the options to be used when initializing a stdout export.
|
||||||
|
type Options struct {
|
||||||
|
// File is the destination. If not set, os.Stdout is used.
|
||||||
|
File io.Writer
|
||||||
|
|
||||||
|
// PrettyPrint will pretty the json representation of the span,
|
||||||
|
// making it print "pretty". Default is false.
|
||||||
|
PrettyPrint bool
|
||||||
|
|
||||||
|
// DoNotPrintTime suppresses timestamp printing. This is
|
||||||
|
// useful to create deterministic test conditions.
|
||||||
|
DoNotPrintTime bool
|
||||||
|
|
||||||
|
// Quantiles are the desired aggregation quantiles for measure
|
||||||
|
// metric data, used when the configured aggregator supports
|
||||||
|
// quantiles.
|
||||||
|
//
|
||||||
|
// Note: this exporter is meant as a demonstration; a real
|
||||||
|
// exporter may wish to configure quantiles on a per-metric
|
||||||
|
// basis.
|
||||||
|
Quantiles []float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type expoBatch struct {
|
||||||
|
Timestamp *time.Time `json:"time,omitempty"`
|
||||||
|
Updates []expoLine `json:"updates"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type expoLine struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Max interface{} `json:"max,omitempty"`
|
||||||
|
Sum interface{} `json:"sum,omitempty"`
|
||||||
|
Count interface{} `json:"count,omitempty"`
|
||||||
|
LastValue interface{} `json:"last,omitempty"`
|
||||||
|
|
||||||
|
Quantiles interface{} `json:"quantiles,omitempty"`
|
||||||
|
|
||||||
|
// Note: this is a pointer because omitempty doesn't work when time.IsZero()
|
||||||
|
Timestamp *time.Time `json:"time,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type expoQuantile struct {
|
||||||
|
Q interface{} `json:"q"`
|
||||||
|
V interface{} `json:"v"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(options Options) (*Exporter, error) {
|
||||||
|
if options.File == nil {
|
||||||
|
options.File = os.Stdout
|
||||||
|
}
|
||||||
|
if options.Quantiles == nil {
|
||||||
|
options.Quantiles = []float64{0.5, 0.9, 0.99}
|
||||||
|
} else {
|
||||||
|
for _, q := range options.Quantiles {
|
||||||
|
if q < 0 || q > 1 {
|
||||||
|
return nil, aggregator.ErrInvalidQuantile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &Exporter{
|
||||||
|
options: options,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Exporter) Export(_ context.Context, checkpointSet export.CheckpointSet) error {
|
||||||
|
// N.B. Only return one aggError, if any occur. They're likely
|
||||||
|
// to be duplicates of the same error.
|
||||||
|
var aggError error
|
||||||
|
var batch expoBatch
|
||||||
|
if !e.options.DoNotPrintTime {
|
||||||
|
ts := time.Now()
|
||||||
|
batch.Timestamp = &ts
|
||||||
|
}
|
||||||
|
checkpointSet.ForEach(func(record export.Record) {
|
||||||
|
desc := record.Descriptor()
|
||||||
|
agg := record.Aggregator()
|
||||||
|
kind := desc.NumberKind()
|
||||||
|
|
||||||
|
var expose expoLine
|
||||||
|
|
||||||
|
if sum, ok := agg.(aggregator.Sum); ok {
|
||||||
|
if value, err := sum.Sum(); err != nil {
|
||||||
|
aggError = err
|
||||||
|
expose.Sum = "NaN"
|
||||||
|
} else {
|
||||||
|
expose.Sum = value.AsInterface(kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if msc, ok := agg.(aggregator.MaxSumCount); ok {
|
||||||
|
if count, err := msc.Count(); err != nil {
|
||||||
|
aggError = err
|
||||||
|
expose.Count = "NaN"
|
||||||
|
} else {
|
||||||
|
expose.Count = count
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Should tolerate ErrEmptyDataSet here,
|
||||||
|
// just like ErrNoLastValue below, since
|
||||||
|
// there's a race condition between creating
|
||||||
|
// the Aggregator and updating the first
|
||||||
|
// value.
|
||||||
|
|
||||||
|
if max, err := msc.Max(); err != nil {
|
||||||
|
aggError = err
|
||||||
|
expose.Max = "NaN"
|
||||||
|
} else {
|
||||||
|
expose.Max = max.AsInterface(kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dist, ok := agg.(aggregator.Distribution); ok && len(e.options.Quantiles) != 0 {
|
||||||
|
summary := make([]expoQuantile, len(e.options.Quantiles))
|
||||||
|
expose.Quantiles = summary
|
||||||
|
|
||||||
|
for i, q := range e.options.Quantiles {
|
||||||
|
var vstr interface{}
|
||||||
|
if value, err := dist.Quantile(q); err != nil {
|
||||||
|
aggError = err
|
||||||
|
vstr = "NaN"
|
||||||
|
} else {
|
||||||
|
vstr = value.AsInterface(kind)
|
||||||
|
}
|
||||||
|
summary[i] = expoQuantile{
|
||||||
|
Q: q,
|
||||||
|
V: vstr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if lv, ok := agg.(aggregator.LastValue); ok {
|
||||||
|
if value, timestamp, err := lv.LastValue(); err != nil {
|
||||||
|
if err == aggregator.ErrNoLastValue {
|
||||||
|
// This is a special case, indicates an aggregator that
|
||||||
|
// was checkpointed before its first value was set.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
aggError = err
|
||||||
|
expose.LastValue = "NaN"
|
||||||
|
} else {
|
||||||
|
expose.LastValue = value.AsInterface(kind)
|
||||||
|
|
||||||
|
if !e.options.DoNotPrintTime {
|
||||||
|
expose.Timestamp = ×tamp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
|
||||||
|
sb.WriteString(desc.Name())
|
||||||
|
|
||||||
|
if labels := record.Labels(); labels.Len() > 0 {
|
||||||
|
sb.WriteRune('{')
|
||||||
|
sb.WriteString(labels.Encoded())
|
||||||
|
sb.WriteRune('}')
|
||||||
|
}
|
||||||
|
|
||||||
|
expose.Name = sb.String()
|
||||||
|
|
||||||
|
batch.Updates = append(batch.Updates, expose)
|
||||||
|
})
|
||||||
|
|
||||||
|
var data []byte
|
||||||
|
var err error
|
||||||
|
if e.options.PrettyPrint {
|
||||||
|
data, err = json.MarshalIndent(batch, "", "\t")
|
||||||
|
} else {
|
||||||
|
data, err = json.Marshal(batch)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
fmt.Fprintln(e.options.File, string(data))
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return aggError
|
||||||
|
}
|
256
exporter/metric/stdout/stdout_test.go
Normal file
256
exporter/metric/stdout/stdout_test.go
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
package stdout_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/api/core"
|
||||||
|
"go.opentelemetry.io/otel/api/key"
|
||||||
|
"go.opentelemetry.io/otel/exporter/metric/stdout"
|
||||||
|
"go.opentelemetry.io/otel/exporter/metric/test"
|
||||||
|
export "go.opentelemetry.io/otel/sdk/export/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/export/metric/aggregator"
|
||||||
|
sdk "go.opentelemetry.io/otel/sdk/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/aggregator/array"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/aggregator/counter"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/aggregator/ddsketch"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/aggregator/gauge"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/aggregator/maxsumcount"
|
||||||
|
aggtest "go.opentelemetry.io/otel/sdk/metric/aggregator/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testFixture struct {
|
||||||
|
t *testing.T
|
||||||
|
ctx context.Context
|
||||||
|
exporter *stdout.Exporter
|
||||||
|
output *bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFixture(t *testing.T, options stdout.Options) testFixture {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
options.File = buf
|
||||||
|
options.DoNotPrintTime = true
|
||||||
|
exp, err := stdout.New(options)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Error building fixture: ", err)
|
||||||
|
}
|
||||||
|
return testFixture{
|
||||||
|
t: t,
|
||||||
|
ctx: context.Background(),
|
||||||
|
exporter: exp,
|
||||||
|
output: buf,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fix testFixture) Output() string {
|
||||||
|
return strings.TrimSpace(fix.output.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fix testFixture) Export(checkpointSet export.CheckpointSet) {
|
||||||
|
err := fix.exporter.Export(fix.ctx, checkpointSet)
|
||||||
|
if err != nil {
|
||||||
|
fix.t.Error("export failed: ", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStdoutInvalidQuantile(t *testing.T) {
|
||||||
|
_, err := stdout.New(stdout.Options{
|
||||||
|
Quantiles: []float64{1.1, 0.9},
|
||||||
|
})
|
||||||
|
require.Error(t, err, "Invalid quantile error expected")
|
||||||
|
require.Equal(t, aggregator.ErrInvalidQuantile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStdoutTimestamp(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
exporter, err := stdout.New(stdout.Options{
|
||||||
|
File: &buf,
|
||||||
|
DoNotPrintTime: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Invalid options: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
before := time.Now()
|
||||||
|
|
||||||
|
checkpointSet := test.NewCheckpointSet(sdk.DefaultLabelEncoder())
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
desc := export.NewDescriptor("test.name", export.GaugeKind, nil, "", "", core.Int64NumberKind, false)
|
||||||
|
gagg := gauge.New()
|
||||||
|
aggtest.CheckedUpdate(t, gagg, core.NewInt64Number(321), desc)
|
||||||
|
gagg.Checkpoint(ctx, desc)
|
||||||
|
|
||||||
|
checkpointSet.Add(desc, gagg)
|
||||||
|
|
||||||
|
if err := exporter.Export(ctx, checkpointSet); err != nil {
|
||||||
|
t.Fatal("Unexpected export error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
after := time.Now()
|
||||||
|
|
||||||
|
var printed map[string]interface{}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(buf.Bytes(), &printed); err != nil {
|
||||||
|
t.Fatal("JSON parse error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTS := printed["time"].(string)
|
||||||
|
updateTimestamp, err := time.Parse(time.RFC3339Nano, updateTS)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("JSON parse error: ", updateTS, ": ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gaugeTS := printed["updates"].([]interface{})[0].(map[string]interface{})["time"].(string)
|
||||||
|
gaugeTimestamp, err := time.Parse(time.RFC3339Nano, gaugeTS)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("JSON parse error: ", gaugeTS, ": ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
require.True(t, updateTimestamp.After(before))
|
||||||
|
require.True(t, updateTimestamp.Before(after))
|
||||||
|
|
||||||
|
require.True(t, gaugeTimestamp.After(before))
|
||||||
|
require.True(t, gaugeTimestamp.Before(after))
|
||||||
|
|
||||||
|
require.True(t, gaugeTimestamp.Before(updateTimestamp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStdoutCounterFormat(t *testing.T) {
|
||||||
|
fix := newFixture(t, stdout.Options{})
|
||||||
|
|
||||||
|
checkpointSet := test.NewCheckpointSet(sdk.DefaultLabelEncoder())
|
||||||
|
|
||||||
|
desc := export.NewDescriptor("test.name", export.CounterKind, nil, "", "", core.Int64NumberKind, false)
|
||||||
|
cagg := counter.New()
|
||||||
|
aggtest.CheckedUpdate(fix.t, cagg, core.NewInt64Number(123), desc)
|
||||||
|
cagg.Checkpoint(fix.ctx, desc)
|
||||||
|
|
||||||
|
checkpointSet.Add(desc, cagg, key.String("A", "B"), key.String("C", "D"))
|
||||||
|
|
||||||
|
fix.Export(checkpointSet)
|
||||||
|
|
||||||
|
require.Equal(t, `{"updates":[{"name":"test.name{A=B,C=D}","sum":123}]}`, fix.Output())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStdoutGaugeFormat(t *testing.T) {
|
||||||
|
fix := newFixture(t, stdout.Options{})
|
||||||
|
|
||||||
|
checkpointSet := test.NewCheckpointSet(sdk.DefaultLabelEncoder())
|
||||||
|
|
||||||
|
desc := export.NewDescriptor("test.name", export.GaugeKind, nil, "", "", core.Float64NumberKind, false)
|
||||||
|
gagg := gauge.New()
|
||||||
|
aggtest.CheckedUpdate(fix.t, gagg, core.NewFloat64Number(123.456), desc)
|
||||||
|
gagg.Checkpoint(fix.ctx, desc)
|
||||||
|
|
||||||
|
checkpointSet.Add(desc, gagg, key.String("A", "B"), key.String("C", "D"))
|
||||||
|
|
||||||
|
fix.Export(checkpointSet)
|
||||||
|
|
||||||
|
require.Equal(t, `{"updates":[{"name":"test.name{A=B,C=D}","last":123.456}]}`, fix.Output())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStdoutMaxSumCount(t *testing.T) {
|
||||||
|
fix := newFixture(t, stdout.Options{})
|
||||||
|
|
||||||
|
checkpointSet := test.NewCheckpointSet(sdk.DefaultLabelEncoder())
|
||||||
|
|
||||||
|
desc := export.NewDescriptor("test.name", export.MeasureKind, nil, "", "", core.Float64NumberKind, false)
|
||||||
|
magg := maxsumcount.New()
|
||||||
|
aggtest.CheckedUpdate(fix.t, magg, core.NewFloat64Number(123.456), desc)
|
||||||
|
aggtest.CheckedUpdate(fix.t, magg, core.NewFloat64Number(876.543), desc)
|
||||||
|
magg.Checkpoint(fix.ctx, desc)
|
||||||
|
|
||||||
|
checkpointSet.Add(desc, magg, key.String("A", "B"), key.String("C", "D"))
|
||||||
|
|
||||||
|
fix.Export(checkpointSet)
|
||||||
|
|
||||||
|
require.Equal(t, `{"updates":[{"name":"test.name{A=B,C=D}","max":876.543,"sum":999.999,"count":2}]}`, fix.Output())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStdoutMeasureFormat(t *testing.T) {
|
||||||
|
fix := newFixture(t, stdout.Options{
|
||||||
|
PrettyPrint: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
checkpointSet := test.NewCheckpointSet(sdk.DefaultLabelEncoder())
|
||||||
|
|
||||||
|
desc := export.NewDescriptor("test.name", export.MeasureKind, nil, "", "", core.Float64NumberKind, false)
|
||||||
|
magg := array.New()
|
||||||
|
|
||||||
|
for i := 0; i < 1000; i++ {
|
||||||
|
aggtest.CheckedUpdate(fix.t, magg, core.NewFloat64Number(float64(i)+0.5), desc)
|
||||||
|
}
|
||||||
|
|
||||||
|
magg.Checkpoint(fix.ctx, desc)
|
||||||
|
|
||||||
|
checkpointSet.Add(desc, magg, key.String("A", "B"), key.String("C", "D"))
|
||||||
|
|
||||||
|
fix.Export(checkpointSet)
|
||||||
|
|
||||||
|
require.Equal(t, `{
|
||||||
|
"updates": [
|
||||||
|
{
|
||||||
|
"name": "test.name{A=B,C=D}",
|
||||||
|
"max": 999.5,
|
||||||
|
"sum": 500000,
|
||||||
|
"count": 1000,
|
||||||
|
"quantiles": [
|
||||||
|
{
|
||||||
|
"q": 0.5,
|
||||||
|
"v": 500.5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"q": 0.9,
|
||||||
|
"v": 900.5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"q": 0.99,
|
||||||
|
"v": 990.5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`, fix.Output())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStdoutAggError(t *testing.T) {
|
||||||
|
fix := newFixture(t, stdout.Options{})
|
||||||
|
|
||||||
|
checkpointSet := test.NewCheckpointSet(sdk.DefaultLabelEncoder())
|
||||||
|
|
||||||
|
desc := export.NewDescriptor("test.name", export.MeasureKind, nil, "", "", core.Float64NumberKind, false)
|
||||||
|
magg := ddsketch.New(ddsketch.NewDefaultConfig(), desc)
|
||||||
|
magg.Checkpoint(fix.ctx, desc)
|
||||||
|
|
||||||
|
checkpointSet.Add(desc, magg)
|
||||||
|
|
||||||
|
err := fix.exporter.Export(fix.ctx, checkpointSet)
|
||||||
|
|
||||||
|
// An error is returned and NaN values are printed.
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Equal(t, aggregator.ErrEmptyDataSet, err)
|
||||||
|
require.Equal(t, `{"updates":[{"name":"test.name","max":"NaN","sum":0,"count":0,"quantiles":[{"q":0.5,"v":"NaN"},{"q":0.9,"v":"NaN"},{"q":0.99,"v":"NaN"}]}]}`, fix.Output())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStdoutGaugeNotSet(t *testing.T) {
|
||||||
|
fix := newFixture(t, stdout.Options{})
|
||||||
|
|
||||||
|
checkpointSet := test.NewCheckpointSet(sdk.DefaultLabelEncoder())
|
||||||
|
|
||||||
|
desc := export.NewDescriptor("test.name", export.GaugeKind, nil, "", "", core.Float64NumberKind, false)
|
||||||
|
gagg := gauge.New()
|
||||||
|
gagg.Checkpoint(fix.ctx, desc)
|
||||||
|
|
||||||
|
checkpointSet.Add(desc, gagg, key.String("A", "B"), key.String("C", "D"))
|
||||||
|
|
||||||
|
fix.Export(checkpointSet)
|
||||||
|
|
||||||
|
require.Equal(t, `{"updates":null}`, fix.Output())
|
||||||
|
}
|
34
exporter/metric/test/test.go
Normal file
34
exporter/metric/test/test.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go.opentelemetry.io/otel/api/core"
|
||||||
|
export "go.opentelemetry.io/otel/sdk/export/metric"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CheckpointSet struct {
|
||||||
|
encoder export.LabelEncoder
|
||||||
|
updates []export.Record
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCheckpointSet(encoder export.LabelEncoder) *CheckpointSet {
|
||||||
|
return &CheckpointSet{
|
||||||
|
encoder: encoder,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CheckpointSet) Reset() {
|
||||||
|
p.updates = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CheckpointSet) Add(desc *export.Descriptor, agg export.Aggregator, labels ...core.KeyValue) {
|
||||||
|
encoded := p.encoder.Encode(labels)
|
||||||
|
elabels := export.NewLabels(labels, encoded, p.encoder)
|
||||||
|
|
||||||
|
p.updates = append(p.updates, export.NewRecord(desc, elabels, agg))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CheckpointSet) ForEach(f func(export.Record)) {
|
||||||
|
for _, r := range p.updates {
|
||||||
|
f(r)
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,7 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
|
|||||||
github.com/apache/thrift v0.13.0 h1:5hryIiq9gtn+MiLVn0wP37kb/uTeRZgN08WoCsAhIhI=
|
github.com/apache/thrift v0.13.0 h1:5hryIiq9gtn+MiLVn0wP37kb/uTeRZgN08WoCsAhIhI=
|
||||||
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||||
|
github.com/benbjohnson/clock v1.0.0/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM=
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
github.com/bombsimon/wsl v1.2.5/go.mod h1:43lEF/i0kpXbLCeDXL9LMT8c92HyBywXb0AsgMHYngM=
|
github.com/bombsimon/wsl v1.2.5/go.mod h1:43lEF/i0kpXbLCeDXL9LMT8c92HyBywXb0AsgMHYngM=
|
||||||
|
@ -21,6 +21,7 @@ github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrU
|
|||||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||||
|
github.com/benbjohnson/clock v1.0.0/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM=
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
github.com/bombsimon/wsl v1.2.5/go.mod h1:43lEF/i0kpXbLCeDXL9LMT8c92HyBywXb0AsgMHYngM=
|
github.com/bombsimon/wsl v1.2.5/go.mod h1:43lEF/i0kpXbLCeDXL9LMT8c92HyBywXb0AsgMHYngM=
|
||||||
|
1
go.mod
1
go.mod
@ -4,6 +4,7 @@ go 1.13
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/DataDog/sketches-go v0.0.0-20190923095040-43f19ad77ff7
|
github.com/DataDog/sketches-go v0.0.0-20190923095040-43f19ad77ff7
|
||||||
|
github.com/benbjohnson/clock v1.0.0
|
||||||
github.com/client9/misspell v0.3.4
|
github.com/client9/misspell v0.3.4
|
||||||
github.com/gogo/protobuf v1.3.1 // indirect
|
github.com/gogo/protobuf v1.3.1 // indirect
|
||||||
github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d // indirect
|
github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d // indirect
|
||||||
|
3
go.sum
3
go.sum
@ -10,6 +10,8 @@ github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrU
|
|||||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||||
|
github.com/benbjohnson/clock v1.0.0 h1:78Jk/r6m4wCi6sndMpty7A//t4dw/RW5fV4ZgDVfX1w=
|
||||||
|
github.com/benbjohnson/clock v1.0.0/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM=
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
github.com/bombsimon/wsl v1.2.5 h1:9gTOkIwVtoDZywvX802SDHokeX4kW1cKnV8ZTVAPkRs=
|
github.com/bombsimon/wsl v1.2.5 h1:9gTOkIwVtoDZywvX802SDHokeX4kW1cKnV8ZTVAPkRs=
|
||||||
@ -138,6 +140,7 @@ github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
|||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
|
github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo=
|
||||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||||
|
116
sdk/export/metric/aggregator/aggregator.go
Normal file
116
sdk/export/metric/aggregator/aggregator.go
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
// Copyright 2019, OpenTelemetry 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 aggregator // import "go.opentelemetry.io/otel/sdk/metric/aggregator"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/api/core"
|
||||||
|
export "go.opentelemetry.io/otel/sdk/export/metric"
|
||||||
|
)
|
||||||
|
|
||||||
|
// These interfaces describe the various ways to access state from an
|
||||||
|
// Aggregator.
|
||||||
|
|
||||||
|
type (
|
||||||
|
// Sum returns an aggregated sum.
|
||||||
|
Sum interface {
|
||||||
|
Sum() (core.Number, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sum returns the number of values that were aggregated.
|
||||||
|
Count interface {
|
||||||
|
Count() (int64, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Max returns the maximum value over the set of values that were aggregated.
|
||||||
|
Max interface {
|
||||||
|
Max() (core.Number, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quantile returns an exact or estimated quantile over the
|
||||||
|
// set of values that were aggregated.
|
||||||
|
Quantile interface {
|
||||||
|
Quantile(float64) (core.Number, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LastValue returns the latest value that was aggregated.
|
||||||
|
LastValue interface {
|
||||||
|
LastValue() (core.Number, time.Time, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaxSumCount supports the Max, Sum, and Count interfaces.
|
||||||
|
MaxSumCount interface {
|
||||||
|
Sum
|
||||||
|
Count
|
||||||
|
Max
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaxSumCount supports the Max, Sum, Count, and Quantile
|
||||||
|
// interfaces.
|
||||||
|
Distribution interface {
|
||||||
|
MaxSumCount
|
||||||
|
Quantile
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidQuantile = fmt.Errorf("The requested quantile is out of range")
|
||||||
|
ErrNegativeInput = fmt.Errorf("Negative value is out of range for this instrument")
|
||||||
|
ErrNaNInput = fmt.Errorf("NaN value is an invalid input")
|
||||||
|
ErrNonMonotoneInput = fmt.Errorf("The new value is not monotone")
|
||||||
|
ErrInconsistentType = fmt.Errorf("Inconsistent aggregator types")
|
||||||
|
|
||||||
|
// ErrNoLastValue is returned by the LastValue interface when
|
||||||
|
// (due to a race with collection) the Aggregator is
|
||||||
|
// checkpointed before the first value is set. The aggregator
|
||||||
|
// should simply be skipped in this case.
|
||||||
|
ErrNoLastValue = fmt.Errorf("No value has been set")
|
||||||
|
|
||||||
|
// ErrEmptyDataSet is returned by Max and Quantile interfaces
|
||||||
|
// when (due to a race with collection) the Aggregator is
|
||||||
|
// checkpointed before the first value is set. The aggregator
|
||||||
|
// should simply be skipped in this case.
|
||||||
|
ErrEmptyDataSet = fmt.Errorf("The result is not defined on an empty data set")
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewInconsistentMergeError formats an error describing an attempt to
|
||||||
|
// merge different-type aggregators. The result can be unwrapped as
|
||||||
|
// an ErrInconsistentType.
|
||||||
|
func NewInconsistentMergeError(a1, a2 export.Aggregator) error {
|
||||||
|
return fmt.Errorf("Cannot merge %T with %T: %w", a1, a2, ErrInconsistentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RangeTest is a commmon routine for testing for valid input values.
|
||||||
|
// This rejects NaN values. This rejects negative values when the
|
||||||
|
// metric instrument does not support negative values, including
|
||||||
|
// monotonic counter metrics and absolute measure metrics.
|
||||||
|
func RangeTest(number core.Number, descriptor *export.Descriptor) error {
|
||||||
|
numberKind := descriptor.NumberKind()
|
||||||
|
|
||||||
|
if numberKind == core.Float64NumberKind && math.IsNaN(number.AsFloat64()) {
|
||||||
|
return ErrNaNInput
|
||||||
|
}
|
||||||
|
|
||||||
|
switch descriptor.MetricKind() {
|
||||||
|
case export.CounterKind, export.MeasureKind:
|
||||||
|
if !descriptor.Alternate() && number.IsNegative(numberKind) {
|
||||||
|
return ErrNegativeInput
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
105
sdk/export/metric/aggregator/aggregator_test.go
Normal file
105
sdk/export/metric/aggregator/aggregator_test.go
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
// Copyright 2019, OpenTelemetry 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 aggregator_test // import "go.opentelemetry.io/otel/sdk/metric/aggregator"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/api/core"
|
||||||
|
export "go.opentelemetry.io/otel/sdk/export/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/export/metric/aggregator"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/aggregator/counter"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/aggregator/gauge"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInconsistentMergeErr(t *testing.T) {
|
||||||
|
err := aggregator.NewInconsistentMergeError(counter.New(), gauge.New())
|
||||||
|
require.Equal(
|
||||||
|
t,
|
||||||
|
"Cannot merge *counter.Aggregator with *gauge.Aggregator: Inconsistent aggregator types",
|
||||||
|
err.Error(),
|
||||||
|
)
|
||||||
|
require.True(t, errors.Is(err, aggregator.ErrInconsistentType))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRangeNaN(t *testing.T, desc *export.Descriptor) {
|
||||||
|
// If the descriptor uses int64 numbers, this won't register as NaN
|
||||||
|
nan := core.NewFloat64Number(math.NaN())
|
||||||
|
err := aggregator.RangeTest(nan, desc)
|
||||||
|
|
||||||
|
if desc.NumberKind() == core.Float64NumberKind {
|
||||||
|
require.Equal(t, aggregator.ErrNaNInput, err)
|
||||||
|
} else {
|
||||||
|
require.Nil(t, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRangeNegative(t *testing.T, alt bool, desc *export.Descriptor) {
|
||||||
|
var neg, pos core.Number
|
||||||
|
|
||||||
|
if desc.NumberKind() == core.Float64NumberKind {
|
||||||
|
pos = core.NewFloat64Number(+1)
|
||||||
|
neg = core.NewFloat64Number(-1)
|
||||||
|
} else {
|
||||||
|
pos = core.NewInt64Number(+1)
|
||||||
|
neg = core.NewInt64Number(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
posErr := aggregator.RangeTest(pos, desc)
|
||||||
|
negErr := aggregator.RangeTest(neg, desc)
|
||||||
|
|
||||||
|
require.Nil(t, posErr)
|
||||||
|
|
||||||
|
if desc.MetricKind() == export.GaugeKind {
|
||||||
|
require.Nil(t, negErr)
|
||||||
|
} else {
|
||||||
|
require.Equal(t, negErr == nil, alt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRangeTest(t *testing.T) {
|
||||||
|
for _, nkind := range []core.NumberKind{core.Float64NumberKind, core.Int64NumberKind} {
|
||||||
|
t.Run(nkind.String(), func(t *testing.T) {
|
||||||
|
for _, mkind := range []export.MetricKind{
|
||||||
|
export.CounterKind,
|
||||||
|
export.GaugeKind,
|
||||||
|
export.MeasureKind,
|
||||||
|
} {
|
||||||
|
t.Run(mkind.String(), func(t *testing.T) {
|
||||||
|
for _, alt := range []bool{true, false} {
|
||||||
|
t.Run(fmt.Sprint(alt), func(t *testing.T) {
|
||||||
|
desc := export.NewDescriptor(
|
||||||
|
"name",
|
||||||
|
mkind,
|
||||||
|
nil,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
nkind,
|
||||||
|
alt,
|
||||||
|
)
|
||||||
|
testRangeNaN(t, desc)
|
||||||
|
testRangeNegative(t, alt, desc)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -12,7 +12,9 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package metric // import "go.opentelemetry.io/otel/sdk/export/metric"
|
package export
|
||||||
|
|
||||||
|
//go:generate stringer -type=MetricKind
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@ -21,70 +23,288 @@ import (
|
|||||||
"go.opentelemetry.io/otel/api/unit"
|
"go.opentelemetry.io/otel/api/unit"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Batcher is responsible for deciding which kind of aggregation
|
// Batcher is responsible for deciding which kind of aggregation to
|
||||||
// to use and gathering exported results from the SDK. The standard SDK
|
// use (via AggregationSelector), gathering exported results from the
|
||||||
// supports binding only one of these interfaces, i.e., a single exporter.
|
// SDK during collection, and deciding over which dimensions to group
|
||||||
|
// the exported data.
|
||||||
//
|
//
|
||||||
// Multiple-exporters could be implemented by implementing this interface
|
// The SDK supports binding only one of these interfaces, as it has
|
||||||
// for a group of Batcher.
|
// the sole responsibility of determining which Aggregator to use for
|
||||||
|
// each record.
|
||||||
|
//
|
||||||
|
// The embedded AggregationSelector interface is called (concurrently)
|
||||||
|
// in instrumentation context to select the appropriate Aggregator for
|
||||||
|
// an instrument.
|
||||||
|
//
|
||||||
|
// The `Process` method is called during collection in a
|
||||||
|
// single-threaded context from the SDK, after the aggregator is
|
||||||
|
// checkpointed, allowing the batcher to build the set of metrics
|
||||||
|
// currently being exported.
|
||||||
|
//
|
||||||
|
// The `CheckpointSet` method is called during collection in a
|
||||||
|
// single-threaded context from the Exporter, giving the exporter
|
||||||
|
// access to a producer for iterating over the complete checkpoint.
|
||||||
type Batcher interface {
|
type Batcher interface {
|
||||||
// AggregatorFor should return the kind of aggregator
|
// AggregationSelector is responsible for selecting the
|
||||||
// suited to the requested export. Returning `nil`
|
// concrete type of Aggregator used for a metric in the SDK.
|
||||||
// indicates to ignore the metric update.
|
|
||||||
//
|
//
|
||||||
// Note: This is context-free because the handle should not be
|
// This may be a static decision based on fields of the
|
||||||
// bound to the incoming context. This call should not block.
|
// Descriptor, or it could use an external configuration
|
||||||
AggregatorFor(Record) Aggregator
|
// source to customize the treatment of each metric
|
||||||
|
// instrument.
|
||||||
|
//
|
||||||
|
// The result from AggregatorSelector.AggregatorFor should be
|
||||||
|
// the same type for a given Descriptor or else nil. The same
|
||||||
|
// type should be returned for a given descriptor, because
|
||||||
|
// Aggregators only know how to Merge with their own type. If
|
||||||
|
// the result is nil, the metric instrument will be disabled.
|
||||||
|
//
|
||||||
|
// Note that the SDK only calls AggregatorFor when new records
|
||||||
|
// require an Aggregator. This does not provide a way to
|
||||||
|
// disable metrics with active records.
|
||||||
|
AggregationSelector
|
||||||
|
|
||||||
// Export receives pairs of records and aggregators
|
// Process is called by the SDK once per internal record,
|
||||||
// during the SDK Collect(). Exporter implementations
|
// passing the export Record (a Descriptor, the corresponding
|
||||||
// must access the specific aggregator to receive the
|
// Labels, and the checkpointed Aggregator). The Batcher
|
||||||
// exporter data, since the format of the data varies
|
// should be prepared to process duplicate (Descriptor,
|
||||||
// by aggregation.
|
// Labels) pairs during this pass due to race conditions, but
|
||||||
Export(context.Context, Record, Aggregator)
|
// this will usually be the ordinary course of events, as
|
||||||
|
// Aggregators are typically merged according the output set
|
||||||
|
// of labels.
|
||||||
|
//
|
||||||
|
// The Context argument originates from the controller that
|
||||||
|
// orchestrates collection.
|
||||||
|
Process(ctx context.Context, record Record) error
|
||||||
|
|
||||||
|
// CheckpointSet is the interface used by the controller to
|
||||||
|
// access the fully aggregated checkpoint after collection.
|
||||||
|
//
|
||||||
|
// The returned CheckpointSet is passed to the Exporter.
|
||||||
|
CheckpointSet() CheckpointSet
|
||||||
|
|
||||||
|
// FinishedCollection informs the Batcher that a complete
|
||||||
|
// collection round was completed. Stateless batchers might
|
||||||
|
// reset state in this method, for example.
|
||||||
|
FinishedCollection()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aggregator implements a specific aggregation behavior, e.g.,
|
// AggregationSelector supports selecting the kind of Aggregator to
|
||||||
// a counter, a gauge, a histogram.
|
// use at runtime for a specific metric instrument.
|
||||||
|
type AggregationSelector interface {
|
||||||
|
// AggregatorFor returns the kind of aggregator suited to the
|
||||||
|
// requested export. Returning `nil` indicates to ignore this
|
||||||
|
// metric instrument. This must return a consistent type to
|
||||||
|
// avoid confusion in later stages of the metrics export
|
||||||
|
// process, i.e., when Merging multiple aggregators for a
|
||||||
|
// specific instrument.
|
||||||
|
//
|
||||||
|
// Note: This is context-free because the aggregator should
|
||||||
|
// not relate to the incoming context. This call should not
|
||||||
|
// block.
|
||||||
|
AggregatorFor(*Descriptor) Aggregator
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggregator implements a specific aggregation behavior, e.g., a
|
||||||
|
// behavior to track a sequence of updates to a counter, a gauge, or a
|
||||||
|
// measure instrument. For the most part, counter and gauge semantics
|
||||||
|
// are fixed and the provided implementations should be used. Measure
|
||||||
|
// metrics offer a wide range of potential tradeoffs and several
|
||||||
|
// implementations are provided.
|
||||||
|
//
|
||||||
|
// Aggregators are meant to compute the change (i.e., delta) in state
|
||||||
|
// from one checkpoint to the next, with the exception of gauge
|
||||||
|
// aggregators. Gauge aggregators are required to maintain the last
|
||||||
|
// value across checkpoints to implement montonic gauge support.
|
||||||
|
//
|
||||||
|
// Note that any Aggregator may be attached to any instrument--this is
|
||||||
|
// the result of the OpenTelemetry API/SDK separation. It is possible
|
||||||
|
// to attach a counter aggregator to a measure instrument (to compute
|
||||||
|
// a simple sum) or a gauge instrument to a measure instrument (to
|
||||||
|
// compute the last value).
|
||||||
type Aggregator interface {
|
type Aggregator interface {
|
||||||
// Update receives a new measured value and incorporates it
|
// Update receives a new measured value and incorporates it
|
||||||
// into the aggregation.
|
// into the aggregation. Update() calls may arrive
|
||||||
Update(context.Context, core.Number, Record)
|
// concurrently as the SDK does not provide synchronization.
|
||||||
|
//
|
||||||
|
// Descriptor.NumberKind() should be consulted to determine
|
||||||
|
// whether the provided number is an int64 or float64.
|
||||||
|
//
|
||||||
|
// The Context argument comes from user-level code and could be
|
||||||
|
// inspected for distributed or span context.
|
||||||
|
Update(context.Context, core.Number, *Descriptor) error
|
||||||
|
|
||||||
// Collect is called during the SDK Collect() to
|
// Checkpoint is called during collection to finish one period
|
||||||
// finish one period of aggregation. Collect() is
|
// of aggregation by atomically saving the current value.
|
||||||
// called in a single-threaded context. Update()
|
// Checkpoint() is called concurrently with Update().
|
||||||
// calls may arrive concurrently.
|
// Checkpoint should reset the current state to the empty
|
||||||
Collect(context.Context, Record, Batcher)
|
// state, in order to begin computing a new delta for the next
|
||||||
|
// collection period.
|
||||||
|
//
|
||||||
|
// After the checkpoint is taken, the current value may be
|
||||||
|
// accessed using by converting to one a suitable interface
|
||||||
|
// types in the `aggregator` sub-package.
|
||||||
|
//
|
||||||
|
// The Context argument originates from the controller that
|
||||||
|
// orchestrates collection.
|
||||||
|
Checkpoint(context.Context, *Descriptor)
|
||||||
|
|
||||||
// Merge combines state from two aggregators into one.
|
// Merge combines the checkpointed state from the argument
|
||||||
Merge(Aggregator, *Descriptor)
|
// aggregator into this aggregator's checkpointed state.
|
||||||
|
// Merge() is called in a single-threaded context, no locking
|
||||||
|
// is required.
|
||||||
|
Merge(Aggregator, *Descriptor) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record is the unit of export, pairing a metric
|
// Exporter handles presentation of the checkpoint of aggregate
|
||||||
// instrument and set of labels.
|
// metrics. This is the final stage of a metrics export pipeline,
|
||||||
type Record interface {
|
// where metric data are formatted for a specific system.
|
||||||
// Descriptor() describes the metric instrument.
|
type Exporter interface {
|
||||||
Descriptor() *Descriptor
|
// Export is called immediately after completing a collection
|
||||||
|
// pass in the SDK.
|
||||||
// Labels() describe the labsels corresponding the
|
//
|
||||||
// aggregation being performed.
|
// The Context comes from the controller that initiated
|
||||||
Labels() []core.KeyValue
|
// collection.
|
||||||
|
//
|
||||||
|
// The CheckpointSet interface refers to the Batcher that just
|
||||||
|
// completed collection.
|
||||||
|
Export(context.Context, CheckpointSet) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kind describes the kind of instrument.
|
// LabelEncoder enables an optimization for export pipelines that use
|
||||||
type Kind int8
|
// text to encode their label sets.
|
||||||
|
//
|
||||||
|
// This interface allows configuring the encoder used in the SDK
|
||||||
|
// and/or the Batcher so that by the time the exporter is called, the
|
||||||
|
// same encoding may be used.
|
||||||
|
//
|
||||||
|
// If none is provided, a default will be used.
|
||||||
|
type LabelEncoder interface {
|
||||||
|
// Encode is called (concurrently) in instrumentation context.
|
||||||
|
// It should return a unique representation of the labels
|
||||||
|
// suitable for the SDK to use as a map key.
|
||||||
|
//
|
||||||
|
// The exported Labels object retains a reference to its
|
||||||
|
// LabelEncoder to determine which encoding was used.
|
||||||
|
//
|
||||||
|
// The expectation is that Exporters with a pre-determined to
|
||||||
|
// syntax for serialized label sets should implement
|
||||||
|
// LabelEncoder, thus avoiding duplicate computation in the
|
||||||
|
// export path.
|
||||||
|
Encode([]core.KeyValue) string
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckpointSet allows a controller to access a complete checkpoint of
|
||||||
|
// aggregated metrics from the Batcher. This is passed to the
|
||||||
|
// Exporter which may then use ForEach to iterate over the collection
|
||||||
|
// of aggregated metrics.
|
||||||
|
type CheckpointSet interface {
|
||||||
|
// ForEach iterates over aggregated checkpoints for all
|
||||||
|
// metrics that were updated during the last collection
|
||||||
|
// period.
|
||||||
|
ForEach(func(Record))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record contains the exported data for a single metric instrument
|
||||||
|
// and label set.
|
||||||
|
type Record struct {
|
||||||
|
descriptor *Descriptor
|
||||||
|
labels Labels
|
||||||
|
aggregator Aggregator
|
||||||
|
}
|
||||||
|
|
||||||
|
// Labels stores complete information about a computed label set,
|
||||||
|
// including the labels in an appropriate order (as defined by the
|
||||||
|
// Batcher). If the batcher does not re-order labels, they are
|
||||||
|
// presented in sorted order by the SDK.
|
||||||
|
type Labels struct {
|
||||||
|
ordered []core.KeyValue
|
||||||
|
encoded string
|
||||||
|
encoder LabelEncoder
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLabels builds a Labels object, consisting of an ordered set of
|
||||||
|
// labels, a unique encoded representation, and the encoder that
|
||||||
|
// produced it.
|
||||||
|
func NewLabels(ordered []core.KeyValue, encoded string, encoder LabelEncoder) Labels {
|
||||||
|
return Labels{
|
||||||
|
ordered: ordered,
|
||||||
|
encoded: encoded,
|
||||||
|
encoder: encoder,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ordered returns the labels in a specified order, according to the
|
||||||
|
// Batcher.
|
||||||
|
func (l Labels) Ordered() []core.KeyValue {
|
||||||
|
return l.ordered
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encoded is a pre-encoded form of the ordered labels.
|
||||||
|
func (l Labels) Encoded() string {
|
||||||
|
return l.encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encoder is the encoder that computed the Encoded() representation.
|
||||||
|
func (l Labels) Encoder() LabelEncoder {
|
||||||
|
return l.encoder
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of labels.
|
||||||
|
func (l Labels) Len() int {
|
||||||
|
return len(l.ordered)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRecord allows Batcher implementations to construct export
|
||||||
|
// records. The Descriptor, Labels, and Aggregator represent
|
||||||
|
// aggregate metric events received over a single collection period.
|
||||||
|
func NewRecord(descriptor *Descriptor, labels Labels, aggregator Aggregator) Record {
|
||||||
|
return Record{
|
||||||
|
descriptor: descriptor,
|
||||||
|
labels: labels,
|
||||||
|
aggregator: aggregator,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggregator returns the checkpointed aggregator. It is safe to
|
||||||
|
// access the checkpointed state without locking.
|
||||||
|
func (r Record) Aggregator() Aggregator {
|
||||||
|
return r.aggregator
|
||||||
|
}
|
||||||
|
|
||||||
|
// Descriptor describes the metric instrument being exported.
|
||||||
|
func (r Record) Descriptor() *Descriptor {
|
||||||
|
return r.descriptor
|
||||||
|
}
|
||||||
|
|
||||||
|
// Labels describes the labels associated with the instrument and the
|
||||||
|
// aggregated data.
|
||||||
|
func (r Record) Labels() Labels {
|
||||||
|
return r.labels
|
||||||
|
}
|
||||||
|
|
||||||
|
// MetricKind describes the kind of instrument.
|
||||||
|
type MetricKind int8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
CounterKind Kind = iota
|
// Counter kind indicates a counter instrument.
|
||||||
|
CounterKind MetricKind = iota
|
||||||
|
|
||||||
|
// Gauge kind indicates a gauge instrument.
|
||||||
GaugeKind
|
GaugeKind
|
||||||
|
|
||||||
|
// Measure kind indicates a measure instrument.
|
||||||
MeasureKind
|
MeasureKind
|
||||||
)
|
)
|
||||||
|
|
||||||
// Descriptor describes a metric instrument to the exporter.
|
// Descriptor describes a metric instrument to the exporter.
|
||||||
|
//
|
||||||
|
// Descriptors are created once per instrument and a pointer to the
|
||||||
|
// descriptor may be used to uniquely identify the instrument in an
|
||||||
|
// exporter.
|
||||||
type Descriptor struct {
|
type Descriptor struct {
|
||||||
name string
|
name string
|
||||||
metricKind Kind
|
metricKind MetricKind
|
||||||
keys []core.Key
|
keys []core.Key
|
||||||
description string
|
description string
|
||||||
unit unit.Unit
|
unit unit.Unit
|
||||||
@ -93,10 +313,14 @@ type Descriptor struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewDescriptor builds a new descriptor, for use by `Meter`
|
// NewDescriptor builds a new descriptor, for use by `Meter`
|
||||||
// implementations to interface with a metric export pipeline.
|
// implementations in constructing new metric instruments.
|
||||||
|
//
|
||||||
|
// Descriptors are created once per instrument and a pointer to the
|
||||||
|
// descriptor may be used to uniquely identify the instrument in an
|
||||||
|
// exporter.
|
||||||
func NewDescriptor(
|
func NewDescriptor(
|
||||||
name string,
|
name string,
|
||||||
metricKind Kind,
|
metricKind MetricKind,
|
||||||
keys []core.Key,
|
keys []core.Key,
|
||||||
description string,
|
description string,
|
||||||
unit unit.Unit,
|
unit unit.Unit,
|
||||||
@ -114,30 +338,51 @@ func NewDescriptor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Name returns the metric instrument's name.
|
||||||
func (d *Descriptor) Name() string {
|
func (d *Descriptor) Name() string {
|
||||||
return d.name
|
return d.name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Descriptor) MetricKind() Kind {
|
// MetricKind returns the kind of instrument: counter, gauge, or
|
||||||
|
// measure.
|
||||||
|
func (d *Descriptor) MetricKind() MetricKind {
|
||||||
return d.metricKind
|
return d.metricKind
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keys returns the recommended keys included in the metric
|
||||||
|
// definition. These keys may be used by a Batcher as a default set
|
||||||
|
// of grouping keys for the metric instrument.
|
||||||
func (d *Descriptor) Keys() []core.Key {
|
func (d *Descriptor) Keys() []core.Key {
|
||||||
return d.keys
|
return d.keys
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Description provides a human-readable description of the metric
|
||||||
|
// instrument.
|
||||||
func (d *Descriptor) Description() string {
|
func (d *Descriptor) Description() string {
|
||||||
return d.description
|
return d.description
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unit describes the units of the metric instrument. Unitless
|
||||||
|
// metrics return the empty string.
|
||||||
func (d *Descriptor) Unit() unit.Unit {
|
func (d *Descriptor) Unit() unit.Unit {
|
||||||
return d.unit
|
return d.unit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NumberKind returns whether this instrument is declared over int64
|
||||||
|
// or a float64 values.
|
||||||
func (d *Descriptor) NumberKind() core.NumberKind {
|
func (d *Descriptor) NumberKind() core.NumberKind {
|
||||||
return d.numberKind
|
return d.numberKind
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Alternate returns true when the non-default behavior of the
|
||||||
|
// instrument was selected. It returns true if:
|
||||||
|
//
|
||||||
|
// - A counter instrument is non-monotonic
|
||||||
|
// - A gauge instrument is monotonic
|
||||||
|
// - A measure instrument is non-absolute
|
||||||
|
//
|
||||||
|
// TODO: Consider renaming this method, or expanding to provide
|
||||||
|
// kind-specific tests (e.g., Monotonic(), Absolute()).
|
||||||
func (d *Descriptor) Alternate() bool {
|
func (d *Descriptor) Alternate() bool {
|
||||||
return d.alternate
|
return d.alternate
|
||||||
}
|
}
|
||||||
|
25
sdk/export/metric/metrickind_string.go
Normal file
25
sdk/export/metric/metrickind_string.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// Code generated by "stringer -type=MetricKind"; DO NOT EDIT.
|
||||||
|
|
||||||
|
package export
|
||||||
|
|
||||||
|
import "strconv"
|
||||||
|
|
||||||
|
func _() {
|
||||||
|
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||||
|
// Re-run the stringer command to generate them again.
|
||||||
|
var x [1]struct{}
|
||||||
|
_ = x[CounterKind-0]
|
||||||
|
_ = x[GaugeKind-1]
|
||||||
|
_ = x[MeasureKind-2]
|
||||||
|
}
|
||||||
|
|
||||||
|
const _MetricKind_name = "CounterKindGaugeKindMeasureKind"
|
||||||
|
|
||||||
|
var _MetricKind_index = [...]uint8{0, 11, 20, 31}
|
||||||
|
|
||||||
|
func (i MetricKind) String() string {
|
||||||
|
if i < 0 || i >= MetricKind(len(_MetricKind_index)-1) {
|
||||||
|
return "MetricKind(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||||
|
}
|
||||||
|
return _MetricKind_name[_MetricKind_index[i]:_MetricKind_index[i+1]]
|
||||||
|
}
|
@ -23,7 +23,7 @@ import (
|
|||||||
|
|
||||||
"go.opentelemetry.io/otel/api/core"
|
"go.opentelemetry.io/otel/api/core"
|
||||||
export "go.opentelemetry.io/otel/sdk/export/metric"
|
export "go.opentelemetry.io/otel/sdk/export/metric"
|
||||||
"go.opentelemetry.io/otel/sdk/metric/aggregator"
|
"go.opentelemetry.io/otel/sdk/export/metric/aggregator"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@ -38,44 +38,55 @@ type (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var _ export.Aggregator = &Aggregator{}
|
var _ export.Aggregator = &Aggregator{}
|
||||||
|
var _ aggregator.MaxSumCount = &Aggregator{}
|
||||||
|
var _ aggregator.Distribution = &Aggregator{}
|
||||||
|
|
||||||
|
// New returns a new array aggregator, which aggregates recorded
|
||||||
|
// measurements by storing them in an array. This type uses a mutex
|
||||||
|
// for Update() and Checkpoint() concurrency.
|
||||||
func New() *Aggregator {
|
func New() *Aggregator {
|
||||||
return &Aggregator{}
|
return &Aggregator{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sum returns the sum of the checkpoint.
|
// Sum returns the sum of values in the checkpoint.
|
||||||
func (c *Aggregator) Sum() core.Number {
|
func (c *Aggregator) Sum() (core.Number, error) {
|
||||||
return c.ckptSum
|
return c.ckptSum, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count returns the count of the checkpoint.
|
// Count returns the number of values in the checkpoint.
|
||||||
func (c *Aggregator) Count() int64 {
|
func (c *Aggregator) Count() (int64, error) {
|
||||||
return int64(len(c.checkpoint))
|
return int64(len(c.checkpoint)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Max returns the max of the checkpoint.
|
// Max returns the maximum value in the checkpoint.
|
||||||
func (c *Aggregator) Max() (core.Number, error) {
|
func (c *Aggregator) Max() (core.Number, error) {
|
||||||
return c.checkpoint.Quantile(1)
|
return c.checkpoint.Quantile(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Min returns the min of the checkpoint.
|
// Min returns the mininum value in the checkpoint.
|
||||||
func (c *Aggregator) Min() (core.Number, error) {
|
func (c *Aggregator) Min() (core.Number, error) {
|
||||||
return c.checkpoint.Quantile(0)
|
return c.checkpoint.Quantile(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quantile returns the estimated quantile of the checkpoint.
|
// Quantile returns the estimated quantile of data in the checkpoint.
|
||||||
|
// It is an error if `q` is less than 0 or greated than 1.
|
||||||
func (c *Aggregator) Quantile(q float64) (core.Number, error) {
|
func (c *Aggregator) Quantile(q float64) (core.Number, error) {
|
||||||
return c.checkpoint.Quantile(q)
|
return c.checkpoint.Quantile(q)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Aggregator) Collect(ctx context.Context, rec export.Record, exp export.Batcher) {
|
// Checkpoint saves the current state and resets the current state to
|
||||||
|
// the empty set, taking a lock to prevent concurrent Update() calls.
|
||||||
|
func (c *Aggregator) Checkpoint(ctx context.Context, desc *export.Descriptor) {
|
||||||
c.lock.Lock()
|
c.lock.Lock()
|
||||||
c.checkpoint, c.current = c.current, nil
|
c.checkpoint, c.current = c.current, nil
|
||||||
c.lock.Unlock()
|
c.lock.Unlock()
|
||||||
|
|
||||||
desc := rec.Descriptor()
|
|
||||||
kind := desc.NumberKind()
|
kind := desc.NumberKind()
|
||||||
|
|
||||||
|
// TODO: This sort should be done lazily, only when quantiles
|
||||||
|
// are requested. The SDK specification says you can use this
|
||||||
|
// aggregator to simply list values in the order they were
|
||||||
|
// received as an alternative to requesting quantile information.
|
||||||
c.sort(kind)
|
c.sort(kind)
|
||||||
|
|
||||||
c.ckptSum = core.Number(0)
|
c.ckptSum = core.Number(0)
|
||||||
@ -83,39 +94,28 @@ func (c *Aggregator) Collect(ctx context.Context, rec export.Record, exp export.
|
|||||||
for _, v := range c.checkpoint {
|
for _, v := range c.checkpoint {
|
||||||
c.ckptSum.AddNumber(kind, v)
|
c.ckptSum.AddNumber(kind, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
exp.Export(ctx, rec, c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Aggregator) Update(_ context.Context, number core.Number, rec export.Record) {
|
// Update adds the recorded measurement to the current data set.
|
||||||
desc := rec.Descriptor()
|
// Update takes a lock to prevent concurrent Update() and Checkpoint()
|
||||||
kind := desc.NumberKind()
|
// calls.
|
||||||
|
func (c *Aggregator) Update(_ context.Context, number core.Number, desc *export.Descriptor) error {
|
||||||
if kind == core.Float64NumberKind && math.IsNaN(number.AsFloat64()) {
|
|
||||||
// TODO warn
|
|
||||||
// NOTE: add this to the specification.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !desc.Alternate() && number.IsNegative(kind) {
|
|
||||||
// TODO warn
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.lock.Lock()
|
c.lock.Lock()
|
||||||
c.current = append(c.current, number)
|
c.current = append(c.current, number)
|
||||||
c.lock.Unlock()
|
c.lock.Unlock()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Aggregator) Merge(oa export.Aggregator, desc *export.Descriptor) {
|
// Merge combines two data sets into one.
|
||||||
|
func (c *Aggregator) Merge(oa export.Aggregator, desc *export.Descriptor) error {
|
||||||
o, _ := oa.(*Aggregator)
|
o, _ := oa.(*Aggregator)
|
||||||
if o == nil {
|
if o == nil {
|
||||||
// TODO warn
|
return aggregator.NewInconsistentMergeError(c, oa)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.ckptSum.AddNumber(desc.NumberKind(), o.ckptSum)
|
c.ckptSum.AddNumber(desc.NumberKind(), o.ckptSum)
|
||||||
c.checkpoint = combine(c.checkpoint, o.checkpoint, desc.NumberKind())
|
c.checkpoint = combine(c.checkpoint, o.checkpoint, desc.NumberKind())
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Aggregator) sort(kind core.NumberKind) {
|
func (c *Aggregator) sort(kind core.NumberKind) {
|
||||||
@ -166,7 +166,8 @@ func (p *Points) Swap(i, j int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Quantile returns the least X such that Pr(x<X)>=q, where X is an
|
// Quantile returns the least X such that Pr(x<X)>=q, where X is an
|
||||||
// element of the data set.
|
// element of the data set. This uses the "Nearest-Rank" definition
|
||||||
|
// of a quantile.
|
||||||
func (p *Points) Quantile(q float64) (core.Number, error) {
|
func (p *Points) Quantile(q float64) (core.Number, error) {
|
||||||
if len(*p) == 0 {
|
if len(*p) == 0 {
|
||||||
return core.Number(0), aggregator.ErrEmptyDataSet
|
return core.Number(0), aggregator.ErrEmptyDataSet
|
||||||
@ -182,9 +183,6 @@ func (p *Points) Quantile(q float64) (core.Number, error) {
|
|||||||
return (*p)[len(*p)-1], nil
|
return (*p)[len(*p)-1], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: There's no interpolation being done here. There are
|
|
||||||
// many definitions for "quantile", some interpolate, some do
|
|
||||||
// not. What is expected?
|
|
||||||
position := float64(len(*p)-1) * q
|
position := float64(len(*p)-1) * q
|
||||||
ceil := int(math.Ceil(position))
|
ceil := int(math.Ceil(position))
|
||||||
return (*p)[ceil], nil
|
return (*p)[ceil], nil
|
||||||
|
@ -24,7 +24,7 @@ import (
|
|||||||
|
|
||||||
"go.opentelemetry.io/otel/api/core"
|
"go.opentelemetry.io/otel/api/core"
|
||||||
export "go.opentelemetry.io/otel/sdk/export/metric"
|
export "go.opentelemetry.io/otel/sdk/export/metric"
|
||||||
"go.opentelemetry.io/otel/sdk/metric/aggregator"
|
"go.opentelemetry.io/otel/sdk/export/metric/aggregator"
|
||||||
"go.opentelemetry.io/otel/sdk/metric/aggregator/test"
|
"go.opentelemetry.io/otel/sdk/metric/aggregator/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -34,9 +34,7 @@ type updateTest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ut *updateTest) run(t *testing.T, profile test.Profile) {
|
func (ut *updateTest) run(t *testing.T, profile test.Profile) {
|
||||||
ctx := context.Background()
|
descriptor := test.NewAggregatorTest(export.MeasureKind, profile.NumberKind, !ut.absolute)
|
||||||
|
|
||||||
batcher, record := test.NewAggregatorTest(export.MeasureKind, profile.NumberKind, !ut.absolute)
|
|
||||||
|
|
||||||
agg := New()
|
agg := New()
|
||||||
|
|
||||||
@ -45,25 +43,30 @@ func (ut *updateTest) run(t *testing.T, profile test.Profile) {
|
|||||||
for i := 0; i < ut.count; i++ {
|
for i := 0; i < ut.count; i++ {
|
||||||
x := profile.Random(+1)
|
x := profile.Random(+1)
|
||||||
all.Append(x)
|
all.Append(x)
|
||||||
agg.Update(ctx, x, record)
|
test.CheckedUpdate(t, agg, x, descriptor)
|
||||||
|
|
||||||
if !ut.absolute {
|
if !ut.absolute {
|
||||||
y := profile.Random(-1)
|
y := profile.Random(-1)
|
||||||
all.Append(y)
|
all.Append(y)
|
||||||
agg.Update(ctx, y, record)
|
test.CheckedUpdate(t, agg, y, descriptor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
agg.Collect(ctx, record, batcher)
|
ctx := context.Background()
|
||||||
|
agg.Checkpoint(ctx, descriptor)
|
||||||
|
|
||||||
all.Sort()
|
all.Sort()
|
||||||
|
|
||||||
|
sum, err := agg.Sum()
|
||||||
require.InEpsilon(t,
|
require.InEpsilon(t,
|
||||||
all.Sum().CoerceToFloat64(profile.NumberKind),
|
all.Sum().CoerceToFloat64(profile.NumberKind),
|
||||||
agg.Sum().CoerceToFloat64(profile.NumberKind),
|
sum.CoerceToFloat64(profile.NumberKind),
|
||||||
0.0000001,
|
0.0000001,
|
||||||
"Same sum - absolute")
|
"Same sum - absolute")
|
||||||
require.Equal(t, all.Count(), agg.Count(), "Same count - absolute")
|
require.Nil(t, err)
|
||||||
|
count, err := agg.Count()
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Equal(t, all.Count(), count, "Same count - absolute")
|
||||||
|
|
||||||
min, err := agg.Min()
|
min, err := agg.Min()
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
@ -106,7 +109,7 @@ type mergeTest struct {
|
|||||||
func (mt *mergeTest) run(t *testing.T, profile test.Profile) {
|
func (mt *mergeTest) run(t *testing.T, profile test.Profile) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
batcher, record := test.NewAggregatorTest(export.MeasureKind, profile.NumberKind, !mt.absolute)
|
descriptor := test.NewAggregatorTest(export.MeasureKind, profile.NumberKind, !mt.absolute)
|
||||||
|
|
||||||
agg1 := New()
|
agg1 := New()
|
||||||
agg2 := New()
|
agg2 := New()
|
||||||
@ -116,36 +119,40 @@ func (mt *mergeTest) run(t *testing.T, profile test.Profile) {
|
|||||||
for i := 0; i < mt.count; i++ {
|
for i := 0; i < mt.count; i++ {
|
||||||
x1 := profile.Random(+1)
|
x1 := profile.Random(+1)
|
||||||
all.Append(x1)
|
all.Append(x1)
|
||||||
agg1.Update(ctx, x1, record)
|
test.CheckedUpdate(t, agg1, x1, descriptor)
|
||||||
|
|
||||||
x2 := profile.Random(+1)
|
x2 := profile.Random(+1)
|
||||||
all.Append(x2)
|
all.Append(x2)
|
||||||
agg2.Update(ctx, x2, record)
|
test.CheckedUpdate(t, agg2, x2, descriptor)
|
||||||
|
|
||||||
if !mt.absolute {
|
if !mt.absolute {
|
||||||
y1 := profile.Random(-1)
|
y1 := profile.Random(-1)
|
||||||
all.Append(y1)
|
all.Append(y1)
|
||||||
agg1.Update(ctx, y1, record)
|
test.CheckedUpdate(t, agg1, y1, descriptor)
|
||||||
|
|
||||||
y2 := profile.Random(-1)
|
y2 := profile.Random(-1)
|
||||||
all.Append(y2)
|
all.Append(y2)
|
||||||
agg2.Update(ctx, y2, record)
|
test.CheckedUpdate(t, agg2, y2, descriptor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
agg1.Collect(ctx, record, batcher)
|
agg1.Checkpoint(ctx, descriptor)
|
||||||
agg2.Collect(ctx, record, batcher)
|
agg2.Checkpoint(ctx, descriptor)
|
||||||
|
|
||||||
agg1.Merge(agg2, record.Descriptor())
|
test.CheckedMerge(t, agg1, agg2, descriptor)
|
||||||
|
|
||||||
all.Sort()
|
all.Sort()
|
||||||
|
|
||||||
|
sum, err := agg1.Sum()
|
||||||
require.InEpsilon(t,
|
require.InEpsilon(t,
|
||||||
all.Sum().CoerceToFloat64(profile.NumberKind),
|
all.Sum().CoerceToFloat64(profile.NumberKind),
|
||||||
agg1.Sum().CoerceToFloat64(profile.NumberKind),
|
sum.CoerceToFloat64(profile.NumberKind),
|
||||||
0.0000001,
|
0.0000001,
|
||||||
"Same sum - absolute")
|
"Same sum - absolute")
|
||||||
require.Equal(t, all.Count(), agg1.Count(), "Same count - absolute")
|
require.Nil(t, err)
|
||||||
|
count, err := agg1.Count()
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Equal(t, all.Count(), count, "Same count - absolute")
|
||||||
|
|
||||||
min, err := agg1.Min()
|
min, err := agg1.Min()
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
@ -198,16 +205,18 @@ func TestArrayErrors(t *testing.T) {
|
|||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
batcher, record := test.NewAggregatorTest(export.MeasureKind, profile.NumberKind, false)
|
descriptor := test.NewAggregatorTest(export.MeasureKind, profile.NumberKind, false)
|
||||||
|
|
||||||
agg.Update(ctx, core.Number(0), record)
|
test.CheckedUpdate(t, agg, core.Number(0), descriptor)
|
||||||
|
|
||||||
if profile.NumberKind == core.Float64NumberKind {
|
if profile.NumberKind == core.Float64NumberKind {
|
||||||
agg.Update(ctx, core.NewFloat64Number(math.NaN()), record)
|
test.CheckedUpdate(t, agg, core.NewFloat64Number(math.NaN()), descriptor)
|
||||||
}
|
}
|
||||||
agg.Collect(ctx, record, batcher)
|
agg.Checkpoint(ctx, descriptor)
|
||||||
|
|
||||||
require.Equal(t, int64(1), agg.Count(), "NaN value was not counted")
|
count, err := agg.Count()
|
||||||
|
require.Equal(t, int64(1), count, "NaN value was not counted")
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
num, err := agg.Quantile(0)
|
num, err := agg.Quantile(0)
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
@ -226,7 +235,7 @@ func TestArrayErrors(t *testing.T) {
|
|||||||
func TestArrayFloat64(t *testing.T) {
|
func TestArrayFloat64(t *testing.T) {
|
||||||
for _, absolute := range []bool{false, true} {
|
for _, absolute := range []bool{false, true} {
|
||||||
t.Run(fmt.Sprint("Absolute=", absolute), func(t *testing.T) {
|
t.Run(fmt.Sprint("Absolute=", absolute), func(t *testing.T) {
|
||||||
batcher, record := test.NewAggregatorTest(export.MeasureKind, core.Float64NumberKind, !absolute)
|
descriptor := test.NewAggregatorTest(export.MeasureKind, core.Float64NumberKind, !absolute)
|
||||||
|
|
||||||
fpsf := func(sign int) []float64 {
|
fpsf := func(sign int) []float64 {
|
||||||
// Check behavior of a bunch of odd floating
|
// Check behavior of a bunch of odd floating
|
||||||
@ -263,23 +272,27 @@ func TestArrayFloat64(t *testing.T) {
|
|||||||
|
|
||||||
for _, f := range fpsf(1) {
|
for _, f := range fpsf(1) {
|
||||||
all.Append(core.NewFloat64Number(f))
|
all.Append(core.NewFloat64Number(f))
|
||||||
agg.Update(ctx, core.NewFloat64Number(f), record)
|
test.CheckedUpdate(t, agg, core.NewFloat64Number(f), descriptor)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !absolute {
|
if !absolute {
|
||||||
for _, f := range fpsf(-1) {
|
for _, f := range fpsf(-1) {
|
||||||
all.Append(core.NewFloat64Number(f))
|
all.Append(core.NewFloat64Number(f))
|
||||||
agg.Update(ctx, core.NewFloat64Number(f), record)
|
test.CheckedUpdate(t, agg, core.NewFloat64Number(f), descriptor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
agg.Collect(ctx, record, batcher)
|
agg.Checkpoint(ctx, descriptor)
|
||||||
|
|
||||||
all.Sort()
|
all.Sort()
|
||||||
|
|
||||||
require.InEpsilon(t, all.Sum().AsFloat64(), agg.Sum().AsFloat64(), 0.0000001, "Same sum")
|
sum, err := agg.Sum()
|
||||||
|
require.InEpsilon(t, all.Sum().AsFloat64(), sum.AsFloat64(), 0.0000001, "Same sum")
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
require.Equal(t, all.Count(), agg.Count(), "Same count")
|
count, err := agg.Count()
|
||||||
|
require.Equal(t, all.Count(), count, "Same count")
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
min, err := agg.Min()
|
min, err := agg.Min()
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
|
@ -19,6 +19,7 @@ import (
|
|||||||
|
|
||||||
"go.opentelemetry.io/otel/api/core"
|
"go.opentelemetry.io/otel/api/core"
|
||||||
export "go.opentelemetry.io/otel/sdk/export/metric"
|
export "go.opentelemetry.io/otel/sdk/export/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/export/metric/aggregator"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Aggregator aggregates counter events.
|
// Aggregator aggregates counter events.
|
||||||
@ -26,47 +27,44 @@ type Aggregator struct {
|
|||||||
// current holds current increments to this counter record
|
// current holds current increments to this counter record
|
||||||
current core.Number
|
current core.Number
|
||||||
|
|
||||||
// checkpoint is a temporary used during Collect()
|
// checkpoint is a temporary used during Checkpoint()
|
||||||
checkpoint core.Number
|
checkpoint core.Number
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ export.Aggregator = &Aggregator{}
|
var _ export.Aggregator = &Aggregator{}
|
||||||
|
var _ aggregator.Sum = &Aggregator{}
|
||||||
|
|
||||||
// New returns a new counter aggregator. This aggregator computes an
|
// New returns a new counter aggregator implemented by atomic
|
||||||
// atomic sum.
|
// operations. This aggregator implements the aggregator.Sum
|
||||||
|
// export interface.
|
||||||
func New() *Aggregator {
|
func New() *Aggregator {
|
||||||
return &Aggregator{}
|
return &Aggregator{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AsNumber returns the accumulated count as an int64.
|
// Sum returns the last-checkpointed sum. This will never return an
|
||||||
func (c *Aggregator) AsNumber() core.Number {
|
// error.
|
||||||
return c.checkpoint.AsNumber()
|
func (c *Aggregator) Sum() (core.Number, error) {
|
||||||
|
return c.checkpoint, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect checkpoints the current value (atomically) and exports it.
|
// Checkpoint atomically saves the current value and resets the
|
||||||
func (c *Aggregator) Collect(ctx context.Context, rec export.Record, exp export.Batcher) {
|
// current sum to zero.
|
||||||
|
func (c *Aggregator) Checkpoint(ctx context.Context, _ *export.Descriptor) {
|
||||||
c.checkpoint = c.current.SwapNumberAtomic(core.Number(0))
|
c.checkpoint = c.current.SwapNumberAtomic(core.Number(0))
|
||||||
|
|
||||||
exp.Export(ctx, rec, c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update modifies the current value (atomically) for later export.
|
// Update atomically adds to the current value.
|
||||||
func (c *Aggregator) Update(_ context.Context, number core.Number, rec export.Record) {
|
func (c *Aggregator) Update(_ context.Context, number core.Number, desc *export.Descriptor) error {
|
||||||
desc := rec.Descriptor()
|
c.current.AddNumberAtomic(desc.NumberKind(), number)
|
||||||
kind := desc.NumberKind()
|
return nil
|
||||||
if !desc.Alternate() && number.IsNegative(kind) {
|
|
||||||
// TODO warn
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.current.AddNumberAtomic(kind, number)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Aggregator) Merge(oa export.Aggregator, desc *export.Descriptor) {
|
// Merge combines two counters by adding their sums.
|
||||||
|
func (c *Aggregator) Merge(oa export.Aggregator, desc *export.Descriptor) error {
|
||||||
o, _ := oa.(*Aggregator)
|
o, _ := oa.(*Aggregator)
|
||||||
if o == nil {
|
if o == nil {
|
||||||
// TODO warn
|
return aggregator.NewInconsistentMergeError(c, oa)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
c.checkpoint.AddNumber(desc.NumberKind(), o.checkpoint)
|
c.checkpoint.AddNumber(desc.NumberKind(), o.checkpoint)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -33,18 +33,20 @@ func TestCounterMonotonic(t *testing.T) {
|
|||||||
test.RunProfiles(t, func(t *testing.T, profile test.Profile) {
|
test.RunProfiles(t, func(t *testing.T, profile test.Profile) {
|
||||||
agg := New()
|
agg := New()
|
||||||
|
|
||||||
batcher, record := test.NewAggregatorTest(export.CounterKind, profile.NumberKind, false)
|
descriptor := test.NewAggregatorTest(export.CounterKind, profile.NumberKind, false)
|
||||||
|
|
||||||
sum := core.Number(0)
|
sum := core.Number(0)
|
||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
x := profile.Random(+1)
|
x := profile.Random(+1)
|
||||||
sum.AddNumber(profile.NumberKind, x)
|
sum.AddNumber(profile.NumberKind, x)
|
||||||
agg.Update(ctx, x, record)
|
test.CheckedUpdate(t, agg, x, descriptor)
|
||||||
}
|
}
|
||||||
|
|
||||||
agg.Collect(ctx, record, batcher)
|
agg.Checkpoint(ctx, descriptor)
|
||||||
|
|
||||||
require.Equal(t, sum, agg.AsNumber(), "Same sum - monotonic")
|
asum, err := agg.Sum()
|
||||||
|
require.Equal(t, sum, asum, "Same sum - monotonic")
|
||||||
|
require.Nil(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,17 +56,19 @@ func TestCounterMonotonicNegative(t *testing.T) {
|
|||||||
test.RunProfiles(t, func(t *testing.T, profile test.Profile) {
|
test.RunProfiles(t, func(t *testing.T, profile test.Profile) {
|
||||||
agg := New()
|
agg := New()
|
||||||
|
|
||||||
batcher, record := test.NewAggregatorTest(export.CounterKind, profile.NumberKind, false)
|
descriptor := test.NewAggregatorTest(export.CounterKind, profile.NumberKind, false)
|
||||||
|
|
||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
agg.Update(ctx, profile.Random(-1), record)
|
test.CheckedUpdate(t, agg, profile.Random(-1), descriptor)
|
||||||
}
|
}
|
||||||
|
|
||||||
sum := profile.Random(+1)
|
sum := profile.Random(+1)
|
||||||
agg.Update(ctx, sum, record)
|
test.CheckedUpdate(t, agg, sum, descriptor)
|
||||||
agg.Collect(ctx, record, batcher)
|
agg.Checkpoint(ctx, descriptor)
|
||||||
|
|
||||||
require.Equal(t, sum, agg.AsNumber(), "Same sum - monotonic")
|
asum, err := agg.Sum()
|
||||||
|
require.Equal(t, sum, asum, "Same sum - monotonic")
|
||||||
|
require.Nil(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,7 +78,7 @@ func TestCounterNonMonotonic(t *testing.T) {
|
|||||||
test.RunProfiles(t, func(t *testing.T, profile test.Profile) {
|
test.RunProfiles(t, func(t *testing.T, profile test.Profile) {
|
||||||
agg := New()
|
agg := New()
|
||||||
|
|
||||||
batcher, record := test.NewAggregatorTest(export.CounterKind, profile.NumberKind, true)
|
descriptor := test.NewAggregatorTest(export.CounterKind, profile.NumberKind, true)
|
||||||
|
|
||||||
sum := core.Number(0)
|
sum := core.Number(0)
|
||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
@ -82,13 +86,15 @@ func TestCounterNonMonotonic(t *testing.T) {
|
|||||||
y := profile.Random(-1)
|
y := profile.Random(-1)
|
||||||
sum.AddNumber(profile.NumberKind, x)
|
sum.AddNumber(profile.NumberKind, x)
|
||||||
sum.AddNumber(profile.NumberKind, y)
|
sum.AddNumber(profile.NumberKind, y)
|
||||||
agg.Update(ctx, x, record)
|
test.CheckedUpdate(t, agg, x, descriptor)
|
||||||
agg.Update(ctx, y, record)
|
test.CheckedUpdate(t, agg, y, descriptor)
|
||||||
}
|
}
|
||||||
|
|
||||||
agg.Collect(ctx, record, batcher)
|
agg.Checkpoint(ctx, descriptor)
|
||||||
|
|
||||||
require.Equal(t, sum, agg.AsNumber(), "Same sum - monotonic")
|
asum, err := agg.Sum()
|
||||||
|
require.Equal(t, sum, asum, "Same sum - monotonic")
|
||||||
|
require.Nil(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,23 +105,25 @@ func TestCounterMerge(t *testing.T) {
|
|||||||
agg1 := New()
|
agg1 := New()
|
||||||
agg2 := New()
|
agg2 := New()
|
||||||
|
|
||||||
batcher, record := test.NewAggregatorTest(export.CounterKind, profile.NumberKind, false)
|
descriptor := test.NewAggregatorTest(export.CounterKind, profile.NumberKind, false)
|
||||||
|
|
||||||
sum := core.Number(0)
|
sum := core.Number(0)
|
||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
x := profile.Random(+1)
|
x := profile.Random(+1)
|
||||||
sum.AddNumber(profile.NumberKind, x)
|
sum.AddNumber(profile.NumberKind, x)
|
||||||
agg1.Update(ctx, x, record)
|
test.CheckedUpdate(t, agg1, x, descriptor)
|
||||||
agg2.Update(ctx, x, record)
|
test.CheckedUpdate(t, agg2, x, descriptor)
|
||||||
}
|
}
|
||||||
|
|
||||||
agg1.Collect(ctx, record, batcher)
|
agg1.Checkpoint(ctx, descriptor)
|
||||||
agg2.Collect(ctx, record, batcher)
|
agg2.Checkpoint(ctx, descriptor)
|
||||||
|
|
||||||
agg1.Merge(agg2, record.Descriptor())
|
test.CheckedMerge(t, agg1, agg2, descriptor)
|
||||||
|
|
||||||
sum.AddNumber(record.Descriptor().NumberKind(), sum)
|
sum.AddNumber(descriptor.NumberKind(), sum)
|
||||||
|
|
||||||
require.Equal(t, sum, agg1.AsNumber(), "Same sum - monotonic")
|
asum, err := agg1.Sum()
|
||||||
|
require.Equal(t, sum, asum, "Same sum - monotonic")
|
||||||
|
require.Nil(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -23,22 +23,27 @@ import (
|
|||||||
"go.opentelemetry.io/otel/api/core"
|
"go.opentelemetry.io/otel/api/core"
|
||||||
|
|
||||||
export "go.opentelemetry.io/otel/sdk/export/metric"
|
export "go.opentelemetry.io/otel/sdk/export/metric"
|
||||||
"go.opentelemetry.io/otel/sdk/metric/aggregator"
|
"go.opentelemetry.io/otel/sdk/export/metric/aggregator"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Config is an alias for the underlying DDSketch config object.
|
||||||
|
type Config = sdk.Config
|
||||||
|
|
||||||
// Aggregator aggregates measure events.
|
// Aggregator aggregates measure events.
|
||||||
type Aggregator struct {
|
type Aggregator struct {
|
||||||
lock sync.Mutex
|
lock sync.Mutex
|
||||||
cfg *sdk.Config
|
cfg *Config
|
||||||
kind core.NumberKind
|
kind core.NumberKind
|
||||||
current *sdk.DDSketch
|
current *sdk.DDSketch
|
||||||
checkpoint *sdk.DDSketch
|
checkpoint *sdk.DDSketch
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ export.Aggregator = &Aggregator{}
|
var _ export.Aggregator = &Aggregator{}
|
||||||
|
var _ aggregator.MaxSumCount = &Aggregator{}
|
||||||
|
var _ aggregator.Distribution = &Aggregator{}
|
||||||
|
|
||||||
// New returns a new DDSketch aggregator.
|
// New returns a new DDSketch aggregator.
|
||||||
func New(cfg *sdk.Config, desc *export.Descriptor) *Aggregator {
|
func New(cfg *Config, desc *export.Descriptor) *Aggregator {
|
||||||
return &Aggregator{
|
return &Aggregator{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
kind: desc.NumberKind(),
|
kind: desc.NumberKind(),
|
||||||
@ -48,35 +53,39 @@ func New(cfg *sdk.Config, desc *export.Descriptor) *Aggregator {
|
|||||||
|
|
||||||
// NewDefaultConfig returns a new, default DDSketch config.
|
// NewDefaultConfig returns a new, default DDSketch config.
|
||||||
//
|
//
|
||||||
// TODO: The Config constructor should probably set minValue to -Inf
|
// TODO: Should the Config constructor set minValue to -Inf to
|
||||||
// to aggregate metrics with absolute=false. This requires providing values
|
// when the descriptor has absolute=false? This requires providing
|
||||||
// for alpha and maxNumBins
|
// values for alpha and maxNumBins, apparently.
|
||||||
func NewDefaultConfig() *sdk.Config {
|
func NewDefaultConfig() *Config {
|
||||||
return sdk.NewDefaultConfig()
|
return sdk.NewDefaultConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sum returns the sum of the checkpoint.
|
// Sum returns the sum of values in the checkpoint.
|
||||||
func (c *Aggregator) Sum() core.Number {
|
func (c *Aggregator) Sum() (core.Number, error) {
|
||||||
return c.toNumber(c.checkpoint.Sum())
|
return c.toNumber(c.checkpoint.Sum()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count returns the count of the checkpoint.
|
// Count returns the number of values in the checkpoint.
|
||||||
func (c *Aggregator) Count() int64 {
|
func (c *Aggregator) Count() (int64, error) {
|
||||||
return c.checkpoint.Count()
|
return c.checkpoint.Count(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Max returns the max of the checkpoint.
|
// Max returns the maximum value in the checkpoint.
|
||||||
func (c *Aggregator) Max() (core.Number, error) {
|
func (c *Aggregator) Max() (core.Number, error) {
|
||||||
return c.Quantile(1)
|
return c.Quantile(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Min returns the min of the checkpoint.
|
// Min returns the mininum value in the checkpoint.
|
||||||
func (c *Aggregator) Min() (core.Number, error) {
|
func (c *Aggregator) Min() (core.Number, error) {
|
||||||
return c.Quantile(0)
|
return c.Quantile(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quantile returns the estimated quantile of the checkpoint.
|
// Quantile returns the estimated quantile of data in the checkpoint.
|
||||||
|
// It is an error if `q` is less than 0 or greated than 1.
|
||||||
func (c *Aggregator) Quantile(q float64) (core.Number, error) {
|
func (c *Aggregator) Quantile(q float64) (core.Number, error) {
|
||||||
|
if c.checkpoint.Count() == 0 {
|
||||||
|
return core.Number(0), aggregator.ErrEmptyDataSet
|
||||||
|
}
|
||||||
f := c.checkpoint.Quantile(q)
|
f := c.checkpoint.Quantile(q)
|
||||||
if math.IsNaN(f) {
|
if math.IsNaN(f) {
|
||||||
return core.Number(0), aggregator.ErrInvalidQuantile
|
return core.Number(0), aggregator.ErrInvalidQuantile
|
||||||
@ -91,41 +100,34 @@ func (c *Aggregator) toNumber(f float64) core.Number {
|
|||||||
return core.NewInt64Number(int64(f))
|
return core.NewInt64Number(int64(f))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect checkpoints the current value (atomically) and exports it.
|
// Checkpoint saves the current state and resets the current state to
|
||||||
func (c *Aggregator) Collect(ctx context.Context, rec export.Record, exp export.Batcher) {
|
// the empty set, taking a lock to prevent concurrent Update() calls.
|
||||||
|
func (c *Aggregator) Checkpoint(ctx context.Context, _ *export.Descriptor) {
|
||||||
replace := sdk.NewDDSketch(c.cfg)
|
replace := sdk.NewDDSketch(c.cfg)
|
||||||
|
|
||||||
c.lock.Lock()
|
c.lock.Lock()
|
||||||
c.checkpoint = c.current
|
c.checkpoint = c.current
|
||||||
c.current = replace
|
c.current = replace
|
||||||
c.lock.Unlock()
|
c.lock.Unlock()
|
||||||
|
|
||||||
if c.checkpoint.Count() != 0 {
|
|
||||||
exp.Export(ctx, rec, c)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update modifies the current value (atomically) for later export.
|
// Update adds the recorded measurement to the current data set.
|
||||||
func (c *Aggregator) Update(_ context.Context, number core.Number, rec export.Record) {
|
// Update takes a lock to prevent concurrent Update() and Checkpoint()
|
||||||
desc := rec.Descriptor()
|
// calls.
|
||||||
kind := desc.NumberKind()
|
func (c *Aggregator) Update(_ context.Context, number core.Number, desc *export.Descriptor) error {
|
||||||
|
|
||||||
if !desc.Alternate() && number.IsNegative(kind) {
|
|
||||||
// TODO warn
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.lock.Lock()
|
c.lock.Lock()
|
||||||
defer c.lock.Unlock()
|
defer c.lock.Unlock()
|
||||||
c.current.Add(number.CoerceToFloat64(kind))
|
c.current.Add(number.CoerceToFloat64(desc.NumberKind()))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Aggregator) Merge(oa export.Aggregator, d *export.Descriptor) {
|
// Merge combines two sketches into one.
|
||||||
|
func (c *Aggregator) Merge(oa export.Aggregator, d *export.Descriptor) error {
|
||||||
o, _ := oa.(*Aggregator)
|
o, _ := oa.(*Aggregator)
|
||||||
if o == nil {
|
if o == nil {
|
||||||
// TODO warn
|
return aggregator.NewInconsistentMergeError(c, oa)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.checkpoint.Merge(o.checkpoint)
|
c.checkpoint.Merge(o.checkpoint)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -34,32 +34,37 @@ type updateTest struct {
|
|||||||
func (ut *updateTest) run(t *testing.T, profile test.Profile) {
|
func (ut *updateTest) run(t *testing.T, profile test.Profile) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
batcher, record := test.NewAggregatorTest(export.MeasureKind, profile.NumberKind, !ut.absolute)
|
descriptor := test.NewAggregatorTest(export.MeasureKind, profile.NumberKind, !ut.absolute)
|
||||||
agg := New(NewDefaultConfig(), record.Descriptor())
|
agg := New(NewDefaultConfig(), descriptor)
|
||||||
|
|
||||||
all := test.NewNumbers(profile.NumberKind)
|
all := test.NewNumbers(profile.NumberKind)
|
||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
x := profile.Random(+1)
|
x := profile.Random(+1)
|
||||||
all.Append(x)
|
all.Append(x)
|
||||||
agg.Update(ctx, x, record)
|
test.CheckedUpdate(t, agg, x, descriptor)
|
||||||
|
|
||||||
if !ut.absolute {
|
if !ut.absolute {
|
||||||
y := profile.Random(-1)
|
y := profile.Random(-1)
|
||||||
all.Append(y)
|
all.Append(y)
|
||||||
agg.Update(ctx, y, record)
|
test.CheckedUpdate(t, agg, y, descriptor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
agg.Collect(ctx, record, batcher)
|
agg.Checkpoint(ctx, descriptor)
|
||||||
|
|
||||||
all.Sort()
|
all.Sort()
|
||||||
|
|
||||||
|
sum, err := agg.Sum()
|
||||||
require.InDelta(t,
|
require.InDelta(t,
|
||||||
all.Sum().CoerceToFloat64(profile.NumberKind),
|
all.Sum().CoerceToFloat64(profile.NumberKind),
|
||||||
agg.Sum().CoerceToFloat64(profile.NumberKind),
|
sum.CoerceToFloat64(profile.NumberKind),
|
||||||
1,
|
1,
|
||||||
"Same sum - absolute")
|
"Same sum - absolute")
|
||||||
require.Equal(t, all.Count(), agg.Count(), "Same count - absolute")
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
count, err := agg.Count()
|
||||||
|
require.Equal(t, all.Count(), count, "Same count - absolute")
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
max, err := agg.Max()
|
max, err := agg.Max()
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
@ -96,49 +101,54 @@ type mergeTest struct {
|
|||||||
|
|
||||||
func (mt *mergeTest) run(t *testing.T, profile test.Profile) {
|
func (mt *mergeTest) run(t *testing.T, profile test.Profile) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
batcher, record := test.NewAggregatorTest(export.MeasureKind, profile.NumberKind, !mt.absolute)
|
descriptor := test.NewAggregatorTest(export.MeasureKind, profile.NumberKind, !mt.absolute)
|
||||||
|
|
||||||
agg1 := New(NewDefaultConfig(), record.Descriptor())
|
agg1 := New(NewDefaultConfig(), descriptor)
|
||||||
agg2 := New(NewDefaultConfig(), record.Descriptor())
|
agg2 := New(NewDefaultConfig(), descriptor)
|
||||||
|
|
||||||
all := test.NewNumbers(profile.NumberKind)
|
all := test.NewNumbers(profile.NumberKind)
|
||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
x := profile.Random(+1)
|
x := profile.Random(+1)
|
||||||
all.Append(x)
|
all.Append(x)
|
||||||
agg1.Update(ctx, x, record)
|
test.CheckedUpdate(t, agg1, x, descriptor)
|
||||||
|
|
||||||
if !mt.absolute {
|
if !mt.absolute {
|
||||||
y := profile.Random(-1)
|
y := profile.Random(-1)
|
||||||
all.Append(y)
|
all.Append(y)
|
||||||
agg1.Update(ctx, y, record)
|
test.CheckedUpdate(t, agg1, y, descriptor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
x := profile.Random(+1)
|
x := profile.Random(+1)
|
||||||
all.Append(x)
|
all.Append(x)
|
||||||
agg2.Update(ctx, x, record)
|
test.CheckedUpdate(t, agg2, x, descriptor)
|
||||||
|
|
||||||
if !mt.absolute {
|
if !mt.absolute {
|
||||||
y := profile.Random(-1)
|
y := profile.Random(-1)
|
||||||
all.Append(y)
|
all.Append(y)
|
||||||
agg2.Update(ctx, y, record)
|
test.CheckedUpdate(t, agg2, y, descriptor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
agg1.Collect(ctx, record, batcher)
|
agg1.Checkpoint(ctx, descriptor)
|
||||||
agg2.Collect(ctx, record, batcher)
|
agg2.Checkpoint(ctx, descriptor)
|
||||||
|
|
||||||
agg1.Merge(agg2, record.Descriptor())
|
test.CheckedMerge(t, agg1, agg2, descriptor)
|
||||||
|
|
||||||
all.Sort()
|
all.Sort()
|
||||||
|
|
||||||
|
asum, err := agg1.Sum()
|
||||||
require.InDelta(t,
|
require.InDelta(t,
|
||||||
all.Sum().CoerceToFloat64(profile.NumberKind),
|
all.Sum().CoerceToFloat64(profile.NumberKind),
|
||||||
agg1.Sum().CoerceToFloat64(profile.NumberKind),
|
asum.CoerceToFloat64(profile.NumberKind),
|
||||||
1,
|
1,
|
||||||
"Same sum - absolute")
|
"Same sum - absolute")
|
||||||
require.Equal(t, all.Count(), agg1.Count(), "Same count - absolute")
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
count, err := agg1.Count()
|
||||||
|
require.Equal(t, all.Count(), count, "Same count - absolute")
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
max, err := agg1.Max()
|
max, err := agg1.Max()
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
// Copyright 2019, OpenTelemetry 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 aggregator
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrEmptyDataSet = fmt.Errorf("The result is not defined on an empty data set")
|
|
||||||
ErrInvalidQuantile = fmt.Errorf("The requested quantile is out of range")
|
|
||||||
)
|
|
@ -22,6 +22,7 @@ import (
|
|||||||
|
|
||||||
"go.opentelemetry.io/otel/api/core"
|
"go.opentelemetry.io/otel/api/core"
|
||||||
export "go.opentelemetry.io/otel/sdk/export/metric"
|
export "go.opentelemetry.io/otel/sdk/export/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/export/metric/aggregator"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Note: This aggregator enforces the behavior of monotonic gauges to
|
// Note: This aggregator enforces the behavior of monotonic gauges to
|
||||||
@ -36,7 +37,7 @@ type (
|
|||||||
// current is an atomic pointer to *gaugeData. It is never nil.
|
// current is an atomic pointer to *gaugeData. It is never nil.
|
||||||
current unsafe.Pointer
|
current unsafe.Pointer
|
||||||
|
|
||||||
// checkpoint is a copy of the current value taken in Collect()
|
// checkpoint is a copy of the current value taken in Checkpoint()
|
||||||
checkpoint unsafe.Pointer
|
checkpoint unsafe.Pointer
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,6 +56,7 @@ type (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var _ export.Aggregator = &Aggregator{}
|
var _ export.Aggregator = &Aggregator{}
|
||||||
|
var _ aggregator.LastValue = &Aggregator{}
|
||||||
|
|
||||||
// An unset gauge has zero timestamp and zero value.
|
// An unset gauge has zero timestamp and zero value.
|
||||||
var unsetGauge = &gaugeData{}
|
var unsetGauge = &gaugeData{}
|
||||||
@ -68,31 +70,30 @@ func New() *Aggregator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AsNumber returns the recorded gauge value as an int64.
|
// LastValue returns the last-recorded gauge value and the
|
||||||
func (g *Aggregator) AsNumber() core.Number {
|
// corresponding timestamp. The error value aggregator.ErrNoLastValue
|
||||||
return (*gaugeData)(g.checkpoint).value.AsNumber()
|
// will be returned if (due to a race condition) the checkpoint was
|
||||||
|
// computed before the first value was set.
|
||||||
|
func (g *Aggregator) LastValue() (core.Number, time.Time, error) {
|
||||||
|
gd := (*gaugeData)(g.checkpoint)
|
||||||
|
if gd == unsetGauge {
|
||||||
|
return core.Number(0), time.Time{}, aggregator.ErrNoLastValue
|
||||||
|
}
|
||||||
|
return gd.value.AsNumber(), gd.timestamp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timestamp returns the timestamp of the alst recorded gauge value.
|
// Checkpoint atomically saves the current value.
|
||||||
func (g *Aggregator) Timestamp() time.Time {
|
func (g *Aggregator) Checkpoint(ctx context.Context, _ *export.Descriptor) {
|
||||||
return (*gaugeData)(g.checkpoint).timestamp
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect checkpoints the current value (atomically) and exports it.
|
|
||||||
func (g *Aggregator) Collect(ctx context.Context, rec export.Record, exp export.Batcher) {
|
|
||||||
g.checkpoint = atomic.LoadPointer(&g.current)
|
g.checkpoint = atomic.LoadPointer(&g.current)
|
||||||
|
|
||||||
exp.Export(ctx, rec, g)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update modifies the current value (atomically) for later export.
|
// Update atomically sets the current "last" value.
|
||||||
func (g *Aggregator) Update(_ context.Context, number core.Number, rec export.Record) {
|
func (g *Aggregator) Update(_ context.Context, number core.Number, desc *export.Descriptor) error {
|
||||||
desc := rec.Descriptor()
|
|
||||||
if !desc.Alternate() {
|
if !desc.Alternate() {
|
||||||
g.updateNonMonotonic(number)
|
g.updateNonMonotonic(number)
|
||||||
} else {
|
return nil
|
||||||
g.updateMonotonic(number, desc)
|
|
||||||
}
|
}
|
||||||
|
return g.updateMonotonic(number, desc)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Aggregator) updateNonMonotonic(number core.Number) {
|
func (g *Aggregator) updateNonMonotonic(number core.Number) {
|
||||||
@ -103,7 +104,7 @@ func (g *Aggregator) updateNonMonotonic(number core.Number) {
|
|||||||
atomic.StorePointer(&g.current, unsafe.Pointer(ngd))
|
atomic.StorePointer(&g.current, unsafe.Pointer(ngd))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Aggregator) updateMonotonic(number core.Number, desc *export.Descriptor) {
|
func (g *Aggregator) updateMonotonic(number core.Number, desc *export.Descriptor) error {
|
||||||
ngd := &gaugeData{
|
ngd := &gaugeData{
|
||||||
timestamp: time.Now(),
|
timestamp: time.Now(),
|
||||||
value: number,
|
value: number,
|
||||||
@ -114,21 +115,23 @@ func (g *Aggregator) updateMonotonic(number core.Number, desc *export.Descriptor
|
|||||||
gd := (*gaugeData)(atomic.LoadPointer(&g.current))
|
gd := (*gaugeData)(atomic.LoadPointer(&g.current))
|
||||||
|
|
||||||
if gd.value.CompareNumber(kind, number) > 0 {
|
if gd.value.CompareNumber(kind, number) > 0 {
|
||||||
// TODO warn
|
return aggregator.ErrNonMonotoneInput
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if atomic.CompareAndSwapPointer(&g.current, unsafe.Pointer(gd), unsafe.Pointer(ngd)) {
|
if atomic.CompareAndSwapPointer(&g.current, unsafe.Pointer(gd), unsafe.Pointer(ngd)) {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Aggregator) Merge(oa export.Aggregator, desc *export.Descriptor) {
|
// Merge combines state from two aggregators. If the gauge is
|
||||||
|
// declared as monotonic, the greater value is chosen. If the gauge
|
||||||
|
// is declared as non-monotonic, the most-recently set value is
|
||||||
|
// chosen.
|
||||||
|
func (g *Aggregator) Merge(oa export.Aggregator, desc *export.Descriptor) error {
|
||||||
o, _ := oa.(*Aggregator)
|
o, _ := oa.(*Aggregator)
|
||||||
if o == nil {
|
if o == nil {
|
||||||
// TODO warn
|
return aggregator.NewInconsistentMergeError(g, oa)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ggd := (*gaugeData)(atomic.LoadPointer(&g.checkpoint))
|
ggd := (*gaugeData)(atomic.LoadPointer(&g.checkpoint))
|
||||||
@ -139,18 +142,19 @@ func (g *Aggregator) Merge(oa export.Aggregator, desc *export.Descriptor) {
|
|||||||
cmp := ggd.value.CompareNumber(desc.NumberKind(), ogd.value)
|
cmp := ggd.value.CompareNumber(desc.NumberKind(), ogd.value)
|
||||||
|
|
||||||
if cmp > 0 {
|
if cmp > 0 {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if cmp < 0 {
|
if cmp < 0 {
|
||||||
g.checkpoint = unsafe.Pointer(ogd)
|
g.checkpoint = unsafe.Pointer(ogd)
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Non-monotonic gauge or equal values
|
// Non-monotonic gauge or equal values
|
||||||
if ggd.timestamp.After(ogd.timestamp) {
|
if ggd.timestamp.After(ogd.timestamp) {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
g.checkpoint = unsafe.Pointer(ogd)
|
g.checkpoint = unsafe.Pointer(ogd)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
|
|
||||||
"go.opentelemetry.io/otel/api/core"
|
"go.opentelemetry.io/otel/api/core"
|
||||||
export "go.opentelemetry.io/otel/sdk/export/metric"
|
export "go.opentelemetry.io/otel/sdk/export/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/export/metric/aggregator"
|
||||||
"go.opentelemetry.io/otel/sdk/metric/aggregator/test"
|
"go.opentelemetry.io/otel/sdk/metric/aggregator/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -36,18 +37,20 @@ func TestGaugeNonMonotonic(t *testing.T) {
|
|||||||
test.RunProfiles(t, func(t *testing.T, profile test.Profile) {
|
test.RunProfiles(t, func(t *testing.T, profile test.Profile) {
|
||||||
agg := New()
|
agg := New()
|
||||||
|
|
||||||
batcher, record := test.NewAggregatorTest(export.GaugeKind, profile.NumberKind, false)
|
record := test.NewAggregatorTest(export.GaugeKind, profile.NumberKind, false)
|
||||||
|
|
||||||
var last core.Number
|
var last core.Number
|
||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
x := profile.Random(rand.Intn(1)*2 - 1)
|
x := profile.Random(rand.Intn(1)*2 - 1)
|
||||||
last = x
|
last = x
|
||||||
agg.Update(ctx, x, record)
|
test.CheckedUpdate(t, agg, x, record)
|
||||||
}
|
}
|
||||||
|
|
||||||
agg.Collect(ctx, record, batcher)
|
agg.Checkpoint(ctx, record)
|
||||||
|
|
||||||
require.Equal(t, last, agg.AsNumber(), "Same last value - non-monotonic")
|
lv, _, err := agg.LastValue()
|
||||||
|
require.Equal(t, last, lv, "Same last value - non-monotonic")
|
||||||
|
require.Nil(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,19 +60,21 @@ func TestGaugeMonotonic(t *testing.T) {
|
|||||||
test.RunProfiles(t, func(t *testing.T, profile test.Profile) {
|
test.RunProfiles(t, func(t *testing.T, profile test.Profile) {
|
||||||
agg := New()
|
agg := New()
|
||||||
|
|
||||||
batcher, record := test.NewAggregatorTest(export.GaugeKind, profile.NumberKind, true)
|
record := test.NewAggregatorTest(export.GaugeKind, profile.NumberKind, true)
|
||||||
|
|
||||||
small := profile.Random(+1)
|
small := profile.Random(+1)
|
||||||
last := small
|
last := small
|
||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
x := profile.Random(+1)
|
x := profile.Random(+1)
|
||||||
last.AddNumber(profile.NumberKind, x)
|
last.AddNumber(profile.NumberKind, x)
|
||||||
agg.Update(ctx, last, record)
|
test.CheckedUpdate(t, agg, last, record)
|
||||||
}
|
}
|
||||||
|
|
||||||
agg.Collect(ctx, record, batcher)
|
agg.Checkpoint(ctx, record)
|
||||||
|
|
||||||
require.Equal(t, last, agg.AsNumber(), "Same last value - monotonic")
|
lv, _, err := agg.LastValue()
|
||||||
|
require.Equal(t, last, lv, "Same last value - monotonic")
|
||||||
|
require.Nil(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,19 +84,25 @@ func TestGaugeMonotonicDescending(t *testing.T) {
|
|||||||
test.RunProfiles(t, func(t *testing.T, profile test.Profile) {
|
test.RunProfiles(t, func(t *testing.T, profile test.Profile) {
|
||||||
agg := New()
|
agg := New()
|
||||||
|
|
||||||
batcher, record := test.NewAggregatorTest(export.GaugeKind, profile.NumberKind, true)
|
record := test.NewAggregatorTest(export.GaugeKind, profile.NumberKind, true)
|
||||||
|
|
||||||
first := profile.Random(+1)
|
first := profile.Random(+1)
|
||||||
agg.Update(ctx, first, record)
|
test.CheckedUpdate(t, agg, first, record)
|
||||||
|
|
||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
x := profile.Random(-1)
|
x := profile.Random(-1)
|
||||||
agg.Update(ctx, x, record)
|
|
||||||
|
err := agg.Update(ctx, x, record)
|
||||||
|
if err != aggregator.ErrNonMonotoneInput {
|
||||||
|
t.Error("Expected ErrNonMonotoneInput", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
agg.Collect(ctx, record, batcher)
|
agg.Checkpoint(ctx, record)
|
||||||
|
|
||||||
require.Equal(t, first, agg.AsNumber(), "Same last value - monotonic")
|
lv, _, err := agg.LastValue()
|
||||||
|
require.Equal(t, first, lv, "Same last value - monotonic")
|
||||||
|
require.Nil(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,26 +113,30 @@ func TestGaugeNormalMerge(t *testing.T) {
|
|||||||
agg1 := New()
|
agg1 := New()
|
||||||
agg2 := New()
|
agg2 := New()
|
||||||
|
|
||||||
batcher, record := test.NewAggregatorTest(export.GaugeKind, profile.NumberKind, false)
|
descriptor := test.NewAggregatorTest(export.GaugeKind, profile.NumberKind, false)
|
||||||
|
|
||||||
first1 := profile.Random(+1)
|
first1 := profile.Random(+1)
|
||||||
first2 := profile.Random(+1)
|
first2 := profile.Random(+1)
|
||||||
first1.AddNumber(profile.NumberKind, first2)
|
first1.AddNumber(profile.NumberKind, first2)
|
||||||
|
|
||||||
agg1.Update(ctx, first1, record)
|
test.CheckedUpdate(t, agg1, first1, descriptor)
|
||||||
agg2.Update(ctx, first2, record)
|
test.CheckedUpdate(t, agg2, first2, descriptor)
|
||||||
|
|
||||||
agg1.Collect(ctx, record, batcher)
|
agg1.Checkpoint(ctx, descriptor)
|
||||||
agg2.Collect(ctx, record, batcher)
|
agg2.Checkpoint(ctx, descriptor)
|
||||||
|
|
||||||
t1 := agg1.Timestamp()
|
_, t1, err := agg1.LastValue()
|
||||||
t2 := agg2.Timestamp()
|
require.Nil(t, err)
|
||||||
|
_, t2, err := agg2.LastValue()
|
||||||
|
require.Nil(t, err)
|
||||||
require.True(t, t1.Before(t2))
|
require.True(t, t1.Before(t2))
|
||||||
|
|
||||||
agg1.Merge(agg2, record.Descriptor())
|
test.CheckedMerge(t, agg1, agg2, descriptor)
|
||||||
|
|
||||||
require.Equal(t, t2, agg1.Timestamp(), "Merged timestamp - non-monotonic")
|
lv, ts, err := agg1.LastValue()
|
||||||
require.Equal(t, first2, agg1.AsNumber(), "Merged value - non-monotonic")
|
require.Nil(t, err)
|
||||||
|
require.Equal(t, t2, ts, "Merged timestamp - non-monotonic")
|
||||||
|
require.Equal(t, first2, lv, "Merged value - non-monotonic")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,21 +147,38 @@ func TestGaugeMonotonicMerge(t *testing.T) {
|
|||||||
agg1 := New()
|
agg1 := New()
|
||||||
agg2 := New()
|
agg2 := New()
|
||||||
|
|
||||||
batcher, record := test.NewAggregatorTest(export.GaugeKind, profile.NumberKind, true)
|
descriptor := test.NewAggregatorTest(export.GaugeKind, profile.NumberKind, true)
|
||||||
|
|
||||||
first1 := profile.Random(+1)
|
first1 := profile.Random(+1)
|
||||||
agg1.Update(ctx, first1, record)
|
test.CheckedUpdate(t, agg1, first1, descriptor)
|
||||||
|
|
||||||
first2 := profile.Random(+1)
|
first2 := profile.Random(+1)
|
||||||
first2.AddNumber(profile.NumberKind, first1)
|
first2.AddNumber(profile.NumberKind, first1)
|
||||||
agg2.Update(ctx, first2, record)
|
test.CheckedUpdate(t, agg2, first2, descriptor)
|
||||||
|
|
||||||
agg1.Collect(ctx, record, batcher)
|
agg1.Checkpoint(ctx, descriptor)
|
||||||
agg2.Collect(ctx, record, batcher)
|
agg2.Checkpoint(ctx, descriptor)
|
||||||
|
|
||||||
agg1.Merge(agg2, record.Descriptor())
|
test.CheckedMerge(t, agg1, agg2, descriptor)
|
||||||
|
|
||||||
require.Equal(t, first2, agg1.AsNumber(), "Merged value - monotonic")
|
_, ts2, err := agg1.LastValue()
|
||||||
require.Equal(t, agg2.Timestamp(), agg1.Timestamp(), "Merged timestamp - monotonic")
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
lv, ts1, err := agg1.LastValue()
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Equal(t, first2, lv, "Merged value - monotonic")
|
||||||
|
require.Equal(t, ts2, ts1, "Merged timestamp - monotonic")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGaugeNotSet(t *testing.T) {
|
||||||
|
descriptor := test.NewAggregatorTest(export.GaugeKind, core.Int64NumberKind, true)
|
||||||
|
|
||||||
|
g := New()
|
||||||
|
g.Checkpoint(context.Background(), descriptor)
|
||||||
|
|
||||||
|
value, timestamp, err := g.LastValue()
|
||||||
|
require.Equal(t, aggregator.ErrNoLastValue, err)
|
||||||
|
require.True(t, timestamp.IsZero())
|
||||||
|
require.Equal(t, core.Number(0), value)
|
||||||
|
}
|
||||||
|
@ -19,6 +19,7 @@ import (
|
|||||||
|
|
||||||
"go.opentelemetry.io/otel/api/core"
|
"go.opentelemetry.io/otel/api/core"
|
||||||
export "go.opentelemetry.io/otel/sdk/export/metric"
|
export "go.opentelemetry.io/otel/sdk/export/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/export/metric/aggregator"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@ -36,30 +37,43 @@ type (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ export.Aggregator = &Aggregator{}
|
// TODO: The SDK specification says this type should support Min
|
||||||
|
// values, see #319.
|
||||||
|
|
||||||
// New returns a new measure aggregator for computing max, sum, and count.
|
var _ export.Aggregator = &Aggregator{}
|
||||||
|
var _ aggregator.MaxSumCount = &Aggregator{}
|
||||||
|
|
||||||
|
// New returns a new measure aggregator for computing max, sum, and
|
||||||
|
// count. It does not compute quantile information other than Max.
|
||||||
|
//
|
||||||
|
// Note that this aggregator maintains each value using independent
|
||||||
|
// atomic operations, which introduces the possibility that
|
||||||
|
// checkpoints are inconsistent. For greater consistency and lower
|
||||||
|
// performance, consider using Array or DDSketch aggregators.
|
||||||
func New() *Aggregator {
|
func New() *Aggregator {
|
||||||
return &Aggregator{}
|
return &Aggregator{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sum returns the accumulated sum as a Number.
|
// Sum returns the sum of values in the checkpoint.
|
||||||
func (c *Aggregator) Sum() core.Number {
|
func (c *Aggregator) Sum() (core.Number, error) {
|
||||||
return c.checkpoint.sum
|
return c.checkpoint.sum, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count returns the accumulated count.
|
// Count returns the number of values in the checkpoint.
|
||||||
func (c *Aggregator) Count() int64 {
|
func (c *Aggregator) Count() (int64, error) {
|
||||||
return int64(c.checkpoint.count.AsUint64())
|
return int64(c.checkpoint.count.AsUint64()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Max returns the accumulated max as a Number.
|
// Max returns the maximum value in the checkpoint.
|
||||||
func (c *Aggregator) Max() (core.Number, error) {
|
func (c *Aggregator) Max() (core.Number, error) {
|
||||||
return c.checkpoint.max, nil
|
return c.checkpoint.max, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect checkpoints the current value (atomically) and exports it.
|
// Checkpoint saves the current state and resets the current state to
|
||||||
func (c *Aggregator) Collect(ctx context.Context, rec export.Record, exp export.Batcher) {
|
// the empty set. Since no locks are taken, there is a chance that
|
||||||
|
// the independent Max, Sum, and Count are not consistent with each
|
||||||
|
// other.
|
||||||
|
func (c *Aggregator) Checkpoint(ctx context.Context, _ *export.Descriptor) {
|
||||||
// N.B. There is no atomic operation that can update all three
|
// N.B. There is no atomic operation that can update all three
|
||||||
// values at once without a memory allocation.
|
// values at once without a memory allocation.
|
||||||
//
|
//
|
||||||
@ -73,20 +87,12 @@ func (c *Aggregator) Collect(ctx context.Context, rec export.Record, exp export.
|
|||||||
c.checkpoint.count.SetUint64(c.current.count.SwapUint64Atomic(0))
|
c.checkpoint.count.SetUint64(c.current.count.SwapUint64Atomic(0))
|
||||||
c.checkpoint.sum = c.current.sum.SwapNumberAtomic(core.Number(0))
|
c.checkpoint.sum = c.current.sum.SwapNumberAtomic(core.Number(0))
|
||||||
c.checkpoint.max = c.current.max.SwapNumberAtomic(core.Number(0))
|
c.checkpoint.max = c.current.max.SwapNumberAtomic(core.Number(0))
|
||||||
|
|
||||||
exp.Export(ctx, rec, c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update modifies the current value (atomically) for later export.
|
// Update adds the recorded measurement to the current data set.
|
||||||
func (c *Aggregator) Update(_ context.Context, number core.Number, rec export.Record) {
|
func (c *Aggregator) Update(_ context.Context, number core.Number, desc *export.Descriptor) error {
|
||||||
desc := rec.Descriptor()
|
|
||||||
kind := desc.NumberKind()
|
kind := desc.NumberKind()
|
||||||
|
|
||||||
if !desc.Alternate() && number.IsNegative(kind) {
|
|
||||||
// TODO warn
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.current.count.AddUint64Atomic(1)
|
c.current.count.AddUint64Atomic(1)
|
||||||
c.current.sum.AddNumberAtomic(kind, number)
|
c.current.sum.AddNumberAtomic(kind, number)
|
||||||
|
|
||||||
@ -100,13 +106,14 @@ func (c *Aggregator) Update(_ context.Context, number core.Number, rec export.Re
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Aggregator) Merge(oa export.Aggregator, desc *export.Descriptor) {
|
// Merge combines two data sets into one.
|
||||||
|
func (c *Aggregator) Merge(oa export.Aggregator, desc *export.Descriptor) error {
|
||||||
o, _ := oa.(*Aggregator)
|
o, _ := oa.(*Aggregator)
|
||||||
if o == nil {
|
if o == nil {
|
||||||
// TODO warn
|
return aggregator.NewInconsistentMergeError(c, oa)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.checkpoint.sum.AddNumber(desc.NumberKind(), o.checkpoint.sum)
|
c.checkpoint.sum.AddNumber(desc.NumberKind(), o.checkpoint.sum)
|
||||||
@ -115,4 +122,5 @@ func (c *Aggregator) Merge(oa export.Aggregator, desc *export.Descriptor) {
|
|||||||
if c.checkpoint.max.CompareNumber(desc.NumberKind(), o.checkpoint.max) < 0 {
|
if c.checkpoint.max.CompareNumber(desc.NumberKind(), o.checkpoint.max) < 0 {
|
||||||
c.checkpoint.max.SetNumber(o.checkpoint.max)
|
c.checkpoint.max.SetNumber(o.checkpoint.max)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ func TestMaxSumCountAbsolute(t *testing.T) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
test.RunProfiles(t, func(t *testing.T, profile test.Profile) {
|
test.RunProfiles(t, func(t *testing.T, profile test.Profile) {
|
||||||
batcher, record := test.NewAggregatorTest(export.MeasureKind, profile.NumberKind, false)
|
record := test.NewAggregatorTest(export.MeasureKind, profile.NumberKind, false)
|
||||||
|
|
||||||
agg := New()
|
agg := New()
|
||||||
|
|
||||||
@ -39,19 +39,24 @@ func TestMaxSumCountAbsolute(t *testing.T) {
|
|||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
x := profile.Random(+1)
|
x := profile.Random(+1)
|
||||||
all.Append(x)
|
all.Append(x)
|
||||||
agg.Update(ctx, x, record)
|
test.CheckedUpdate(t, agg, x, record)
|
||||||
}
|
}
|
||||||
|
|
||||||
agg.Collect(ctx, record, batcher)
|
agg.Checkpoint(ctx, record)
|
||||||
|
|
||||||
all.Sort()
|
all.Sort()
|
||||||
|
|
||||||
|
asum, err := agg.Sum()
|
||||||
require.InEpsilon(t,
|
require.InEpsilon(t,
|
||||||
all.Sum().CoerceToFloat64(profile.NumberKind),
|
all.Sum().CoerceToFloat64(profile.NumberKind),
|
||||||
agg.Sum().CoerceToFloat64(profile.NumberKind),
|
asum.CoerceToFloat64(profile.NumberKind),
|
||||||
0.000000001,
|
0.000000001,
|
||||||
"Same sum - absolute")
|
"Same sum - absolute")
|
||||||
require.Equal(t, all.Count(), agg.Count(), "Same count - absolute")
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
count, err := agg.Count()
|
||||||
|
require.Equal(t, all.Count(), count, "Same count - absolute")
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
max, err := agg.Max()
|
max, err := agg.Max()
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
@ -66,7 +71,7 @@ func TestMaxSumCountMerge(t *testing.T) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
test.RunProfiles(t, func(t *testing.T, profile test.Profile) {
|
test.RunProfiles(t, func(t *testing.T, profile test.Profile) {
|
||||||
batcher, record := test.NewAggregatorTest(export.MeasureKind, profile.NumberKind, false)
|
descriptor := test.NewAggregatorTest(export.MeasureKind, profile.NumberKind, false)
|
||||||
|
|
||||||
agg1 := New()
|
agg1 := New()
|
||||||
agg2 := New()
|
agg2 := New()
|
||||||
@ -76,27 +81,32 @@ func TestMaxSumCountMerge(t *testing.T) {
|
|||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
x := profile.Random(+1)
|
x := profile.Random(+1)
|
||||||
all.Append(x)
|
all.Append(x)
|
||||||
agg1.Update(ctx, x, record)
|
test.CheckedUpdate(t, agg1, x, descriptor)
|
||||||
}
|
}
|
||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
x := profile.Random(+1)
|
x := profile.Random(+1)
|
||||||
all.Append(x)
|
all.Append(x)
|
||||||
agg2.Update(ctx, x, record)
|
test.CheckedUpdate(t, agg2, x, descriptor)
|
||||||
}
|
}
|
||||||
|
|
||||||
agg1.Collect(ctx, record, batcher)
|
agg1.Checkpoint(ctx, descriptor)
|
||||||
agg2.Collect(ctx, record, batcher)
|
agg2.Checkpoint(ctx, descriptor)
|
||||||
|
|
||||||
agg1.Merge(agg2, record.Descriptor())
|
test.CheckedMerge(t, agg1, agg2, descriptor)
|
||||||
|
|
||||||
all.Sort()
|
all.Sort()
|
||||||
|
|
||||||
|
asum, err := agg1.Sum()
|
||||||
require.InEpsilon(t,
|
require.InEpsilon(t,
|
||||||
all.Sum().CoerceToFloat64(profile.NumberKind),
|
all.Sum().CoerceToFloat64(profile.NumberKind),
|
||||||
agg1.Sum().CoerceToFloat64(profile.NumberKind),
|
asum.CoerceToFloat64(profile.NumberKind),
|
||||||
0.000000001,
|
0.000000001,
|
||||||
"Same sum - absolute")
|
"Same sum - absolute")
|
||||||
require.Equal(t, all.Count(), agg1.Count(), "Same count - absolute")
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
count, err := agg1.Count()
|
||||||
|
require.Equal(t, all.Count(), count, "Same count - absolute")
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
max, err := agg1.Max()
|
max, err := agg1.Max()
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
|
@ -22,11 +22,9 @@ import (
|
|||||||
|
|
||||||
"go.opentelemetry.io/otel/api/core"
|
"go.opentelemetry.io/otel/api/core"
|
||||||
export "go.opentelemetry.io/otel/sdk/export/metric"
|
export "go.opentelemetry.io/otel/sdk/export/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/export/metric/aggregator"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ export.Batcher = &metricBatcher{}
|
|
||||||
var _ export.Record = &metricRecord{}
|
|
||||||
|
|
||||||
const Magnitude = 1000
|
const Magnitude = 1000
|
||||||
|
|
||||||
type Profile struct {
|
type Profile struct {
|
||||||
@ -52,31 +50,9 @@ func newProfiles() []Profile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type metricBatcher struct {
|
func NewAggregatorTest(mkind export.MetricKind, nkind core.NumberKind, alternate bool) *export.Descriptor {
|
||||||
}
|
|
||||||
|
|
||||||
type metricRecord struct {
|
|
||||||
descriptor *export.Descriptor
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAggregatorTest(mkind export.Kind, nkind core.NumberKind, alternate bool) (export.Batcher, export.Record) {
|
|
||||||
desc := export.NewDescriptor("test.name", mkind, nil, "", "", nkind, alternate)
|
desc := export.NewDescriptor("test.name", mkind, nil, "", "", nkind, alternate)
|
||||||
return &metricBatcher{}, &metricRecord{descriptor: desc}
|
return desc
|
||||||
}
|
|
||||||
|
|
||||||
func (t *metricRecord) Descriptor() *export.Descriptor {
|
|
||||||
return t.descriptor
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *metricRecord) Labels() []core.KeyValue {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *metricBatcher) AggregatorFor(rec export.Record) export.Aggregator {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *metricBatcher) Export(context.Context, export.Record, export.Aggregator) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunProfiles(t *testing.T, f func(*testing.T, Profile)) {
|
func RunProfiles(t *testing.T, f func(*testing.T, Profile)) {
|
||||||
@ -147,3 +123,26 @@ func (n *Numbers) Median() core.Number {
|
|||||||
// specified quantile.
|
// specified quantile.
|
||||||
return n.numbers[len(n.numbers)/2]
|
return n.numbers[len(n.numbers)/2]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Performs the same range test the SDK does on behalf of the aggregator.
|
||||||
|
func CheckedUpdate(t *testing.T, agg export.Aggregator, number core.Number, descriptor *export.Descriptor) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Note: Aggregator tests are written assuming that the SDK
|
||||||
|
// has performed the RangeTest. Therefore we skip errors that
|
||||||
|
// would have been detected by the RangeTest.
|
||||||
|
err := aggregator.RangeTest(number, descriptor)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := agg.Update(ctx, number, descriptor); err != nil {
|
||||||
|
t.Error("Unexpected Update failure", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckedMerge(t *testing.T, aggInto, aggFrom export.Aggregator, descriptor *export.Descriptor) {
|
||||||
|
if err := aggInto.Merge(aggFrom, descriptor); err != nil {
|
||||||
|
t.Error("Unexpected Merge failure", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
146
sdk/metric/batcher/defaultkeys/defaultkeys.go
Normal file
146
sdk/metric/batcher/defaultkeys/defaultkeys.go
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
// Copyright 2019, OpenTelemetry 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 defaultkeys // import "go.opentelemetry.io/otel/sdk/metric/batcher/defaultkeys"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/api/core"
|
||||||
|
export "go.opentelemetry.io/otel/sdk/export/metric"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Batcher struct {
|
||||||
|
selector export.AggregationSelector
|
||||||
|
labelEncoder export.LabelEncoder
|
||||||
|
stateful bool
|
||||||
|
descKeyIndex descKeyIndexMap
|
||||||
|
aggCheckpoint aggCheckpointMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// descKeyIndexMap is a mapping, for each Descriptor, from the
|
||||||
|
// Key to the position in the descriptor's recommended keys.
|
||||||
|
descKeyIndexMap map[*export.Descriptor]map[core.Key]int
|
||||||
|
|
||||||
|
// aggCheckpointMap is a mapping from encoded label set to current
|
||||||
|
// export record. If the batcher is stateful, this map is
|
||||||
|
// never cleared.
|
||||||
|
aggCheckpointMap map[string]export.Record
|
||||||
|
|
||||||
|
checkpointSet struct {
|
||||||
|
aggCheckpointMap aggCheckpointMap
|
||||||
|
labelEncoder export.LabelEncoder
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ export.Batcher = &Batcher{}
|
||||||
|
var _ export.CheckpointSet = &checkpointSet{}
|
||||||
|
|
||||||
|
func New(selector export.AggregationSelector, labelEncoder export.LabelEncoder, stateful bool) *Batcher {
|
||||||
|
return &Batcher{
|
||||||
|
selector: selector,
|
||||||
|
labelEncoder: labelEncoder,
|
||||||
|
descKeyIndex: descKeyIndexMap{},
|
||||||
|
aggCheckpoint: aggCheckpointMap{},
|
||||||
|
stateful: stateful,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Batcher) AggregatorFor(descriptor *export.Descriptor) export.Aggregator {
|
||||||
|
return b.selector.AggregatorFor(descriptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Batcher) Process(_ context.Context, record export.Record) error {
|
||||||
|
desc := record.Descriptor()
|
||||||
|
keys := desc.Keys()
|
||||||
|
|
||||||
|
// Cache the mapping from Descriptor->Key->Index
|
||||||
|
ki, ok := b.descKeyIndex[desc]
|
||||||
|
if !ok {
|
||||||
|
ki = map[core.Key]int{}
|
||||||
|
b.descKeyIndex[desc] = ki
|
||||||
|
|
||||||
|
for i, k := range keys {
|
||||||
|
ki[k] = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute the value list. Note: Unspecified values become
|
||||||
|
// empty strings. TODO: pin this down, we have no appropriate
|
||||||
|
// Value constructor.
|
||||||
|
outputLabels := make([]core.KeyValue, len(keys))
|
||||||
|
|
||||||
|
for i, key := range keys {
|
||||||
|
outputLabels[i] = key.String("")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note also the possibility to speed this computation of
|
||||||
|
// "encoded" via "outputLabels" in the form of a (Descriptor,
|
||||||
|
// LabelSet)->(Labels, Encoded) cache.
|
||||||
|
for _, kv := range record.Labels().Ordered() {
|
||||||
|
pos, ok := ki[kv.Key]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
outputLabels[pos].Value = kv.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute an encoded lookup key.
|
||||||
|
encoded := b.labelEncoder.Encode(outputLabels)
|
||||||
|
|
||||||
|
// Merge this aggregator with all preceding aggregators that
|
||||||
|
// map to the same set of `outputLabels` labels.
|
||||||
|
agg := record.Aggregator()
|
||||||
|
rag, ok := b.aggCheckpoint[encoded]
|
||||||
|
if ok {
|
||||||
|
return rag.Aggregator().Merge(agg, desc)
|
||||||
|
}
|
||||||
|
// If this Batcher is stateful, create a copy of the
|
||||||
|
// Aggregator for long-term storage. Otherwise the
|
||||||
|
// Meter implementation will checkpoint the aggregator
|
||||||
|
// again, overwriting the long-lived state.
|
||||||
|
if b.stateful {
|
||||||
|
tmp := agg
|
||||||
|
agg = b.AggregatorFor(desc)
|
||||||
|
if err := agg.Merge(tmp, desc); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.aggCheckpoint[encoded] = export.NewRecord(
|
||||||
|
desc,
|
||||||
|
export.NewLabels(outputLabels, encoded, b.labelEncoder),
|
||||||
|
agg,
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Batcher) CheckpointSet() export.CheckpointSet {
|
||||||
|
return &checkpointSet{
|
||||||
|
aggCheckpointMap: b.aggCheckpoint,
|
||||||
|
labelEncoder: b.labelEncoder,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Batcher) FinishedCollection() {
|
||||||
|
if !b.stateful {
|
||||||
|
b.aggCheckpoint = aggCheckpointMap{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *checkpointSet) ForEach(f func(export.Record)) {
|
||||||
|
for _, entry := range p.aggCheckpointMap {
|
||||||
|
f(entry)
|
||||||
|
}
|
||||||
|
}
|
116
sdk/metric/batcher/defaultkeys/defaultkeys_test.go
Normal file
116
sdk/metric/batcher/defaultkeys/defaultkeys_test.go
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
// Copyright 2019, OpenTelemetry 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 defaultkeys_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/api/core"
|
||||||
|
export "go.opentelemetry.io/otel/sdk/export/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/batcher/defaultkeys"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/batcher/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGroupingStateless(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
b := defaultkeys.New(test.NewAggregationSelector(), test.GroupEncoder, false)
|
||||||
|
|
||||||
|
_ = b.Process(ctx, export.NewRecord(test.GaugeDesc, test.Labels1, test.GaugeAgg(10)))
|
||||||
|
_ = b.Process(ctx, export.NewRecord(test.GaugeDesc, test.Labels2, test.GaugeAgg(20)))
|
||||||
|
_ = b.Process(ctx, export.NewRecord(test.GaugeDesc, test.Labels3, test.GaugeAgg(30)))
|
||||||
|
|
||||||
|
_ = b.Process(ctx, export.NewRecord(test.CounterDesc, test.Labels1, test.CounterAgg(10)))
|
||||||
|
_ = b.Process(ctx, export.NewRecord(test.CounterDesc, test.Labels2, test.CounterAgg(20)))
|
||||||
|
_ = b.Process(ctx, export.NewRecord(test.CounterDesc, test.Labels3, test.CounterAgg(40)))
|
||||||
|
|
||||||
|
checkpointSet := b.CheckpointSet()
|
||||||
|
b.FinishedCollection()
|
||||||
|
|
||||||
|
records := test.Output{}
|
||||||
|
checkpointSet.ForEach(records.AddTo)
|
||||||
|
|
||||||
|
// Output gauge should have only the "G=H" and "G=" keys.
|
||||||
|
// Output counter should have only the "C=D" and "C=" keys.
|
||||||
|
require.EqualValues(t, map[string]int64{
|
||||||
|
"counter/C=D": 30, // labels1 + labels2
|
||||||
|
"counter/C=": 40, // labels3
|
||||||
|
"gauge/G=H": 10, // labels1
|
||||||
|
"gauge/G=": 30, // labels3 = last value
|
||||||
|
}, records)
|
||||||
|
|
||||||
|
// Verify that state is reset by FinishedCollection()
|
||||||
|
checkpointSet = b.CheckpointSet()
|
||||||
|
b.FinishedCollection()
|
||||||
|
checkpointSet.ForEach(func(rec export.Record) {
|
||||||
|
t.Fatal("Unexpected call")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGroupingStateful(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
b := defaultkeys.New(test.NewAggregationSelector(), test.GroupEncoder, true)
|
||||||
|
|
||||||
|
cagg := test.CounterAgg(10)
|
||||||
|
_ = b.Process(ctx, export.NewRecord(test.CounterDesc, test.Labels1, cagg))
|
||||||
|
|
||||||
|
checkpointSet := b.CheckpointSet()
|
||||||
|
b.FinishedCollection()
|
||||||
|
|
||||||
|
records1 := test.Output{}
|
||||||
|
checkpointSet.ForEach(records1.AddTo)
|
||||||
|
|
||||||
|
require.EqualValues(t, map[string]int64{
|
||||||
|
"counter/C=D": 10, // labels1
|
||||||
|
}, records1)
|
||||||
|
|
||||||
|
// Test that state was NOT reset
|
||||||
|
checkpointSet = b.CheckpointSet()
|
||||||
|
b.FinishedCollection()
|
||||||
|
|
||||||
|
records2 := test.Output{}
|
||||||
|
checkpointSet.ForEach(records2.AddTo)
|
||||||
|
|
||||||
|
require.EqualValues(t, records1, records2)
|
||||||
|
|
||||||
|
// Update and re-checkpoint the original record.
|
||||||
|
_ = cagg.Update(ctx, core.NewInt64Number(20), test.CounterDesc)
|
||||||
|
cagg.Checkpoint(ctx, test.CounterDesc)
|
||||||
|
|
||||||
|
// As yet cagg has not been passed to Batcher.Process. Should
|
||||||
|
// not see an update.
|
||||||
|
checkpointSet = b.CheckpointSet()
|
||||||
|
b.FinishedCollection()
|
||||||
|
|
||||||
|
records3 := test.Output{}
|
||||||
|
checkpointSet.ForEach(records3.AddTo)
|
||||||
|
|
||||||
|
require.EqualValues(t, records1, records3)
|
||||||
|
|
||||||
|
// Now process the second update
|
||||||
|
_ = b.Process(ctx, export.NewRecord(test.CounterDesc, test.Labels1, cagg))
|
||||||
|
|
||||||
|
checkpointSet = b.CheckpointSet()
|
||||||
|
b.FinishedCollection()
|
||||||
|
|
||||||
|
records4 := test.Output{}
|
||||||
|
checkpointSet.ForEach(records4.AddTo)
|
||||||
|
|
||||||
|
require.EqualValues(t, map[string]int64{
|
||||||
|
"counter/C=D": 30,
|
||||||
|
}, records4)
|
||||||
|
}
|
136
sdk/metric/batcher/test/test.go
Normal file
136
sdk/metric/batcher/test/test.go
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
// Copyright 2019, OpenTelemetry 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 test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/api/core"
|
||||||
|
"go.opentelemetry.io/otel/api/key"
|
||||||
|
export "go.opentelemetry.io/otel/sdk/export/metric"
|
||||||
|
sdk "go.opentelemetry.io/otel/sdk/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/aggregator/counter"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/aggregator/gauge"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// Encoder is an alternate label encoder to validate grouping logic.
|
||||||
|
Encoder struct{}
|
||||||
|
|
||||||
|
// Output collects distinct metric/label set outputs.
|
||||||
|
Output map[string]int64
|
||||||
|
|
||||||
|
// testAggregationSelector returns aggregators consistent with
|
||||||
|
// the test variables below, needed for testing stateful
|
||||||
|
// batchers, which clone Aggregators using AggregatorFor(desc).
|
||||||
|
testAggregationSelector struct{}
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// GaugeDesc groups by "G"
|
||||||
|
GaugeDesc = export.NewDescriptor(
|
||||||
|
"gauge", export.GaugeKind, []core.Key{key.New("G")}, "", "", core.Int64NumberKind, false)
|
||||||
|
// CounterDesc groups by "C"
|
||||||
|
CounterDesc = export.NewDescriptor(
|
||||||
|
"counter", export.CounterKind, []core.Key{key.New("C")}, "", "", core.Int64NumberKind, false)
|
||||||
|
|
||||||
|
// SdkEncoder uses a non-standard encoder like K1~V1&K2~V2
|
||||||
|
SdkEncoder = &Encoder{}
|
||||||
|
// GroupEncoder uses the SDK default encoder
|
||||||
|
GroupEncoder = sdk.DefaultLabelEncoder()
|
||||||
|
|
||||||
|
// Gauge groups are (labels1), (labels2+labels3)
|
||||||
|
// Counter groups are (labels1+labels2), (labels3)
|
||||||
|
|
||||||
|
// Labels1 has G=H and C=D
|
||||||
|
Labels1 = makeLabels(SdkEncoder, key.String("G", "H"), key.String("C", "D"))
|
||||||
|
// Labels2 has C=D and E=F
|
||||||
|
Labels2 = makeLabels(SdkEncoder, key.String("C", "D"), key.String("E", "F"))
|
||||||
|
// Labels3 is the empty set
|
||||||
|
Labels3 = makeLabels(SdkEncoder)
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewAggregationSelector returns a policy that is consistent with the
|
||||||
|
// test descriptors above. I.e., it returns counter.New() for counter
|
||||||
|
// instruments and gauge.New for gauge instruments.
|
||||||
|
func NewAggregationSelector() export.AggregationSelector {
|
||||||
|
return &testAggregationSelector{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*testAggregationSelector) AggregatorFor(desc *export.Descriptor) export.Aggregator {
|
||||||
|
switch desc.MetricKind() {
|
||||||
|
case export.CounterKind:
|
||||||
|
return counter.New()
|
||||||
|
case export.GaugeKind:
|
||||||
|
return gauge.New()
|
||||||
|
default:
|
||||||
|
panic("Invalid descriptor MetricKind for this test")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeLabels(encoder export.LabelEncoder, labels ...core.KeyValue) export.Labels {
|
||||||
|
encoded := encoder.Encode(labels)
|
||||||
|
return export.NewLabels(labels, encoded, encoder)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Encoder) Encode(labels []core.KeyValue) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
for i, l := range labels {
|
||||||
|
if i > 0 {
|
||||||
|
sb.WriteString("&")
|
||||||
|
}
|
||||||
|
sb.WriteString(string(l.Key))
|
||||||
|
sb.WriteString("~")
|
||||||
|
sb.WriteString(l.Value.Emit())
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GaugeAgg returns a checkpointed gauge aggregator w/ the specified value.
|
||||||
|
func GaugeAgg(v int64) export.Aggregator {
|
||||||
|
ctx := context.Background()
|
||||||
|
gagg := gauge.New()
|
||||||
|
_ = gagg.Update(ctx, core.NewInt64Number(v), GaugeDesc)
|
||||||
|
gagg.Checkpoint(ctx, CounterDesc)
|
||||||
|
return gagg
|
||||||
|
}
|
||||||
|
|
||||||
|
// CounterAgg returns a checkpointed counter aggregator w/ the specified value.
|
||||||
|
func CounterAgg(v int64) export.Aggregator {
|
||||||
|
ctx := context.Background()
|
||||||
|
cagg := counter.New()
|
||||||
|
_ = cagg.Update(ctx, core.NewInt64Number(v), CounterDesc)
|
||||||
|
cagg.Checkpoint(ctx, CounterDesc)
|
||||||
|
return cagg
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddTo adds a name/label-encoding entry with the gauge or counter
|
||||||
|
// value to the output map.
|
||||||
|
func (o Output) AddTo(rec export.Record) {
|
||||||
|
labels := rec.Labels()
|
||||||
|
key := fmt.Sprint(rec.Descriptor().Name(), "/", labels.Encoded())
|
||||||
|
var value int64
|
||||||
|
switch t := rec.Aggregator().(type) {
|
||||||
|
case *counter.Aggregator:
|
||||||
|
sum, _ := t.Sum()
|
||||||
|
value = sum.AsInt64()
|
||||||
|
case *gauge.Aggregator:
|
||||||
|
lv, _, _ := t.LastValue()
|
||||||
|
value = lv.AsInt64()
|
||||||
|
}
|
||||||
|
o[key] = value
|
||||||
|
}
|
105
sdk/metric/batcher/ungrouped/ungrouped.go
Normal file
105
sdk/metric/batcher/ungrouped/ungrouped.go
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
// Copyright 2019, OpenTelemetry 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 ungrouped // import "go.opentelemetry.io/otel/sdk/metric/batcher/ungrouped"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
export "go.opentelemetry.io/otel/sdk/export/metric"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Batcher struct {
|
||||||
|
selector export.AggregationSelector
|
||||||
|
batchMap batchMap
|
||||||
|
stateful bool
|
||||||
|
}
|
||||||
|
|
||||||
|
batchKey struct {
|
||||||
|
descriptor *export.Descriptor
|
||||||
|
encoded string
|
||||||
|
}
|
||||||
|
|
||||||
|
batchValue struct {
|
||||||
|
aggregator export.Aggregator
|
||||||
|
labels export.Labels
|
||||||
|
}
|
||||||
|
|
||||||
|
batchMap map[batchKey]batchValue
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ export.Batcher = &Batcher{}
|
||||||
|
var _ export.CheckpointSet = batchMap{}
|
||||||
|
|
||||||
|
func New(selector export.AggregationSelector, stateful bool) *Batcher {
|
||||||
|
return &Batcher{
|
||||||
|
selector: selector,
|
||||||
|
batchMap: batchMap{},
|
||||||
|
stateful: stateful,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Batcher) AggregatorFor(descriptor *export.Descriptor) export.Aggregator {
|
||||||
|
return b.selector.AggregatorFor(descriptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Batcher) Process(_ context.Context, record export.Record) error {
|
||||||
|
desc := record.Descriptor()
|
||||||
|
key := batchKey{
|
||||||
|
descriptor: desc,
|
||||||
|
encoded: record.Labels().Encoded(),
|
||||||
|
}
|
||||||
|
agg := record.Aggregator()
|
||||||
|
value, ok := b.batchMap[key]
|
||||||
|
if ok {
|
||||||
|
return value.aggregator.Merge(agg, desc)
|
||||||
|
}
|
||||||
|
// If this Batcher is stateful, create a copy of the
|
||||||
|
// Aggregator for long-term storage. Otherwise the
|
||||||
|
// Meter implementation will checkpoint the aggregator
|
||||||
|
// again, overwriting the long-lived state.
|
||||||
|
if b.stateful {
|
||||||
|
tmp := agg
|
||||||
|
agg = b.AggregatorFor(desc)
|
||||||
|
if err := agg.Merge(tmp, desc); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.batchMap[key] = batchValue{
|
||||||
|
aggregator: agg,
|
||||||
|
labels: record.Labels(),
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Batcher) CheckpointSet() export.CheckpointSet {
|
||||||
|
return b.batchMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Batcher) FinishedCollection() {
|
||||||
|
if !b.stateful {
|
||||||
|
b.batchMap = batchMap{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c batchMap) ForEach(f func(export.Record)) {
|
||||||
|
for key, value := range c {
|
||||||
|
f(export.NewRecord(
|
||||||
|
key.descriptor,
|
||||||
|
value.labels,
|
||||||
|
value.aggregator,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
128
sdk/metric/batcher/ungrouped/ungrouped_test.go
Normal file
128
sdk/metric/batcher/ungrouped/ungrouped_test.go
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
// Copyright 2019, OpenTelemetry 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 ungrouped_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/api/core"
|
||||||
|
export "go.opentelemetry.io/otel/sdk/export/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/batcher/test"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/batcher/ungrouped"
|
||||||
|
)
|
||||||
|
|
||||||
|
// These tests use the ../test label encoding.
|
||||||
|
|
||||||
|
func TestUngroupedStateless(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
b := ungrouped.New(test.NewAggregationSelector(), false)
|
||||||
|
|
||||||
|
// Set initial gauge values
|
||||||
|
_ = b.Process(ctx, export.NewRecord(test.GaugeDesc, test.Labels1, test.GaugeAgg(10)))
|
||||||
|
_ = b.Process(ctx, export.NewRecord(test.GaugeDesc, test.Labels2, test.GaugeAgg(20)))
|
||||||
|
_ = b.Process(ctx, export.NewRecord(test.GaugeDesc, test.Labels3, test.GaugeAgg(30)))
|
||||||
|
|
||||||
|
// Another gauge Set for Labels1
|
||||||
|
_ = b.Process(ctx, export.NewRecord(test.GaugeDesc, test.Labels1, test.GaugeAgg(50)))
|
||||||
|
|
||||||
|
// Set initial counter values
|
||||||
|
_ = b.Process(ctx, export.NewRecord(test.CounterDesc, test.Labels1, test.CounterAgg(10)))
|
||||||
|
_ = b.Process(ctx, export.NewRecord(test.CounterDesc, test.Labels2, test.CounterAgg(20)))
|
||||||
|
_ = b.Process(ctx, export.NewRecord(test.CounterDesc, test.Labels3, test.CounterAgg(40)))
|
||||||
|
|
||||||
|
// Another counter Add for Labels1
|
||||||
|
_ = b.Process(ctx, export.NewRecord(test.CounterDesc, test.Labels1, test.CounterAgg(50)))
|
||||||
|
|
||||||
|
checkpointSet := b.CheckpointSet()
|
||||||
|
b.FinishedCollection()
|
||||||
|
|
||||||
|
records := test.Output{}
|
||||||
|
checkpointSet.ForEach(records.AddTo)
|
||||||
|
|
||||||
|
// Output gauge should have only the "G=H" and "G=" keys.
|
||||||
|
// Output counter should have only the "C=D" and "C=" keys.
|
||||||
|
require.EqualValues(t, map[string]int64{
|
||||||
|
"counter/G~H&C~D": 60, // labels1
|
||||||
|
"counter/C~D&E~F": 20, // labels2
|
||||||
|
"counter/": 40, // labels3
|
||||||
|
"gauge/G~H&C~D": 50, // labels1
|
||||||
|
"gauge/C~D&E~F": 20, // labels2
|
||||||
|
"gauge/": 30, // labels3
|
||||||
|
}, records)
|
||||||
|
|
||||||
|
// Verify that state was reset
|
||||||
|
checkpointSet = b.CheckpointSet()
|
||||||
|
b.FinishedCollection()
|
||||||
|
checkpointSet.ForEach(func(rec export.Record) {
|
||||||
|
t.Fatal("Unexpected call")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUngroupedStateful(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
b := ungrouped.New(test.NewAggregationSelector(), true)
|
||||||
|
|
||||||
|
cagg := test.CounterAgg(10)
|
||||||
|
_ = b.Process(ctx, export.NewRecord(test.CounterDesc, test.Labels1, cagg))
|
||||||
|
|
||||||
|
checkpointSet := b.CheckpointSet()
|
||||||
|
b.FinishedCollection()
|
||||||
|
|
||||||
|
records1 := test.Output{}
|
||||||
|
checkpointSet.ForEach(records1.AddTo)
|
||||||
|
|
||||||
|
require.EqualValues(t, map[string]int64{
|
||||||
|
"counter/G~H&C~D": 10, // labels1
|
||||||
|
}, records1)
|
||||||
|
|
||||||
|
// Test that state was NOT reset
|
||||||
|
checkpointSet = b.CheckpointSet()
|
||||||
|
b.FinishedCollection()
|
||||||
|
|
||||||
|
records2 := test.Output{}
|
||||||
|
checkpointSet.ForEach(records2.AddTo)
|
||||||
|
|
||||||
|
require.EqualValues(t, records1, records2)
|
||||||
|
|
||||||
|
// Update and re-checkpoint the original record.
|
||||||
|
_ = cagg.Update(ctx, core.NewInt64Number(20), test.CounterDesc)
|
||||||
|
cagg.Checkpoint(ctx, test.CounterDesc)
|
||||||
|
|
||||||
|
// As yet cagg has not been passed to Batcher.Process. Should
|
||||||
|
// not see an update.
|
||||||
|
checkpointSet = b.CheckpointSet()
|
||||||
|
b.FinishedCollection()
|
||||||
|
|
||||||
|
records3 := test.Output{}
|
||||||
|
checkpointSet.ForEach(records3.AddTo)
|
||||||
|
|
||||||
|
require.EqualValues(t, records1, records3)
|
||||||
|
|
||||||
|
// Now process the second update
|
||||||
|
_ = b.Process(ctx, export.NewRecord(test.CounterDesc, test.Labels1, cagg))
|
||||||
|
|
||||||
|
checkpointSet = b.CheckpointSet()
|
||||||
|
b.FinishedCollection()
|
||||||
|
|
||||||
|
records4 := test.Output{}
|
||||||
|
checkpointSet.ForEach(records4.AddTo)
|
||||||
|
|
||||||
|
require.EqualValues(t, map[string]int64{
|
||||||
|
"counter/G~H&C~D": 30,
|
||||||
|
}, records4)
|
||||||
|
}
|
@ -42,29 +42,37 @@ func newFixture(b *testing.B) *benchFixture {
|
|||||||
bf := &benchFixture{
|
bf := &benchFixture{
|
||||||
B: b,
|
B: b,
|
||||||
}
|
}
|
||||||
bf.sdk = sdk.New(bf)
|
bf.sdk = sdk.New(bf, sdk.DefaultLabelEncoder())
|
||||||
return bf
|
return bf
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bf *benchFixture) AggregatorFor(rec export.Record) export.Aggregator {
|
func (*benchFixture) AggregatorFor(descriptor *export.Descriptor) export.Aggregator {
|
||||||
switch rec.Descriptor().MetricKind() {
|
switch descriptor.MetricKind() {
|
||||||
case export.CounterKind:
|
case export.CounterKind:
|
||||||
return counter.New()
|
return counter.New()
|
||||||
case export.GaugeKind:
|
case export.GaugeKind:
|
||||||
return gauge.New()
|
return gauge.New()
|
||||||
case export.MeasureKind:
|
case export.MeasureKind:
|
||||||
if strings.HasSuffix(rec.Descriptor().Name(), "maxsumcount") {
|
if strings.HasSuffix(descriptor.Name(), "maxsumcount") {
|
||||||
return maxsumcount.New()
|
return maxsumcount.New()
|
||||||
} else if strings.HasSuffix(rec.Descriptor().Name(), "ddsketch") {
|
} else if strings.HasSuffix(descriptor.Name(), "ddsketch") {
|
||||||
return ddsketch.New(ddsketch.NewDefaultConfig(), rec.Descriptor())
|
return ddsketch.New(ddsketch.NewDefaultConfig(), descriptor)
|
||||||
} else if strings.HasSuffix(rec.Descriptor().Name(), "array") {
|
} else if strings.HasSuffix(descriptor.Name(), "array") {
|
||||||
return ddsketch.New(ddsketch.NewDefaultConfig(), rec.Descriptor())
|
return ddsketch.New(ddsketch.NewDefaultConfig(), descriptor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bf *benchFixture) Export(ctx context.Context, rec export.Record, agg export.Aggregator) {
|
func (*benchFixture) Process(context.Context, export.Record) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*benchFixture) CheckpointSet() export.CheckpointSet {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*benchFixture) FinishedCollection() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeLabelSets(n int) [][]core.KeyValue {
|
func makeLabelSets(n int) [][]core.KeyValue {
|
||||||
|
186
sdk/metric/controller/push/push.go
Normal file
186
sdk/metric/controller/push/push.go
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
// Copyright 2019, OpenTelemetry 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 push // import "go.opentelemetry.io/otel/sdk/metric/controller/push"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/api/metric"
|
||||||
|
export "go.opentelemetry.io/otel/sdk/export/metric"
|
||||||
|
sdk "go.opentelemetry.io/otel/sdk/metric"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Controller organizes a periodic push of metric data.
|
||||||
|
type Controller struct {
|
||||||
|
lock sync.Mutex
|
||||||
|
sdk *sdk.SDK
|
||||||
|
errorHandler sdk.ErrorHandler
|
||||||
|
batcher export.Batcher
|
||||||
|
exporter export.Exporter
|
||||||
|
wg sync.WaitGroup
|
||||||
|
ch chan struct{}
|
||||||
|
period time.Duration
|
||||||
|
ticker Ticker
|
||||||
|
clock Clock
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ metric.Provider = &Controller{}
|
||||||
|
|
||||||
|
// Several types below are created to match "github.com/benbjohnson/clock"
|
||||||
|
// so that it remains a test-only dependency.
|
||||||
|
|
||||||
|
type Clock interface {
|
||||||
|
Now() time.Time
|
||||||
|
Ticker(time.Duration) Ticker
|
||||||
|
}
|
||||||
|
|
||||||
|
type Ticker interface {
|
||||||
|
Stop()
|
||||||
|
C() <-chan time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type realClock struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
type realTicker struct {
|
||||||
|
ticker *time.Ticker
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Clock = realClock{}
|
||||||
|
var _ Ticker = realTicker{}
|
||||||
|
|
||||||
|
// New constructs a Controller, an implementation of metric.Provider,
|
||||||
|
// using the provided batcher, exporter, and collection period to
|
||||||
|
// configure an SDK with periodic collection. The batcher itself is
|
||||||
|
// configured with the aggregation selector policy.
|
||||||
|
//
|
||||||
|
// If the Exporter implements the export.LabelEncoder interface, the
|
||||||
|
// exporter will be used as the label encoder for the SDK itself,
|
||||||
|
// otherwise the SDK will be configured with the default label
|
||||||
|
// encoder.
|
||||||
|
func New(batcher export.Batcher, exporter export.Exporter, period time.Duration) *Controller {
|
||||||
|
lencoder, _ := exporter.(export.LabelEncoder)
|
||||||
|
|
||||||
|
if lencoder == nil {
|
||||||
|
lencoder = sdk.DefaultLabelEncoder()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Controller{
|
||||||
|
sdk: sdk.New(batcher, lencoder),
|
||||||
|
errorHandler: sdk.DefaultErrorHandler,
|
||||||
|
batcher: batcher,
|
||||||
|
exporter: exporter,
|
||||||
|
ch: make(chan struct{}),
|
||||||
|
period: period,
|
||||||
|
clock: realClock{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetClock supports setting a mock clock for testing. This must be
|
||||||
|
// called before Start().
|
||||||
|
func (c *Controller) SetClock(clock Clock) {
|
||||||
|
c.lock.Lock()
|
||||||
|
defer c.lock.Unlock()
|
||||||
|
c.clock = clock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) SetErrorHandler(errorHandler sdk.ErrorHandler) {
|
||||||
|
c.lock.Lock()
|
||||||
|
defer c.lock.Unlock()
|
||||||
|
c.errorHandler = errorHandler
|
||||||
|
c.sdk.SetErrorHandler(errorHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMeter returns a named Meter, satisifying the metric.Provider
|
||||||
|
// interface.
|
||||||
|
func (c *Controller) GetMeter(name string) metric.Meter {
|
||||||
|
return c.sdk
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start begins a ticker that periodically collects and exports
|
||||||
|
// metrics with the configured interval.
|
||||||
|
func (c *Controller) Start() {
|
||||||
|
c.lock.Lock()
|
||||||
|
defer c.lock.Unlock()
|
||||||
|
|
||||||
|
if c.ticker != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.ticker = c.clock.Ticker(c.period)
|
||||||
|
c.wg.Add(1)
|
||||||
|
go c.run(c.ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop waits for the background goroutine to return and then collects
|
||||||
|
// and exports metrics one last time before returning.
|
||||||
|
func (c *Controller) Stop() {
|
||||||
|
c.lock.Lock()
|
||||||
|
defer c.lock.Unlock()
|
||||||
|
|
||||||
|
if c.ch == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
close(c.ch)
|
||||||
|
c.ch = nil
|
||||||
|
c.wg.Wait()
|
||||||
|
c.ticker.Stop()
|
||||||
|
|
||||||
|
c.tick()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) run(ch chan struct{}) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ch:
|
||||||
|
c.wg.Done()
|
||||||
|
return
|
||||||
|
case <-c.ticker.C():
|
||||||
|
c.tick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) tick() {
|
||||||
|
// TODO: either remove the context argument from Export() or
|
||||||
|
// configure a timeout here?
|
||||||
|
ctx := context.Background()
|
||||||
|
c.sdk.Collect(ctx)
|
||||||
|
err := c.exporter.Export(ctx, c.batcher.CheckpointSet())
|
||||||
|
c.batcher.FinishedCollection()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.errorHandler(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (realClock) Now() time.Time {
|
||||||
|
return time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (realClock) Ticker(period time.Duration) Ticker {
|
||||||
|
return realTicker{time.NewTicker(period)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t realTicker) Stop() {
|
||||||
|
t.ticker.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t realTicker) C() <-chan time.Time {
|
||||||
|
return t.ticker.C
|
||||||
|
}
|
229
sdk/metric/controller/push/push_test.go
Normal file
229
sdk/metric/controller/push/push_test.go
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
// Copyright 2019, OpenTelemetry 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 push_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/benbjohnson/clock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/exporter/metric/test"
|
||||||
|
export "go.opentelemetry.io/otel/sdk/export/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/export/metric/aggregator"
|
||||||
|
sdk "go.opentelemetry.io/otel/sdk/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/aggregator/counter"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/controller/push"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testBatcher struct {
|
||||||
|
t *testing.T
|
||||||
|
checkpointSet *test.CheckpointSet
|
||||||
|
checkpoints int
|
||||||
|
finishes int
|
||||||
|
}
|
||||||
|
|
||||||
|
type testExporter struct {
|
||||||
|
t *testing.T
|
||||||
|
exports int
|
||||||
|
records []export.Record
|
||||||
|
retErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
type testFixture struct {
|
||||||
|
checkpointSet *test.CheckpointSet
|
||||||
|
batcher *testBatcher
|
||||||
|
exporter *testExporter
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockClock struct {
|
||||||
|
mock *clock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockTicker struct {
|
||||||
|
ticker *clock.Ticker
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ push.Clock = mockClock{}
|
||||||
|
var _ push.Ticker = mockTicker{}
|
||||||
|
|
||||||
|
func newFixture(t *testing.T) testFixture {
|
||||||
|
checkpointSet := test.NewCheckpointSet(sdk.DefaultLabelEncoder())
|
||||||
|
|
||||||
|
batcher := &testBatcher{
|
||||||
|
t: t,
|
||||||
|
checkpointSet: checkpointSet,
|
||||||
|
}
|
||||||
|
exporter := &testExporter{
|
||||||
|
t: t,
|
||||||
|
}
|
||||||
|
return testFixture{
|
||||||
|
checkpointSet: checkpointSet,
|
||||||
|
batcher: batcher,
|
||||||
|
exporter: exporter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *testBatcher) AggregatorFor(*export.Descriptor) export.Aggregator {
|
||||||
|
return counter.New()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *testBatcher) CheckpointSet() export.CheckpointSet {
|
||||||
|
b.checkpoints++
|
||||||
|
return b.checkpointSet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *testBatcher) FinishedCollection() {
|
||||||
|
b.finishes++
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *testBatcher) Process(_ context.Context, record export.Record) error {
|
||||||
|
b.checkpointSet.Add(record.Descriptor(), record.Aggregator(), record.Labels().Ordered()...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *testExporter) Export(_ context.Context, checkpointSet export.CheckpointSet) error {
|
||||||
|
e.exports++
|
||||||
|
checkpointSet.ForEach(func(r export.Record) {
|
||||||
|
e.records = append(e.records, r)
|
||||||
|
})
|
||||||
|
return e.retErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c mockClock) Now() time.Time {
|
||||||
|
return c.mock.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c mockClock) Ticker(period time.Duration) push.Ticker {
|
||||||
|
return mockTicker{c.mock.Ticker(period)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c mockClock) Add(d time.Duration) {
|
||||||
|
c.mock.Add(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t mockTicker) Stop() {
|
||||||
|
t.ticker.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t mockTicker) C() <-chan time.Time {
|
||||||
|
return t.ticker.C
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPushDoubleStop(t *testing.T) {
|
||||||
|
fix := newFixture(t)
|
||||||
|
p := push.New(fix.batcher, fix.exporter, time.Second)
|
||||||
|
p.Start()
|
||||||
|
p.Stop()
|
||||||
|
p.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPushDoubleStart(t *testing.T) {
|
||||||
|
fix := newFixture(t)
|
||||||
|
p := push.New(fix.batcher, fix.exporter, time.Second)
|
||||||
|
p.Start()
|
||||||
|
p.Start()
|
||||||
|
p.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPushTicker(t *testing.T) {
|
||||||
|
fix := newFixture(t)
|
||||||
|
|
||||||
|
p := push.New(fix.batcher, fix.exporter, time.Second)
|
||||||
|
meter := p.GetMeter("name")
|
||||||
|
|
||||||
|
mock := mockClock{clock.NewMock()}
|
||||||
|
p.SetClock(mock)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
counter := meter.NewInt64Counter("counter")
|
||||||
|
|
||||||
|
p.Start()
|
||||||
|
|
||||||
|
counter.Add(ctx, 3, meter.Labels())
|
||||||
|
|
||||||
|
require.Equal(t, 0, fix.batcher.checkpoints)
|
||||||
|
require.Equal(t, 0, fix.batcher.finishes)
|
||||||
|
require.Equal(t, 0, fix.exporter.exports)
|
||||||
|
require.Equal(t, 0, len(fix.exporter.records))
|
||||||
|
|
||||||
|
mock.Add(time.Second)
|
||||||
|
runtime.Gosched()
|
||||||
|
|
||||||
|
require.Equal(t, 1, fix.batcher.checkpoints)
|
||||||
|
require.Equal(t, 1, fix.exporter.exports)
|
||||||
|
require.Equal(t, 1, fix.batcher.finishes)
|
||||||
|
require.Equal(t, 1, len(fix.exporter.records))
|
||||||
|
require.Equal(t, "counter", fix.exporter.records[0].Descriptor().Name())
|
||||||
|
|
||||||
|
sum, err := fix.exporter.records[0].Aggregator().(aggregator.Sum).Sum()
|
||||||
|
require.Equal(t, int64(3), sum.AsInt64())
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
fix.checkpointSet.Reset()
|
||||||
|
fix.exporter.records = nil
|
||||||
|
|
||||||
|
counter.Add(ctx, 7, meter.Labels())
|
||||||
|
|
||||||
|
mock.Add(time.Second)
|
||||||
|
runtime.Gosched()
|
||||||
|
|
||||||
|
require.Equal(t, 2, fix.batcher.checkpoints)
|
||||||
|
require.Equal(t, 2, fix.batcher.finishes)
|
||||||
|
require.Equal(t, 2, fix.exporter.exports)
|
||||||
|
require.Equal(t, 1, len(fix.exporter.records))
|
||||||
|
require.Equal(t, "counter", fix.exporter.records[0].Descriptor().Name())
|
||||||
|
|
||||||
|
sum, err = fix.exporter.records[0].Aggregator().(aggregator.Sum).Sum()
|
||||||
|
require.Equal(t, int64(7), sum.AsInt64())
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
p.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPushExportError(t *testing.T) {
|
||||||
|
fix := newFixture(t)
|
||||||
|
fix.exporter.retErr = fmt.Errorf("Test export error")
|
||||||
|
|
||||||
|
p := push.New(fix.batcher, fix.exporter, time.Second)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
p.SetErrorHandler(func(sdkErr error) {
|
||||||
|
err = sdkErr
|
||||||
|
})
|
||||||
|
|
||||||
|
mock := mockClock{clock.NewMock()}
|
||||||
|
p.SetClock(mock)
|
||||||
|
|
||||||
|
p.Start()
|
||||||
|
runtime.Gosched()
|
||||||
|
|
||||||
|
require.Equal(t, 0, fix.exporter.exports)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
mock.Add(time.Second)
|
||||||
|
runtime.Gosched()
|
||||||
|
|
||||||
|
require.Equal(t, 1, fix.exporter.exports)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Equal(t, fix.exporter.retErr, err)
|
||||||
|
|
||||||
|
p.Stop()
|
||||||
|
}
|
194
sdk/metric/correct_test.go
Normal file
194
sdk/metric/correct_test.go
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
// Copyright 2019, OpenTelemetry 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 metric_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/api/core"
|
||||||
|
"go.opentelemetry.io/otel/api/key"
|
||||||
|
"go.opentelemetry.io/otel/api/metric"
|
||||||
|
export "go.opentelemetry.io/otel/sdk/export/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/export/metric/aggregator"
|
||||||
|
sdk "go.opentelemetry.io/otel/sdk/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/aggregator/array"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/aggregator/counter"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/aggregator/gauge"
|
||||||
|
)
|
||||||
|
|
||||||
|
type correctnessBatcher struct {
|
||||||
|
t *testing.T
|
||||||
|
agg export.Aggregator
|
||||||
|
records []export.Record
|
||||||
|
}
|
||||||
|
|
||||||
|
type testLabelEncoder struct{}
|
||||||
|
|
||||||
|
func (cb *correctnessBatcher) AggregatorFor(*export.Descriptor) export.Aggregator {
|
||||||
|
return cb.agg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cb *correctnessBatcher) CheckpointSet() export.CheckpointSet {
|
||||||
|
cb.t.Fatal("Should not be called")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*correctnessBatcher) FinishedCollection() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cb *correctnessBatcher) Process(_ context.Context, record export.Record) error {
|
||||||
|
cb.records = append(cb.records, record)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (testLabelEncoder) Encode(labels []core.KeyValue) string {
|
||||||
|
return fmt.Sprint(labels)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInputRangeTestCounter(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
cagg := counter.New()
|
||||||
|
batcher := &correctnessBatcher{
|
||||||
|
t: t,
|
||||||
|
agg: cagg,
|
||||||
|
}
|
||||||
|
sdk := sdk.New(batcher, sdk.DefaultLabelEncoder())
|
||||||
|
|
||||||
|
var sdkErr error
|
||||||
|
sdk.SetErrorHandler(func(handleErr error) {
|
||||||
|
sdkErr = handleErr
|
||||||
|
})
|
||||||
|
|
||||||
|
counter := sdk.NewInt64Counter("counter.name", metric.WithMonotonic(true))
|
||||||
|
|
||||||
|
counter.Add(ctx, -1, sdk.Labels())
|
||||||
|
require.Equal(t, aggregator.ErrNegativeInput, sdkErr)
|
||||||
|
sdkErr = nil
|
||||||
|
|
||||||
|
sdk.Collect(ctx)
|
||||||
|
sum, err := cagg.Sum()
|
||||||
|
require.Equal(t, int64(0), sum.AsInt64())
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
counter.Add(ctx, 1, sdk.Labels())
|
||||||
|
checkpointed := sdk.Collect(ctx)
|
||||||
|
|
||||||
|
sum, err = cagg.Sum()
|
||||||
|
require.Equal(t, int64(1), sum.AsInt64())
|
||||||
|
require.Equal(t, 1, checkpointed)
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Nil(t, sdkErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInputRangeTestMeasure(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
magg := array.New()
|
||||||
|
batcher := &correctnessBatcher{
|
||||||
|
t: t,
|
||||||
|
agg: magg,
|
||||||
|
}
|
||||||
|
sdk := sdk.New(batcher, sdk.DefaultLabelEncoder())
|
||||||
|
|
||||||
|
var sdkErr error
|
||||||
|
sdk.SetErrorHandler(func(handleErr error) {
|
||||||
|
sdkErr = handleErr
|
||||||
|
})
|
||||||
|
|
||||||
|
measure := sdk.NewFloat64Measure("measure.name", metric.WithAbsolute(true))
|
||||||
|
|
||||||
|
measure.Record(ctx, -1, sdk.Labels())
|
||||||
|
require.Equal(t, aggregator.ErrNegativeInput, sdkErr)
|
||||||
|
sdkErr = nil
|
||||||
|
|
||||||
|
sdk.Collect(ctx)
|
||||||
|
count, err := magg.Count()
|
||||||
|
require.Equal(t, int64(0), count)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
measure.Record(ctx, 1, sdk.Labels())
|
||||||
|
measure.Record(ctx, 2, sdk.Labels())
|
||||||
|
checkpointed := sdk.Collect(ctx)
|
||||||
|
|
||||||
|
count, err = magg.Count()
|
||||||
|
require.Equal(t, int64(2), count)
|
||||||
|
require.Equal(t, 1, checkpointed)
|
||||||
|
require.Nil(t, sdkErr)
|
||||||
|
require.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDisabledInstrument(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
batcher := &correctnessBatcher{
|
||||||
|
t: t,
|
||||||
|
agg: nil,
|
||||||
|
}
|
||||||
|
sdk := sdk.New(batcher, sdk.DefaultLabelEncoder())
|
||||||
|
measure := sdk.NewFloat64Measure("measure.name", metric.WithAbsolute(true))
|
||||||
|
|
||||||
|
measure.Record(ctx, -1, sdk.Labels())
|
||||||
|
checkpointed := sdk.Collect(ctx)
|
||||||
|
|
||||||
|
require.Equal(t, 0, checkpointed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRecordNaN(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
batcher := &correctnessBatcher{
|
||||||
|
t: t,
|
||||||
|
agg: gauge.New(),
|
||||||
|
}
|
||||||
|
sdk := sdk.New(batcher, sdk.DefaultLabelEncoder())
|
||||||
|
|
||||||
|
var sdkErr error
|
||||||
|
sdk.SetErrorHandler(func(handleErr error) {
|
||||||
|
sdkErr = handleErr
|
||||||
|
})
|
||||||
|
g := sdk.NewFloat64Gauge("gauge.name")
|
||||||
|
|
||||||
|
require.Nil(t, sdkErr)
|
||||||
|
g.Set(ctx, math.NaN(), sdk.Labels())
|
||||||
|
require.Error(t, sdkErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSDKLabelEncoder(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
cagg := counter.New()
|
||||||
|
batcher := &correctnessBatcher{
|
||||||
|
t: t,
|
||||||
|
agg: cagg,
|
||||||
|
}
|
||||||
|
sdk := sdk.New(batcher, testLabelEncoder{})
|
||||||
|
|
||||||
|
measure := sdk.NewFloat64Measure("measure")
|
||||||
|
measure.Record(ctx, 1, sdk.Labels(key.String("A", "B"), key.String("C", "D")))
|
||||||
|
|
||||||
|
sdk.Collect(ctx)
|
||||||
|
|
||||||
|
require.Equal(t, 1, len(batcher.records))
|
||||||
|
|
||||||
|
labels := batcher.records[0].Labels()
|
||||||
|
require.Equal(t, `[{A {8 0 B}} {C {8 0 D}}]`, labels.Encoded())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultLabelEncoder(t *testing.T) {
|
||||||
|
encoder := sdk.DefaultLabelEncoder()
|
||||||
|
encoded := encoder.Encode([]core.KeyValue{key.String("A", "B"), key.String("C", "D")})
|
||||||
|
require.Equal(t, `A=B,C=D`, encoded)
|
||||||
|
}
|
@ -13,48 +13,157 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
Package metric implements the OpenTelemetry metric.Meter API. The SDK
|
||||||
|
supports configurable metrics export behavior through a collection of
|
||||||
|
export interfaces that support various export strategies, described below.
|
||||||
|
|
||||||
Package metric implements the OpenTelemetry `Meter` API. The SDK
|
The metric.Meter API consists of methods for constructing each of the
|
||||||
supports configurable metrics export behavior through a
|
basic kinds of metric instrument. There are six types of instrument
|
||||||
`export.MetricBatcher` API. Most metrics behavior is controlled
|
available to the end user, comprised of three basic kinds of metric
|
||||||
by the `MetricBatcher`, including:
|
instrument (Counter, Gauge, Measure) crossed with two kinds of number
|
||||||
|
(int64, float64).
|
||||||
|
|
||||||
1. Selecting the concrete type of aggregation to use
|
The API assists the SDK by consolidating the variety of metric instruments
|
||||||
2. Receiving exported data during SDK.Collect()
|
into a narrower interface, allowing the SDK to avoid repetition of
|
||||||
|
boilerplate. The API and SDK are separated such that an event reaching
|
||||||
|
the SDK has a uniform structure: an instrument, a label set, and a
|
||||||
|
numerical value.
|
||||||
|
|
||||||
The call to SDK.Collect() initiates collection. The SDK calls the
|
To this end, the API uses a core.Number type to represent either an int64
|
||||||
`MetricBatcher` for each current record, asking the aggregator to
|
or a float64, depending on the instrument's definition. A single
|
||||||
export itself. Aggregators, found in `./aggregators`, are responsible
|
implementation interface is used for instruments, metric.InstrumentImpl,
|
||||||
for receiving updates and exporting their current state.
|
and a single implementation interface is used for handles,
|
||||||
|
metric.HandleImpl.
|
||||||
|
|
||||||
The SDK.Collect() API should be called by an exporter. During the
|
There are three entry points for events in the Metrics API: via instrument
|
||||||
call to Collect(), the exporter receives calls in a single-threaded
|
handles, via direct instrument calls, and via BatchRecord. The SDK is
|
||||||
context. No locking is required because the SDK.Collect() call
|
designed with handles as the primary entry point, the other two entry
|
||||||
prevents concurrency.
|
points are implemented in terms of short-lived handles. For example, the
|
||||||
|
implementation of a direct call allocates a handle, operates on the
|
||||||
|
handle, and releases the handle. Similarly, the implementation of
|
||||||
|
RecordBatch uses a short-lived handle for each measurement in the batch.
|
||||||
|
|
||||||
The SDK uses lock-free algorithms to maintain its internal state.
|
Internal Structure
|
||||||
There are three central data structures at work:
|
|
||||||
|
|
||||||
1. A sync.Map maps unique (InstrumentID, LabelSet) to records
|
The SDK is designed with minimal use of locking, to avoid adding
|
||||||
2. A "primary" atomic list of records
|
contention for user-level code. For each handle, whether it is held by
|
||||||
3. A "reclaim" atomic list of records
|
user-level code or a short-lived device, there exists an internal record
|
||||||
|
managed by the SDK. Each internal record corresponds to a specific
|
||||||
|
instrument and label set combination.
|
||||||
|
|
||||||
Collection is oriented around epochs. The SDK internally has a
|
A sync.Map maintains the mapping of current instruments and label sets to
|
||||||
notion of the "current" epoch, which is incremented each time
|
internal records. To create a new handle, the SDK consults the Map to
|
||||||
Collect() is called. Records contain two atomic counter values,
|
locate an existing record, otherwise it constructs a new record. The SDK
|
||||||
the epoch in which it was last modified and the epoch in which it
|
maintains a count of the number of references to each record, ensuring
|
||||||
was last collected. Records may be garbage collected when the
|
that records are not reclaimed from the Map while they are still active
|
||||||
epoch in which they were last updated is less than the epoch in
|
from the user's perspective.
|
||||||
which they were last collected.
|
|
||||||
|
|
||||||
Collect() performs a record-by-record scan of all active records
|
Metric collection is performed via a single-threaded call to Collect that
|
||||||
and exports their current state, before incrementing the current
|
sweeps through all records in the SDK, checkpointing their state. When a
|
||||||
epoch. Collection events happen at a point in time during
|
record is discovered that has no references and has not been updated since
|
||||||
`Collect()`, but all records are not collected in the same instant.
|
the prior collection pass, it is marked for reclamation and removed from
|
||||||
|
the Map. There exists, at this moment, a race condition since another
|
||||||
|
goroutine could, in the same instant, obtain a reference to the handle.
|
||||||
|
|
||||||
|
The SDK is designed to tolerate this sort of race condition, in the name
|
||||||
|
of reducing lock contention. It is possible for more than one record with
|
||||||
|
identical instrument and label set to exist simultaneously, though only
|
||||||
|
one can be linked from the Map at a time. To avoid lost updates, the SDK
|
||||||
|
maintains two additional linked lists of records, one managed by the
|
||||||
|
collection code path and one managed by the instrumentation code path.
|
||||||
|
|
||||||
|
The SDK maintains a current epoch number, corresponding to the number of
|
||||||
|
completed collections. Each record contains the last epoch during which
|
||||||
|
it was collected and updated. These variables allow the collection code
|
||||||
|
path to detect stale records while allowing the instrumentation code path
|
||||||
|
to detect potential reclamations. When the instrumentation code path
|
||||||
|
detects a potential reclamation, it adds itself to the second linked list,
|
||||||
|
where records are saved from reclamation.
|
||||||
|
|
||||||
|
Each record has an associated aggregator, which maintains the current
|
||||||
|
state resulting from all metric events since its last checkpoint.
|
||||||
|
Aggregators may be lock-free or they may use locking, but they should
|
||||||
|
expect to be called concurrently. Because of the tolerated race condition
|
||||||
|
described above, aggregators must be capable of merging with another
|
||||||
|
aggregator of the same type.
|
||||||
|
|
||||||
|
Export Pipeline
|
||||||
|
|
||||||
|
While the SDK serves to maintain a current set of records and
|
||||||
|
coordinate collection, the behavior of a metrics export pipeline is
|
||||||
|
configured through the export types in
|
||||||
|
go.opentelemetry.io/otel/sdk/export/metric. It is important to keep
|
||||||
|
in mind the context these interfaces are called from. There are two
|
||||||
|
contexts, instrumentation context, where a user-level goroutine that
|
||||||
|
enters the SDK resulting in a new record, and collection context,
|
||||||
|
where a system-level thread performs a collection pass through the
|
||||||
|
SDK.
|
||||||
|
|
||||||
|
Descriptor is a struct that describes the metric instrument to the
|
||||||
|
export pipeline, containing the name, recommended aggregation keys,
|
||||||
|
units, description, metric kind (counter, gauge, or measure), number
|
||||||
|
kind (int64 or float64), and whether the instrument has alternate
|
||||||
|
semantics or not (i.e., monotonic=false counter, monotonic=true gauge,
|
||||||
|
absolute=false measure). A Descriptor accompanies metric data as it
|
||||||
|
passes through the export pipeline.
|
||||||
|
|
||||||
|
The AggregationSelector interface supports choosing the method of
|
||||||
|
aggregation to apply to a particular instrument. Given the
|
||||||
|
Descriptor, this AggregatorFor method returns an implementation of
|
||||||
|
Aggregator. If this interface returns nil, the metric will be
|
||||||
|
disabled. The aggregator should be matched to the capabilities of the
|
||||||
|
exporter. Selecting the aggregator for counter and gauge instruments
|
||||||
|
is relatively straightforward, but for measure instruments there are
|
||||||
|
numerous choices with different cost and quality tradeoffs.
|
||||||
|
|
||||||
|
Aggregator is an interface which implements a concrete strategy for
|
||||||
|
aggregating metric updates. Several Aggregator implementations are
|
||||||
|
provided by the SDK. Aggregators may be lock-free or use locking,
|
||||||
|
depending on their structure and semantics. Aggregators implement an
|
||||||
|
Update method, called in instrumentation context, to receive a single
|
||||||
|
metric event. Aggregators implement a Checkpoint method, called in
|
||||||
|
collection context, to save a checkpoint of the current state.
|
||||||
|
Aggregators implement a Merge method, also called in collection
|
||||||
|
context, that combines state from two aggregators into one. Each SDK
|
||||||
|
record has an associated aggregator.
|
||||||
|
|
||||||
|
Batcher is an interface which sits between the SDK and an exporter.
|
||||||
|
The Batcher embeds an AggregationSelector, used by the SDK to assign
|
||||||
|
new Aggregators. The Batcher supports a Process() API for submitting
|
||||||
|
checkpointed aggregators to the batcher, and a CheckpointSet() API
|
||||||
|
for producing a complete checkpoint for the exporter. Two default
|
||||||
|
Batcher implementations are provided, the "defaultkeys" Batcher groups
|
||||||
|
aggregate metrics by their recommended Descriptor.Keys(), the
|
||||||
|
"ungrouped" Batcher aggregates metrics at full dimensionality.
|
||||||
|
|
||||||
|
LabelEncoder is an optional optimization that allows an exporter to
|
||||||
|
provide the serialization logic for labels. This allows avoiding
|
||||||
|
duplicate serialization of labels, once as a unique key in the SDK (or
|
||||||
|
Batcher) and once in the exporter.
|
||||||
|
|
||||||
|
CheckpointSet is an interface between the Batcher and the Exporter.
|
||||||
|
After completing a collection pass, the Batcher.CheckpointSet() method
|
||||||
|
returns a CheckpointSet, which the Exporter uses to iterate over all
|
||||||
|
the updated metrics.
|
||||||
|
|
||||||
|
Record is a struct containing the state of an individual exported
|
||||||
|
metric. This is the result of one collection interface for one
|
||||||
|
instrument and one label set.
|
||||||
|
|
||||||
|
Labels is a struct containing an ordered set of labels, the
|
||||||
|
corresponding unique encoding, and the encoder that produced it.
|
||||||
|
|
||||||
|
Exporter is the final stage of an export pipeline. It is called with
|
||||||
|
a CheckpointSet capable of enumerating all the updated metrics.
|
||||||
|
|
||||||
|
Controller is not an export interface per se, but it orchestrates the
|
||||||
|
export pipeline. For example, a "push" controller will establish a
|
||||||
|
periodic timer to regularly collect and export metrics. A "pull"
|
||||||
|
controller will await a pull request before initiating metric
|
||||||
|
collection. Either way, the job of the controller is to call the SDK
|
||||||
|
Collect() method, then read the checkpoint, then invoke the exporter.
|
||||||
|
Controllers are expected to implement the public metric.MeterProvider
|
||||||
|
API, meaning they can be installed as the global Meter provider.
|
||||||
|
|
||||||
The purpose of the two lists: the primary list is appended-to when
|
|
||||||
new handles are created and atomically cleared during collect. The
|
|
||||||
reclaim list is used as a second chance, in case there is a race
|
|
||||||
between looking up a record and record deletion.
|
|
||||||
*/
|
*/
|
||||||
package metric
|
package metric // import "go.opentelemetry.io/otel/sdk/metric"
|
||||||
|
64
sdk/metric/example_test.go
Normal file
64
sdk/metric/example_test.go
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
// Copyright 2019, OpenTelemetry 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 metric_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/api/key"
|
||||||
|
"go.opentelemetry.io/otel/api/metric"
|
||||||
|
"go.opentelemetry.io/otel/exporter/metric/stdout"
|
||||||
|
sdk "go.opentelemetry.io/otel/sdk/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/batcher/defaultkeys"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/controller/push"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/selector/simple"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleNew() {
|
||||||
|
selector := simple.NewWithInexpensiveMeasure()
|
||||||
|
exporter, err := stdout.New(stdout.Options{
|
||||||
|
PrettyPrint: true,
|
||||||
|
DoNotPrintTime: true, // This makes the output deterministic
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintln("Could not initialize stdout exporter:", err))
|
||||||
|
}
|
||||||
|
batcher := defaultkeys.New(selector, sdk.DefaultLabelEncoder(), true)
|
||||||
|
pusher := push.New(batcher, exporter, time.Second)
|
||||||
|
pusher.Start()
|
||||||
|
defer pusher.Stop()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
key := key.New("key")
|
||||||
|
meter := pusher.GetMeter("example")
|
||||||
|
|
||||||
|
counter := meter.NewInt64Counter("a.counter", metric.WithKeys(key))
|
||||||
|
labels := meter.Labels(key.String("value"))
|
||||||
|
|
||||||
|
counter.Add(ctx, 100, labels)
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// {
|
||||||
|
// "updates": [
|
||||||
|
// {
|
||||||
|
// "name": "a.counter{key=value}",
|
||||||
|
// "sum": 100
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
}
|
62
sdk/metric/labelencoder.go
Normal file
62
sdk/metric/labelencoder.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
// Copyright 2019, OpenTelemetry 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 metric
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/api/core"
|
||||||
|
export "go.opentelemetry.io/otel/sdk/export/metric"
|
||||||
|
)
|
||||||
|
|
||||||
|
type defaultLabelEncoder struct {
|
||||||
|
// pool is a pool of labelset builders. The buffers in this
|
||||||
|
// pool grow to a size that most label encodings will not
|
||||||
|
// allocate new memory. This pool reduces the number of
|
||||||
|
// allocations per new LabelSet to 3, typically, as seen in
|
||||||
|
// the benchmarks. (It should be 2--one for the LabelSet
|
||||||
|
// object and one for the buffer.String() here--see the extra
|
||||||
|
// allocation in the call to sort.Stable).
|
||||||
|
pool sync.Pool // *bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ export.LabelEncoder = &defaultLabelEncoder{}
|
||||||
|
|
||||||
|
func DefaultLabelEncoder() export.LabelEncoder {
|
||||||
|
return &defaultLabelEncoder{
|
||||||
|
pool: sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
return &bytes.Buffer{}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *defaultLabelEncoder) Encode(labels []core.KeyValue) string {
|
||||||
|
buf := d.pool.Get().(*bytes.Buffer)
|
||||||
|
defer d.pool.Put(buf)
|
||||||
|
buf.Reset()
|
||||||
|
|
||||||
|
for i, kv := range labels {
|
||||||
|
if i > 0 {
|
||||||
|
_, _ = buf.WriteRune(',')
|
||||||
|
}
|
||||||
|
_, _ = buf.WriteString(string(kv.Key))
|
||||||
|
_, _ = buf.WriteRune('=')
|
||||||
|
_, _ = buf.WriteString(kv.Value.Emit())
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
@ -25,6 +25,7 @@ import (
|
|||||||
"go.opentelemetry.io/otel/api/key"
|
"go.opentelemetry.io/otel/api/key"
|
||||||
"go.opentelemetry.io/otel/api/metric"
|
"go.opentelemetry.io/otel/api/metric"
|
||||||
export "go.opentelemetry.io/otel/sdk/export/metric"
|
export "go.opentelemetry.io/otel/sdk/export/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/export/metric/aggregator"
|
||||||
sdk "go.opentelemetry.io/otel/sdk/metric"
|
sdk "go.opentelemetry.io/otel/sdk/metric"
|
||||||
"go.opentelemetry.io/otel/sdk/metric/aggregator/gauge"
|
"go.opentelemetry.io/otel/sdk/metric/aggregator/gauge"
|
||||||
)
|
)
|
||||||
@ -37,23 +38,31 @@ type monotoneBatcher struct {
|
|||||||
currentTime *time.Time
|
currentTime *time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *monotoneBatcher) AggregatorFor(rec export.Record) export.Aggregator {
|
func (*monotoneBatcher) AggregatorFor(*export.Descriptor) export.Aggregator {
|
||||||
return gauge.New()
|
return gauge.New()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *monotoneBatcher) Export(_ context.Context, record export.Record, agg export.Aggregator) {
|
func (*monotoneBatcher) CheckpointSet() export.CheckpointSet {
|
||||||
require.Equal(m.t, "my.gauge.name", record.Descriptor().Name())
|
return nil
|
||||||
require.Equal(m.t, 1, len(record.Labels()))
|
}
|
||||||
require.Equal(m.t, "a", string(record.Labels()[0].Key))
|
|
||||||
require.Equal(m.t, "b", record.Labels()[0].Value.Emit())
|
|
||||||
|
|
||||||
gauge := agg.(*gauge.Aggregator)
|
func (*monotoneBatcher) FinishedCollection() {
|
||||||
val := gauge.AsNumber()
|
}
|
||||||
ts := gauge.Timestamp()
|
|
||||||
|
func (m *monotoneBatcher) Process(_ context.Context, record export.Record) error {
|
||||||
|
require.Equal(m.t, "my.gauge.name", record.Descriptor().Name())
|
||||||
|
require.Equal(m.t, 1, record.Labels().Len())
|
||||||
|
require.Equal(m.t, "a", string(record.Labels().Ordered()[0].Key))
|
||||||
|
require.Equal(m.t, "b", record.Labels().Ordered()[0].Value.Emit())
|
||||||
|
|
||||||
|
gauge := record.Aggregator().(*gauge.Aggregator)
|
||||||
|
val, ts, err := gauge.LastValue()
|
||||||
|
require.Nil(m.t, err)
|
||||||
|
|
||||||
m.currentValue = &val
|
m.currentValue = &val
|
||||||
m.currentTime = &ts
|
m.currentTime = &ts
|
||||||
m.collections++
|
m.collections++
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMonotoneGauge(t *testing.T) {
|
func TestMonotoneGauge(t *testing.T) {
|
||||||
@ -61,7 +70,9 @@ func TestMonotoneGauge(t *testing.T) {
|
|||||||
batcher := &monotoneBatcher{
|
batcher := &monotoneBatcher{
|
||||||
t: t,
|
t: t,
|
||||||
}
|
}
|
||||||
sdk := sdk.New(batcher)
|
sdk := sdk.New(batcher, sdk.DefaultLabelEncoder())
|
||||||
|
|
||||||
|
sdk.SetErrorHandler(func(error) { t.Fatal("Unexpected") })
|
||||||
|
|
||||||
gauge := sdk.NewInt64Gauge("my.gauge.name", metric.WithMonotonic(true))
|
gauge := sdk.NewInt64Gauge("my.gauge.name", metric.WithMonotonic(true))
|
||||||
|
|
||||||
@ -106,7 +117,14 @@ func TestMonotoneGauge(t *testing.T) {
|
|||||||
require.Equal(t, 4, batcher.collections)
|
require.Equal(t, 4, batcher.collections)
|
||||||
|
|
||||||
// Try to lower the value to 1, it will fail.
|
// Try to lower the value to 1, it will fail.
|
||||||
|
var err error
|
||||||
|
sdk.SetErrorHandler(func(sdkErr error) {
|
||||||
|
err = sdkErr
|
||||||
|
})
|
||||||
handle.Set(ctx, 1)
|
handle.Set(ctx, 1)
|
||||||
|
require.Equal(t, aggregator.ErrNonMonotoneInput, err)
|
||||||
|
sdk.SetErrorHandler(func(error) { t.Fatal("Unexpected") })
|
||||||
|
|
||||||
sdk.Collect(ctx)
|
sdk.Collect(ctx)
|
||||||
|
|
||||||
// The value and timestamp are both unmodified
|
// The value and timestamp are both unmodified
|
||||||
|
@ -15,8 +15,9 @@
|
|||||||
package metric
|
package metric
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@ -26,24 +27,22 @@ import (
|
|||||||
"go.opentelemetry.io/otel/api/metric"
|
"go.opentelemetry.io/otel/api/metric"
|
||||||
api "go.opentelemetry.io/otel/api/metric"
|
api "go.opentelemetry.io/otel/api/metric"
|
||||||
export "go.opentelemetry.io/otel/sdk/export/metric"
|
export "go.opentelemetry.io/otel/sdk/export/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/export/metric/aggregator"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// SDK implements the OpenTelemetry Meter API. The SDK is
|
// SDK implements the OpenTelemetry Meter API. The SDK is
|
||||||
// bound to a single export.MetricBatcher in `New()`.
|
// bound to a single export.Batcher in `New()`.
|
||||||
//
|
//
|
||||||
// The SDK supports a Collect() API to gather and export
|
// The SDK supports a Collect() API to gather and export
|
||||||
// current data. Collect() should be arranged according to
|
// current data. Collect() should be arranged according to
|
||||||
// the exporter model. Push-based exporters will setup a
|
// the batcher model. Push-based batchers will setup a
|
||||||
// timer to call Collect() periodically. Pull-based exporters
|
// timer to call Collect() periodically. Pull-based batchers
|
||||||
// will call Collect() when a pull request arrives.
|
// will call Collect() when a pull request arrives.
|
||||||
SDK struct {
|
SDK struct {
|
||||||
// current maps `mapkey` to *record.
|
// current maps `mapkey` to *record.
|
||||||
current sync.Map
|
current sync.Map
|
||||||
|
|
||||||
// pool is a pool of labelset builders.
|
|
||||||
pool sync.Pool // *bytes.Buffer
|
|
||||||
|
|
||||||
// empty is the (singleton) result of Labels()
|
// empty is the (singleton) result of Labels()
|
||||||
// w/ zero arguments.
|
// w/ zero arguments.
|
||||||
empty labels
|
empty labels
|
||||||
@ -56,11 +55,17 @@ type (
|
|||||||
// incremented in `Collect()`.
|
// incremented in `Collect()`.
|
||||||
currentEpoch int64
|
currentEpoch int64
|
||||||
|
|
||||||
// exporter is the configured exporter+configuration.
|
// batcher is the configured batcher+configuration.
|
||||||
exporter export.Batcher
|
batcher export.Batcher
|
||||||
|
|
||||||
|
// lencoder determines how labels are uniquely encoded.
|
||||||
|
labelEncoder export.LabelEncoder
|
||||||
|
|
||||||
// collectLock prevents simultaneous calls to Collect().
|
// collectLock prevents simultaneous calls to Collect().
|
||||||
collectLock sync.Mutex
|
collectLock sync.Mutex
|
||||||
|
|
||||||
|
// errorHandler supports delivering errors to the user.
|
||||||
|
errorHandler ErrorHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
instrument struct {
|
instrument struct {
|
||||||
@ -127,6 +132,8 @@ type (
|
|||||||
next doublePtr
|
next doublePtr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ErrorHandler func(error)
|
||||||
|
|
||||||
// singlePointer wraps an unsafe.Pointer and supports basic
|
// singlePointer wraps an unsafe.Pointer and supports basic
|
||||||
// load(), store(), clear(), and swapNil() operations.
|
// load(), store(), clear(), and swapNil() operations.
|
||||||
singlePtr struct {
|
singlePtr struct {
|
||||||
@ -145,7 +152,6 @@ var (
|
|||||||
_ api.LabelSet = &labels{}
|
_ api.LabelSet = &labels{}
|
||||||
_ api.InstrumentImpl = &instrument{}
|
_ api.InstrumentImpl = &instrument{}
|
||||||
_ api.HandleImpl = &record{}
|
_ api.HandleImpl = &record{}
|
||||||
_ export.Record = &record{}
|
|
||||||
|
|
||||||
// hazardRecord is used as a pointer value that indicates the
|
// hazardRecord is used as a pointer value that indicates the
|
||||||
// value is not included in any list. (`nil` would be
|
// value is not included in any list. (`nil` would be
|
||||||
@ -158,6 +164,10 @@ func (i *instrument) Meter() api.Meter {
|
|||||||
return i.meter
|
return i.meter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *SDK) SetErrorHandler(f ErrorHandler) {
|
||||||
|
m.errorHandler = f
|
||||||
|
}
|
||||||
|
|
||||||
func (i *instrument) acquireHandle(ls *labels) *record {
|
func (i *instrument) acquireHandle(ls *labels) *record {
|
||||||
// Create lookup key for sync.Map (one allocation)
|
// Create lookup key for sync.Map (one allocation)
|
||||||
mk := mapkey{
|
mk := mapkey{
|
||||||
@ -179,10 +189,9 @@ func (i *instrument) acquireHandle(ls *labels) *record {
|
|||||||
refcount: 1,
|
refcount: 1,
|
||||||
collectedEpoch: -1,
|
collectedEpoch: -1,
|
||||||
modifiedEpoch: 0,
|
modifiedEpoch: 0,
|
||||||
|
recorder: i.meter.batcher.AggregatorFor(i.descriptor),
|
||||||
}
|
}
|
||||||
|
|
||||||
rec.recorder = i.meter.exporter.AggregatorFor(rec)
|
|
||||||
|
|
||||||
// Load/Store: there's a memory allocation to place `mk` into
|
// Load/Store: there's a memory allocation to place `mk` into
|
||||||
// an interface here.
|
// an interface here.
|
||||||
if actual, loaded := i.meter.current.LoadOrStore(mk, rec); loaded {
|
if actual, loaded := i.meter.current.LoadOrStore(mk, rec); loaded {
|
||||||
@ -208,37 +217,38 @@ func (i *instrument) RecordOne(ctx context.Context, number core.Number, ls api.L
|
|||||||
h.RecordOne(ctx, number)
|
h.RecordOne(ctx, number)
|
||||||
}
|
}
|
||||||
|
|
||||||
// New constructs a new SDK for the given exporter. This SDK supports
|
// New constructs a new SDK for the given batcher. This SDK supports
|
||||||
// only a single exporter.
|
// only a single batcher.
|
||||||
//
|
//
|
||||||
// The SDK does not start any background process to collect itself
|
// The SDK does not start any background process to collect itself
|
||||||
// periodically, this responsbility lies with the exporter, typically,
|
// periodically, this responsbility lies with the batcher, typically,
|
||||||
// depending on the type of export. For example, a pull-based
|
// depending on the type of export. For example, a pull-based
|
||||||
// exporter will call Collect() when it receives a request to scrape
|
// batcher will call Collect() when it receives a request to scrape
|
||||||
// current metric values. A push-based exporter should configure its
|
// current metric values. A push-based batcher should configure its
|
||||||
// own periodic collection.
|
// own periodic collection.
|
||||||
func New(exporter export.Batcher) *SDK {
|
func New(batcher export.Batcher, labelEncoder export.LabelEncoder) *SDK {
|
||||||
m := &SDK{
|
m := &SDK{
|
||||||
pool: sync.Pool{
|
batcher: batcher,
|
||||||
New: func() interface{} {
|
labelEncoder: labelEncoder,
|
||||||
return &bytes.Buffer{}
|
errorHandler: DefaultErrorHandler,
|
||||||
},
|
|
||||||
},
|
|
||||||
exporter: exporter,
|
|
||||||
}
|
}
|
||||||
m.empty.meter = m
|
m.empty.meter = m
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DefaultErrorHandler(err error) {
|
||||||
|
fmt.Fprintln(os.Stderr, "Metrics SDK error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Labels returns a LabelSet corresponding to the arguments. Passed
|
// Labels returns a LabelSet corresponding to the arguments. Passed
|
||||||
// labels are de-duplicated, with last-value-wins semantics.
|
// labels are de-duplicated, with last-value-wins semantics.
|
||||||
func (m *SDK) Labels(kvs ...core.KeyValue) api.LabelSet {
|
func (m *SDK) Labels(kvs ...core.KeyValue) api.LabelSet {
|
||||||
// Note: This computes a canonical encoding of the labels to
|
// Note: This computes a canonical encoding of the labels to
|
||||||
// use as a map key. It happens to use the encoding used by
|
// use as a map key. It happens to use the encoding used by
|
||||||
// statsd for labels, allowing an optimization for statsd
|
// statsd for labels, allowing an optimization for statsd
|
||||||
// exporters. This could be made configurable in the
|
// batchers. This could be made configurable in the
|
||||||
// constructor, to support the same optimization for different
|
// constructor, to support the same optimization for different
|
||||||
// exporters.
|
// batchers.
|
||||||
|
|
||||||
// Check for empty set.
|
// Check for empty set.
|
||||||
if len(kvs) == 0 {
|
if len(kvs) == 0 {
|
||||||
@ -263,21 +273,7 @@ func (m *SDK) Labels(kvs ...core.KeyValue) api.LabelSet {
|
|||||||
}
|
}
|
||||||
ls.sorted = ls.sorted[0:oi]
|
ls.sorted = ls.sorted[0:oi]
|
||||||
|
|
||||||
// Serialize.
|
ls.encoded = m.labelEncoder.Encode(ls.sorted)
|
||||||
buf := m.pool.Get().(*bytes.Buffer)
|
|
||||||
defer m.pool.Put(buf)
|
|
||||||
buf.Reset()
|
|
||||||
_, _ = buf.WriteRune('|')
|
|
||||||
delimiter := '#'
|
|
||||||
for _, kv := range ls.sorted {
|
|
||||||
_, _ = buf.WriteRune(delimiter)
|
|
||||||
_, _ = buf.WriteString(string(kv.Key))
|
|
||||||
_, _ = buf.WriteRune(':')
|
|
||||||
_, _ = buf.WriteString(kv.Value.Emit())
|
|
||||||
delimiter = ','
|
|
||||||
}
|
|
||||||
|
|
||||||
ls.encoded = buf.String()
|
|
||||||
|
|
||||||
return ls
|
return ls
|
||||||
}
|
}
|
||||||
@ -291,7 +287,7 @@ func (m *SDK) labsFor(ls api.LabelSet) *labels {
|
|||||||
return &m.empty
|
return &m.empty
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *SDK) newInstrument(name string, metricKind export.Kind, numberKind core.NumberKind, opts *api.Options) *instrument {
|
func (m *SDK) newInstrument(name string, metricKind export.MetricKind, numberKind core.NumberKind, opts *api.Options) *instrument {
|
||||||
descriptor := export.NewDescriptor(
|
descriptor := export.NewDescriptor(
|
||||||
name,
|
name,
|
||||||
metricKind,
|
metricKind,
|
||||||
@ -370,10 +366,14 @@ func (m *SDK) saveFromReclaim(rec *record) {
|
|||||||
//
|
//
|
||||||
// During the collection pass, the export.Batcher will receive
|
// During the collection pass, the export.Batcher will receive
|
||||||
// one Export() call per current aggregation.
|
// one Export() call per current aggregation.
|
||||||
func (m *SDK) Collect(ctx context.Context) {
|
//
|
||||||
|
// Returns the number of records that were checkpointed.
|
||||||
|
func (m *SDK) Collect(ctx context.Context) int {
|
||||||
m.collectLock.Lock()
|
m.collectLock.Lock()
|
||||||
defer m.collectLock.Unlock()
|
defer m.collectLock.Unlock()
|
||||||
|
|
||||||
|
checkpointed := 0
|
||||||
|
|
||||||
var next *record
|
var next *record
|
||||||
for inuse := m.records.primary.swapNil(); inuse != nil; inuse = next {
|
for inuse := m.records.primary.swapNil(); inuse != nil; inuse = next {
|
||||||
next = inuse.next.primary.load()
|
next = inuse.next.primary.load()
|
||||||
@ -381,14 +381,14 @@ func (m *SDK) Collect(ctx context.Context) {
|
|||||||
refcount := atomic.LoadInt64(&inuse.refcount)
|
refcount := atomic.LoadInt64(&inuse.refcount)
|
||||||
|
|
||||||
if refcount > 0 {
|
if refcount > 0 {
|
||||||
m.collect(ctx, inuse)
|
checkpointed += m.checkpoint(ctx, inuse)
|
||||||
m.addPrimary(inuse)
|
m.addPrimary(inuse)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
modified := atomic.LoadInt64(&inuse.modifiedEpoch)
|
modified := atomic.LoadInt64(&inuse.modifiedEpoch)
|
||||||
collected := atomic.LoadInt64(&inuse.collectedEpoch)
|
collected := atomic.LoadInt64(&inuse.collectedEpoch)
|
||||||
m.collect(ctx, inuse)
|
checkpointed += m.checkpoint(ctx, inuse)
|
||||||
|
|
||||||
if modified >= collected {
|
if modified >= collected {
|
||||||
atomic.StoreInt64(&inuse.collectedEpoch, m.currentEpoch)
|
atomic.StoreInt64(&inuse.collectedEpoch, m.currentEpoch)
|
||||||
@ -409,18 +409,27 @@ func (m *SDK) Collect(ctx context.Context) {
|
|||||||
atomic.StoreInt64(&chances.reclaim, 0)
|
atomic.StoreInt64(&chances.reclaim, 0)
|
||||||
|
|
||||||
if chances.next.primary.load() == hazardRecord {
|
if chances.next.primary.load() == hazardRecord {
|
||||||
m.collect(ctx, chances)
|
checkpointed += m.checkpoint(ctx, chances)
|
||||||
m.addPrimary(chances)
|
m.addPrimary(chances)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m.currentEpoch++
|
m.currentEpoch++
|
||||||
|
return checkpointed
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *SDK) collect(ctx context.Context, r *record) {
|
func (m *SDK) checkpoint(ctx context.Context, r *record) int {
|
||||||
if r.recorder != nil {
|
if r.recorder == nil {
|
||||||
r.recorder.Collect(ctx, r, m.exporter)
|
return 0
|
||||||
}
|
}
|
||||||
|
r.recorder.Checkpoint(ctx, r.descriptor)
|
||||||
|
labels := export.NewLabels(r.labels.sorted, r.labels.encoded, m.labelEncoder)
|
||||||
|
err := m.batcher.Process(ctx, export.NewRecord(r.descriptor, labels, r.recorder))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
m.errorHandler(err)
|
||||||
|
}
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// RecordBatch enters a batch of metric events.
|
// RecordBatch enters a batch of metric events.
|
||||||
@ -439,13 +448,18 @@ func (m *SDK) GetDescriptor(inst metric.InstrumentImpl) *export.Descriptor {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *labels) Meter() api.Meter {
|
|
||||||
return l.meter
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *record) RecordOne(ctx context.Context, number core.Number) {
|
func (r *record) RecordOne(ctx context.Context, number core.Number) {
|
||||||
if r.recorder != nil {
|
if r.recorder == nil {
|
||||||
r.recorder.Update(ctx, number, r)
|
// The instrument is disabled according to the AggregationSelector.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := aggregator.RangeTest(number, r.descriptor); err != nil {
|
||||||
|
r.labels.meter.errorHandler(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := r.recorder.Update(ctx, number, r.descriptor); err != nil {
|
||||||
|
r.labels.meter.errorHandler(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -481,11 +495,3 @@ func (r *record) mapkey() mapkey {
|
|||||||
encoded: r.labels.encoded,
|
encoded: r.labels.encoded,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *record) Descriptor() *export.Descriptor {
|
|
||||||
return r.descriptor
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *record) Labels() []core.KeyValue {
|
|
||||||
return r.labels.sorted
|
|
||||||
}
|
|
||||||
|
100
sdk/metric/selector/simple/simple.go
Normal file
100
sdk/metric/selector/simple/simple.go
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
// Copyright 2019, OpenTelemetry 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 simple // import "go.opentelemetry.io/otel/sdk/metric/selector/simple"
|
||||||
|
|
||||||
|
import (
|
||||||
|
export "go.opentelemetry.io/otel/sdk/export/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/aggregator/array"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/aggregator/counter"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/aggregator/ddsketch"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/aggregator/gauge"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/aggregator/maxsumcount"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
selectorInexpensive struct{}
|
||||||
|
selectorExact struct{}
|
||||||
|
selectorSketch struct {
|
||||||
|
config *ddsketch.Config
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ export.AggregationSelector = selectorInexpensive{}
|
||||||
|
_ export.AggregationSelector = selectorSketch{}
|
||||||
|
_ export.AggregationSelector = selectorExact{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewWithInexpensiveMeasure returns a simple aggregation selector
|
||||||
|
// that uses counter, gauge, and maxsumcount aggregators for the three
|
||||||
|
// kinds of metric. This selector is faster and uses less memory than
|
||||||
|
// the others because maxsumcount does not aggregate quantile
|
||||||
|
// information.
|
||||||
|
func NewWithInexpensiveMeasure() export.AggregationSelector {
|
||||||
|
return selectorInexpensive{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWithSketchMeasure returns a simple aggregation selector that
|
||||||
|
// uses counter, gauge, and ddsketch aggregators for the three kinds
|
||||||
|
// of metric. This selector uses more cpu and memory than the
|
||||||
|
// NewWithInexpensiveMeasure because it uses one DDSketch per distinct
|
||||||
|
// measure and labelset.
|
||||||
|
func NewWithSketchMeasure(config *ddsketch.Config) export.AggregationSelector {
|
||||||
|
return selectorSketch{
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWithExactMeasure returns a simple aggregation selector that uses
|
||||||
|
// counter, gauge, and array behavior for the three kinds of metric.
|
||||||
|
// This selector uses more memory than the NewWithSketchMeasure
|
||||||
|
// because it aggregates an array of all values, therefore is able to
|
||||||
|
// compute exact quantiles.
|
||||||
|
func NewWithExactMeasure() export.AggregationSelector {
|
||||||
|
return selectorExact{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (selectorInexpensive) AggregatorFor(descriptor *export.Descriptor) export.Aggregator {
|
||||||
|
switch descriptor.MetricKind() {
|
||||||
|
case export.GaugeKind:
|
||||||
|
return gauge.New()
|
||||||
|
case export.MeasureKind:
|
||||||
|
return maxsumcount.New()
|
||||||
|
default:
|
||||||
|
return counter.New()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s selectorSketch) AggregatorFor(descriptor *export.Descriptor) export.Aggregator {
|
||||||
|
switch descriptor.MetricKind() {
|
||||||
|
case export.GaugeKind:
|
||||||
|
return gauge.New()
|
||||||
|
case export.MeasureKind:
|
||||||
|
return ddsketch.New(s.config, descriptor)
|
||||||
|
default:
|
||||||
|
return counter.New()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (selectorExact) AggregatorFor(descriptor *export.Descriptor) export.Aggregator {
|
||||||
|
switch descriptor.MetricKind() {
|
||||||
|
case export.GaugeKind:
|
||||||
|
return gauge.New()
|
||||||
|
case export.MeasureKind:
|
||||||
|
return array.New()
|
||||||
|
default:
|
||||||
|
return counter.New()
|
||||||
|
}
|
||||||
|
}
|
57
sdk/metric/selector/simple/simple_test.go
Normal file
57
sdk/metric/selector/simple/simple_test.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
// Copyright 2019, OpenTelemetry 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 simple_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/api/core"
|
||||||
|
export "go.opentelemetry.io/otel/sdk/export/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/aggregator/array"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/aggregator/counter"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/aggregator/ddsketch"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/aggregator/gauge"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/aggregator/maxsumcount"
|
||||||
|
"go.opentelemetry.io/otel/sdk/metric/selector/simple"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
testGaugeDesc = export.NewDescriptor("gauge", export.GaugeKind, nil, "", "", core.Int64NumberKind, false)
|
||||||
|
testCounterDesc = export.NewDescriptor("counter", export.CounterKind, nil, "", "", core.Int64NumberKind, false)
|
||||||
|
testMeasureDesc = export.NewDescriptor("measure", export.MeasureKind, nil, "", "", core.Int64NumberKind, false)
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInexpensiveMeasure(t *testing.T) {
|
||||||
|
inex := simple.NewWithInexpensiveMeasure()
|
||||||
|
require.NotPanics(t, func() { _ = inex.AggregatorFor(testGaugeDesc).(*gauge.Aggregator) })
|
||||||
|
require.NotPanics(t, func() { _ = inex.AggregatorFor(testCounterDesc).(*counter.Aggregator) })
|
||||||
|
require.NotPanics(t, func() { _ = inex.AggregatorFor(testMeasureDesc).(*maxsumcount.Aggregator) })
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSketchMeasure(t *testing.T) {
|
||||||
|
sk := simple.NewWithSketchMeasure(ddsketch.NewDefaultConfig())
|
||||||
|
require.NotPanics(t, func() { _ = sk.AggregatorFor(testGaugeDesc).(*gauge.Aggregator) })
|
||||||
|
require.NotPanics(t, func() { _ = sk.AggregatorFor(testCounterDesc).(*counter.Aggregator) })
|
||||||
|
require.NotPanics(t, func() { _ = sk.AggregatorFor(testMeasureDesc).(*ddsketch.Aggregator) })
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExactMeasure(t *testing.T) {
|
||||||
|
ex := simple.NewWithExactMeasure()
|
||||||
|
require.NotPanics(t, func() { _ = ex.AggregatorFor(testGaugeDesc).(*gauge.Aggregator) })
|
||||||
|
require.NotPanics(t, func() { _ = ex.AggregatorFor(testCounterDesc).(*counter.Aggregator) })
|
||||||
|
require.NotPanics(t, func() { _ = ex.AggregatorFor(testMeasureDesc).(*array.Aggregator) })
|
||||||
|
}
|
@ -36,6 +36,7 @@ import (
|
|||||||
"go.opentelemetry.io/otel/api/metric"
|
"go.opentelemetry.io/otel/api/metric"
|
||||||
api "go.opentelemetry.io/otel/api/metric"
|
api "go.opentelemetry.io/otel/api/metric"
|
||||||
export "go.opentelemetry.io/otel/sdk/export/metric"
|
export "go.opentelemetry.io/otel/sdk/export/metric"
|
||||||
|
"go.opentelemetry.io/otel/sdk/export/metric/aggregator"
|
||||||
sdk "go.opentelemetry.io/otel/sdk/metric"
|
sdk "go.opentelemetry.io/otel/sdk/metric"
|
||||||
"go.opentelemetry.io/otel/sdk/metric/aggregator/counter"
|
"go.opentelemetry.io/otel/sdk/metric/aggregator/counter"
|
||||||
"go.opentelemetry.io/otel/sdk/metric/aggregator/gauge"
|
"go.opentelemetry.io/otel/sdk/metric/aggregator/gauge"
|
||||||
@ -222,13 +223,13 @@ func (f *testFixture) assertTest(numCollect int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *testFixture) preCollect() {
|
func (f *testFixture) preCollect() {
|
||||||
// Collect calls Export in a single-threaded context. No need
|
// Collect calls Process in a single-threaded context. No need
|
||||||
// to lock this struct.
|
// to lock this struct.
|
||||||
f.dupCheck = map[testKey]int{}
|
f.dupCheck = map[testKey]int{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *testFixture) AggregatorFor(record export.Record) export.Aggregator {
|
func (*testFixture) AggregatorFor(descriptor *export.Descriptor) export.Aggregator {
|
||||||
switch record.Descriptor().MetricKind() {
|
switch descriptor.MetricKind() {
|
||||||
case export.CounterKind:
|
case export.CounterKind:
|
||||||
return counter.New()
|
return counter.New()
|
||||||
case export.GaugeKind:
|
case export.GaugeKind:
|
||||||
@ -238,11 +239,17 @@ func (f *testFixture) AggregatorFor(record export.Record) export.Aggregator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *testFixture) Export(ctx context.Context, record export.Record, agg export.Aggregator) {
|
func (*testFixture) CheckpointSet() export.CheckpointSet {
|
||||||
desc := record.Descriptor()
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*testFixture) FinishedCollection() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *testFixture) Process(_ context.Context, record export.Record) error {
|
||||||
key := testKey{
|
key := testKey{
|
||||||
labels: canonicalizeLabels(record.Labels()),
|
labels: canonicalizeLabels(record.Labels().Ordered()),
|
||||||
descriptor: desc,
|
descriptor: record.Descriptor(),
|
||||||
}
|
}
|
||||||
if f.dupCheck[key] == 0 {
|
if f.dupCheck[key] == 0 {
|
||||||
f.dupCheck[key]++
|
f.dupCheck[key]++
|
||||||
@ -252,15 +259,26 @@ func (f *testFixture) Export(ctx context.Context, record export.Record, agg expo
|
|||||||
|
|
||||||
actual, _ := f.received.LoadOrStore(key, f.impl.newStore())
|
actual, _ := f.received.LoadOrStore(key, f.impl.newStore())
|
||||||
|
|
||||||
switch desc.MetricKind() {
|
agg := record.Aggregator()
|
||||||
|
switch record.Descriptor().MetricKind() {
|
||||||
case export.CounterKind:
|
case export.CounterKind:
|
||||||
f.impl.storeCollect(actual, agg.(*counter.Aggregator).AsNumber(), time.Time{})
|
counter := agg.(aggregator.Sum)
|
||||||
|
sum, err := counter.Sum()
|
||||||
|
if err != nil {
|
||||||
|
f.T.Fatal("Sum error: ", err)
|
||||||
|
}
|
||||||
|
f.impl.storeCollect(actual, sum, time.Time{})
|
||||||
case export.GaugeKind:
|
case export.GaugeKind:
|
||||||
gauge := agg.(*gauge.Aggregator)
|
gauge := agg.(aggregator.LastValue)
|
||||||
f.impl.storeCollect(actual, gauge.AsNumber(), gauge.Timestamp())
|
lv, ts, err := gauge.LastValue()
|
||||||
|
if err != nil && err != aggregator.ErrNoLastValue {
|
||||||
|
f.T.Fatal("Last value error: ", err)
|
||||||
|
}
|
||||||
|
f.impl.storeCollect(actual, lv, ts)
|
||||||
default:
|
default:
|
||||||
panic("Not used in this test")
|
panic("Not used in this test")
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func stressTest(t *testing.T, impl testImpl) {
|
func stressTest(t *testing.T, impl testImpl) {
|
||||||
@ -272,7 +290,7 @@ func stressTest(t *testing.T, impl testImpl) {
|
|||||||
lused: map[string]bool{},
|
lused: map[string]bool{},
|
||||||
}
|
}
|
||||||
cc := concurrency()
|
cc := concurrency()
|
||||||
sdk := sdk.New(fixture)
|
sdk := sdk.New(fixture, sdk.DefaultLabelEncoder())
|
||||||
fixture.wg.Add(cc + 1)
|
fixture.wg.Add(cc + 1)
|
||||||
|
|
||||||
for i := 0; i < cc; i++ {
|
for i := 0; i < cc; i++ {
|
||||||
|
Reference in New Issue
Block a user