From 376cda96a7b53d50e2a5612cc4ee3c1573d54abd Mon Sep 17 00:00:00 2001 From: Pavel <177363085+pkcll@users.noreply.github.com> Date: Fri, 21 Aug 2026 17:42:43 -0400 Subject: [PATCH] Add OTel metric export batching support --- go.mod | 18 +-- go.sum | 18 +++ pkg/beholder/client.go | 2 + pkg/beholder/client_test.go | 1 + pkg/beholder/config.go | 6 + pkg/beholder/httpclient.go | 2 + pkg/beholder/metric_export_batch_test.go | 167 +++++++++++++++++++++++ pkg/beholder/metric_export_batching.md | 20 +++ 8 files changed, 225 insertions(+), 9 deletions(-) create mode 100644 pkg/beholder/metric_export_batch_test.go create mode 100644 pkg/beholder/metric_export_batching.md diff --git a/go.mod b/go.mod index 10a6de1921..fba6a32cf0 100644 --- a/go.mod +++ b/go.mod @@ -57,10 +57,10 @@ require ( github.com/stretchr/testify v1.11.1 go.opentelemetry.io/contrib/bridges/prometheus v0.68.0 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 - go.opentelemetry.io/otel v1.43.0 + go.opentelemetry.io/otel v1.44.0 go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 @@ -68,11 +68,11 @@ require ( go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 go.opentelemetry.io/otel/log v0.19.0 - go.opentelemetry.io/otel/metric v1.43.0 - go.opentelemetry.io/otel/sdk v1.43.0 + go.opentelemetry.io/otel/metric v1.44.0 + go.opentelemetry.io/otel/sdk v1.44.0 go.opentelemetry.io/otel/sdk/log v0.19.0 - go.opentelemetry.io/otel/sdk/metric v1.43.0 - go.opentelemetry.io/otel/trace v1.43.0 + go.opentelemetry.io/otel/sdk/metric v1.44.0 + go.opentelemetry.io/otel/trace v1.44.0 go.uber.org/goleak v1.3.0 go.uber.org/zap v1.27.1 golang.org/x/crypto v0.53.0 @@ -81,7 +81,7 @@ require ( golang.org/x/time v0.15.0 golang.org/x/tools v0.45.0 gonum.org/v1/gonum v0.17.0 - google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa google.golang.org/grpc v1.82.1 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 @@ -110,7 +110,7 @@ require ( github.com/google/flatbuffers v25.2.10+incompatible // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect github.com/hako/durafmt v0.0.0-20200710122514-c0fb7b4da026 // indirect github.com/hashicorp/yamux v0.1.2 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect @@ -156,7 +156,7 @@ require ( golang.org/x/term v0.44.0 // indirect golang.org/x/text v0.38.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 9b24c80f67..cde085bf5a 100644 --- a/go.sum +++ b/go.sum @@ -134,6 +134,8 @@ github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4z github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= github.com/hako/durafmt v0.0.0-20200710122514-c0fb7b4da026 h1:BpJ2o0OR5FV7vrkDYfXYVJQeMNWa8RhklZOpW2ITAIQ= github.com/hako/durafmt v0.0.0-20200710122514-c0fb7b4da026/go.mod h1:5Scbynm8dF1XAPwIwkGPqzkM/shndPm79Jd1003hTjE= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= @@ -317,12 +319,16 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6 go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 h1:06ZeJRe5BnYXceSM9Vya83XXVaNGe3H1QqsvqRANQq8= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2/go.mod h1:DvPtKE63knkDVP88qpatBj81JxN+w1bqfVbsbCbj1WY= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0/go.mod h1:ji9vId85hMxqfvICA0Jt8JqEdrXaAkcpkI9HPXya0ro= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 h1:zwdo1gS2eH26Rg+CoqVQpEK1h8gvt5qyU5Kk5Bixvow= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0/go.mod h1:rUKCPscaRWWcqGT6HnEmYrK+YNe5+Sw64xgQTOJ5b30= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0 h1:SUplec5dp06reu1zaXmOXdvqH398taqrDXqUl99jxSc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0/go.mod h1:ho2g4N+ane+swq5I/VBkKWnRDY4kUINH3FuqyZqX/Ug= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= @@ -342,18 +348,26 @@ go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vud go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko= go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg= go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk= go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk= go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -464,8 +478,12 @@ google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEY google.golang.org/genproto v0.0.0-20210401141331-865547bb08e2/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec= google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= diff --git a/pkg/beholder/client.go b/pkg/beholder/client.go index 7f52d911bf..9ffbad0a41 100644 --- a/pkg/beholder/client.go +++ b/pkg/beholder/client.go @@ -551,6 +551,8 @@ func newMeterProvider(cfg Config, resource *sdkresource.Resource, auth Auth, cre for _, p := range cfg.MetricProducers { readerOpts = append(readerOpts, sdkmetric.WithProducer(p)) } + // NewPeriodicReader reads OTEL_GO_X_METRIC_EXPORT_BATCH_SIZE at + // construction and applies the upstream experimental data-point batching. mpOpts := append(cfg.metricOptions(), sdkmetric.WithReader(sdkmetric.NewPeriodicReader(exporter, readerOpts...)), sdkmetric.WithResource(resource), diff --git a/pkg/beholder/client_test.go b/pkg/beholder/client_test.go index 3a2e5a334c..8fde9dc82f 100644 --- a/pkg/beholder/client_test.go +++ b/pkg/beholder/client_test.go @@ -280,6 +280,7 @@ func TestNewClient(t *testing.T) { t.Run("HTTP endpoint set", func(t *testing.T) { client, err := beholder.NewClient(beholder.Config{ OtelExporterHTTPEndpoint: "http-endpoint", + InsecureConnection: true, }) require.NoError(t, err) assert.NotNil(t, client) diff --git a/pkg/beholder/config.go b/pkg/beholder/config.go index b65ab7300e..c11718b44e 100644 --- a/pkg/beholder/config.go +++ b/pkg/beholder/config.go @@ -37,6 +37,12 @@ type Config struct { TraceCompressor string // OTel Metric + // Metric export batching is controlled by the OTel SDK's experimental + // OTEL_GO_X_METRIC_EXPORT_BATCH_SIZE environment variable. Set it to a + // positive integer before the client is created to limit each exporter + // call to that many data points. The value is process-wide and is read + // when each PeriodicReader is constructed; unset, invalid, zero, and + // negative values preserve unbatched export behavior. MetricReaderInterval time.Duration MetricRetryConfig *RetryConfig MetricViews []metric.View diff --git a/pkg/beholder/httpclient.go b/pkg/beholder/httpclient.go index 7553a68ece..1b2e28f20e 100644 --- a/pkg/beholder/httpclient.go +++ b/pkg/beholder/httpclient.go @@ -287,6 +287,8 @@ func newHTTPMeterProvider(config Config, resource *sdkresource.Resource, tlsConf mpOpts := append(config.metricOptions(), sdkmetric.WithReader( + // NewPeriodicReader reads OTEL_GO_X_METRIC_EXPORT_BATCH_SIZE at + // construction and applies the upstream experimental data-point batching. sdkmetric.NewPeriodicReader( exporter, sdkmetric.WithInterval(config.MetricReaderInterval), // Default is 10s diff --git a/pkg/beholder/metric_export_batch_test.go b/pkg/beholder/metric_export_batch_test.go new file mode 100644 index 0000000000..6cb9f46b3c --- /dev/null +++ b/pkg/beholder/metric_export_batch_test.go @@ -0,0 +1,167 @@ +package beholder + +import ( + "context" + "os" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/metric/metricdata" +) + +const metricExportBatchSizeEnv = "OTEL_GO_X_METRIC_EXPORT_BATCH_SIZE" + +func TestPeriodicReaderMetricExportBatchSize(t *testing.T) { + testCases := []struct { + name string + envValue *string + wantExporters int + wantMaxPoints int + }{ + { + name: "positive value batches exports", + envValue: stringPtr("2"), + wantExporters: 3, + wantMaxPoints: 2, + }, + { + name: "invalid value is unbatched", + envValue: stringPtr("invalid"), + wantExporters: 1, + wantMaxPoints: 5, + }, + { + name: "zero is unbatched", + envValue: stringPtr("0"), + wantExporters: 1, + wantMaxPoints: 5, + }, + { + name: "negative value is unbatched", + envValue: stringPtr("-1"), + wantExporters: 1, + wantMaxPoints: 5, + }, + { + name: "unset value is unbatched", + wantExporters: 1, + wantMaxPoints: 5, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + setMetricExportBatchSize(t, tc.envValue) + + exporter := &metricBatchRecorder{} + reader := sdkmetric.NewPeriodicReader(exporter, sdkmetric.WithInterval(time.Hour)) + provider := sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader)) + t.Cleanup(func() { require.NoError(t, provider.Shutdown(context.Background())) }) + + meter := provider.Meter("beholder/metric-export-batching-test") + counter, err := meter.Int64Counter("metric_export_batching_points") + require.NoError(t, err) + for i := range 5 { + counter.Add(context.Background(), 1, metric.WithAttributes(attribute.Int("point", i))) + } + + require.NoError(t, provider.ForceFlush(context.Background())) + + batches := exporter.batchesSnapshot() + require.Len(t, batches, tc.wantExporters) + seen := make(map[int]int, 5) + for _, batch := range batches { + require.LessOrEqual(t, len(batch), tc.wantMaxPoints) + for _, point := range batch { + seen[point.attribute]++ + } + } + require.Len(t, seen, 5) + for i := range 5 { + require.Equal(t, 1, seen[i]) + } + }) + } +} + +func setMetricExportBatchSize(t *testing.T, value *string) { + t.Helper() + previous, wasSet := os.LookupEnv(metricExportBatchSizeEnv) + if value == nil { + require.NoError(t, os.Unsetenv(metricExportBatchSizeEnv)) + } else { + require.NoError(t, os.Setenv(metricExportBatchSizeEnv, *value)) + } + t.Cleanup(func() { + if wasSet { + _ = os.Setenv(metricExportBatchSizeEnv, previous) + return + } + _ = os.Unsetenv(metricExportBatchSizeEnv) + }) +} + +func stringPtr(value string) *string { return &value } + +type metricBatchRecorder struct { + mu sync.Mutex + batches [][]metricBatchPoint +} + +type metricBatchPoint struct { + attribute int +} + +var _ sdkmetric.Exporter = (*metricBatchRecorder)(nil) + +func (e *metricBatchRecorder) Temporality(sdkmetric.InstrumentKind) metricdata.Temporality { + return metricdata.CumulativeTemporality +} + +func (e *metricBatchRecorder) Aggregation(sdkmetric.InstrumentKind) sdkmetric.Aggregation { + return sdkmetric.AggregationDefault{} +} + +func (e *metricBatchRecorder) Export(_ context.Context, metrics *metricdata.ResourceMetrics) error { + var points []metricBatchPoint + for _, scope := range metrics.ScopeMetrics { + for _, metric := range scope.Metrics { + sum, ok := metric.Data.(metricdata.Sum[int64]) + if !ok { + continue + } + for _, point := range sum.DataPoints { + value, ok := point.Attributes.Value("point") + if !ok { + continue + } + points = append(points, metricBatchPoint{attribute: int(value.AsInt64())}) + } + } + } + + e.mu.Lock() + defer e.mu.Unlock() + e.batches = append(e.batches, points) + return nil +} + +func (e *metricBatchRecorder) ForceFlush(context.Context) error { return nil } + +func (e *metricBatchRecorder) Shutdown(context.Context) error { return nil } + +func (e *metricBatchRecorder) batchesSnapshot() [][]metricBatchPoint { + e.mu.Lock() + defer e.mu.Unlock() + + batches := make([][]metricBatchPoint, len(e.batches)) + for i, batch := range e.batches { + batches[i] = append([]metricBatchPoint(nil), batch...) + } + return batches +} diff --git a/pkg/beholder/metric_export_batching.md b/pkg/beholder/metric_export_batching.md new file mode 100644 index 0000000000..2d3b672b00 --- /dev/null +++ b/pkg/beholder/metric_export_batching.md @@ -0,0 +1,20 @@ +# OTel metric export batching + +The OpenTelemetry SDK can split one metric collection into multiple exporter +calls by setting this environment variable before the node constructs its +Beholder meter provider: + +```text +OTEL_GO_X_METRIC_EXPORT_BATCH_SIZE= +``` + +This is an experimental, process-wide SDK setting. It is read when each +`PeriodicReader` is constructed, so changing the environment afterward does +not reconfigure an existing reader. The value limits the number of metric +data points per exporter call; it is not a serialized-byte limit. Large +attributes, histograms, or a single oversized data point can still exceed a +collector receive limit. + +An unset, invalid, zero, or negative value preserves the default unbatched +behavior. Configure this at deployment time; library code must not mutate the +process environment.