You've already forked opentelemetry-go
							
							
				mirror of
				https://github.com/open-telemetry/opentelemetry-go.git
				synced 2025-10-31 00:07:40 +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
						rghetia
					
				
			
			
				
	
			
			
			
						parent
						
							c3d5b7b16d
						
					
				
				
					commit
					9878f3b700
				
			| @@ -18,6 +18,7 @@ package core | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"math" | ||||
| 	"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 | ||||
|  | ||||
| func (n Number) compareWithZero(kind NumberKind) int { | ||||
|   | ||||
| @@ -17,6 +17,8 @@ package core | ||||
| import ( | ||||
| 	"testing" | ||||
| 	"unsafe" | ||||
|  | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
|  | ||||
| func TestNumber(t *testing.T) { | ||||
| @@ -157,3 +159,9 @@ func TestNumberZero(t *testing.T) { | ||||
| 		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= | ||||
| 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/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= | ||||
| 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/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/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 v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= | ||||
| 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/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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||||
| 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/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.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= | ||||
| 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/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= | ||||
| 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.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/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= | ||||
| 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.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/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= | ||||
| @@ -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/pkg/errors v0.8.0/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/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= | ||||
| @@ -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/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.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= | ||||
| 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/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/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-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= | ||||
| 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/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.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.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= | ||||
| 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-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= | ||||
|   | ||||
| @@ -17,13 +17,19 @@ package main | ||||
| import ( | ||||
| 	"context" | ||||
| 	"log" | ||||
| 	"time" | ||||
|  | ||||
| 	"go.opentelemetry.io/otel/api/distributedcontext" | ||||
| 	"go.opentelemetry.io/otel/api/key" | ||||
| 	"go.opentelemetry.io/otel/api/metric" | ||||
| 	"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" | ||||
| 	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" | ||||
| ) | ||||
|  | ||||
| @@ -37,23 +43,44 @@ var ( | ||||
| // initTracer creates and registers trace provider instance. | ||||
| func initTracer() { | ||||
| 	var err error | ||||
| 	exp, err := stdout.NewExporter(stdout.Options{PrettyPrint: false}) | ||||
| 	exp, err := tracestdout.NewExporter(tracestdout.Options{PrettyPrint: false}) | ||||
| 	if err != nil { | ||||
| 		log.Panicf("failed to initialize stdout exporter %v\n", err) | ||||
| 		log.Panicf("failed to initialize trace stdout exporter %v", err) | ||||
| 		return | ||||
| 	} | ||||
| 	tp, err := sdktrace.NewProvider(sdktrace.WithSyncer(exp), | ||||
| 		sdktrace.WithConfig(sdktrace.Config{DefaultSampler: sdktrace.AlwaysSample()})) | ||||
| 	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) | ||||
| } | ||||
|  | ||||
| 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() { | ||||
| 	defer initMeter().Stop() | ||||
| 	initTracer() | ||||
|  | ||||
| 	// Note: Have to get the meter and tracer after the global is | ||||
| 	// initialized.  See OTEP 0005. | ||||
|  | ||||
| 	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") | ||||
|  | ||||
| 	oneMetric := meter.NewFloat64Gauge("ex.com.one", | ||||
| @@ -70,7 +97,7 @@ func main() { | ||||
| 		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) | ||||
| 	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/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/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 v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= | ||||
| 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/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/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 v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= | ||||
| 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/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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||||
| 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/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/pkg/errors v0.8.0/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/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= | ||||
| @@ -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/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.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= | ||||
| 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/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 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-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= | ||||
| 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/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/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= | ||||
| 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= | ||||
|   | ||||
| @@ -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/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= | ||||
| 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 v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= | ||||
| 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/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/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 v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= | ||||
| 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/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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||||
| 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/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.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/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= | ||||
| 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.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/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= | ||||
| @@ -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/pkg/errors v0.8.0/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/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= | ||||
| @@ -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/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.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= | ||||
| 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/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/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-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= | ||||
| 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/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.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.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= | ||||
| 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-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/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= | ||||
| 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 v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= | ||||
| 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/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/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 v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= | ||||
| 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 ( | ||||
| 	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/gogo/protobuf v1.3.1 // 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/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/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 v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= | ||||
| 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/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= | ||||
| 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/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= | ||||
| 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 | ||||
| // limitations under the License. | ||||
|  | ||||
| package metric // import "go.opentelemetry.io/otel/sdk/export/metric" | ||||
| package export | ||||
|  | ||||
| //go:generate stringer -type=MetricKind | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| @@ -21,70 +23,288 @@ import ( | ||||
| 	"go.opentelemetry.io/otel/api/unit" | ||||
| ) | ||||
|  | ||||
| // Batcher is responsible for deciding which kind of aggregation | ||||
| // to use and gathering exported results from the SDK.  The standard SDK | ||||
| // supports binding only one of these interfaces, i.e., a single exporter. | ||||
| // Batcher is responsible for deciding which kind of aggregation to | ||||
| // use (via AggregationSelector), gathering exported results from the | ||||
| // SDK during collection, and deciding over which dimensions to group | ||||
| // the exported data. | ||||
| // | ||||
| // Multiple-exporters could be implemented by implementing this interface | ||||
| // for a group of Batcher. | ||||
| // The SDK supports binding only one of these interfaces, as it has | ||||
| // 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 { | ||||
| 	// AggregatorFor should return the kind of aggregator | ||||
| 	// suited to the requested export.  Returning `nil` | ||||
| 	// indicates to ignore the metric update. | ||||
| 	// AggregationSelector is responsible for selecting the | ||||
| 	// concrete type of Aggregator used for a metric in the SDK. | ||||
| 	// | ||||
| 	// Note: This is context-free because the handle should not be | ||||
| 	// bound to the incoming context.  This call should not block. | ||||
| 	AggregatorFor(Record) Aggregator | ||||
| 	// This may be a static decision based on fields of the | ||||
| 	// Descriptor, or it could use an external configuration | ||||
| 	// 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 | ||||
| 	// during the SDK Collect().  Exporter implementations | ||||
| 	// must access the specific aggregator to receive the | ||||
| 	// exporter data, since the format of the data varies | ||||
| 	// by aggregation. | ||||
| 	Export(context.Context, Record, Aggregator) | ||||
| 	// Process is called by the SDK once per internal record, | ||||
| 	// passing the export Record (a Descriptor, the corresponding | ||||
| 	// Labels, and the checkpointed Aggregator).  The Batcher | ||||
| 	// should be prepared to process duplicate (Descriptor, | ||||
| 	// Labels) pairs during this pass due to race conditions, but | ||||
| 	// 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., | ||||
| // a counter, a gauge, a histogram. | ||||
| // AggregationSelector supports selecting the kind of Aggregator to | ||||
| // 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 { | ||||
| 	// Update receives a new measured value and incorporates it | ||||
| 	// into the aggregation. | ||||
| 	Update(context.Context, core.Number, Record) | ||||
| 	// into the aggregation.  Update() calls may arrive | ||||
| 	// 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 | ||||
| 	// finish one period of aggregation.  Collect() is | ||||
| 	// called in a single-threaded context.  Update() | ||||
| 	// calls may arrive concurrently. | ||||
| 	Collect(context.Context, Record, Batcher) | ||||
| 	// Checkpoint is called during collection to finish one period | ||||
| 	// of aggregation by atomically saving the current value. | ||||
| 	// Checkpoint() is called concurrently with Update(). | ||||
| 	// Checkpoint should reset the current state to the empty | ||||
| 	// 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(Aggregator, *Descriptor) | ||||
| 	// Merge combines the checkpointed state from the argument | ||||
| 	// 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 | ||||
| // instrument and set of labels. | ||||
| type Record interface { | ||||
| 	// Descriptor() describes the metric instrument. | ||||
| 	Descriptor() *Descriptor | ||||
|  | ||||
| 	// Labels() describe the labsels corresponding the | ||||
| 	// aggregation being performed. | ||||
| 	Labels() []core.KeyValue | ||||
| // Exporter handles presentation of the checkpoint of aggregate | ||||
| // metrics.  This is the final stage of a metrics export pipeline, | ||||
| // where metric data are formatted for a specific system. | ||||
| type Exporter interface { | ||||
| 	// Export is called immediately after completing a collection | ||||
| 	// pass in the SDK. | ||||
| 	// | ||||
| 	// The Context comes from the controller that initiated | ||||
| 	// collection. | ||||
| 	// | ||||
| 	// The CheckpointSet interface refers to the Batcher that just | ||||
| 	// completed collection. | ||||
| 	Export(context.Context, CheckpointSet) error | ||||
| } | ||||
|  | ||||
| // Kind describes the kind of instrument. | ||||
| type Kind int8 | ||||
| // LabelEncoder enables an optimization for export pipelines that use | ||||
| // 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 ( | ||||
| 	CounterKind Kind = iota | ||||
| 	// Counter kind indicates a counter instrument. | ||||
| 	CounterKind MetricKind = iota | ||||
|  | ||||
| 	// Gauge kind indicates a gauge instrument. | ||||
| 	GaugeKind | ||||
|  | ||||
| 	// Measure kind indicates a measure instrument. | ||||
| 	MeasureKind | ||||
| ) | ||||
|  | ||||
| // 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 { | ||||
| 	name        string | ||||
| 	metricKind  Kind | ||||
| 	metricKind  MetricKind | ||||
| 	keys        []core.Key | ||||
| 	description string | ||||
| 	unit        unit.Unit | ||||
| @@ -93,10 +313,14 @@ type Descriptor struct { | ||||
| } | ||||
|  | ||||
| // 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( | ||||
| 	name string, | ||||
| 	metricKind Kind, | ||||
| 	metricKind MetricKind, | ||||
| 	keys []core.Key, | ||||
| 	description string, | ||||
| 	unit unit.Unit, | ||||
| @@ -114,30 +338,51 @@ func NewDescriptor( | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Name returns the metric instrument's name. | ||||
| func (d *Descriptor) Name() string { | ||||
| 	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 | ||||
| } | ||||
|  | ||||
| // 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 { | ||||
| 	return d.keys | ||||
| } | ||||
|  | ||||
| // Description provides a human-readable description of the metric | ||||
| // instrument. | ||||
| func (d *Descriptor) Description() string { | ||||
| 	return d.description | ||||
| } | ||||
|  | ||||
| // Unit describes the units of the metric instrument.  Unitless | ||||
| // metrics return the empty string. | ||||
| func (d *Descriptor) Unit() unit.Unit { | ||||
| 	return d.unit | ||||
| } | ||||
|  | ||||
| // NumberKind returns whether this instrument is declared over int64 | ||||
| // or a float64 values. | ||||
| func (d *Descriptor) NumberKind() core.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 { | ||||
| 	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" | ||||
| 	export "go.opentelemetry.io/otel/sdk/export/metric" | ||||
| 	"go.opentelemetry.io/otel/sdk/metric/aggregator" | ||||
| 	"go.opentelemetry.io/otel/sdk/export/metric/aggregator" | ||||
| ) | ||||
|  | ||||
| type ( | ||||
| @@ -38,44 +38,55 @@ type ( | ||||
| ) | ||||
|  | ||||
| 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 { | ||||
| 	return &Aggregator{} | ||||
| } | ||||
|  | ||||
| // Sum returns the sum of the checkpoint. | ||||
| func (c *Aggregator) Sum() core.Number { | ||||
| 	return c.ckptSum | ||||
| // Sum returns the sum of values in the checkpoint. | ||||
| func (c *Aggregator) Sum() (core.Number, error) { | ||||
| 	return c.ckptSum, nil | ||||
| } | ||||
|  | ||||
| // Count returns the count of the checkpoint. | ||||
| func (c *Aggregator) Count() int64 { | ||||
| 	return int64(len(c.checkpoint)) | ||||
| // Count returns the number of values in the checkpoint. | ||||
| func (c *Aggregator) Count() (int64, error) { | ||||
| 	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) { | ||||
| 	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) { | ||||
| 	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) { | ||||
| 	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.checkpoint, c.current = c.current, nil | ||||
| 	c.lock.Unlock() | ||||
|  | ||||
| 	desc := rec.Descriptor() | ||||
| 	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.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 { | ||||
| 		c.ckptSum.AddNumber(kind, v) | ||||
| 	} | ||||
|  | ||||
| 	exp.Export(ctx, rec, c) | ||||
| } | ||||
|  | ||||
| func (c *Aggregator) Update(_ context.Context, number core.Number, rec export.Record) { | ||||
| 	desc := rec.Descriptor() | ||||
| 	kind := desc.NumberKind() | ||||
|  | ||||
| 	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 | ||||
| 	} | ||||
|  | ||||
| // Update adds the recorded measurement to the current data set. | ||||
| // Update takes a lock to prevent concurrent Update() and Checkpoint() | ||||
| // calls. | ||||
| func (c *Aggregator) Update(_ context.Context, number core.Number, desc *export.Descriptor) error { | ||||
| 	c.lock.Lock() | ||||
| 	c.current = append(c.current, number) | ||||
| 	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) | ||||
| 	if o == nil { | ||||
| 		// TODO warn | ||||
| 		return | ||||
| 		return aggregator.NewInconsistentMergeError(c, oa) | ||||
| 	} | ||||
|  | ||||
| 	c.ckptSum.AddNumber(desc.NumberKind(), o.ckptSum) | ||||
| 	c.checkpoint = combine(c.checkpoint, o.checkpoint, desc.NumberKind()) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| 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 | ||||
| // 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) { | ||||
| 	if len(*p) == 0 { | ||||
| 		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 | ||||
| 	} | ||||
|  | ||||
| 	// 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 | ||||
| 	ceil := int(math.Ceil(position)) | ||||
| 	return (*p)[ceil], nil | ||||
|   | ||||
| @@ -24,7 +24,7 @@ import ( | ||||
|  | ||||
| 	"go.opentelemetry.io/otel/api/core" | ||||
| 	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" | ||||
| ) | ||||
|  | ||||
| @@ -34,9 +34,7 @@ type updateTest struct { | ||||
| } | ||||
|  | ||||
| func (ut *updateTest) run(t *testing.T, profile test.Profile) { | ||||
| 	ctx := context.Background() | ||||
|  | ||||
| 	batcher, record := test.NewAggregatorTest(export.MeasureKind, profile.NumberKind, !ut.absolute) | ||||
| 	descriptor := test.NewAggregatorTest(export.MeasureKind, profile.NumberKind, !ut.absolute) | ||||
|  | ||||
| 	agg := New() | ||||
|  | ||||
| @@ -45,25 +43,30 @@ func (ut *updateTest) run(t *testing.T, profile test.Profile) { | ||||
| 	for i := 0; i < ut.count; i++ { | ||||
| 		x := profile.Random(+1) | ||||
| 		all.Append(x) | ||||
| 		agg.Update(ctx, x, record) | ||||
| 		test.CheckedUpdate(t, agg, x, descriptor) | ||||
|  | ||||
| 		if !ut.absolute { | ||||
| 			y := profile.Random(-1) | ||||
| 			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() | ||||
|  | ||||
| 	sum, err := agg.Sum() | ||||
| 	require.InEpsilon(t, | ||||
| 		all.Sum().CoerceToFloat64(profile.NumberKind), | ||||
| 		agg.Sum().CoerceToFloat64(profile.NumberKind), | ||||
| 		sum.CoerceToFloat64(profile.NumberKind), | ||||
| 		0.0000001, | ||||
| 		"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() | ||||
| 	require.Nil(t, err) | ||||
| @@ -106,7 +109,7 @@ type mergeTest struct { | ||||
| func (mt *mergeTest) run(t *testing.T, profile test.Profile) { | ||||
| 	ctx := context.Background() | ||||
|  | ||||
| 	batcher, record := test.NewAggregatorTest(export.MeasureKind, profile.NumberKind, !mt.absolute) | ||||
| 	descriptor := test.NewAggregatorTest(export.MeasureKind, profile.NumberKind, !mt.absolute) | ||||
|  | ||||
| 	agg1 := New() | ||||
| 	agg2 := New() | ||||
| @@ -116,36 +119,40 @@ func (mt *mergeTest) run(t *testing.T, profile test.Profile) { | ||||
| 	for i := 0; i < mt.count; i++ { | ||||
| 		x1 := profile.Random(+1) | ||||
| 		all.Append(x1) | ||||
| 		agg1.Update(ctx, x1, record) | ||||
| 		test.CheckedUpdate(t, agg1, x1, descriptor) | ||||
|  | ||||
| 		x2 := profile.Random(+1) | ||||
| 		all.Append(x2) | ||||
| 		agg2.Update(ctx, x2, record) | ||||
| 		test.CheckedUpdate(t, agg2, x2, descriptor) | ||||
|  | ||||
| 		if !mt.absolute { | ||||
| 			y1 := profile.Random(-1) | ||||
| 			all.Append(y1) | ||||
| 			agg1.Update(ctx, y1, record) | ||||
| 			test.CheckedUpdate(t, agg1, y1, descriptor) | ||||
|  | ||||
| 			y2 := profile.Random(-1) | ||||
| 			all.Append(y2) | ||||
| 			agg2.Update(ctx, y2, record) | ||||
| 			test.CheckedUpdate(t, agg2, y2, descriptor) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	agg1.Collect(ctx, record, batcher) | ||||
| 	agg2.Collect(ctx, record, batcher) | ||||
| 	agg1.Checkpoint(ctx, descriptor) | ||||
| 	agg2.Checkpoint(ctx, descriptor) | ||||
|  | ||||
| 	agg1.Merge(agg2, record.Descriptor()) | ||||
| 	test.CheckedMerge(t, agg1, agg2, descriptor) | ||||
|  | ||||
| 	all.Sort() | ||||
|  | ||||
| 	sum, err := agg1.Sum() | ||||
| 	require.InEpsilon(t, | ||||
| 		all.Sum().CoerceToFloat64(profile.NumberKind), | ||||
| 		agg1.Sum().CoerceToFloat64(profile.NumberKind), | ||||
| 		sum.CoerceToFloat64(profile.NumberKind), | ||||
| 		0.0000001, | ||||
| 		"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() | ||||
| 	require.Nil(t, err) | ||||
| @@ -198,16 +205,18 @@ func TestArrayErrors(t *testing.T) { | ||||
|  | ||||
| 		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 { | ||||
| 			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) | ||||
| 		require.Nil(t, err) | ||||
| @@ -226,7 +235,7 @@ func TestArrayErrors(t *testing.T) { | ||||
| func TestArrayFloat64(t *testing.T) { | ||||
| 	for _, absolute := range []bool{false, true} { | ||||
| 		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 { | ||||
| 				// Check behavior of a bunch of odd floating | ||||
| @@ -263,23 +272,27 @@ func TestArrayFloat64(t *testing.T) { | ||||
|  | ||||
| 			for _, f := range fpsf(1) { | ||||
| 				all.Append(core.NewFloat64Number(f)) | ||||
| 				agg.Update(ctx, core.NewFloat64Number(f), record) | ||||
| 				test.CheckedUpdate(t, agg, core.NewFloat64Number(f), descriptor) | ||||
| 			} | ||||
|  | ||||
| 			if !absolute { | ||||
| 				for _, f := range fpsf(-1) { | ||||
| 					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() | ||||
|  | ||||
| 			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() | ||||
| 			require.Nil(t, err) | ||||
|   | ||||
| @@ -19,6 +19,7 @@ import ( | ||||
|  | ||||
| 	"go.opentelemetry.io/otel/api/core" | ||||
| 	export "go.opentelemetry.io/otel/sdk/export/metric" | ||||
| 	"go.opentelemetry.io/otel/sdk/export/metric/aggregator" | ||||
| ) | ||||
|  | ||||
| // Aggregator aggregates counter events. | ||||
| @@ -26,47 +27,44 @@ type Aggregator struct { | ||||
| 	// current holds current increments to this counter record | ||||
| 	current core.Number | ||||
|  | ||||
| 	// checkpoint is a temporary used during Collect() | ||||
| 	// checkpoint is a temporary used during Checkpoint() | ||||
| 	checkpoint core.Number | ||||
| } | ||||
|  | ||||
| var _ export.Aggregator = &Aggregator{} | ||||
| var _ aggregator.Sum = &Aggregator{} | ||||
|  | ||||
| // New returns a new counter aggregator.  This aggregator computes an | ||||
| // atomic sum. | ||||
| // New returns a new counter aggregator implemented by atomic | ||||
| // operations.  This aggregator implements the aggregator.Sum | ||||
| // export interface. | ||||
| func New() *Aggregator { | ||||
| 	return &Aggregator{} | ||||
| } | ||||
|  | ||||
| // AsNumber returns the accumulated count as an int64. | ||||
| func (c *Aggregator) AsNumber() core.Number { | ||||
| 	return c.checkpoint.AsNumber() | ||||
| // Sum returns the last-checkpointed sum.  This will never return an | ||||
| // error. | ||||
| func (c *Aggregator) Sum() (core.Number, error) { | ||||
| 	return c.checkpoint, nil | ||||
| } | ||||
|  | ||||
| // Collect checkpoints the current value (atomically) and exports it. | ||||
| func (c *Aggregator) Collect(ctx context.Context, rec export.Record, exp export.Batcher) { | ||||
| // Checkpoint atomically saves the current value and resets the | ||||
| // current sum to zero. | ||||
| func (c *Aggregator) Checkpoint(ctx context.Context, _ *export.Descriptor) { | ||||
| 	c.checkpoint = c.current.SwapNumberAtomic(core.Number(0)) | ||||
|  | ||||
| 	exp.Export(ctx, rec, c) | ||||
| } | ||||
|  | ||||
| // Update modifies the current value (atomically) for later export. | ||||
| func (c *Aggregator) Update(_ context.Context, number core.Number, rec export.Record) { | ||||
| 	desc := rec.Descriptor() | ||||
| 	kind := desc.NumberKind() | ||||
| 	if !desc.Alternate() && number.IsNegative(kind) { | ||||
| 		// TODO warn | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	c.current.AddNumberAtomic(kind, number) | ||||
| // Update atomically adds to the current value. | ||||
| func (c *Aggregator) Update(_ context.Context, number core.Number, desc *export.Descriptor) error { | ||||
| 	c.current.AddNumberAtomic(desc.NumberKind(), number) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| 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) | ||||
| 	if o == nil { | ||||
| 		// TODO warn | ||||
| 		return | ||||
| 		return aggregator.NewInconsistentMergeError(c, oa) | ||||
| 	} | ||||
| 	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) { | ||||
| 		agg := New() | ||||
|  | ||||
| 		batcher, record := test.NewAggregatorTest(export.CounterKind, profile.NumberKind, false) | ||||
| 		descriptor := test.NewAggregatorTest(export.CounterKind, profile.NumberKind, false) | ||||
|  | ||||
| 		sum := core.Number(0) | ||||
| 		for i := 0; i < count; i++ { | ||||
| 			x := profile.Random(+1) | ||||
| 			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) { | ||||
| 		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++ { | ||||
| 			agg.Update(ctx, profile.Random(-1), record) | ||||
| 			test.CheckedUpdate(t, agg, profile.Random(-1), descriptor) | ||||
| 		} | ||||
|  | ||||
| 		sum := profile.Random(+1) | ||||
| 		agg.Update(ctx, sum, record) | ||||
| 		agg.Collect(ctx, record, batcher) | ||||
| 		test.CheckedUpdate(t, agg, sum, descriptor) | ||||
| 		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) { | ||||
| 		agg := New() | ||||
|  | ||||
| 		batcher, record := test.NewAggregatorTest(export.CounterKind, profile.NumberKind, true) | ||||
| 		descriptor := test.NewAggregatorTest(export.CounterKind, profile.NumberKind, true) | ||||
|  | ||||
| 		sum := core.Number(0) | ||||
| 		for i := 0; i < count; i++ { | ||||
| @@ -82,13 +86,15 @@ func TestCounterNonMonotonic(t *testing.T) { | ||||
| 			y := profile.Random(-1) | ||||
| 			sum.AddNumber(profile.NumberKind, x) | ||||
| 			sum.AddNumber(profile.NumberKind, y) | ||||
| 			agg.Update(ctx, x, record) | ||||
| 			agg.Update(ctx, y, record) | ||||
| 			test.CheckedUpdate(t, agg, x, descriptor) | ||||
| 			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() | ||||
| 		agg2 := New() | ||||
|  | ||||
| 		batcher, record := test.NewAggregatorTest(export.CounterKind, profile.NumberKind, false) | ||||
| 		descriptor := test.NewAggregatorTest(export.CounterKind, profile.NumberKind, false) | ||||
|  | ||||
| 		sum := core.Number(0) | ||||
| 		for i := 0; i < count; i++ { | ||||
| 			x := profile.Random(+1) | ||||
| 			sum.AddNumber(profile.NumberKind, x) | ||||
| 			agg1.Update(ctx, x, record) | ||||
| 			agg2.Update(ctx, x, record) | ||||
| 			test.CheckedUpdate(t, agg1, x, descriptor) | ||||
| 			test.CheckedUpdate(t, agg2, x, descriptor) | ||||
| 		} | ||||
|  | ||||
| 		agg1.Collect(ctx, record, batcher) | ||||
| 		agg2.Collect(ctx, record, batcher) | ||||
| 		agg1.Checkpoint(ctx, descriptor) | ||||
| 		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" | ||||
|  | ||||
| 	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. | ||||
| type Aggregator struct { | ||||
| 	lock       sync.Mutex | ||||
| 	cfg        *sdk.Config | ||||
| 	cfg        *Config | ||||
| 	kind       core.NumberKind | ||||
| 	current    *sdk.DDSketch | ||||
| 	checkpoint *sdk.DDSketch | ||||
| } | ||||
|  | ||||
| var _ export.Aggregator = &Aggregator{} | ||||
| var _ aggregator.MaxSumCount = &Aggregator{} | ||||
| var _ aggregator.Distribution = &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{ | ||||
| 		cfg:     cfg, | ||||
| 		kind:    desc.NumberKind(), | ||||
| @@ -48,35 +53,39 @@ func New(cfg *sdk.Config, desc *export.Descriptor) *Aggregator { | ||||
|  | ||||
| // NewDefaultConfig returns a new, default DDSketch config. | ||||
| // | ||||
| // TODO: The Config constructor should probably set minValue to -Inf | ||||
| // to aggregate metrics with absolute=false.  This requires providing values | ||||
| // for alpha and maxNumBins | ||||
| func NewDefaultConfig() *sdk.Config { | ||||
| // TODO: Should the Config constructor set minValue to -Inf to | ||||
| // when the descriptor has absolute=false?  This requires providing | ||||
| // values for alpha and maxNumBins, apparently. | ||||
| func NewDefaultConfig() *Config { | ||||
| 	return sdk.NewDefaultConfig() | ||||
| } | ||||
|  | ||||
| // Sum returns the sum of the checkpoint. | ||||
| func (c *Aggregator) Sum() core.Number { | ||||
| 	return c.toNumber(c.checkpoint.Sum()) | ||||
| // Sum returns the sum of values in the checkpoint. | ||||
| func (c *Aggregator) Sum() (core.Number, error) { | ||||
| 	return c.toNumber(c.checkpoint.Sum()), nil | ||||
| } | ||||
|  | ||||
| // Count returns the count of the checkpoint. | ||||
| func (c *Aggregator) Count() int64 { | ||||
| 	return c.checkpoint.Count() | ||||
| // Count returns the number of values in the checkpoint. | ||||
| func (c *Aggregator) Count() (int64, error) { | ||||
| 	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) { | ||||
| 	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) { | ||||
| 	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) { | ||||
| 	if c.checkpoint.Count() == 0 { | ||||
| 		return core.Number(0), aggregator.ErrEmptyDataSet | ||||
| 	} | ||||
| 	f := c.checkpoint.Quantile(q) | ||||
| 	if math.IsNaN(f) { | ||||
| 		return core.Number(0), aggregator.ErrInvalidQuantile | ||||
| @@ -91,41 +100,34 @@ func (c *Aggregator) toNumber(f float64) core.Number { | ||||
| 	return core.NewInt64Number(int64(f)) | ||||
| } | ||||
|  | ||||
| // Collect checkpoints the current value (atomically) and exports it. | ||||
| 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, _ *export.Descriptor) { | ||||
| 	replace := sdk.NewDDSketch(c.cfg) | ||||
|  | ||||
| 	c.lock.Lock() | ||||
| 	c.checkpoint = c.current | ||||
| 	c.current = replace | ||||
| 	c.lock.Unlock() | ||||
|  | ||||
| 	if c.checkpoint.Count() != 0 { | ||||
| 		exp.Export(ctx, rec, c) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Update modifies the current value (atomically) for later export. | ||||
| func (c *Aggregator) Update(_ context.Context, number core.Number, rec export.Record) { | ||||
| 	desc := rec.Descriptor() | ||||
| 	kind := desc.NumberKind() | ||||
|  | ||||
| 	if !desc.Alternate() && number.IsNegative(kind) { | ||||
| 		// TODO warn | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| // Update adds the recorded measurement to the current data set. | ||||
| // Update takes a lock to prevent concurrent Update() and Checkpoint() | ||||
| // calls. | ||||
| func (c *Aggregator) Update(_ context.Context, number core.Number, desc *export.Descriptor) error { | ||||
| 	c.lock.Lock() | ||||
| 	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) | ||||
| 	if o == nil { | ||||
| 		// TODO warn | ||||
| 		return | ||||
| 		return aggregator.NewInconsistentMergeError(c, oa) | ||||
| 	} | ||||
|  | ||||
| 	c.checkpoint.Merge(o.checkpoint) | ||||
| 	return nil | ||||
| } | ||||
|   | ||||
| @@ -34,32 +34,37 @@ type updateTest struct { | ||||
| func (ut *updateTest) run(t *testing.T, profile test.Profile) { | ||||
| 	ctx := context.Background() | ||||
|  | ||||
| 	batcher, record := test.NewAggregatorTest(export.MeasureKind, profile.NumberKind, !ut.absolute) | ||||
| 	agg := New(NewDefaultConfig(), record.Descriptor()) | ||||
| 	descriptor := test.NewAggregatorTest(export.MeasureKind, profile.NumberKind, !ut.absolute) | ||||
| 	agg := New(NewDefaultConfig(), descriptor) | ||||
|  | ||||
| 	all := test.NewNumbers(profile.NumberKind) | ||||
| 	for i := 0; i < count; i++ { | ||||
| 		x := profile.Random(+1) | ||||
| 		all.Append(x) | ||||
| 		agg.Update(ctx, x, record) | ||||
| 		test.CheckedUpdate(t, agg, x, descriptor) | ||||
|  | ||||
| 		if !ut.absolute { | ||||
| 			y := profile.Random(-1) | ||||
| 			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() | ||||
|  | ||||
| 	sum, err := agg.Sum() | ||||
| 	require.InDelta(t, | ||||
| 		all.Sum().CoerceToFloat64(profile.NumberKind), | ||||
| 		agg.Sum().CoerceToFloat64(profile.NumberKind), | ||||
| 		sum.CoerceToFloat64(profile.NumberKind), | ||||
| 		1, | ||||
| 		"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() | ||||
| 	require.Nil(t, err) | ||||
| @@ -96,49 +101,54 @@ type mergeTest struct { | ||||
|  | ||||
| func (mt *mergeTest) run(t *testing.T, profile test.Profile) { | ||||
| 	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()) | ||||
| 	agg2 := New(NewDefaultConfig(), record.Descriptor()) | ||||
| 	agg1 := New(NewDefaultConfig(), descriptor) | ||||
| 	agg2 := New(NewDefaultConfig(), descriptor) | ||||
|  | ||||
| 	all := test.NewNumbers(profile.NumberKind) | ||||
| 	for i := 0; i < count; i++ { | ||||
| 		x := profile.Random(+1) | ||||
| 		all.Append(x) | ||||
| 		agg1.Update(ctx, x, record) | ||||
| 		test.CheckedUpdate(t, agg1, x, descriptor) | ||||
|  | ||||
| 		if !mt.absolute { | ||||
| 			y := profile.Random(-1) | ||||
| 			all.Append(y) | ||||
| 			agg1.Update(ctx, y, record) | ||||
| 			test.CheckedUpdate(t, agg1, y, descriptor) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	for i := 0; i < count; i++ { | ||||
| 		x := profile.Random(+1) | ||||
| 		all.Append(x) | ||||
| 		agg2.Update(ctx, x, record) | ||||
| 		test.CheckedUpdate(t, agg2, x, descriptor) | ||||
|  | ||||
| 		if !mt.absolute { | ||||
| 			y := profile.Random(-1) | ||||
| 			all.Append(y) | ||||
| 			agg2.Update(ctx, y, record) | ||||
| 			test.CheckedUpdate(t, agg2, y, descriptor) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	agg1.Collect(ctx, record, batcher) | ||||
| 	agg2.Collect(ctx, record, batcher) | ||||
| 	agg1.Checkpoint(ctx, descriptor) | ||||
| 	agg2.Checkpoint(ctx, descriptor) | ||||
|  | ||||
| 	agg1.Merge(agg2, record.Descriptor()) | ||||
| 	test.CheckedMerge(t, agg1, agg2, descriptor) | ||||
|  | ||||
| 	all.Sort() | ||||
|  | ||||
| 	asum, err := agg1.Sum() | ||||
| 	require.InDelta(t, | ||||
| 		all.Sum().CoerceToFloat64(profile.NumberKind), | ||||
| 		agg1.Sum().CoerceToFloat64(profile.NumberKind), | ||||
| 		asum.CoerceToFloat64(profile.NumberKind), | ||||
| 		1, | ||||
| 		"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() | ||||
| 	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" | ||||
| 	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 | ||||
| @@ -36,7 +37,7 @@ type ( | ||||
| 		// current is an atomic pointer to *gaugeData.  It is never nil. | ||||
| 		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 | ||||
| 	} | ||||
|  | ||||
| @@ -55,6 +56,7 @@ type ( | ||||
| ) | ||||
|  | ||||
| var _ export.Aggregator = &Aggregator{} | ||||
| var _ aggregator.LastValue = &Aggregator{} | ||||
|  | ||||
| // An unset gauge has zero timestamp and zero value. | ||||
| var unsetGauge = &gaugeData{} | ||||
| @@ -68,31 +70,30 @@ func New() *Aggregator { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // AsNumber returns the recorded gauge value as an int64. | ||||
| func (g *Aggregator) AsNumber() core.Number { | ||||
| 	return (*gaugeData)(g.checkpoint).value.AsNumber() | ||||
| // LastValue returns the last-recorded gauge value and the | ||||
| // corresponding timestamp.  The error value aggregator.ErrNoLastValue | ||||
| // 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. | ||||
| func (g *Aggregator) Timestamp() time.Time { | ||||
| 	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) { | ||||
| // Checkpoint atomically saves the current value. | ||||
| func (g *Aggregator) Checkpoint(ctx context.Context, _ *export.Descriptor) { | ||||
| 	g.checkpoint = atomic.LoadPointer(&g.current) | ||||
|  | ||||
| 	exp.Export(ctx, rec, g) | ||||
| } | ||||
|  | ||||
| // Update modifies the current value (atomically) for later export. | ||||
| func (g *Aggregator) Update(_ context.Context, number core.Number, rec export.Record) { | ||||
| 	desc := rec.Descriptor() | ||||
| // Update atomically sets the current "last" value. | ||||
| func (g *Aggregator) Update(_ context.Context, number core.Number, desc *export.Descriptor) error { | ||||
| 	if !desc.Alternate() { | ||||
| 		g.updateNonMonotonic(number) | ||||
| 	} else { | ||||
| 		g.updateMonotonic(number, desc) | ||||
| 		return nil | ||||
| 	} | ||||
| 	return g.updateMonotonic(number, desc) | ||||
| } | ||||
|  | ||||
| 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)) | ||||
| } | ||||
|  | ||||
| func (g *Aggregator) updateMonotonic(number core.Number, desc *export.Descriptor) { | ||||
| func (g *Aggregator) updateMonotonic(number core.Number, desc *export.Descriptor) error { | ||||
| 	ngd := &gaugeData{ | ||||
| 		timestamp: time.Now(), | ||||
| 		value:     number, | ||||
| @@ -114,21 +115,23 @@ func (g *Aggregator) updateMonotonic(number core.Number, desc *export.Descriptor | ||||
| 		gd := (*gaugeData)(atomic.LoadPointer(&g.current)) | ||||
|  | ||||
| 		if gd.value.CompareNumber(kind, number) > 0 { | ||||
| 			// TODO warn | ||||
| 			return | ||||
| 			return aggregator.ErrNonMonotoneInput | ||||
| 		} | ||||
|  | ||||
| 		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) | ||||
| 	if o == nil { | ||||
| 		// TODO warn | ||||
| 		return | ||||
| 		return aggregator.NewInconsistentMergeError(g, oa) | ||||
| 	} | ||||
|  | ||||
| 	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) | ||||
|  | ||||
| 		if cmp > 0 { | ||||
| 			return | ||||
| 			return nil | ||||
| 		} | ||||
|  | ||||
| 		if cmp < 0 { | ||||
| 			g.checkpoint = unsafe.Pointer(ogd) | ||||
| 			return | ||||
| 			return nil | ||||
| 		} | ||||
| 	} | ||||
| 	// Non-monotonic gauge or equal values | ||||
| 	if ggd.timestamp.After(ogd.timestamp) { | ||||
| 		return | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	g.checkpoint = unsafe.Pointer(ogd) | ||||
| 	return nil | ||||
| } | ||||
|   | ||||
| @@ -23,6 +23,7 @@ import ( | ||||
|  | ||||
| 	"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/test" | ||||
| ) | ||||
|  | ||||
| @@ -36,18 +37,20 @@ func TestGaugeNonMonotonic(t *testing.T) { | ||||
| 	test.RunProfiles(t, func(t *testing.T, profile test.Profile) { | ||||
| 		agg := New() | ||||
|  | ||||
| 		batcher, record := test.NewAggregatorTest(export.GaugeKind, profile.NumberKind, false) | ||||
| 		record := test.NewAggregatorTest(export.GaugeKind, profile.NumberKind, false) | ||||
|  | ||||
| 		var last core.Number | ||||
| 		for i := 0; i < count; i++ { | ||||
| 			x := profile.Random(rand.Intn(1)*2 - 1) | ||||
| 			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) { | ||||
| 		agg := New() | ||||
|  | ||||
| 		batcher, record := test.NewAggregatorTest(export.GaugeKind, profile.NumberKind, true) | ||||
| 		record := test.NewAggregatorTest(export.GaugeKind, profile.NumberKind, true) | ||||
|  | ||||
| 		small := profile.Random(+1) | ||||
| 		last := small | ||||
| 		for i := 0; i < count; i++ { | ||||
| 			x := profile.Random(+1) | ||||
| 			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) { | ||||
| 		agg := New() | ||||
|  | ||||
| 		batcher, record := test.NewAggregatorTest(export.GaugeKind, profile.NumberKind, true) | ||||
| 		record := test.NewAggregatorTest(export.GaugeKind, profile.NumberKind, true) | ||||
|  | ||||
| 		first := profile.Random(+1) | ||||
| 		agg.Update(ctx, first, record) | ||||
| 		test.CheckedUpdate(t, agg, first, record) | ||||
|  | ||||
| 		for i := 0; i < count; i++ { | ||||
| 			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() | ||||
| 		agg2 := New() | ||||
|  | ||||
| 		batcher, record := test.NewAggregatorTest(export.GaugeKind, profile.NumberKind, false) | ||||
| 		descriptor := test.NewAggregatorTest(export.GaugeKind, profile.NumberKind, false) | ||||
|  | ||||
| 		first1 := profile.Random(+1) | ||||
| 		first2 := profile.Random(+1) | ||||
| 		first1.AddNumber(profile.NumberKind, first2) | ||||
|  | ||||
| 		agg1.Update(ctx, first1, record) | ||||
| 		agg2.Update(ctx, first2, record) | ||||
| 		test.CheckedUpdate(t, agg1, first1, descriptor) | ||||
| 		test.CheckedUpdate(t, agg2, first2, descriptor) | ||||
|  | ||||
| 		agg1.Collect(ctx, record, batcher) | ||||
| 		agg2.Collect(ctx, record, batcher) | ||||
| 		agg1.Checkpoint(ctx, descriptor) | ||||
| 		agg2.Checkpoint(ctx, descriptor) | ||||
|  | ||||
| 		t1 := agg1.Timestamp() | ||||
| 		t2 := agg2.Timestamp() | ||||
| 		_, t1, err := agg1.LastValue() | ||||
| 		require.Nil(t, err) | ||||
| 		_, t2, err := agg2.LastValue() | ||||
| 		require.Nil(t, err) | ||||
| 		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") | ||||
| 		require.Equal(t, first2, agg1.AsNumber(), "Merged value - non-monotonic") | ||||
| 		lv, ts, err := agg1.LastValue() | ||||
| 		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() | ||||
| 		agg2 := New() | ||||
|  | ||||
| 		batcher, record := test.NewAggregatorTest(export.GaugeKind, profile.NumberKind, true) | ||||
| 		descriptor := test.NewAggregatorTest(export.GaugeKind, profile.NumberKind, true) | ||||
|  | ||||
| 		first1 := profile.Random(+1) | ||||
| 		agg1.Update(ctx, first1, record) | ||||
| 		test.CheckedUpdate(t, agg1, first1, descriptor) | ||||
|  | ||||
| 		first2 := profile.Random(+1) | ||||
| 		first2.AddNumber(profile.NumberKind, first1) | ||||
| 		agg2.Update(ctx, first2, record) | ||||
| 		test.CheckedUpdate(t, agg2, first2, descriptor) | ||||
|  | ||||
| 		agg1.Collect(ctx, record, batcher) | ||||
| 		agg2.Collect(ctx, record, batcher) | ||||
| 		agg1.Checkpoint(ctx, descriptor) | ||||
| 		agg2.Checkpoint(ctx, descriptor) | ||||
|  | ||||
| 		agg1.Merge(agg2, record.Descriptor()) | ||||
| 		test.CheckedMerge(t, agg1, agg2, descriptor) | ||||
|  | ||||
| 		require.Equal(t, first2, agg1.AsNumber(), "Merged value - monotonic") | ||||
| 		require.Equal(t, agg2.Timestamp(), agg1.Timestamp(), "Merged timestamp - monotonic") | ||||
| 		_, ts2, err := agg1.LastValue() | ||||
| 		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" | ||||
| 	export "go.opentelemetry.io/otel/sdk/export/metric" | ||||
| 	"go.opentelemetry.io/otel/sdk/export/metric/aggregator" | ||||
| ) | ||||
|  | ||||
| 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 { | ||||
| 	return &Aggregator{} | ||||
| } | ||||
|  | ||||
| // Sum returns the accumulated sum as a Number. | ||||
| func (c *Aggregator) Sum() core.Number { | ||||
| 	return c.checkpoint.sum | ||||
| // Sum returns the sum of values in the checkpoint. | ||||
| func (c *Aggregator) Sum() (core.Number, error) { | ||||
| 	return c.checkpoint.sum, nil | ||||
| } | ||||
|  | ||||
| // Count returns the accumulated count. | ||||
| func (c *Aggregator) Count() int64 { | ||||
| 	return int64(c.checkpoint.count.AsUint64()) | ||||
| // Count returns the number of values in the checkpoint. | ||||
| func (c *Aggregator) Count() (int64, error) { | ||||
| 	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) { | ||||
| 	return c.checkpoint.max, nil | ||||
| } | ||||
|  | ||||
| // Collect checkpoints the current value (atomically) and exports it. | ||||
| 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.  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 | ||||
| 	// 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.sum = c.current.sum.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. | ||||
| func (c *Aggregator) Update(_ context.Context, number core.Number, rec export.Record) { | ||||
| 	desc := rec.Descriptor() | ||||
| // Update adds the recorded measurement to the current data set. | ||||
| func (c *Aggregator) Update(_ context.Context, number core.Number, desc *export.Descriptor) error { | ||||
| 	kind := desc.NumberKind() | ||||
|  | ||||
| 	if !desc.Alternate() && number.IsNegative(kind) { | ||||
| 		// TODO warn | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	c.current.count.AddUint64Atomic(1) | ||||
| 	c.current.sum.AddNumberAtomic(kind, number) | ||||
|  | ||||
| @@ -100,13 +106,14 @@ func (c *Aggregator) Update(_ context.Context, number core.Number, rec export.Re | ||||
| 			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) | ||||
| 	if o == nil { | ||||
| 		// TODO warn | ||||
| 		return | ||||
| 		return aggregator.NewInconsistentMergeError(c, oa) | ||||
| 	} | ||||
|  | ||||
| 	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 { | ||||
| 		c.checkpoint.max.SetNumber(o.checkpoint.max) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|   | ||||
| @@ -30,7 +30,7 @@ func TestMaxSumCountAbsolute(t *testing.T) { | ||||
| 	ctx := context.Background() | ||||
|  | ||||
| 	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() | ||||
|  | ||||
| @@ -39,19 +39,24 @@ func TestMaxSumCountAbsolute(t *testing.T) { | ||||
| 		for i := 0; i < count; i++ { | ||||
| 			x := profile.Random(+1) | ||||
| 			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() | ||||
|  | ||||
| 		asum, err := agg.Sum() | ||||
| 		require.InEpsilon(t, | ||||
| 			all.Sum().CoerceToFloat64(profile.NumberKind), | ||||
| 			agg.Sum().CoerceToFloat64(profile.NumberKind), | ||||
| 			asum.CoerceToFloat64(profile.NumberKind), | ||||
| 			0.000000001, | ||||
| 			"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() | ||||
| 		require.Nil(t, err) | ||||
| @@ -66,7 +71,7 @@ func TestMaxSumCountMerge(t *testing.T) { | ||||
| 	ctx := context.Background() | ||||
|  | ||||
| 	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() | ||||
| 		agg2 := New() | ||||
| @@ -76,27 +81,32 @@ func TestMaxSumCountMerge(t *testing.T) { | ||||
| 		for i := 0; i < count; i++ { | ||||
| 			x := profile.Random(+1) | ||||
| 			all.Append(x) | ||||
| 			agg1.Update(ctx, x, record) | ||||
| 			test.CheckedUpdate(t, agg1, x, descriptor) | ||||
| 		} | ||||
| 		for i := 0; i < count; i++ { | ||||
| 			x := profile.Random(+1) | ||||
| 			all.Append(x) | ||||
| 			agg2.Update(ctx, x, record) | ||||
| 			test.CheckedUpdate(t, agg2, x, descriptor) | ||||
| 		} | ||||
|  | ||||
| 		agg1.Collect(ctx, record, batcher) | ||||
| 		agg2.Collect(ctx, record, batcher) | ||||
| 		agg1.Checkpoint(ctx, descriptor) | ||||
| 		agg2.Checkpoint(ctx, descriptor) | ||||
|  | ||||
| 		agg1.Merge(agg2, record.Descriptor()) | ||||
| 		test.CheckedMerge(t, agg1, agg2, descriptor) | ||||
|  | ||||
| 		all.Sort() | ||||
|  | ||||
| 		asum, err := agg1.Sum() | ||||
| 		require.InEpsilon(t, | ||||
| 			all.Sum().CoerceToFloat64(profile.NumberKind), | ||||
| 			agg1.Sum().CoerceToFloat64(profile.NumberKind), | ||||
| 			asum.CoerceToFloat64(profile.NumberKind), | ||||
| 			0.000000001, | ||||
| 			"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() | ||||
| 		require.Nil(t, err) | ||||
|   | ||||
| @@ -22,11 +22,9 @@ import ( | ||||
|  | ||||
| 	"go.opentelemetry.io/otel/api/core" | ||||
| 	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 | ||||
|  | ||||
| type Profile struct { | ||||
| @@ -52,31 +50,9 @@ func newProfiles() []Profile { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| type metricBatcher struct { | ||||
| } | ||||
|  | ||||
| type metricRecord struct { | ||||
| 	descriptor *export.Descriptor | ||||
| } | ||||
|  | ||||
| func NewAggregatorTest(mkind export.Kind, nkind core.NumberKind, alternate bool) (export.Batcher, export.Record) { | ||||
| func NewAggregatorTest(mkind export.MetricKind, nkind core.NumberKind, alternate bool) *export.Descriptor { | ||||
| 	desc := export.NewDescriptor("test.name", mkind, nil, "", "", nkind, alternate) | ||||
| 	return &metricBatcher{}, &metricRecord{descriptor: 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) { | ||||
| 	return desc | ||||
| } | ||||
|  | ||||
| func RunProfiles(t *testing.T, f func(*testing.T, Profile)) { | ||||
| @@ -147,3 +123,26 @@ func (n *Numbers) Median() core.Number { | ||||
| 	// specified quantile. | ||||
| 	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{ | ||||
| 		B: b, | ||||
| 	} | ||||
| 	bf.sdk = sdk.New(bf) | ||||
| 	bf.sdk = sdk.New(bf, sdk.DefaultLabelEncoder()) | ||||
| 	return bf | ||||
| } | ||||
|  | ||||
| func (bf *benchFixture) AggregatorFor(rec export.Record) export.Aggregator { | ||||
| 	switch rec.Descriptor().MetricKind() { | ||||
| func (*benchFixture) AggregatorFor(descriptor *export.Descriptor) export.Aggregator { | ||||
| 	switch descriptor.MetricKind() { | ||||
| 	case export.CounterKind: | ||||
| 		return counter.New() | ||||
| 	case export.GaugeKind: | ||||
| 		return gauge.New() | ||||
| 	case export.MeasureKind: | ||||
| 		if strings.HasSuffix(rec.Descriptor().Name(), "maxsumcount") { | ||||
| 		if strings.HasSuffix(descriptor.Name(), "maxsumcount") { | ||||
| 			return maxsumcount.New() | ||||
| 		} else if strings.HasSuffix(rec.Descriptor().Name(), "ddsketch") { | ||||
| 			return ddsketch.New(ddsketch.NewDefaultConfig(), rec.Descriptor()) | ||||
| 		} else if strings.HasSuffix(rec.Descriptor().Name(), "array") { | ||||
| 			return ddsketch.New(ddsketch.NewDefaultConfig(), rec.Descriptor()) | ||||
| 		} else if strings.HasSuffix(descriptor.Name(), "ddsketch") { | ||||
| 			return ddsketch.New(ddsketch.NewDefaultConfig(), descriptor) | ||||
| 		} else if strings.HasSuffix(descriptor.Name(), "array") { | ||||
| 			return ddsketch.New(ddsketch.NewDefaultConfig(), descriptor) | ||||
| 		} | ||||
| 	} | ||||
| 	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 { | ||||
|   | ||||
							
								
								
									
										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. | ||||
|  | ||||
| /* | ||||
| 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 | ||||
| supports configurable metrics export behavior through a | ||||
| `export.MetricBatcher` API.  Most metrics behavior is controlled | ||||
| by the `MetricBatcher`, including: | ||||
| The metric.Meter API consists of methods for constructing each of the | ||||
| basic kinds of metric instrument.  There are six types of instrument | ||||
| available to the end user, comprised of three basic kinds of metric | ||||
| instrument (Counter, Gauge, Measure) crossed with two kinds of number | ||||
| (int64, float64). | ||||
|  | ||||
| 1. Selecting the concrete type of aggregation to use | ||||
| 2. Receiving exported data during SDK.Collect() | ||||
| The API assists the SDK by consolidating the variety of metric instruments | ||||
| 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 | ||||
| `MetricBatcher` for each current record, asking the aggregator to | ||||
| export itself.  Aggregators, found in `./aggregators`, are responsible | ||||
| for receiving updates and exporting their current state. | ||||
| To this end, the API uses a core.Number type to represent either an int64 | ||||
| or a float64, depending on the instrument's definition.  A single | ||||
| implementation interface is used for instruments, metric.InstrumentImpl, | ||||
| and a single implementation interface is used for handles, | ||||
| metric.HandleImpl. | ||||
|  | ||||
| The SDK.Collect() API should be called by an exporter.  During the | ||||
| call to Collect(), the exporter receives calls in a single-threaded | ||||
| context.  No locking is required because the SDK.Collect() call | ||||
| prevents concurrency. | ||||
| There are three entry points for events in the Metrics API: via instrument | ||||
| handles, via direct instrument calls, and via BatchRecord.  The SDK is | ||||
| designed with handles as the primary entry point, the other two entry | ||||
| 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. | ||||
| There are three central data structures at work: | ||||
| Internal Structure | ||||
|  | ||||
| 1. A sync.Map maps unique (InstrumentID, LabelSet) to records | ||||
| 2. A "primary" atomic list of records | ||||
| 3. A "reclaim" atomic list of records | ||||
| The SDK is designed with minimal use of locking, to avoid adding | ||||
| contention for user-level code.  For each handle, whether it is held by | ||||
| 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 | ||||
| notion of the "current" epoch, which is incremented each time | ||||
| Collect() is called.  Records contain two atomic counter values, | ||||
| the epoch in which it was last modified and the epoch in which it | ||||
| was last collected.  Records may be garbage collected when the | ||||
| epoch in which they were last updated is less than the epoch in | ||||
| which they were last collected. | ||||
| A sync.Map maintains the mapping of current instruments and label sets to | ||||
| internal records.  To create a new handle, the SDK consults the Map to | ||||
| locate an existing record, otherwise it constructs a new record.  The SDK | ||||
| maintains a count of the number of references to each record, ensuring | ||||
| that records are not reclaimed from the Map while they are still active | ||||
| from the user's perspective. | ||||
|  | ||||
| Collect() performs a record-by-record scan of all active records | ||||
| and exports their current state, before incrementing the current | ||||
| epoch.  Collection events happen at a point in time during | ||||
| `Collect()`, but all records are not collected in the same instant. | ||||
| Metric collection is performed via a single-threaded call to Collect that | ||||
| sweeps through all records in the SDK, checkpointing their state.  When a | ||||
| record is discovered that has no references and has not been updated since | ||||
| 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/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/gauge" | ||||
| ) | ||||
| @@ -37,23 +38,31 @@ type monotoneBatcher struct { | ||||
| 	currentTime  *time.Time | ||||
| } | ||||
|  | ||||
| func (m *monotoneBatcher) AggregatorFor(rec export.Record) export.Aggregator { | ||||
| func (*monotoneBatcher) AggregatorFor(*export.Descriptor) export.Aggregator { | ||||
| 	return gauge.New() | ||||
| } | ||||
|  | ||||
| func (m *monotoneBatcher) Export(_ context.Context, record export.Record, agg export.Aggregator) { | ||||
| 	require.Equal(m.t, "my.gauge.name", record.Descriptor().Name()) | ||||
| 	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()) | ||||
| func (*monotoneBatcher) CheckpointSet() export.CheckpointSet { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| 	gauge := agg.(*gauge.Aggregator) | ||||
| 	val := gauge.AsNumber() | ||||
| 	ts := gauge.Timestamp() | ||||
| func (*monotoneBatcher) FinishedCollection() { | ||||
| } | ||||
|  | ||||
| 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.currentTime = &ts | ||||
| 	m.collections++ | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func TestMonotoneGauge(t *testing.T) { | ||||
| @@ -61,7 +70,9 @@ func TestMonotoneGauge(t *testing.T) { | ||||
| 	batcher := &monotoneBatcher{ | ||||
| 		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)) | ||||
|  | ||||
| @@ -106,7 +117,14 @@ func TestMonotoneGauge(t *testing.T) { | ||||
| 	require.Equal(t, 4, batcher.collections) | ||||
|  | ||||
| 	// Try to lower the value to 1, it will fail. | ||||
| 	var err error | ||||
| 	sdk.SetErrorHandler(func(sdkErr error) { | ||||
| 		err = sdkErr | ||||
| 	}) | ||||
| 	handle.Set(ctx, 1) | ||||
| 	require.Equal(t, aggregator.ErrNonMonotoneInput, err) | ||||
| 	sdk.SetErrorHandler(func(error) { t.Fatal("Unexpected") }) | ||||
|  | ||||
| 	sdk.Collect(ctx) | ||||
|  | ||||
| 	// The value and timestamp are both unmodified | ||||
|   | ||||
| @@ -15,8 +15,9 @@ | ||||
| package metric | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"sort" | ||||
| 	"sync" | ||||
| 	"sync/atomic" | ||||
| @@ -26,24 +27,22 @@ import ( | ||||
| 	"go.opentelemetry.io/otel/api/metric" | ||||
| 	api "go.opentelemetry.io/otel/api/metric" | ||||
| 	export "go.opentelemetry.io/otel/sdk/export/metric" | ||||
| 	"go.opentelemetry.io/otel/sdk/export/metric/aggregator" | ||||
| ) | ||||
|  | ||||
| type ( | ||||
| 	// 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 | ||||
| 	// current data.  Collect() should be arranged according to | ||||
| 	// the exporter model.  Push-based exporters will setup a | ||||
| 	// timer to call Collect() periodically.  Pull-based exporters | ||||
| 	// the batcher model.  Push-based batchers will setup a | ||||
| 	// timer to call Collect() periodically.  Pull-based batchers | ||||
| 	// will call Collect() when a pull request arrives. | ||||
| 	SDK struct { | ||||
| 		// current maps `mapkey` to *record. | ||||
| 		current sync.Map | ||||
|  | ||||
| 		// pool is a pool of labelset builders. | ||||
| 		pool sync.Pool // *bytes.Buffer | ||||
|  | ||||
| 		// empty is the (singleton) result of Labels() | ||||
| 		// w/ zero arguments. | ||||
| 		empty labels | ||||
| @@ -56,11 +55,17 @@ type ( | ||||
| 		// incremented in `Collect()`. | ||||
| 		currentEpoch int64 | ||||
|  | ||||
| 		// exporter is the configured exporter+configuration. | ||||
| 		exporter export.Batcher | ||||
| 		// batcher is the configured batcher+configuration. | ||||
| 		batcher export.Batcher | ||||
|  | ||||
| 		// lencoder determines how labels are uniquely encoded. | ||||
| 		labelEncoder export.LabelEncoder | ||||
|  | ||||
| 		// collectLock prevents simultaneous calls to Collect(). | ||||
| 		collectLock sync.Mutex | ||||
|  | ||||
| 		// errorHandler supports delivering errors to the user. | ||||
| 		errorHandler ErrorHandler | ||||
| 	} | ||||
|  | ||||
| 	instrument struct { | ||||
| @@ -127,6 +132,8 @@ type ( | ||||
| 		next doublePtr | ||||
| 	} | ||||
|  | ||||
| 	ErrorHandler func(error) | ||||
|  | ||||
| 	// singlePointer wraps an unsafe.Pointer and supports basic | ||||
| 	// load(), store(), clear(), and swapNil() operations. | ||||
| 	singlePtr struct { | ||||
| @@ -145,7 +152,6 @@ var ( | ||||
| 	_ api.LabelSet       = &labels{} | ||||
| 	_ api.InstrumentImpl = &instrument{} | ||||
| 	_ api.HandleImpl     = &record{} | ||||
| 	_ export.Record      = &record{} | ||||
|  | ||||
| 	// hazardRecord is used as a pointer value that indicates the | ||||
| 	// value is not included in any list.  (`nil` would be | ||||
| @@ -158,6 +164,10 @@ func (i *instrument) Meter() api.Meter { | ||||
| 	return i.meter | ||||
| } | ||||
|  | ||||
| func (m *SDK) SetErrorHandler(f ErrorHandler) { | ||||
| 	m.errorHandler = f | ||||
| } | ||||
|  | ||||
| func (i *instrument) acquireHandle(ls *labels) *record { | ||||
| 	// Create lookup key for sync.Map (one allocation) | ||||
| 	mk := mapkey{ | ||||
| @@ -179,10 +189,9 @@ func (i *instrument) acquireHandle(ls *labels) *record { | ||||
| 		refcount:       1, | ||||
| 		collectedEpoch: -1, | ||||
| 		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 | ||||
| 	// an interface here. | ||||
| 	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) | ||||
| } | ||||
|  | ||||
| // New constructs a new SDK for the given exporter.  This SDK supports | ||||
| // only a single exporter. | ||||
| // New constructs a new SDK for the given batcher.  This SDK supports | ||||
| // only a single batcher. | ||||
| // | ||||
| // 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 | ||||
| // exporter will call Collect() when it receives a request to scrape | ||||
| // current metric values.  A push-based exporter should configure its | ||||
| // batcher will call Collect() when it receives a request to scrape | ||||
| // current metric values.  A push-based batcher should configure its | ||||
| // own periodic collection. | ||||
| func New(exporter export.Batcher) *SDK { | ||||
| func New(batcher export.Batcher, labelEncoder export.LabelEncoder) *SDK { | ||||
| 	m := &SDK{ | ||||
| 		pool: sync.Pool{ | ||||
| 			New: func() interface{} { | ||||
| 				return &bytes.Buffer{} | ||||
| 			}, | ||||
| 		}, | ||||
| 		exporter: exporter, | ||||
| 		batcher:      batcher, | ||||
| 		labelEncoder: labelEncoder, | ||||
| 		errorHandler: DefaultErrorHandler, | ||||
| 	} | ||||
| 	m.empty.meter = 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 are de-duplicated, with last-value-wins semantics. | ||||
| func (m *SDK) Labels(kvs ...core.KeyValue) api.LabelSet { | ||||
| 	// Note: This computes a canonical encoding of the labels to | ||||
| 	// use as a map key.  It happens to use the encoding used by | ||||
| 	// 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 | ||||
| 	// exporters. | ||||
| 	// batchers. | ||||
|  | ||||
| 	// Check for empty set. | ||||
| 	if len(kvs) == 0 { | ||||
| @@ -263,21 +273,7 @@ func (m *SDK) Labels(kvs ...core.KeyValue) api.LabelSet { | ||||
| 	} | ||||
| 	ls.sorted = ls.sorted[0:oi] | ||||
|  | ||||
| 	// Serialize. | ||||
| 	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() | ||||
| 	ls.encoded = m.labelEncoder.Encode(ls.sorted) | ||||
|  | ||||
| 	return ls | ||||
| } | ||||
| @@ -291,7 +287,7 @@ func (m *SDK) labsFor(ls api.LabelSet) *labels { | ||||
| 	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( | ||||
| 		name, | ||||
| 		metricKind, | ||||
| @@ -370,10 +366,14 @@ func (m *SDK) saveFromReclaim(rec *record) { | ||||
| // | ||||
| // During the collection pass, the export.Batcher will receive | ||||
| // 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() | ||||
| 	defer m.collectLock.Unlock() | ||||
|  | ||||
| 	checkpointed := 0 | ||||
|  | ||||
| 	var next *record | ||||
| 	for inuse := m.records.primary.swapNil(); inuse != nil; inuse = next { | ||||
| 		next = inuse.next.primary.load() | ||||
| @@ -381,14 +381,14 @@ func (m *SDK) Collect(ctx context.Context) { | ||||
| 		refcount := atomic.LoadInt64(&inuse.refcount) | ||||
|  | ||||
| 		if refcount > 0 { | ||||
| 			m.collect(ctx, inuse) | ||||
| 			checkpointed += m.checkpoint(ctx, inuse) | ||||
| 			m.addPrimary(inuse) | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		modified := atomic.LoadInt64(&inuse.modifiedEpoch) | ||||
| 		collected := atomic.LoadInt64(&inuse.collectedEpoch) | ||||
| 		m.collect(ctx, inuse) | ||||
| 		checkpointed += m.checkpoint(ctx, inuse) | ||||
|  | ||||
| 		if modified >= collected { | ||||
| 			atomic.StoreInt64(&inuse.collectedEpoch, m.currentEpoch) | ||||
| @@ -409,18 +409,27 @@ func (m *SDK) Collect(ctx context.Context) { | ||||
| 		atomic.StoreInt64(&chances.reclaim, 0) | ||||
|  | ||||
| 		if chances.next.primary.load() == hazardRecord { | ||||
| 			m.collect(ctx, chances) | ||||
| 			checkpointed += m.checkpoint(ctx, chances) | ||||
| 			m.addPrimary(chances) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	m.currentEpoch++ | ||||
| 	return checkpointed | ||||
| } | ||||
|  | ||||
| func (m *SDK) collect(ctx context.Context, r *record) { | ||||
| 	if r.recorder != nil { | ||||
| 		r.recorder.Collect(ctx, r, m.exporter) | ||||
| func (m *SDK) checkpoint(ctx context.Context, r *record) int { | ||||
| 	if r.recorder == nil { | ||||
| 		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. | ||||
| @@ -439,13 +448,18 @@ func (m *SDK) GetDescriptor(inst metric.InstrumentImpl) *export.Descriptor { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (l *labels) Meter() api.Meter { | ||||
| 	return l.meter | ||||
| } | ||||
|  | ||||
| func (r *record) RecordOne(ctx context.Context, number core.Number) { | ||||
| 	if r.recorder != nil { | ||||
| 		r.recorder.Update(ctx, number, r) | ||||
| 	if r.recorder == nil { | ||||
| 		// 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, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| 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" | ||||
| 	api "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/counter" | ||||
| 	"go.opentelemetry.io/otel/sdk/metric/aggregator/gauge" | ||||
| @@ -222,13 +223,13 @@ func (f *testFixture) assertTest(numCollect int) { | ||||
| } | ||||
|  | ||||
| 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. | ||||
| 	f.dupCheck = map[testKey]int{} | ||||
| } | ||||
|  | ||||
| func (f *testFixture) AggregatorFor(record export.Record) export.Aggregator { | ||||
| 	switch record.Descriptor().MetricKind() { | ||||
| func (*testFixture) AggregatorFor(descriptor *export.Descriptor) export.Aggregator { | ||||
| 	switch descriptor.MetricKind() { | ||||
| 	case export.CounterKind: | ||||
| 		return counter.New() | ||||
| 	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) { | ||||
| 	desc := record.Descriptor() | ||||
| func (*testFixture) CheckpointSet() export.CheckpointSet { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (*testFixture) FinishedCollection() { | ||||
| } | ||||
|  | ||||
| func (f *testFixture) Process(_ context.Context, record export.Record) error { | ||||
| 	key := testKey{ | ||||
| 		labels:     canonicalizeLabels(record.Labels()), | ||||
| 		descriptor: desc, | ||||
| 		labels:     canonicalizeLabels(record.Labels().Ordered()), | ||||
| 		descriptor: record.Descriptor(), | ||||
| 	} | ||||
| 	if f.dupCheck[key] == 0 { | ||||
| 		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()) | ||||
|  | ||||
| 	switch desc.MetricKind() { | ||||
| 	agg := record.Aggregator() | ||||
| 	switch record.Descriptor().MetricKind() { | ||||
| 	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: | ||||
| 		gauge := agg.(*gauge.Aggregator) | ||||
| 		f.impl.storeCollect(actual, gauge.AsNumber(), gauge.Timestamp()) | ||||
| 		gauge := agg.(aggregator.LastValue) | ||||
| 		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: | ||||
| 		panic("Not used in this test") | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func stressTest(t *testing.T, impl testImpl) { | ||||
| @@ -272,7 +290,7 @@ func stressTest(t *testing.T, impl testImpl) { | ||||
| 		lused: map[string]bool{}, | ||||
| 	} | ||||
| 	cc := concurrency() | ||||
| 	sdk := sdk.New(fixture) | ||||
| 	sdk := sdk.New(fixture, sdk.DefaultLabelEncoder()) | ||||
| 	fixture.wg.Add(cc + 1) | ||||
|  | ||||
| 	for i := 0; i < cc; i++ { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user