diff --git a/structure/config.go b/structure/config.go index 16ab325..2a0bb2a 100644 --- a/structure/config.go +++ b/structure/config.go @@ -14,7 +14,14 @@ package structure // import "github.com/lightstep/go-expohisto/structure" -import "fmt" +import ( + "errors" + "fmt" + "strings" + + "github.com/lightstep/go-expohisto/mapping/exponent" + "github.com/lightstep/go-expohisto/mapping/logarithm" +) // DefaultMaxSize is the default maximum number of buckets per // positive or negative number range. The value 160 is specified by @@ -33,9 +40,17 @@ const MinSize = 2 // of giant histograms. const MaximumMaxSize = 16384 +const DefaultMaxScale = logarithm.MaxScale +const MinScale = exponent.MinScale +const MaximumMaxScale = logarithm.MaxScale + // Config contains configuration for exponential histogram creation. type Config struct { maxSize int32 + // the hasMaxScale boolean is used to apply the default value for maxScale if none has + // been set + hasMaxScale bool + maxScale int32 } // Option is the interface that applies a configuration option. @@ -59,6 +74,21 @@ func (ms maxSize) apply(cfg Config) Config { return cfg } +// WithMaxScale sets the maximum scale of the histogram. +func WithMaxScale(scale int32) Option { + return maxScale(scale) +} + +// maxSize is an option to set the maximum histogram size. +type maxScale int32 + +// apply implements Option. +func (ms maxScale) apply(cfg Config) Config { + cfg.maxScale = int32(ms) + cfg.hasMaxScale = true + return cfg +} + // NewConfig returns an exponential histogram configuration with // defaults and limits applied. func NewConfig(opts ...Option) Config { @@ -79,12 +109,25 @@ func (c Config) Valid() bool { // boolean indicating whether the the input was a valid // configurations. func (c Config) Validate() (Config, error) { - if c.maxSize >= MinSize && c.maxSize <= MaximumMaxSize { - return c, nil + var errs []error + + if err := c.validateMaxSize(); err != nil { + errs = append(errs, err) + } + if err := c.validateMaxScale(); err != nil { + errs = append(errs, err) } + + // not using errors.Join() as that requires Go 1.20 + return c, joinErrors(errs) +} + +func (c *Config) validateMaxSize() error { if c.maxSize == 0 { c.maxSize = DefaultMaxSize - return c, nil + } + if c.maxSize >= MinSize && c.maxSize <= MaximumMaxSize { + return nil } err := fmt.Errorf("invalid histogram size: %d", c.maxSize) if c.maxSize < 0 { @@ -94,5 +137,39 @@ func (c Config) Validate() (Config, error) { } else if c.maxSize > MaximumMaxSize { c.maxSize = MaximumMaxSize } - return c, err + + return err +} + +func (c *Config) validateMaxScale() error { + if !c.hasMaxScale { + c.hasMaxScale = true + c.maxScale = DefaultMaxScale + } + if c.maxScale >= MinScale && c.maxScale <= MaximumMaxScale { + return nil + } + err := fmt.Errorf("invalid histogram max scale: %d", c.maxScale) + if c.maxScale < MinScale { + c.maxScale = MinScale + } else if c.maxScale > MaximumMaxScale { + c.maxScale = MaximumMaxSize + } + + return err +} + +func joinErrors(errs []error) error { + switch len(errs) { + case 0: + return nil + case 1: + return errs[0] + default: + errStrings := make([]string, len(errs)) + for i, err := range errs { + errStrings[i] = err.Error() + } + return errors.New(strings.Join(errStrings, ", ")) + } } diff --git a/structure/config_test.go b/structure/config_test.go index 8fa4bc0..e76df09 100644 --- a/structure/config_test.go +++ b/structure/config_test.go @@ -23,11 +23,40 @@ import ( func TestConfigValid(t *testing.T) { require.True(t, Config{}.Valid()) require.True(t, NewConfig().Valid()) - require.True(t, NewConfig(WithMaxSize(MinSize)).Valid()) - require.True(t, NewConfig(WithMaxSize(MaximumMaxSize)).Valid()) - require.True(t, NewConfig(WithMaxSize((MinSize+MaximumMaxSize)/2)).Valid()) + require.True(t, NewConfig(WithMaxSize(MinSize), WithMaxScale(MinScale)).Valid()) + require.True(t, NewConfig(WithMaxSize(MaximumMaxSize), WithMaxScale(MaximumMaxScale)).Valid()) + require.True(t, NewConfig(WithMaxSize((MinSize+MaximumMaxSize)/2), WithMaxScale((MinScale+MaximumMaxScale)/2)).Valid()) require.False(t, NewConfig(WithMaxSize(-1)).Valid()) require.False(t, NewConfig(WithMaxSize(1<<20)).Valid()) require.False(t, NewConfig(WithMaxSize(1)).Valid()) + + require.False(t, NewConfig(WithMaxScale(-100)).Valid()) + require.False(t, NewConfig(WithMaxScale(100)).Valid()) +} + +func TestConfigOptions(t *testing.T) { + // zero value defaults + { + c, err := Config{}.Validate() + require.NoError(t, err) + require.Equal(t, DefaultMaxSize, c.maxSize) + require.Equal(t, DefaultMaxScale, c.maxScale) + } + + // NewConfig() value defaults + { + c, err := NewConfig().Validate() + require.NoError(t, err) + require.Equal(t, DefaultMaxSize, c.maxSize) + require.Equal(t, DefaultMaxScale, c.maxScale) + } + + // options + { + c, err := NewConfig(WithMaxSize(100), WithMaxScale(4)).Validate() + require.NoError(t, err) + require.Equal(t, int32(100), c.maxSize) + require.Equal(t, int32(4), c.maxScale) + } } diff --git a/structure/exponential.go b/structure/exponential.go index c6bcd9e..8fb3abc 100644 --- a/structure/exponential.go +++ b/structure/exponential.go @@ -37,6 +37,10 @@ type ( // Copy and Move. maxSize int32 + // maxScale is the maximum scale factor. it is set by + // Init(), preserved by Copy and Move. + maxScale int32 + // sum is the sum of all Updates reflected in the // aggregator. It has the same type number as the // corresponding sdkinstrument.Descriptor. @@ -160,8 +164,9 @@ func (h *Histogram[N]) Init(cfg Config) { cfg, _ = cfg.Validate() h.maxSize = cfg.maxSize + h.maxScale = cfg.maxScale - m, _ := newMapping(logarithm.MaxScale) + m, _ := newMapping(h.maxScale) h.mapping = m } @@ -249,7 +254,7 @@ func (h *Histogram[N]) Clear() { h.zeroCount = 0 h.min = 0 h.max = 0 - h.mapping, _ = newMapping(logarithm.MaxScale) + h.mapping, _ = newMapping(h.maxScale) } // clear zeros the backing array. diff --git a/structure/exponential_test.go b/structure/exponential_test.go index 79f1939..ae483f6 100644 --- a/structure/exponential_test.go +++ b/structure/exponential_test.go @@ -752,6 +752,17 @@ func TestZeroCountByIncr(t *testing.T) { requireEqual(t, h1, h2) } +// Tests that the maxScale option is respected +func TestMaxScale(t *testing.T) { + agg := NewFloat64(NewConfig(WithMaxScale(0))) + agg.Update(4) + agg.Update(1) + + require.Equal(t, int32(-1), agg.Positive().Offset()) + require.Equal(t, int32(0), agg.Scale()) + require.Equal(t, []uint64{1, 0, 1}, getCounts(agg.Positive())) +} + // Benchmarks the Update() function for values in the range [1,2). func BenchmarkLinear(b *testing.B) { src := rand.NewSource(77777677777)