From 2eb77f5257d4bb83a488c692d6bfffc348fbd3b7 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Wed, 30 Apr 2025 13:20:36 -0700 Subject: [PATCH 01/14] lint --- extension/extensionlimiter/Makefile | 1 + extension/extensionlimiter/README.md | 137 ++++++++++++ .../extensionlimiter/extensionlimiter.go | 103 +++++++++ extension/extensionlimiter/go.mod | 66 ++++++ extension/extensionlimiter/go.sum | 97 +++++++++ .../limiterhelper/consumer.go | 197 ++++++++++++++++++ .../limiterhelper/middleware.go | 140 +++++++++++++ extension/extensionlimiter/rate.go | 100 +++++++++ extension/extensionlimiter/resource.go | 125 +++++++++++ extension/extensionlimiter/weight.go | 71 +++++++ receiver/otlpreceiver/go.mod | 20 +- receiver/otlpreceiver/otlp.go | 61 +++++- 12 files changed, 1102 insertions(+), 16 deletions(-) create mode 100644 extension/extensionlimiter/Makefile create mode 100644 extension/extensionlimiter/README.md create mode 100644 extension/extensionlimiter/extensionlimiter.go create mode 100644 extension/extensionlimiter/go.mod create mode 100644 extension/extensionlimiter/go.sum create mode 100644 extension/extensionlimiter/limiterhelper/consumer.go create mode 100644 extension/extensionlimiter/limiterhelper/middleware.go create mode 100644 extension/extensionlimiter/rate.go create mode 100644 extension/extensionlimiter/resource.go create mode 100644 extension/extensionlimiter/weight.go diff --git a/extension/extensionlimiter/Makefile b/extension/extensionlimiter/Makefile new file mode 100644 index 00000000000..ded7a36092d --- /dev/null +++ b/extension/extensionlimiter/Makefile @@ -0,0 +1 @@ +include ../../Makefile.Common diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md new file mode 100644 index 00000000000..c64b85d7676 --- /dev/null +++ b/extension/extensionlimiter/README.md @@ -0,0 +1,137 @@ +# OpenTelemetry Collector Extension Limiter Package + +**Document status: development** + +The `extensionlimiter` package provides interfaces for rate limiting +and resource limiting in the OpenTelemetry Collector, enabling control +over data flow and resource usage through extensions which can be +configured through and middleware and/or directly by pipeline +components. + +## Overview + +This package defines two primary limiter types with their respective +interfaces: + +- **Rate Limiters**: Control time-based limits on quantities such as + bytes or items per second. +- **Resource Limiters**: Manage physical limits on quantities such as + concurrent requests or memory usage. + +Both limiter types are unified through the `LimiterWrapper` interface, +which simplifies consumer usage by providing a consistent `LimitCall` +interface. + +A limiter is **saturated** by definition when a limit is completely +overloaded, generally it means a limit request of any size would fail. + +Each each base limiter type and the wrapper type have corresponding +providers that give access to a limiter instance based on a weight +key. + +Weight keys describes the standard limiting dimensions. There are +currently four standard weight keys: network bytes, request count, +request items, and memory size. + +## Key Interfaces + +- `LimiterWrapper`: Provides a callback-based limiting interface that + works with both rate and resource limiters, has a `LimitCall` method. +- `RateLimiter`: Applies time-based limits, has a `Limit` method. +- `ResourceLimiter`: Manages physical resource limits, has + an `Acquire` method and corresponding `ReleaseFunc`. + +### Limiter helpers + +The `limiterhelper` subpackage provides: + +- Consumer wrappers apply limits to a collector pipeline (e.g., + `NewLimitedLogs` to combine a limiter using `consumer.NewLogs`) +- Multi-limiter combinators: `MultiLimiterWrapperProvider` builds a sequence of wrapped limiters. +- Middleware conversion utilities: Convert middleware configurations to `LimiterWrapperProvider`. + +## Recommendations + +For general use cases, prefer the `LimiterWrapper` interface with its +callback-based approach because it is agnostic to the difference between +rate and resource limiters. + +Use the direct `RateLimiter` or `ResourceLimiter` interfaces only in +special cases where control flow can't be easily scoped. + +Middleware configuration typically automates the configuration of +network bytes and request count weight keys relatively early in a +pipeline. Receivers are responsible for limiting request items and +memory size through one of the available helpers. + +Processors can apply limiters for specific reasons, for example to +apply limits in data-dependent ways. Exporters can apply limiters for +the same reasons, for example to apply limits in destination-dependent +ways. + +### Limiter blocking and failing + +Limiters implementations MAY block the request or fail immediately, +subject to internal logic. A limiter aims to avoid waste, which +requires balancing several factors. To fail a request that has already +been transmitted, received and parsed is sometimes more wasteful than +waiting for a little while; on the other hand waiting for a long time +risks wasting memory. In general, an overloaded limiter that is saturated SHOULD +fail requests immediately. + +Limiters implementations SHOULD consider the context deadline when +they block. If the deadline is likely to expire before the limit +becomes available, they should return a standard overload signal. + +### Limiter saturation + +All limiters feature a `MustDeny` method which is made available for +applications to test when a limit is fully saturated. This special +limit request is defined as the equivalent of passing a zero value to +the limiter. + +Limiters SHOULD treat a request for zero units of the limit as a +special case, used for indicating when non-zero limit requests are +likely to fail. This is not an exact requirement; implementations are +free to define their own saturation parameters. + +### Limit before or after use + +It is sometimes possible to request a limit before it is actually +used. As an example, consider a protocol using a compressed payload, +such that the receivers knows how much memory will be allocated before +the fact. In this case the receiver can request the limit before using +it, but this will not always be the case. Generally, prefer to limit +before use, but either way be consistent. + +When using the low-level interfaces directly, limits SHOULD be applied +before creating new concurrent work. + +### Examples + +Limiters applied through middleware are an implementation detail, +simply configure them using `configgrpc` or `confighttp`. For the +OTLP receiver (e.g., with two `ratelimiter` extensions): + +``` +extensions: + ratelimiter/limit_for_grpc: + # rate limiter settings for gRPC + ratelimiter/limit_for_grpc: + # rate limiter settings for HTTP + +receivers: + otlp: + protocols: + grpc: + middlewares: + - ratelimiter/limit_for_grpc + http: + middlewares: + - ratelimiter/limit_for_http +``` + +@@@ +a stream one +a pull-based one +a data-dependent one diff --git a/extension/extensionlimiter/extensionlimiter.go b/extension/extensionlimiter/extensionlimiter.go new file mode 100644 index 00000000000..3e2fa915513 --- /dev/null +++ b/extension/extensionlimiter/extensionlimiter.go @@ -0,0 +1,103 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package extensionlimiter // import "go.opentelemetry.io/collector/extension/extensionlimiter" + +import ( + "context" +) + +// LimiterWrapper is a general-purpose interface for limiter consumers +// to limit resources with use of a callback. This is the simplest +// form of rate limiting interface from a callers perspective. If the +// caller is a pipeline component, consider using a consumer-oriented +// limiterhelper (e.g., limiterhelper.NewLimitedLogs) to apply a list of +// +// Limiter implementions are meant to implement either the RateLimiter +// or ResourceLimiter interfaces. LimiterWrappers can be constructed +// from either of the underlying limiters and their corresponding +// providers. Usually configmiddleware or limiterhelper is responsible +// for constructing the correct wrapper from these two kinds of limiter; +// users will use this interface consistently. +type LimiterWrapper interface { + // Must deny is the logical equivalent of Acquire(0). If the + // Acquire would fail even for 0 units of a rate, the + // caller must deny the request. Implementations are + // encouraged to ensure that when MustDeny() is false, + // Acquire(0) is also false, however callers could use a + // faster code path to implement MustDeny() since it does not + // depend on the value. + MustDeny(context.Context) error + + // LimitCall applies the limiter and with the rate or resource + // granted makes a scoped call, returning success or an error + // from either the limiter or the enclosed callback. + LimitCall(context.Context, uint64, func(ctx context.Context) error) error +} + +// LimiterWrapperProvider provides access to LimiterWrappers, which is +// the appropriate interface for callers that can easily wrap a +// function call, because for wrapped calls there is no distinction +// between rate limiters and resource limiters. +type LimiterWrapperProvider interface { + LimiterWrapper(WeightKey) (LimiterWrapper, error) +} + +// LimiterWrapperFunc is a functional way to build LimiterWrappers. +type LimiterWrapperFunc func(context.Context, uint64, func(ctx context.Context) error) error + +var _ LimiterWrapper = LimiterWrapperFunc(nil) + +// MustDeny implements LimiterWrapper. +func (f LimiterWrapperFunc) MustDeny(ctx context.Context) error { + return f.LimitCall(ctx, 0, func(_ context.Context) error { + return nil + }) +} + +// LimitCall implements LimiterWrapper. +func (f LimiterWrapperFunc) LimitCall(ctx context.Context, value uint64, call func(ctx context.Context) error) error { + if f == nil { + return call(ctx) + } + return f(ctx, value, call) +} + +// PassThrough returns a LimiterWrapper that imposes no limit. +func PassThrough() LimiterWrapper { + return LimiterWrapperFunc(nil) +} + +// LimiterWrapperProviderFunc is a functional way to build LimiterWrappers. +type LimiterWrapperProviderFunc func(WeightKey) (LimiterWrapper, error) + +var _ LimiterWrapperProvider = LimiterWrapperProviderFunc(nil) + +// LimiterWrapper implements LimiterWrapperProvider. +func (f LimiterWrapperProviderFunc) LimiterWrapper(key WeightKey) (LimiterWrapper, error) { + return f(key) +} + +// NewResourceLimiterWrapperProvider constructs a +// LimiterWrapperProvider for a resource limiter extension. +func NewResourceLimiterWrapperProvider(rp ResourceLimiterProvider) LimiterWrapperProvider { + return LimiterWrapperProviderFunc(func(key WeightKey) (LimiterWrapper, error) { + lim, err := rp.ResourceLimiter(key) + if err == nil { + return nil, err + } + return NewResourceLimiterWrapper(lim), err + }) +} + +// NewRateLimiterWrapperProvider constructs a LimiterWrapperProvider +// for a rate limiter extension. +func NewRateLimiterWrapperProvider(rp RateLimiterProvider) LimiterWrapperProvider { + return LimiterWrapperProviderFunc(func(key WeightKey) (LimiterWrapper, error) { + lim, err := rp.RateLimiter(key) + if err == nil { + return nil, err + } + return NewRateLimiterWrapper(lim), err + }) +} diff --git a/extension/extensionlimiter/go.mod b/extension/extensionlimiter/go.mod new file mode 100644 index 00000000000..a9da7b72867 --- /dev/null +++ b/extension/extensionlimiter/go.mod @@ -0,0 +1,66 @@ +module go.opentelemetry.io/collector/extension/extensionlimiter + +go 1.23.0 + +require ( + go.opentelemetry.io/collector/component v1.31.0 + go.opentelemetry.io/collector/config/configmiddleware v0.0.0-00010101000000-000000000000 + go.opentelemetry.io/collector/consumer v1.31.0 + go.opentelemetry.io/collector/consumer/xconsumer v0.0.0-00010101000000-000000000000 + go.opentelemetry.io/collector/extension v1.31.0 + go.opentelemetry.io/collector/pdata v1.31.0 + go.opentelemetry.io/collector/pdata/pprofile v0.125.0 +) + +require ( + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/collector/extension/extensionmiddleware v1.30.0 // indirect + go.opentelemetry.io/collector/featuregate v1.31.0 // indirect + go.opentelemetry.io/collector/internal/telemetry v0.125.0 // indirect + go.opentelemetry.io/contrib/bridges/otelzap v0.10.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/log v0.11.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/sdk v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/text v0.24.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect + google.golang.org/grpc v1.72.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect +) + +replace go.opentelemetry.io/collector/consumer => ../../consumer + +replace go.opentelemetry.io/collector/featuregate => ../../featuregate + +replace go.opentelemetry.io/collector/component => ../../component + +replace go.opentelemetry.io/collector/consumer/xconsumer => ../../consumer/xconsumer + +replace go.opentelemetry.io/collector/pdata => ../../pdata + +replace go.opentelemetry.io/collector/pdata/pprofile => ../../pdata/pprofile + +replace go.opentelemetry.io/collector/config/configmiddleware => ../../config/configmiddleware + +replace go.opentelemetry.io/collector/extension/extensionmiddleware => ../../extension/extensionmiddleware + +replace go.opentelemetry.io/collector/pipeline => ../../pipeline + +replace go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest => ../extensionmiddleware/extensionmiddlewaretest + +replace go.opentelemetry.io/collector/extension => ../ + +replace go.opentelemetry.io/collector/internal/telemetry => ../../internal/telemetry diff --git a/extension/extensionlimiter/go.sum b/extension/extensionlimiter/go.sum new file mode 100644 index 00000000000..26de4444ce4 --- /dev/null +++ b/extension/extensionlimiter/go.sum @@ -0,0 +1,97 @@ +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/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/bridges/otelzap v0.10.0 h1:ojdSRDvjrnm30beHOmwsSvLpoRF40MlwNCA+Oo93kXU= +go.opentelemetry.io/contrib/bridges/otelzap v0.10.0/go.mod h1:oTTm4g7NEtHSV2i/0FeVdPaPgUIZPfQkFbq0vbzqnv0= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/log v0.11.0 h1:c24Hrlk5WJ8JWcwbQxdBqxZdOK7PcP/LFtOtwpDTe3Y= +go.opentelemetry.io/otel/log v0.11.0/go.mod h1:U/sxQ83FPmT29trrifhQg+Zj2lo1/IPN1PF6RTFqdwc= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= +google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM= +google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/extension/extensionlimiter/limiterhelper/consumer.go b/extension/extensionlimiter/limiterhelper/consumer.go new file mode 100644 index 00000000000..258d157d773 --- /dev/null +++ b/extension/extensionlimiter/limiterhelper/consumer.go @@ -0,0 +1,197 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package limiterhelper // import "go.opentelemetry.io/collector/extension/extensionlimiter/limiterhelper" + +import ( + "context" + "errors" + "slices" + + "go.opentelemetry.io/collector/consumer" + "go.opentelemetry.io/collector/consumer/xconsumer" + "go.opentelemetry.io/collector/extension/extensionlimiter" + "go.opentelemetry.io/collector/pdata/plog" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/pdata/pprofile" + "go.opentelemetry.io/collector/pdata/ptrace" +) + +// Traits object interface is generalized by P the pipeline data type +// (e.g., ptrace.Traces) and C the consumer type (e.g., +// consumer.Traces) +type traits[P, C any] interface { + // itemCount is SpanCount(), DataPointCount(), or LogRecordCount(). + itemCount(P) uint64 + // memorySize uses the appropriate protobuf Sizer as a proxy + // for memory used. + memorySize(data P) uint64 + // consume calls the appropriate consumer method (e.g., ConsumeTraces) + consume(ctx context.Context, data P, next C) error + // create is a functional constructor the consumer type (e.g., consumer.NewTraces) + create(func(ctx context.Context, data P) error, ...consumer.Option) (C, error) +} + +// Traces traits + +type traceTraits struct{} + +func (traceTraits) itemCount(data ptrace.Traces) uint64 { + return uint64(data.SpanCount()) +} + +func (traceTraits) memorySize(data ptrace.Traces) uint64 { + var sizer ptrace.MarshalSizer + return uint64(sizer.TracesSize(data)) +} + +func (traceTraits) create(next func(ctx context.Context, data ptrace.Traces) error, opts ...consumer.Option) (consumer.Traces, error) { + return consumer.NewTraces(next, opts...) +} + +func (traceTraits) consume(ctx context.Context, data ptrace.Traces, next consumer.Traces) error { + return next.ConsumeTraces(ctx, data) +} + +// Metrics traits + +type metricTraits struct{} + +func (metricTraits) itemCount(data pmetric.Metrics) uint64 { + return uint64(data.DataPointCount()) +} + +func (metricTraits) memorySize(data pmetric.Metrics) uint64 { + var sizer pmetric.MarshalSizer + return uint64(sizer.MetricsSize(data)) +} + +func (metricTraits) create(next func(ctx context.Context, data pmetric.Metrics) error, opts ...consumer.Option) (consumer.Metrics, error) { + return consumer.NewMetrics(next, opts...) +} + +func (metricTraits) consume(ctx context.Context, data pmetric.Metrics, next consumer.Metrics) error { + return next.ConsumeMetrics(ctx, data) +} + +// Logs traits + +type logTraits struct{} + +func (logTraits) itemCount(data plog.Logs) uint64 { + return uint64(data.LogRecordCount()) +} + +func (logTraits) memorySize(data plog.Logs) uint64 { + var sizer plog.MarshalSizer + return uint64(sizer.LogsSize(data)) +} + +func (logTraits) create(next func(ctx context.Context, data plog.Logs) error, opts ...consumer.Option) (consumer.Logs, error) { + return consumer.NewLogs(next, opts...) +} + +func (logTraits) consume(ctx context.Context, data plog.Logs, next consumer.Logs) error { + return next.ConsumeLogs(ctx, data) +} + +// Profiles traits + +type profileTraits struct{} + +func (profileTraits) itemCount(data pprofile.Profiles) uint64 { + return uint64(data.SampleCount()) +} + +func (profileTraits) memorySize(data pprofile.Profiles) uint64 { + var sizer pprofile.MarshalSizer + return uint64(sizer.ProfilesSize(data)) +} + +func (profileTraits) create(next func(ctx context.Context, data pprofile.Profiles) error, opts ...consumer.Option) (xconsumer.Profiles, error) { + return xconsumer.NewProfiles(next, opts...) +} + +func (profileTraits) consume(ctx context.Context, data pprofile.Profiles, next xconsumer.Profiles) error { + return next.ConsumeProfiles(ctx, data) +} + +// limitOne obtains a LimiterWrapper and applies a single weight limit. +func limitOne[P any, C any]( + next C, + keys []extensionlimiter.WeightKey, + provider extensionlimiter.LimiterWrapperProvider, + m traits[P, C], + key extensionlimiter.WeightKey, + opts []consumer.Option, + quantify func(P) uint64, +) (C, error) { + if !slices.Contains(keys, key) { + return next, nil + } + lim, err := provider.LimiterWrapper(key) + if err != nil { + return next, err + } + if lim == nil { + return next, nil + } + return m.create(func(ctx context.Context, data P) error { + return lim.LimitCall(ctx, quantify(data), func(ctx context.Context) error { + return m.consume(ctx, data, next) + }) + }, opts...) +} + +// newLimited is signal-generic limiting logic. +func newLimited[P any, C any]( + next C, + keys []extensionlimiter.WeightKey, + provider extensionlimiter.LimiterWrapperProvider, + m traits[P, C], + opts ...consumer.Option, +) (C, error) { + if provider == nil { + return next, nil + } + var err1, err2, err3 error + // Note: reverse order of evaluation cost => least-cost applied first. + next, err1 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyMemorySize, opts, + + func(data P) uint64 { + return m.memorySize(data) + }) + next, err2 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyRequestItems, opts, + func(data P) uint64 { + return m.itemCount(data) + }) + next, err3 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyRequestCount, opts, + func(_ P) uint64 { + return 1 + }) + return next, errors.Join(err1, err2, err3) +} + +// NewLimitedTraces applies a limiter using the provider over keys before calling next. +func NewLimitedTraces(next consumer.Traces, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (consumer.Traces, error) { + return newLimited(next, keys, provider, traceTraits{}, + consumer.WithCapabilities(next.Capabilities())) +} + +// NewLimitedLogs applies a limiter using the provider over keys before calling next. +func NewLimitedLogs(next consumer.Logs, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (consumer.Logs, error) { + return newLimited(next, keys, provider, logTraits{}, + consumer.WithCapabilities(next.Capabilities())) +} + +// NewLimitedMetrics applies a limiter using the provider over keys before calling next. +func NewLimitedMetrics(next consumer.Metrics, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (consumer.Metrics, error) { + return newLimited(next, keys, provider, metricTraits{}, + consumer.WithCapabilities(next.Capabilities())) +} + +// NewLimitedProfiles applies a limiter using the provider over keys before calling next. +func NewLimitedProfiles(next xconsumer.Profiles, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (xconsumer.Profiles, error) { + return newLimited(next, keys, provider, profileTraits{}, + consumer.WithCapabilities(next.Capabilities())) +} diff --git a/extension/extensionlimiter/limiterhelper/middleware.go b/extension/extensionlimiter/limiterhelper/middleware.go new file mode 100644 index 00000000000..91735349f63 --- /dev/null +++ b/extension/extensionlimiter/limiterhelper/middleware.go @@ -0,0 +1,140 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package limiterhelper // import "go.opentelemetry.io/collector/extension/extensionlimiter/limiterhelper" + +import ( + "context" + "errors" + "fmt" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/config/configmiddleware" + "go.opentelemetry.io/collector/extension" + "go.opentelemetry.io/collector/extension/extensionlimiter" +) + +var ( + ErrNotALimiter = errors.New("middleware is not a limiter") + ErrLimiterConflict = errors.New("limiter implements both rate and resource-limiters") + ErrUnresolvedLimiter = errors.New("could not resolve middleware limiter") +) + +// MiddlewareIsLimiter returns true if a middleware configuration +// represents a valid limiter, returns false for not found or invalid +// cases. If the named extension is found but is not a limiter, +// returns (false, nil). +func MiddlewareIsLimiter(host component.Host, middleware configmiddleware.Config) (bool, error) { + _, ok, err := middlewareIsLimiter(host, middleware) + return ok, err +} + +// MiddlewaresToLimiterWrapperProvider constructs a combined limiter +// from an ordered list of middlewares. This constructor ignores +// middleware configs that are not limiters. +// +// When no limiters are found (with no errors), the returned provider +// is nil. When a nil is passed to the consumer helpers (e.g., +// NewLimitedLogs) it will pass-through when the limiter is nil. +func MiddlewaresToLimiterWrapperProvider(host component.Host, middleware []configmiddleware.Config) (extensionlimiter.LimiterWrapperProvider, error) { + var retErr error + var providers []extensionlimiter.LimiterWrapperProvider + for _, mid := range middleware { + ok, err := MiddlewareIsLimiter(host, mid) + retErr = errors.Join(retErr, err) + if !ok { + continue + } + provider, err := MiddlewareToLimiterWrapperProvider(host, mid) + providers = append(providers, provider) + retErr = errors.Join(retErr, err) + } + if len(providers) == 0 { + return nil, nil + } + return MultiLimiterWrapperProvider(providers), nil +} + +// MiddlewareToLimiterWrapperProvider returns a limiter wrapper +// provider from middleware. Returns a package-level error if the +// middleware does not implement exactly one of the limiter +// interfaces (i.e., rate or resource). +func MiddlewareToLimiterWrapperProvider(host component.Host, middleware configmiddleware.Config) (extensionlimiter.LimiterWrapperProvider, error) { + ext, ok, err := middlewareIsLimiter(host, middleware) + if err != nil { + return nil, err + } + if ok { + if lim, ok := ext.(extensionlimiter.ResourceLimiterProvider); ok { + return extensionlimiter.NewResourceLimiterWrapperProvider(lim), nil + } + if lim, ok := ext.(extensionlimiter.RateLimiterProvider); ok { + return extensionlimiter.NewRateLimiterWrapperProvider(lim), nil + } + } + return nil, fmt.Errorf("%w: %s", ErrNotALimiter, ext) +} + +// middlewareIsLimiter applies consistency checks and returns a valid +// limiter extensions. +func middlewareIsLimiter(host component.Host, middleware configmiddleware.Config) (extension.Extension, bool, error) { + exts := host.GetExtensions() + ext := exts[middleware.ID] + if ext == nil { + return nil, false, fmt.Errorf("%w: %s", ErrUnresolvedLimiter, ext) + } + _, isResource := ext.(extensionlimiter.ResourceLimiterProvider) + _, isRate := ext.(extensionlimiter.RateLimiterProvider) + + switch { + case isResource && isRate: + return nil, false, fmt.Errorf("%w: %s", ErrLimiterConflict, ext) + case isResource, isRate: + return ext, true, nil + default: + return nil, false, nil + } +} + +// MultiLimiterWrapperProvider combines multiple limiter wrappers +// providers into a single provider by sequencing wrapped limiters. +// Returns errors from the underlying LimiterWrapper() calls, if any. +type MultiLimiterWrapperProvider []extensionlimiter.LimiterWrapperProvider + +var _ extensionlimiter.LimiterWrapperProvider = MultiLimiterWrapperProvider{} + +// LimiterWrapper implements LimiterWrapperProvider. +func (ps MultiLimiterWrapperProvider) LimiterWrapper(key extensionlimiter.WeightKey) (extensionlimiter.LimiterWrapper, error) { + if len(ps) == 0 { + return extensionlimiter.PassThrough(), nil + } + + // Map provider list to limiter list. + var lims []extensionlimiter.LimiterWrapper + + for _, provider := range ps { + lim, err := provider.LimiterWrapper(key) + if err == nil { + return nil, err + } + lims = append(lims, lim) + } + + // Compose limiters in sequence. + return sequenceLimiters(lims), nil +} + +func sequenceLimiters(lims []extensionlimiter.LimiterWrapper) extensionlimiter.LimiterWrapper { + if len(lims) == 1 { + return lims[0] + } + return composeLimiters(lims[0], sequenceLimiters(lims[1:])) +} + +func composeLimiters(first, second extensionlimiter.LimiterWrapper) extensionlimiter.LimiterWrapper { + return extensionlimiter.LimiterWrapperFunc(func(ctx context.Context, value uint64, call func(ctx context.Context) error) error { + return first.LimitCall(ctx, value, func(ctx context.Context) error { + return second.LimitCall(ctx, value, call) + }) + }) +} diff --git a/extension/extensionlimiter/rate.go b/extension/extensionlimiter/rate.go new file mode 100644 index 00000000000..70bc9986e35 --- /dev/null +++ b/extension/extensionlimiter/rate.go @@ -0,0 +1,100 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package extensionlimiter // import "go.opentelemetry.io/collector/extension/extensionlimiter" + +import ( + "context" +) + +// RateLimiterProvider is a provider for rate limiters. +// +// Limiter implementations will implement this or the +// ResourceLimiterProvider interface, but MUST not implement both. +// Limiters are covered by configmiddleware configuration, which is +// able to construct LimiterWrappers from these providers. +type RateLimiterProvider interface { + RateLimiter(WeightKey) (RateLimiter, error) +} + +// RateLimiterProviderFunc is a functional way to build RateLimters. +type RateLimiterProviderFunc func(WeightKey) (RateLimiter, error) + +var _ RateLimiterProvider = RateLimiterProviderFunc(nil) + +// RateLimiter implements RateLimiterProvider. +func (f RateLimiterProviderFunc) RateLimiter(key WeightKey) (RateLimiter, error) { + return f(key) +} + +// RateLimiter is an interface that an implementation makes available +// to apply time-based limits on quantities such as the number of +// bytes or items per second. +// +// This is a relatively low-level interface. Callers that can use a +// LimiterWrapper should choose that interface instead. This interface +// is meant for direct use only in special cases where control flow +// cannot be easily scoped to a callback, for example inside +// middleware (e.g., grpc.StatsHandler). +// +// See the README for more recommendations. +type RateLimiter interface { + // Must deny is the logical equivalent of Acquire(0). If the + // Acquire would fail even for 0 units of a rate, the + // caller must deny the request. Implementations are + // encouraged to ensure that when MustDeny() is false, + // Acquire(0) is also false, however callers could use a + // faster code path to implement MustDeny() since it does not + // depend on the value. + MustDeny(context.Context) error + + // Limit attempts to apply rate limiting with the provided + // weight, based on the key that was given to the provider. + // + // This is expected to block the caller until the weight can + // be admitted, or when the limit is completely saturated, + // limiters may also return immediate errors. + Limit(ctx context.Context, value uint64) error +} + +// RateLimiterFunc is an easy way to construct RateLimiters. +type RateLimiterFunc func(ctx context.Context, value uint64) error + +var _ RateLimiter = RateLimiterFunc(nil) + +// MustDeny implements RateLimiter. +func (f RateLimiterFunc) MustDeny(ctx context.Context) error { + return f.Limit(ctx, 0) +} + +// Limit implements RateLimiter. +func (f RateLimiterFunc) Limit(ctx context.Context, value uint64) error { + if f == nil { + return nil + } + return f(ctx, value) +} + +// NewRateLimiterWrapper returns a LimiterWrapper from a RateLimiter. +func NewRateLimiterWrapper(limiter RateLimiter) LimiterWrapper { + return rateLimiterWrapper{limiter: limiter} +} + +type rateLimiterWrapper struct { + limiter RateLimiter +} + +var _ LimiterWrapper = rateLimiterWrapper{} + +// MustDeny implements LimiterWrapper. +func (w rateLimiterWrapper) MustDeny(ctx context.Context) error { + return w.limiter.MustDeny(ctx) +} + +// LimitCall implements LimiterWrapper. +func (w rateLimiterWrapper) LimitCall(ctx context.Context, value uint64, call func(context.Context) error) error { + if err := w.limiter.Limit(ctx, value); err != nil { + return err + } + return call(ctx) +} diff --git a/extension/extensionlimiter/resource.go b/extension/extensionlimiter/resource.go new file mode 100644 index 00000000000..704a39e7d5b --- /dev/null +++ b/extension/extensionlimiter/resource.go @@ -0,0 +1,125 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package extensionlimiter // import "go.opentelemetry.io/collector/extension/extensionlimiter" + +import ( + "context" +) + +// ResourceLimiterProvider is a provider for resource limiters. +// +// Limiter implementations will implement this or the +// RateLimiterProvider interface, but MUST not implement both. +// Limiters are covered by configmiddleware configuration, which +// is able to construct LimiterWrappers from these providers. +type ResourceLimiterProvider interface { + ResourceLimiter(WeightKey) (ResourceLimiter, error) +} + +// ResourceLimiterProviderFunc is a functional way to build ResourceLimters. +type ResourceLimiterProviderFunc func(WeightKey) (ResourceLimiter, error) + +var _ ResourceLimiterProvider = ResourceLimiterProviderFunc(nil) + +// ResourceLimiter implements ResourceLimiterProvider. +func (f ResourceLimiterProviderFunc) ResourceLimiter(key WeightKey) (ResourceLimiter, error) { + return f(key) +} + +// ResourceLimiter is an interface that an implementation makes +// available to apply physical limits on quantities such as the number +// of concurrent requests or amount of memory in use. +// +// This is a relatively low-level interface. Callers that can use a +// LimiterWrapper should choose that interface instead. This +// interface is meant for direct use only in special cases where +// control flow is not scoped to a callback, for example in a +// streaming receiver where a limiter might be Acquired in the body of +// Send() and released prior to a corresponding Recv() (e.g., +// OTel-Arrow receiver). +// +// See the README for more recommendations. +type ResourceLimiter interface { + // Must deny is the logical equivalent of Acquire(0). If the + // Acquire would fail even for 0 units of a resource, the + // caller must deny the request. Implementations are + // encouraged to ensure that when MustDeny() is false, + // Acquire(0) is also false, however callers could use a + // faster code path to implement MustDeny() since it does not + // depend on the value. + MustDeny(context.Context) error + + // Acquire attempts to acquire a quantified resource with the + // provided weight, based on the key that was given to the + // provider. The caller has these options: + // + // - Accept and let the request proceed by returning a release func and a nil error + // - Fail and return a non-nil error and a nil release func + // - Block until the resource becomes available, then accept + // - Block until the context times out, return the error. + // + // See the README for more recommendations. + // + // On success, it returns a ReleaseFunc that should be called + // when the resources are no longer needed. + // + // Implementations are not required to call a release func + // when Acquire(0) is called, because there is nothing to + // release. Acquire(0) the equivalent of MustDeny(). + Acquire(ctx context.Context, value uint64) (ReleaseFunc, error) +} + +// ReleaseFunc is called when resources should be released after limiting. +// +// RelaseFunc values are never nil values, even in the error case, for +// safety. Users should unconditionally defer these. +type ReleaseFunc func() + +// ResourceLimiterFunc is a functional way to construct ResourceLimiters. +type ResourceLimiterFunc func(ctx context.Context, value uint64) (ReleaseFunc, error) + +var _ ResourceLimiter = ResourceLimiterFunc(nil) + +// MustDeny implements ResourceLimiter +func (f ResourceLimiterFunc) MustDeny(ctx context.Context) error { + _, err := f.Acquire(ctx, 0) + return err +} + +// Acquire implements ResourceLimiter +func (f ResourceLimiterFunc) Acquire(ctx context.Context, value uint64) (ReleaseFunc, error) { + if f == nil { + return func() {}, nil + } + return f(ctx, value) +} + +// NewResourceLimiterWrapper returns a LimiterWrapper from a ResourceLimiter. +func NewResourceLimiterWrapper(limiter ResourceLimiter) LimiterWrapper { + return resourceLimiterWrapper{limiter: limiter} +} + +type resourceLimiterWrapper struct { + limiter ResourceLimiter +} + +var _ LimiterWrapper = resourceLimiterWrapper{} + +// MustDeny implements LimiterWrapper. +func (w resourceLimiterWrapper) MustDeny(ctx context.Context) error { + if w.limiter == nil { + return nil + } + return w.limiter.MustDeny(ctx) +} + +// LimitCall implements LimiterWrapper. +func (w resourceLimiterWrapper) LimitCall(ctx context.Context, value uint64, call func(context.Context) error) error { + release, err := w.limiter.Acquire(ctx, value) + if err != nil { + return err + } + defer release() + return call(ctx) +} diff --git a/extension/extensionlimiter/weight.go b/extension/extensionlimiter/weight.go new file mode 100644 index 00000000000..16d5c8c4ec9 --- /dev/null +++ b/extension/extensionlimiter/weight.go @@ -0,0 +1,71 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package extensionlimiter // import "go.opentelemetry.io/collector/extension/extensionlimiter" + +// WeightKey is an enum type for common rate limits. The +// StandardAllKeys, StandardMiddlewareKeys, and +// StandardNotMiddlewareKeys methods return the list of middleware +// keys that can be automatically configured through middleware and +// not. +type WeightKey string + +// Predefined weight keys for common rate limits. This is not meant +// to be a closed set, new weight keys may be added in the future, +// possibly to restrict other kinds of event (e.g., auths, retries). +// +// Providers should return errors when they do not recognize a weight +// key. +const ( + // WeightKeyNetworkBytes is for network bytes. This is + // typically used with rate limiters. + WeightKeyNetworkBytes WeightKey = "network_bytes" + + // WeightKeyRequestCount can be used to limit the rate or + // total concurrent number of requests (i.e., pipeline data + // objects). This is typically used with both rate and + // resource limiters. + WeightKeyRequestCount WeightKey = "request_count" + + // WeightKeyRequestItems can be used to limit the rate or + // total concurrent number of items (log records, metric data + // points, spans, profiles). This is typically used with both + // rate and resource limiters. + WeightKeyRequestItems WeightKey = "request_items" + + // WeightKeyMemorySize is typically used with ResourceLimiters + // for limiting active memory usage. + WeightKeyMemorySize WeightKey = "memory_size" +) + +// StandardAllKeys is all the keys that can be automatically +// implemented by middleware and/or limiterhelper. +func StandardAllKeys() []WeightKey { + return []WeightKey{ + WeightKeyNetworkBytes, + WeightKeyRequestCount, + WeightKeyRequestItems, + WeightKeyMemorySize, + } +} + +// StandardMiddlewareKeys are typically handled in middleware for +// protocols that support it. Receivers should be careful not to +// re-apply these limits, especially not to twice-limit by +// WeightKeyRequestItems. +func StandardMiddlewareKeys() []WeightKey { + return []WeightKey{ + WeightKeyNetworkBytes, + WeightKeyRequestCount, + } +} + +// StandardNotMiddlewareKeys are the keys that are typically not +// handled through middlware because they are protocol specific and +// generally easier to handle after the input has become pdata. +func StandardNotMiddlewareKeys() []WeightKey { + return []WeightKey{ + WeightKeyRequestItems, + WeightKeyMemorySize, + } +} diff --git a/receiver/otlpreceiver/go.mod b/receiver/otlpreceiver/go.mod index 8ce013f88cd..ec076100e16 100644 --- a/receiver/otlpreceiver/go.mod +++ b/receiver/otlpreceiver/go.mod @@ -6,30 +6,31 @@ require ( github.com/gogo/protobuf v1.3.2 github.com/klauspost/compress v1.18.0 github.com/stretchr/testify v1.10.0 - go.opentelemetry.io/collector v0.125.0 + go.opentelemetry.io/collector v0.124.0 go.opentelemetry.io/collector/component v1.31.0 go.opentelemetry.io/collector/component/componentstatus v0.125.0 go.opentelemetry.io/collector/component/componenttest v0.125.0 go.opentelemetry.io/collector/config/configauth v0.125.0 - go.opentelemetry.io/collector/config/configgrpc v0.125.0 - go.opentelemetry.io/collector/config/confighttp v0.125.0 + go.opentelemetry.io/collector/config/configgrpc v0.124.0 + go.opentelemetry.io/collector/config/confighttp v0.124.0 go.opentelemetry.io/collector/config/confignet v1.31.0 go.opentelemetry.io/collector/config/configopaque v1.31.0 go.opentelemetry.io/collector/config/configtls v1.31.0 go.opentelemetry.io/collector/confmap v1.31.0 - go.opentelemetry.io/collector/confmap/xconfmap v0.125.0 + go.opentelemetry.io/collector/confmap/xconfmap v0.124.0 go.opentelemetry.io/collector/consumer v1.31.0 go.opentelemetry.io/collector/consumer/consumererror v0.125.0 go.opentelemetry.io/collector/consumer/consumertest v0.125.0 go.opentelemetry.io/collector/consumer/xconsumer v0.125.0 - go.opentelemetry.io/collector/internal/sharedcomponent v0.125.0 + go.opentelemetry.io/collector/extension/extensionlimiter v0.0.0-00010101000000-000000000000 + go.opentelemetry.io/collector/internal/sharedcomponent v0.124.0 go.opentelemetry.io/collector/internal/telemetry v0.125.0 go.opentelemetry.io/collector/pdata v1.31.0 go.opentelemetry.io/collector/pdata/pprofile v0.125.0 go.opentelemetry.io/collector/pdata/testdata v0.125.0 go.opentelemetry.io/collector/receiver v1.31.0 - go.opentelemetry.io/collector/receiver/receiverhelper v0.125.0 - go.opentelemetry.io/collector/receiver/receivertest v0.125.0 + go.opentelemetry.io/collector/receiver/receiverhelper v0.124.0 + go.opentelemetry.io/collector/receiver/receivertest v0.124.0 go.opentelemetry.io/collector/receiver/xreceiver v0.125.0 go.opentelemetry.io/otel v1.35.0 go.opentelemetry.io/otel/sdk/metric v1.35.0 @@ -68,8 +69,9 @@ require ( go.opentelemetry.io/collector/client v1.31.0 // indirect go.opentelemetry.io/collector/config/configcompression v1.31.0 // indirect go.opentelemetry.io/collector/config/configmiddleware v0.125.0 // indirect + go.opentelemetry.io/collector/extension v1.31.0 // indirect go.opentelemetry.io/collector/extension/extensionauth v1.31.0 // indirect - go.opentelemetry.io/collector/extension/extensionmiddleware v0.125.0 // indirect + go.opentelemetry.io/collector/extension/extensionmiddleware v1.30.0 // indirect go.opentelemetry.io/collector/featuregate v1.31.0 // indirect go.opentelemetry.io/collector/pipeline v0.125.0 // indirect go.opentelemetry.io/contrib/bridges/otelzap v0.10.0 // indirect @@ -159,6 +161,8 @@ replace go.opentelemetry.io/collector/extension/extensionauth/extensionauthtest replace go.opentelemetry.io/collector/extension/extensionmiddleware => ../../extension/extensionmiddleware +replace go.opentelemetry.io/collector/extension/extensionlimiter => ../../extension/extensionlimiter + replace go.opentelemetry.io/collector/config/configmiddleware => ../../config/configmiddleware replace go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest => ../../extension/extensionmiddleware/extensionmiddlewaretest diff --git a/receiver/otlpreceiver/otlp.go b/receiver/otlpreceiver/otlp.go index 7e5ab5d4b30..4a92a634a28 100644 --- a/receiver/otlpreceiver/otlp.go +++ b/receiver/otlpreceiver/otlp.go @@ -18,6 +18,8 @@ import ( "go.opentelemetry.io/collector/config/confighttp" "go.opentelemetry.io/collector/consumer" "go.opentelemetry.io/collector/consumer/xconsumer" + "go.opentelemetry.io/collector/extension/extensionlimiter" + "go.opentelemetry.io/collector/extension/extensionlimiter/limiterhelper" "go.opentelemetry.io/collector/internal/telemetry" "go.opentelemetry.io/collector/internal/telemetry/componentattribute" "go.opentelemetry.io/collector/pdata/plog/plogotlp" @@ -97,20 +99,42 @@ func (r *otlpReceiver) startGRPCServer(host component.Host) error { return err } + limitKeys := extensionlimiter.StandardNotMiddlewareKeys() + limiterProvider, err := limiterhelper.MiddlewaresToLimiterWrapperProvider(host, r.cfg.GRPC.Middlewares) + if err != nil { + return err + } + if r.nextTraces != nil { - ptraceotlp.RegisterGRPCServer(r.serverGRPC, trace.New(r.nextTraces, r.obsrepGRPC)) + next, err := limiterhelper.NewLimitedTraces(r.nextTraces, limitKeys, limiterProvider) + if err != nil { + return err + } + ptraceotlp.RegisterGRPCServer(r.serverGRPC, trace.New(next, r.obsrepGRPC)) } if r.nextMetrics != nil { - pmetricotlp.RegisterGRPCServer(r.serverGRPC, metrics.New(r.nextMetrics, r.obsrepGRPC)) + next, err := limiterhelper.NewLimitedMetrics(r.nextMetrics, limitKeys, limiterProvider) + if err != nil { + return err + } + pmetricotlp.RegisterGRPCServer(r.serverGRPC, metrics.New(next, r.obsrepGRPC)) } if r.nextLogs != nil { - plogotlp.RegisterGRPCServer(r.serverGRPC, logs.New(r.nextLogs, r.obsrepGRPC)) + next, err := limiterhelper.NewLimitedLogs(r.nextLogs, limitKeys, limiterProvider) + if err != nil { + return err + } + plogotlp.RegisterGRPCServer(r.serverGRPC, logs.New(next, r.obsrepGRPC)) } if r.nextProfiles != nil { - pprofileotlp.RegisterGRPCServer(r.serverGRPC, profiles.New(r.nextProfiles)) + next, err := limiterhelper.NewLimitedProfiles(r.nextProfiles, limitKeys, limiterProvider) + if err != nil { + return err + } + pprofileotlp.RegisterGRPCServer(r.serverGRPC, profiles.New(next)) } r.settings.Logger.Info("Starting GRPC server", zap.String("endpoint", r.cfg.GRPC.NetAddr.Endpoint)) @@ -136,15 +160,29 @@ func (r *otlpReceiver) startHTTPServer(ctx context.Context, host component.Host) return nil } + limitKeys := extensionlimiter.StandardNotMiddlewareKeys() + limiterProvider, err := limiterhelper.MiddlewaresToLimiterWrapperProvider(host, r.cfg.HTTP.ServerConfig.Middlewares) + if err != nil { + return err + } + httpMux := http.NewServeMux() if r.nextTraces != nil { - httpTracesReceiver := trace.New(r.nextTraces, r.obsrepHTTP) + next, err := limiterhelper.NewLimitedTraces(r.nextTraces, limitKeys, limiterProvider) + if err != nil { + return err + } + httpTracesReceiver := trace.New(next, r.obsrepHTTP) httpMux.HandleFunc(r.cfg.HTTP.TracesURLPath, func(resp http.ResponseWriter, req *http.Request) { handleTraces(resp, req, httpTracesReceiver) }) } if r.nextMetrics != nil { + _, err := limiterhelper.NewLimitedMetrics(r.nextMetrics, limitKeys, limiterProvider) + if err != nil { + return err + } httpMetricsReceiver := metrics.New(r.nextMetrics, r.obsrepHTTP) httpMux.HandleFunc(r.cfg.HTTP.MetricsURLPath, func(resp http.ResponseWriter, req *http.Request) { handleMetrics(resp, req, httpMetricsReceiver) @@ -152,20 +190,27 @@ func (r *otlpReceiver) startHTTPServer(ctx context.Context, host component.Host) } if r.nextLogs != nil { - httpLogsReceiver := logs.New(r.nextLogs, r.obsrepHTTP) + next, err := limiterhelper.NewLimitedLogs(r.nextLogs, limitKeys, limiterProvider) + if err != nil { + return err + } + httpLogsReceiver := logs.New(next, r.obsrepHTTP) httpMux.HandleFunc(r.cfg.HTTP.LogsURLPath, func(resp http.ResponseWriter, req *http.Request) { handleLogs(resp, req, httpLogsReceiver) }) } if r.nextProfiles != nil { - httpProfilesReceiver := profiles.New(r.nextProfiles) + next, err := limiterhelper.NewLimitedProfiles(r.nextProfiles, limitKeys, limiterProvider) + if err != nil { + return err + } + httpProfilesReceiver := profiles.New(next) httpMux.HandleFunc(defaultProfilesURLPath, func(resp http.ResponseWriter, req *http.Request) { handleProfiles(resp, req, httpProfilesReceiver) }) } - var err error if r.serverHTTP, err = r.cfg.HTTP.ServerConfig.ToServer(ctx, host, r.settings.TelemetrySettings, httpMux, confighttp.WithErrorHandler(errorHandler)); err != nil { return err } From 04380f4243bbf0bd9057235ed48b363d1cc7e99d Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Wed, 30 Apr 2025 14:39:19 -0700 Subject: [PATCH 02/14] close --- extension/extensionlimiter/README.md | 137 +++++++++++++++++- .../extensionlimiter/extensionlimiter.go | 96 +++--------- .../limiterhelper/consumer.go | 32 ++-- .../limiterhelper/middleware.go | 4 + extension/extensionlimiter/rate.go | 10 +- extension/extensionlimiter/resource.go | 10 +- extension/extensionlimiter/wrapper.go | 97 +++++++++++++ 7 files changed, 278 insertions(+), 108 deletions(-) create mode 100644 extension/extensionlimiter/wrapper.go diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index c64b85d7676..0c7645e6109 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -109,6 +109,8 @@ before creating new concurrent work. ### Examples +#### OTLP receiver + Limiters applied through middleware are an implementation detail, simply configure them using `configgrpc` or `confighttp`. For the OTLP receiver (e.g., with two `ratelimiter` extensions): @@ -131,7 +133,134 @@ receivers: - ratelimiter/limit_for_http ``` -@@@ -a stream one -a pull-based one -a data-dependent one +Note that the OTLP receiver specifically supports multiple protocols +with separate middleware configurations, thus it configures limiters +for request items and memory size on a protocol-by-protocol +basis. + +#### HTTP metrics scraper + +A HTTP pull-based receiver can implement a basic limited scraper loop +as follows. The HTTP client config object's `middlewares` field +automatically configures network bytes and request count limits: + +``` +receivers: + scraper: + http: + middlewares: + - ratelimiter/scraper +``` + +Limiter extensions are derived from a host, a middlewares list, and a +list of weight keys. When middleware is configurable at the factory +level, it may be added via `receiver.NewFactory` using +`receiver.WithLimiters(getLimiters)`: + +``` +func NewFactory() receiver.Factory { + return xreceiver.NewFactory( + metadata.Type, + createDefaultConfig, + xreceiver.WithMetrics(createMetrics, metadata.MetricsStability), + xreceiver.WithLimiters(getLimiters), + ) +} +``` + +Here, `getLimiters` is a function to get the effective +`[]configmiddleware.Config` and derive pipeline consumers using +`limiterhelper` adapters. + +To acquire a limiter, use `MiddlewaresToLimiterWrapperProvider` to +obtain a combined limiter wrapper around the input `nextMetrics` +consumer. It will pass `StandardNotMiddlewareKeys()` indicating to +apply request items and memory size: + +``` + // Extract limiter provider from middlewares. + s.limiterProvider, err = limiterhelper.MiddlewaresToLimiterWrapperProvider( + cfg.Middlewares) + if err != nil { ... } + + // Here get a limiter-wrapped pipeline and a combination of weight-specific + // limiters for MustDeny() functionality. + s.anyLimiter, s.nextMetrics, err = limiterhelper.NewLimitedMetrics( + s.nextMetrics, limiterhelper.StandardNotMiddlewareKeys(), s.limiterProvider) + if err != nil { ... } +``` + +In the scraper loop, use `MustDeny` before starting a scrape: + +``` +func (s *scraper) scrapeOnce(ctx context.Context) error { + if err := s.anyLimiter.MustDeny(ctx); err != nil { + return err + } + + // Network bytes and request count limits are applied in middleware. + // before this returns: + data, err := s.getData(ctx) + if err != nil { + return err + } + + // Request items and memory size are applied in the pipeline. + return s.nextMetrics.ConsumeMetrics(ctx, data) +} +``` + +#### gRPC stream receiver + +A gRPC streaming receiver that holds memory across its allocated in +`Send()` and does not release it until after a corresponding `Recv()` +requires use of the lower-level `ResourceLimiter` interface. +The gRPC config object's `middlewares` field +automatically configures network bytes and request count limits: + +``` +receivers: + streamer: + grpc: + middlewares: + - ratelimiter/streamer +``` + +The receiver will check `s.anyLimiter.MustDeny()` as above. In a +stream, limiters are expected to block the stream until limit requests +succeed, however after the limit requests succeed, the receiver may +wish to return from `Send()` to continue accepting new requests while +the consumer works in a separate goroutine. The limit will be released +after the consumer returns. + +``` +func (s *scraper) LogsStream(ctx context.Context, stream *Stream) error { + for { + // Check saturation for all limiters. + err := s.anyLimiter.MustDeny(ctx) + if err != nil { ... } + + // The network bytes and request count are applied in middleware. + req, err := stream.Recv() + if err != nil { ... } + + // Allocate memory objects. + data, err := s.getLogs(ctx, req) + if err != nil { ... } + + release, err := s.memorySizeLimiter.Acquire(ctx, pdataSize(data)) + if err != nil { ... } + + go func() { + // Request items limit is applied in the pipeline consumer + err := s.nextMetrics.ConsumeMetrics(ctx, data) + + // Release the memory. + release() + + // Reply to the caller. + stream.Send(streamResponseFromConsumerError(err)) + } + } +} +``` diff --git a/extension/extensionlimiter/extensionlimiter.go b/extension/extensionlimiter/extensionlimiter.go index 3e2fa915513..78fcbf32d90 100644 --- a/extension/extensionlimiter/extensionlimiter.go +++ b/extension/extensionlimiter/extensionlimiter.go @@ -7,19 +7,10 @@ import ( "context" ) -// LimiterWrapper is a general-purpose interface for limiter consumers -// to limit resources with use of a callback. This is the simplest -// form of rate limiting interface from a callers perspective. If the -// caller is a pipeline component, consider using a consumer-oriented -// limiterhelper (e.g., limiterhelper.NewLimitedLogs) to apply a list of -// -// Limiter implementions are meant to implement either the RateLimiter -// or ResourceLimiter interfaces. LimiterWrappers can be constructed -// from either of the underlying limiters and their corresponding -// providers. Usually configmiddleware or limiterhelper is responsible -// for constructing the correct wrapper from these two kinds of limiter; -// users will use this interface consistently. -type LimiterWrapper interface { +// Limiter is the common functionality implemented by LimiterWrapper, +// RateLimiter, and ResourceLimiter. This can be called prior to the +// start of work to check for limiter saturation. +type Limiter interface { // Must deny is the logical equivalent of Acquire(0). If the // Acquire would fail even for 0 units of a rate, the // caller must deny the request. Implementations are @@ -28,76 +19,35 @@ type LimiterWrapper interface { // faster code path to implement MustDeny() since it does not // depend on the value. MustDeny(context.Context) error - - // LimitCall applies the limiter and with the rate or resource - // granted makes a scoped call, returning success or an error - // from either the limiter or the enclosed callback. - LimitCall(context.Context, uint64, func(ctx context.Context) error) error -} - -// LimiterWrapperProvider provides access to LimiterWrappers, which is -// the appropriate interface for callers that can easily wrap a -// function call, because for wrapped calls there is no distinction -// between rate limiters and resource limiters. -type LimiterWrapperProvider interface { - LimiterWrapper(WeightKey) (LimiterWrapper, error) } -// LimiterWrapperFunc is a functional way to build LimiterWrappers. -type LimiterWrapperFunc func(context.Context, uint64, func(ctx context.Context) error) error +// LimiterFunc is a functional way to build MustDeny functions. +type LimiterFunc func(context.Context) error -var _ LimiterWrapper = LimiterWrapperFunc(nil) +var _ Limiter = LimiterFunc(nil) -// MustDeny implements LimiterWrapper. -func (f LimiterWrapperFunc) MustDeny(ctx context.Context) error { - return f.LimitCall(ctx, 0, func(_ context.Context) error { - return nil - }) -} - -// LimitCall implements LimiterWrapper. -func (f LimiterWrapperFunc) LimitCall(ctx context.Context, value uint64, call func(ctx context.Context) error) error { +// MustDeny implements Limiter. +func (f LimiterFunc) MustDeny(ctx context.Context) error { if f == nil { - return call(ctx) + return nil } - return f(ctx, value, call) + return f(ctx) } -// PassThrough returns a LimiterWrapper that imposes no limit. -func PassThrough() LimiterWrapper { - return LimiterWrapperFunc(nil) -} - -// LimiterWrapperProviderFunc is a functional way to build LimiterWrappers. -type LimiterWrapperProviderFunc func(WeightKey) (LimiterWrapper, error) +// MultiLimiter returns MustDeny when any element returns MustDeny. +type MultiLimiter []Limiter -var _ LimiterWrapperProvider = LimiterWrapperProviderFunc(nil) - -// LimiterWrapper implements LimiterWrapperProvider. -func (f LimiterWrapperProviderFunc) LimiterWrapper(key WeightKey) (LimiterWrapper, error) { - return f(key) -} +var _ Limiter = MultiLimiter{} -// NewResourceLimiterWrapperProvider constructs a -// LimiterWrapperProvider for a resource limiter extension. -func NewResourceLimiterWrapperProvider(rp ResourceLimiterProvider) LimiterWrapperProvider { - return LimiterWrapperProviderFunc(func(key WeightKey) (LimiterWrapper, error) { - lim, err := rp.ResourceLimiter(key) - if err == nil { - return nil, err +// MustDeny implements Limiter. +func (ls MultiLimiter) MustDeny(ctx context.Context) error { + for _, lim := range ls { + if lim == nil { + continue } - return NewResourceLimiterWrapper(lim), err - }) -} - -// NewRateLimiterWrapperProvider constructs a LimiterWrapperProvider -// for a rate limiter extension. -func NewRateLimiterWrapperProvider(rp RateLimiterProvider) LimiterWrapperProvider { - return LimiterWrapperProviderFunc(func(key WeightKey) (LimiterWrapper, error) { - lim, err := rp.RateLimiter(key) - if err == nil { - return nil, err + if err := lim.MustDeny(ctx); err != nil { + return err } - return NewRateLimiterWrapper(lim), err - }) + } + return nil } diff --git a/extension/extensionlimiter/limiterhelper/consumer.go b/extension/extensionlimiter/limiterhelper/consumer.go index 258d157d773..d84b9fe4732 100644 --- a/extension/extensionlimiter/limiterhelper/consumer.go +++ b/extension/extensionlimiter/limiterhelper/consumer.go @@ -125,22 +125,23 @@ func limitOne[P any, C any]( key extensionlimiter.WeightKey, opts []consumer.Option, quantify func(P) uint64, -) (C, error) { +) (extensionlimiter.Limiter, C, error) { if !slices.Contains(keys, key) { - return next, nil + return nil, next, nil } lim, err := provider.LimiterWrapper(key) if err != nil { - return next, err + return nil, next, err } if lim == nil { - return next, nil + return nil, next, nil } - return m.create(func(ctx context.Context, data P) error { + con, err := m.create(func(ctx context.Context, data P) error { return lim.LimitCall(ctx, quantify(data), func(ctx context.Context) error { return m.consume(ctx, data, next) }) }, opts...) + return lim, con, err } // newLimited is signal-generic limiting logic. @@ -150,48 +151,49 @@ func newLimited[P any, C any]( provider extensionlimiter.LimiterWrapperProvider, m traits[P, C], opts ...consumer.Option, -) (C, error) { +) (extensionlimiter.Limiter, C, error) { if provider == nil { - return next, nil + return nil, next, nil } + var lim1, lim2, lim3 extensionlimiter.Limiter var err1, err2, err3 error // Note: reverse order of evaluation cost => least-cost applied first. - next, err1 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyMemorySize, opts, + lim1, next, err1 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyMemorySize, opts, func(data P) uint64 { return m.memorySize(data) }) - next, err2 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyRequestItems, opts, + lim2, next, err2 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyRequestItems, opts, func(data P) uint64 { return m.itemCount(data) }) - next, err3 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyRequestCount, opts, + lim3, next, err3 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyRequestCount, opts, func(_ P) uint64 { return 1 }) - return next, errors.Join(err1, err2, err3) + return extensionlimiter.MultiLimiter{lim1, lim2, lim3}, next, errors.Join(err1, err2, err3) } // NewLimitedTraces applies a limiter using the provider over keys before calling next. -func NewLimitedTraces(next consumer.Traces, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (consumer.Traces, error) { +func NewLimitedTraces(next consumer.Traces, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (extensionlimiter.Limiter, consumer.Traces, error) { return newLimited(next, keys, provider, traceTraits{}, consumer.WithCapabilities(next.Capabilities())) } // NewLimitedLogs applies a limiter using the provider over keys before calling next. -func NewLimitedLogs(next consumer.Logs, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (consumer.Logs, error) { +func NewLimitedLogs(next consumer.Logs, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (extensionlimiter.Limiter, consumer.Logs, error) { return newLimited(next, keys, provider, logTraits{}, consumer.WithCapabilities(next.Capabilities())) } // NewLimitedMetrics applies a limiter using the provider over keys before calling next. -func NewLimitedMetrics(next consumer.Metrics, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (consumer.Metrics, error) { +func NewLimitedMetrics(next consumer.Metrics, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (extensionlimiter.Limiter, consumer.Metrics, error) { return newLimited(next, keys, provider, metricTraits{}, consumer.WithCapabilities(next.Capabilities())) } // NewLimitedProfiles applies a limiter using the provider over keys before calling next. -func NewLimitedProfiles(next xconsumer.Profiles, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (xconsumer.Profiles, error) { +func NewLimitedProfiles(next xconsumer.Profiles, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (extensionlimiter.Limiter, xconsumer.Profiles, error) { return newLimited(next, keys, provider, profileTraits{}, consumer.WithCapabilities(next.Capabilities())) } diff --git a/extension/extensionlimiter/limiterhelper/middleware.go b/extension/extensionlimiter/limiterhelper/middleware.go index 91735349f63..95b3788bf63 100644 --- a/extension/extensionlimiter/limiterhelper/middleware.go +++ b/extension/extensionlimiter/limiterhelper/middleware.go @@ -55,6 +55,10 @@ func MiddlewaresToLimiterWrapperProvider(host component.Host, middleware []confi return MultiLimiterWrapperProvider(providers), nil } +// Note: MiddlewaresToRateLimiterProvider, MiddlewaresToResourceLimiterProvider +// are needed for special cases, however these functions can be implemented +// manually, they are similar to the above. + // MiddlewareToLimiterWrapperProvider returns a limiter wrapper // provider from middleware. Returns a package-level error if the // middleware does not implement exactly one of the limiter diff --git a/extension/extensionlimiter/rate.go b/extension/extensionlimiter/rate.go index 70bc9986e35..cb16f86c78b 100644 --- a/extension/extensionlimiter/rate.go +++ b/extension/extensionlimiter/rate.go @@ -39,14 +39,8 @@ func (f RateLimiterProviderFunc) RateLimiter(key WeightKey) (RateLimiter, error) // // See the README for more recommendations. type RateLimiter interface { - // Must deny is the logical equivalent of Acquire(0). If the - // Acquire would fail even for 0 units of a rate, the - // caller must deny the request. Implementations are - // encouraged to ensure that when MustDeny() is false, - // Acquire(0) is also false, however callers could use a - // faster code path to implement MustDeny() since it does not - // depend on the value. - MustDeny(context.Context) error + // Limiter includes MustDeny(). + Limiter // Limit attempts to apply rate limiting with the provided // weight, based on the key that was given to the provider. diff --git a/extension/extensionlimiter/resource.go b/extension/extensionlimiter/resource.go index 704a39e7d5b..11dddd3b223 100644 --- a/extension/extensionlimiter/resource.go +++ b/extension/extensionlimiter/resource.go @@ -41,14 +41,8 @@ func (f ResourceLimiterProviderFunc) ResourceLimiter(key WeightKey) (ResourceLim // // See the README for more recommendations. type ResourceLimiter interface { - // Must deny is the logical equivalent of Acquire(0). If the - // Acquire would fail even for 0 units of a resource, the - // caller must deny the request. Implementations are - // encouraged to ensure that when MustDeny() is false, - // Acquire(0) is also false, however callers could use a - // faster code path to implement MustDeny() since it does not - // depend on the value. - MustDeny(context.Context) error + // Limiter includes MustDeny(). + Limiter // Acquire attempts to acquire a quantified resource with the // provided weight, based on the key that was given to the diff --git a/extension/extensionlimiter/wrapper.go b/extension/extensionlimiter/wrapper.go new file mode 100644 index 00000000000..85b115d009f --- /dev/null +++ b/extension/extensionlimiter/wrapper.go @@ -0,0 +1,97 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package extensionlimiter // import "go.opentelemetry.io/collector/extension/extensionlimiter" + +import ( + "context" +) + +// LimiterWrapper is a general-purpose interface for limiter consumers +// to limit resources with use of a callback. This is the simplest +// form of rate limiting interface from a callers perspective. If the +// caller is a pipeline component, consider using a consumer-oriented +// limiterhelper (e.g., limiterhelper.NewLimitedLogs) to apply a list of +// +// Limiter implementions are meant to implement either the RateLimiter +// or ResourceLimiter interfaces. LimiterWrappers can be constructed +// from either of the underlying limiters and their corresponding +// providers. Usually configmiddleware or limiterhelper is responsible +// for constructing the correct wrapper from these two kinds of limiter; +// users will use this interface consistently. +type LimiterWrapper interface { + // Limiter includes MustDeny(). + Limiter + + // LimitCall applies the limiter and with the rate or resource + // granted makes a scoped call, returning success or an error + // from either the limiter or the enclosed callback. + LimitCall(context.Context, uint64, func(ctx context.Context) error) error +} + +// LimiterWrapperProvider provides access to LimiterWrappers, which is +// the appropriate interface for callers that can easily wrap a +// function call, because for wrapped calls there is no distinction +// between rate limiters and resource limiters. +type LimiterWrapperProvider interface { + LimiterWrapper(WeightKey) (LimiterWrapper, error) +} + +// LimiterWrapperFunc is a functional way to build LimiterWrappers. +type LimiterWrapperFunc func(context.Context, uint64, func(ctx context.Context) error) error + +var _ LimiterWrapper = LimiterWrapperFunc(nil) + +// MustDeny implements LimiterWrapper. +func (f LimiterWrapperFunc) MustDeny(ctx context.Context) error { + return f.LimitCall(ctx, 0, func(_ context.Context) error { + return nil + }) +} + +// LimitCall implements LimiterWrapper. +func (f LimiterWrapperFunc) LimitCall(ctx context.Context, value uint64, call func(ctx context.Context) error) error { + if f == nil { + return call(ctx) + } + return f(ctx, value, call) +} + +// PassThrough returns a LimiterWrapper that imposes no limit. +func PassThrough() LimiterWrapper { + return LimiterWrapperFunc(nil) +} + +// LimiterWrapperProviderFunc is a functional way to build LimiterWrappers. +type LimiterWrapperProviderFunc func(WeightKey) (LimiterWrapper, error) + +var _ LimiterWrapperProvider = LimiterWrapperProviderFunc(nil) + +// LimiterWrapper implements LimiterWrapperProvider. +func (f LimiterWrapperProviderFunc) LimiterWrapper(key WeightKey) (LimiterWrapper, error) { + return f(key) +} + +// NewResourceLimiterWrapperProvider constructs a +// LimiterWrapperProvider for a resource limiter extension. +func NewResourceLimiterWrapperProvider(rp ResourceLimiterProvider) LimiterWrapperProvider { + return LimiterWrapperProviderFunc(func(key WeightKey) (LimiterWrapper, error) { + lim, err := rp.ResourceLimiter(key) + if err == nil { + return nil, err + } + return NewResourceLimiterWrapper(lim), err + }) +} + +// NewRateLimiterWrapperProvider constructs a LimiterWrapperProvider +// for a rate limiter extension. +func NewRateLimiterWrapperProvider(rp RateLimiterProvider) LimiterWrapperProvider { + return LimiterWrapperProviderFunc(func(key WeightKey) (LimiterWrapper, error) { + lim, err := rp.RateLimiter(key) + if err == nil { + return nil, err + } + return NewRateLimiterWrapper(lim), err + }) +} From 7444153a89d9fcd5e11af97c92e1e28e277bd731 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Wed, 30 Apr 2025 14:42:58 -0700 Subject: [PATCH 03/14] readme --- extension/extensionlimiter/README.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index 0c7645e6109..a1fdb090b84 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -5,27 +5,28 @@ The `extensionlimiter` package provides interfaces for rate limiting and resource limiting in the OpenTelemetry Collector, enabling control over data flow and resource usage through extensions which can be -configured through and middleware and/or directly by pipeline -components. +configured through middleware and/or directly by pipeline components. ## Overview -This package defines two primary limiter types with their respective -interfaces: +This package defines two primary limiter **kinds**, which have +different interfaces: - **Rate Limiters**: Control time-based limits on quantities such as bytes or items per second. - **Resource Limiters**: Manage physical limits on quantities such as concurrent requests or memory usage. -Both limiter types are unified through the `LimiterWrapper` interface, -which simplifies consumer usage by providing a consistent `LimitCall` -interface. +Both limiter kinds are unified through the `LimiterWrapper` interface, +which simplifies consumers in most cases by providing a consistent +`LimitCall` interface. A limiter is **saturated** by definition when a limit is completely -overloaded, generally it means a limit request of any size would fail. +overloaded, generally it means a limit request of any size would fail +at this moment and should be taken as a strong signal to stop +accepting requests. -Each each base limiter type and the wrapper type have corresponding +Each kind of limiter as well as the wrapper type have corresponding providers that give access to a limiter instance based on a weight key. @@ -40,6 +41,7 @@ request items, and memory size. - `RateLimiter`: Applies time-based limits, has a `Limit` method. - `ResourceLimiter`: Manages physical resource limits, has an `Acquire` method and corresponding `ReleaseFunc`. +- `Limiter`: Any of the above, has a `MustDeny` method. ### Limiter helpers From e597fa1d1fc5c6e455cf910bface64650b0b0191 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Wed, 30 Apr 2025 14:50:15 -0700 Subject: [PATCH 04/14] move multi-limiter --- extension/extensionlimiter/README.md | 22 +++++++++++-------- .../extensionlimiter/extensionlimiter.go | 18 --------------- .../limiterhelper/consumer.go | 20 ++++++++++++++++- receiver/otlpreceiver/otlp.go | 18 +++++++-------- 4 files changed, 41 insertions(+), 37 deletions(-) diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index a1fdb090b84..29e43943d20 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -27,20 +27,23 @@ at this moment and should be taken as a strong signal to stop accepting requests. Each kind of limiter as well as the wrapper type have corresponding -providers that give access to a limiter instance based on a weight -key. +**provider** interface that returns a limiter instance based on a +weight key or keys. -Weight keys describes the standard limiting dimensions. There are +Weight keys describe the standard limiting dimensions. There are currently four standard weight keys: network bytes, request count, request items, and memory size. ## Key Interfaces - `LimiterWrapper`: Provides a callback-based limiting interface that - works with both rate and resource limiters, has a `LimitCall` method. -- `RateLimiter`: Applies time-based limits, has a `Limit` method. + works with both rate and resource limiters, has a `LimitCall` method, + plus a provider type. +- `RateLimiter`: Applies time-based limits, has a `Limit` method, + plus provider type. - `ResourceLimiter`: Manages physical resource limits, has - an `Acquire` method and corresponding `ReleaseFunc`. + an `Acquire` method and corresponding `ReleaseFunc`, + plus a provider type. - `Limiter`: Any of the above, has a `MustDeny` method. ### Limiter helpers @@ -48,9 +51,10 @@ request items, and memory size. The `limiterhelper` subpackage provides: - Consumer wrappers apply limits to a collector pipeline (e.g., - `NewLimitedLogs` to combine a limiter using `consumer.NewLogs`) -- Multi-limiter combinators: `MultiLimiterWrapperProvider` builds a sequence of wrapped limiters. -- Middleware conversion utilities: Convert middleware configurations to `LimiterWrapperProvider`. + `NewLimitedLogs` for a limiter combined with `consumer.NewLogs`) +- Multi-limiter combinators: for simple combined limiter functionality +- Middleware conversion utilities: convert middleware configurations to + liiter providers. ## Recommendations diff --git a/extension/extensionlimiter/extensionlimiter.go b/extension/extensionlimiter/extensionlimiter.go index 78fcbf32d90..0578c4aa246 100644 --- a/extension/extensionlimiter/extensionlimiter.go +++ b/extension/extensionlimiter/extensionlimiter.go @@ -33,21 +33,3 @@ func (f LimiterFunc) MustDeny(ctx context.Context) error { } return f(ctx) } - -// MultiLimiter returns MustDeny when any element returns MustDeny. -type MultiLimiter []Limiter - -var _ Limiter = MultiLimiter{} - -// MustDeny implements Limiter. -func (ls MultiLimiter) MustDeny(ctx context.Context) error { - for _, lim := range ls { - if lim == nil { - continue - } - if err := lim.MustDeny(ctx); err != nil { - return err - } - } - return nil -} diff --git a/extension/extensionlimiter/limiterhelper/consumer.go b/extension/extensionlimiter/limiterhelper/consumer.go index d84b9fe4732..e56e24233db 100644 --- a/extension/extensionlimiter/limiterhelper/consumer.go +++ b/extension/extensionlimiter/limiterhelper/consumer.go @@ -17,6 +17,24 @@ import ( "go.opentelemetry.io/collector/pdata/ptrace" ) +// MultiLimiter returns MustDeny when any element returns MustDeny. +type MultiLimiter []extensionlimiter.Limiter + +var _ extensionlimiter.Limiter = MultiLimiter{} + +// MustDeny implements Limiter. +func (ls MultiLimiter) MustDeny(ctx context.Context) error { + for _, lim := range ls { + if lim == nil { + continue + } + if err := lim.MustDeny(ctx); err != nil { + return err + } + } + return nil +} + // Traits object interface is generalized by P the pipeline data type // (e.g., ptrace.Traces) and C the consumer type (e.g., // consumer.Traces) @@ -171,7 +189,7 @@ func newLimited[P any, C any]( func(_ P) uint64 { return 1 }) - return extensionlimiter.MultiLimiter{lim1, lim2, lim3}, next, errors.Join(err1, err2, err3) + return MultiLimiter{lim1, lim2, lim3}, next, errors.Join(err1, err2, err3) } // NewLimitedTraces applies a limiter using the provider over keys before calling next. diff --git a/receiver/otlpreceiver/otlp.go b/receiver/otlpreceiver/otlp.go index 4a92a634a28..c91ed2ea39c 100644 --- a/receiver/otlpreceiver/otlp.go +++ b/receiver/otlpreceiver/otlp.go @@ -106,7 +106,7 @@ func (r *otlpReceiver) startGRPCServer(host component.Host) error { } if r.nextTraces != nil { - next, err := limiterhelper.NewLimitedTraces(r.nextTraces, limitKeys, limiterProvider) + _, next, err := limiterhelper.NewLimitedTraces(r.nextTraces, limitKeys, limiterProvider) if err != nil { return err } @@ -114,7 +114,7 @@ func (r *otlpReceiver) startGRPCServer(host component.Host) error { } if r.nextMetrics != nil { - next, err := limiterhelper.NewLimitedMetrics(r.nextMetrics, limitKeys, limiterProvider) + _, next, err := limiterhelper.NewLimitedMetrics(r.nextMetrics, limitKeys, limiterProvider) if err != nil { return err } @@ -122,7 +122,7 @@ func (r *otlpReceiver) startGRPCServer(host component.Host) error { } if r.nextLogs != nil { - next, err := limiterhelper.NewLimitedLogs(r.nextLogs, limitKeys, limiterProvider) + _, next, err := limiterhelper.NewLimitedLogs(r.nextLogs, limitKeys, limiterProvider) if err != nil { return err } @@ -130,7 +130,7 @@ func (r *otlpReceiver) startGRPCServer(host component.Host) error { } if r.nextProfiles != nil { - next, err := limiterhelper.NewLimitedProfiles(r.nextProfiles, limitKeys, limiterProvider) + _, next, err := limiterhelper.NewLimitedProfiles(r.nextProfiles, limitKeys, limiterProvider) if err != nil { return err } @@ -168,7 +168,7 @@ func (r *otlpReceiver) startHTTPServer(ctx context.Context, host component.Host) httpMux := http.NewServeMux() if r.nextTraces != nil { - next, err := limiterhelper.NewLimitedTraces(r.nextTraces, limitKeys, limiterProvider) + _, next, err := limiterhelper.NewLimitedTraces(r.nextTraces, limitKeys, limiterProvider) if err != nil { return err } @@ -179,18 +179,18 @@ func (r *otlpReceiver) startHTTPServer(ctx context.Context, host component.Host) } if r.nextMetrics != nil { - _, err := limiterhelper.NewLimitedMetrics(r.nextMetrics, limitKeys, limiterProvider) + _, next, err := limiterhelper.NewLimitedMetrics(r.nextMetrics, limitKeys, limiterProvider) if err != nil { return err } - httpMetricsReceiver := metrics.New(r.nextMetrics, r.obsrepHTTP) + httpMetricsReceiver := metrics.New(next, r.obsrepHTTP) httpMux.HandleFunc(r.cfg.HTTP.MetricsURLPath, func(resp http.ResponseWriter, req *http.Request) { handleMetrics(resp, req, httpMetricsReceiver) }) } if r.nextLogs != nil { - next, err := limiterhelper.NewLimitedLogs(r.nextLogs, limitKeys, limiterProvider) + _, next, err := limiterhelper.NewLimitedLogs(r.nextLogs, limitKeys, limiterProvider) if err != nil { return err } @@ -201,7 +201,7 @@ func (r *otlpReceiver) startHTTPServer(ctx context.Context, host component.Host) } if r.nextProfiles != nil { - next, err := limiterhelper.NewLimitedProfiles(r.nextProfiles, limitKeys, limiterProvider) + _, next, err := limiterhelper.NewLimitedProfiles(r.nextProfiles, limitKeys, limiterProvider) if err != nil { return err } From e6675c8fed3d5b2cb1d60ab22fdf375759317ef4 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Wed, 30 Apr 2025 15:11:02 -0700 Subject: [PATCH 05/14] data-dep example --- extension/extensionlimiter/README.md | 32 +++++++++++++ .../extensionlimiter/extensionlimiter.go | 10 ++++ .../limiterhelper/middleware.go | 4 +- extension/extensionlimiter/rate.go | 9 ++-- extension/extensionlimiter/resource.go | 8 ++-- extension/extensionlimiter/wrapper.go | 46 +++++++++---------- 6 files changed, 76 insertions(+), 33 deletions(-) diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index 29e43943d20..24d9fdc600d 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -270,3 +270,35 @@ func (s *scraper) LogsStream(ctx context.Context, stream *Stream) error { } } ``` + +#### Data-dependent limiter processor + +**NOTE: This is not implemented.** + +The provider interfaces can be extended to accept a +`map[string]string` that identify limiter instances based on +additional metadata, such as tenant information. Since the limits are +data specific, the limiter will be computed for each request and for +each specific weight key. + +Limiter implementations would support options, likely assisted by +`limiterhelper` features to configure them, for configuring +metadata-specific limits. + +``` +func handleRequest(ctx context.Context, req *Request) error { + // Get a data-specific limiter: + md := metadataFromRequest(req) + lim, err := s.limiterProvider.LimiterWrapper(weightKey, md) + if err != nil { ... } + + if err = lim.MustDeny(ctx); err != nil { ... } + + // Calculate the data and its weight. + data := dataFromReq(req) + weight := getWeight(data) + + return lim.LimitCall(ctx, weight, func(ctx context.Context) error { + return s.nextLogs.ConsumeLogs(ctx, data) + }) +``` diff --git a/extension/extensionlimiter/extensionlimiter.go b/extension/extensionlimiter/extensionlimiter.go index 0578c4aa246..7ada56bc045 100644 --- a/extension/extensionlimiter/extensionlimiter.go +++ b/extension/extensionlimiter/extensionlimiter.go @@ -7,6 +7,16 @@ import ( "context" ) +// Option is passed to limiter providers. +// +// NOTE: For data-specific or tenant-specific limits we will extend +// providers with Options and add a Config type, but none are +// supported yet and this PR contains only interfaces, not need for +// options in core repository components. +type Option interface { + apply() +} + // Limiter is the common functionality implemented by LimiterWrapper, // RateLimiter, and ResourceLimiter. This can be called prior to the // start of work to check for limiter saturation. diff --git a/extension/extensionlimiter/limiterhelper/middleware.go b/extension/extensionlimiter/limiterhelper/middleware.go index 95b3788bf63..533327479d6 100644 --- a/extension/extensionlimiter/limiterhelper/middleware.go +++ b/extension/extensionlimiter/limiterhelper/middleware.go @@ -108,7 +108,7 @@ type MultiLimiterWrapperProvider []extensionlimiter.LimiterWrapperProvider var _ extensionlimiter.LimiterWrapperProvider = MultiLimiterWrapperProvider{} // LimiterWrapper implements LimiterWrapperProvider. -func (ps MultiLimiterWrapperProvider) LimiterWrapper(key extensionlimiter.WeightKey) (extensionlimiter.LimiterWrapper, error) { +func (ps MultiLimiterWrapperProvider) LimiterWrapper(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (extensionlimiter.LimiterWrapper, error) { if len(ps) == 0 { return extensionlimiter.PassThrough(), nil } @@ -117,7 +117,7 @@ func (ps MultiLimiterWrapperProvider) LimiterWrapper(key extensionlimiter.Weight var lims []extensionlimiter.LimiterWrapper for _, provider := range ps { - lim, err := provider.LimiterWrapper(key) + lim, err := provider.LimiterWrapper(key, opts...) if err == nil { return nil, err } diff --git a/extension/extensionlimiter/rate.go b/extension/extensionlimiter/rate.go index cb16f86c78b..a294bb84cb6 100644 --- a/extension/extensionlimiter/rate.go +++ b/extension/extensionlimiter/rate.go @@ -14,17 +14,18 @@ import ( // Limiters are covered by configmiddleware configuration, which is // able to construct LimiterWrappers from these providers. type RateLimiterProvider interface { - RateLimiter(WeightKey) (RateLimiter, error) + // RateLimiter returns a provider for rate limiters. + RateLimiter(WeightKey, ...Option) (RateLimiter, error) } // RateLimiterProviderFunc is a functional way to build RateLimters. -type RateLimiterProviderFunc func(WeightKey) (RateLimiter, error) +type RateLimiterProviderFunc func(WeightKey, ...Option) (RateLimiter, error) var _ RateLimiterProvider = RateLimiterProviderFunc(nil) // RateLimiter implements RateLimiterProvider. -func (f RateLimiterProviderFunc) RateLimiter(key WeightKey) (RateLimiter, error) { - return f(key) +func (f RateLimiterProviderFunc) RateLimiter(key WeightKey, opts ...Option) (RateLimiter, error) { + return f(key, opts...) } // RateLimiter is an interface that an implementation makes available diff --git a/extension/extensionlimiter/resource.go b/extension/extensionlimiter/resource.go index 11dddd3b223..2bd62df41f2 100644 --- a/extension/extensionlimiter/resource.go +++ b/extension/extensionlimiter/resource.go @@ -14,17 +14,17 @@ import ( // Limiters are covered by configmiddleware configuration, which // is able to construct LimiterWrappers from these providers. type ResourceLimiterProvider interface { - ResourceLimiter(WeightKey) (ResourceLimiter, error) + ResourceLimiter(WeightKey, ...Option) (ResourceLimiter, error) } // ResourceLimiterProviderFunc is a functional way to build ResourceLimters. -type ResourceLimiterProviderFunc func(WeightKey) (ResourceLimiter, error) +type ResourceLimiterProviderFunc func(WeightKey, ...Option) (ResourceLimiter, error) var _ ResourceLimiterProvider = ResourceLimiterProviderFunc(nil) // ResourceLimiter implements ResourceLimiterProvider. -func (f ResourceLimiterProviderFunc) ResourceLimiter(key WeightKey) (ResourceLimiter, error) { - return f(key) +func (f ResourceLimiterProviderFunc) ResourceLimiter(key WeightKey, opts ...Option) (ResourceLimiter, error) { + return f(key, opts...) } // ResourceLimiter is an interface that an implementation makes diff --git a/extension/extensionlimiter/wrapper.go b/extension/extensionlimiter/wrapper.go index 85b115d009f..cd521f4487a 100644 --- a/extension/extensionlimiter/wrapper.go +++ b/extension/extensionlimiter/wrapper.go @@ -7,6 +7,19 @@ import ( "context" ) +// LimiterWrapperProvider provides access to LimiterWrappers, which is +// the appropriate interface for callers that can easily wrap a +// function call, because for wrapped calls there is no distinction +// between rate limiters and resource limiters. +type LimiterWrapperProvider interface { + LimiterWrapper(WeightKey, ...Option) (LimiterWrapper, error) +} + +// LimiterWrapperFunc is a functional way to build LimiterWrappers. +type LimiterWrapperFunc func(context.Context, uint64, func(ctx context.Context) error) error + +var _ LimiterWrapper = LimiterWrapperFunc(nil) + // LimiterWrapper is a general-purpose interface for limiter consumers // to limit resources with use of a callback. This is the simplest // form of rate limiting interface from a callers perspective. If the @@ -29,19 +42,11 @@ type LimiterWrapper interface { LimitCall(context.Context, uint64, func(ctx context.Context) error) error } -// LimiterWrapperProvider provides access to LimiterWrappers, which is -// the appropriate interface for callers that can easily wrap a -// function call, because for wrapped calls there is no distinction -// between rate limiters and resource limiters. -type LimiterWrapperProvider interface { - LimiterWrapper(WeightKey) (LimiterWrapper, error) +// PassThrough returns a LimiterWrapper that imposes no limit. +func PassThrough() LimiterWrapper { + return LimiterWrapperFunc(nil) } -// LimiterWrapperFunc is a functional way to build LimiterWrappers. -type LimiterWrapperFunc func(context.Context, uint64, func(ctx context.Context) error) error - -var _ LimiterWrapper = LimiterWrapperFunc(nil) - // MustDeny implements LimiterWrapper. func (f LimiterWrapperFunc) MustDeny(ctx context.Context) error { return f.LimitCall(ctx, 0, func(_ context.Context) error { @@ -57,26 +62,21 @@ func (f LimiterWrapperFunc) LimitCall(ctx context.Context, value uint64, call fu return f(ctx, value, call) } -// PassThrough returns a LimiterWrapper that imposes no limit. -func PassThrough() LimiterWrapper { - return LimiterWrapperFunc(nil) -} - // LimiterWrapperProviderFunc is a functional way to build LimiterWrappers. -type LimiterWrapperProviderFunc func(WeightKey) (LimiterWrapper, error) +type LimiterWrapperProviderFunc func(WeightKey, ...Option) (LimiterWrapper, error) var _ LimiterWrapperProvider = LimiterWrapperProviderFunc(nil) // LimiterWrapper implements LimiterWrapperProvider. -func (f LimiterWrapperProviderFunc) LimiterWrapper(key WeightKey) (LimiterWrapper, error) { - return f(key) +func (f LimiterWrapperProviderFunc) LimiterWrapper(key WeightKey, opts ...Option) (LimiterWrapper, error) { + return f(key, opts...) } // NewResourceLimiterWrapperProvider constructs a // LimiterWrapperProvider for a resource limiter extension. func NewResourceLimiterWrapperProvider(rp ResourceLimiterProvider) LimiterWrapperProvider { - return LimiterWrapperProviderFunc(func(key WeightKey) (LimiterWrapper, error) { - lim, err := rp.ResourceLimiter(key) + return LimiterWrapperProviderFunc(func(key WeightKey, opts ...Option) (LimiterWrapper, error) { + lim, err := rp.ResourceLimiter(key, opts...) if err == nil { return nil, err } @@ -87,8 +87,8 @@ func NewResourceLimiterWrapperProvider(rp ResourceLimiterProvider) LimiterWrappe // NewRateLimiterWrapperProvider constructs a LimiterWrapperProvider // for a rate limiter extension. func NewRateLimiterWrapperProvider(rp RateLimiterProvider) LimiterWrapperProvider { - return LimiterWrapperProviderFunc(func(key WeightKey) (LimiterWrapper, error) { - lim, err := rp.RateLimiter(key) + return LimiterWrapperProviderFunc(func(key WeightKey, opts ...Option) (LimiterWrapper, error) { + lim, err := rp.RateLimiter(key, opts...) if err == nil { return nil, err } From ac0d1ec638d0ac9708d5471252aea28e72f2b066 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Wed, 30 Apr 2025 15:13:38 -0700 Subject: [PATCH 06/14] lint --- extension/extensionlimiter/README.md | 4 +++- receiver/otlpreceiver/otlp.go | 12 ++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index 24d9fdc600d..1651302d357 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -273,7 +273,9 @@ func (s *scraper) LogsStream(ctx context.Context, stream *Stream) error { #### Data-dependent limiter processor -**NOTE: This is not implemented.** +**NOTE: This is not implemented.** An `Option` type has been added as +a placeholder in the provider interfaces to support adding this +feature. The provider interfaces can be extended to accept a `map[string]string` that identify limiter instances based on diff --git a/receiver/otlpreceiver/otlp.go b/receiver/otlpreceiver/otlp.go index c91ed2ea39c..4b2a52e565f 100644 --- a/receiver/otlpreceiver/otlp.go +++ b/receiver/otlpreceiver/otlp.go @@ -106,7 +106,8 @@ func (r *otlpReceiver) startGRPCServer(host component.Host) error { } if r.nextTraces != nil { - _, next, err := limiterhelper.NewLimitedTraces(r.nextTraces, limitKeys, limiterProvider) + var next consumer.Traces + _, next, err = limiterhelper.NewLimitedTraces(r.nextTraces, limitKeys, limiterProvider) if err != nil { return err } @@ -114,7 +115,8 @@ func (r *otlpReceiver) startGRPCServer(host component.Host) error { } if r.nextMetrics != nil { - _, next, err := limiterhelper.NewLimitedMetrics(r.nextMetrics, limitKeys, limiterProvider) + var next consumer.Metrics + _, next, err = limiterhelper.NewLimitedMetrics(r.nextMetrics, limitKeys, limiterProvider) if err != nil { return err } @@ -122,7 +124,8 @@ func (r *otlpReceiver) startGRPCServer(host component.Host) error { } if r.nextLogs != nil { - _, next, err := limiterhelper.NewLimitedLogs(r.nextLogs, limitKeys, limiterProvider) + var next consumer.Logs + _, next, err = limiterhelper.NewLimitedLogs(r.nextLogs, limitKeys, limiterProvider) if err != nil { return err } @@ -130,7 +133,8 @@ func (r *otlpReceiver) startGRPCServer(host component.Host) error { } if r.nextProfiles != nil { - _, next, err := limiterhelper.NewLimitedProfiles(r.nextProfiles, limitKeys, limiterProvider) + var next xconsumer.Profiles + _, next, err = limiterhelper.NewLimitedProfiles(r.nextProfiles, limitKeys, limiterProvider) if err != nil { return err } From b3e4554c3564779bb2e6ab93fa049db230f58ad8 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Wed, 30 Apr 2025 15:25:22 -0700 Subject: [PATCH 07/14] lint --- extension/extensionlimiter/README.md | 199 +++++++++++++-------------- 1 file changed, 99 insertions(+), 100 deletions(-) diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index 1651302d357..51cd0efecc6 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -4,7 +4,7 @@ The `extensionlimiter` package provides interfaces for rate limiting and resource limiting in the OpenTelemetry Collector, enabling control -over data flow and resource usage through extensions which can be +over data flow and resource usage through extensions that can be configured through middleware and/or directly by pipeline components. ## Overview @@ -23,11 +23,11 @@ which simplifies consumers in most cases by providing a consistent A limiter is **saturated** by definition when a limit is completely overloaded, generally it means a limit request of any size would fail -at this moment and should be taken as a strong signal to stop +at that moment and should be taken as a strong signal to stop accepting requests. Each kind of limiter as well as the wrapper type have corresponding -**provider** interface that returns a limiter instance based on a +**provider** interfaces that return a limiter instance based on a weight key or keys. Weight keys describe the standard limiting dimensions. There are @@ -42,7 +42,7 @@ request items, and memory size. - `RateLimiter`: Applies time-based limits, has a `Limit` method, plus provider type. - `ResourceLimiter`: Manages physical resource limits, has - an `Acquire` method and corresponding `ReleaseFunc`, + an `Acquire` method and a corresponding `ReleaseFunc`, plus a provider type. - `Limiter`: Any of the above, has a `MustDeny` method. @@ -53,8 +53,8 @@ The `limiterhelper` subpackage provides: - Consumer wrappers apply limits to a collector pipeline (e.g., `NewLimitedLogs` for a limiter combined with `consumer.NewLogs`) - Multi-limiter combinators: for simple combined limiter functionality -- Middleware conversion utilities: convert middleware configurations to - liiter providers. +- Middleware conversion utilities: convert middleware configurations + to limiter providers. ## Recommendations @@ -82,10 +82,10 @@ subject to internal logic. A limiter aims to avoid waste, which requires balancing several factors. To fail a request that has already been transmitted, received and parsed is sometimes more wasteful than waiting for a little while; on the other hand waiting for a long time -risks wasting memory. In general, an overloaded limiter that is saturated SHOULD -fail requests immediately. +risks wasting memory. In general, an overloaded limiter that is +saturated SHOULD fail requests immediately. -Limiters implementations SHOULD consider the context deadline when +Limiter implementations SHOULD consider the context deadline when they block. If the deadline is likely to expire before the limit becomes available, they should return a standard overload signal. @@ -105,7 +105,7 @@ free to define their own saturation parameters. It is sometimes possible to request a limit before it is actually used. As an example, consider a protocol using a compressed payload, -such that the receivers knows how much memory will be allocated before +such that the receiver knows how much memory will be allocated before the fact. In this case the receiver can request the limit before using it, but this will not always be the case. Generally, prefer to limit before use, but either way be consistent. @@ -118,10 +118,10 @@ before creating new concurrent work. #### OTLP receiver Limiters applied through middleware are an implementation detail, -simply configure them using `configgrpc` or `confighttp`. For the +simply configure them using `configgrpc` or `confighttp`. For the OTLP receiver (e.g., with two `ratelimiter` extensions): -``` +```yaml extensions: ratelimiter/limit_for_grpc: # rate limiter settings for gRPC @@ -131,18 +131,17 @@ extensions: receivers: otlp: protocols: - grpc: - middlewares: - - ratelimiter/limit_for_grpc - http: - middlewares: - - ratelimiter/limit_for_http + grpc: + middlewares: + - ratelimiter/limit_for_grpc + http: + middlewares: + - ratelimiter/limit_for_http ``` Note that the OTLP receiver specifically supports multiple protocols with separate middleware configurations, thus it configures limiters -for request items and memory size on a protocol-by-protocol -basis. +for request items and memory size on a protocol-by-protocol basis. #### HTTP metrics scraper @@ -150,12 +149,12 @@ A HTTP pull-based receiver can implement a basic limited scraper loop as follows. The HTTP client config object's `middlewares` field automatically configures network bytes and request count limits: -``` +```yaml receivers: scraper: http: middlewares: - - ratelimiter/scraper + - ratelimiter/scraper ``` Limiter extensions are derived from a host, a middlewares list, and a @@ -163,14 +162,14 @@ list of weight keys. When middleware is configurable at the factory level, it may be added via `receiver.NewFactory` using `receiver.WithLimiters(getLimiters)`: -``` +```golang func NewFactory() receiver.Factory { - return xreceiver.NewFactory( - metadata.Type, - createDefaultConfig, - xreceiver.WithMetrics(createMetrics, metadata.MetricsStability), - xreceiver.WithLimiters(getLimiters), - ) + return xreceiver.NewFactory( + metadata.Type, + createDefaultConfig, + xreceiver.WithMetrics(createMetrics, metadata.MetricsStability), + xreceiver.WithLimiters(getLimiters), + ) } ``` @@ -180,39 +179,39 @@ Here, `getLimiters` is a function to get the effective To acquire a limiter, use `MiddlewaresToLimiterWrapperProvider` to obtain a combined limiter wrapper around the input `nextMetrics` -consumer. It will pass `StandardNotMiddlewareKeys()` indicating to +consumer. It will pass `StandardNotMiddlewareKeys()` indicating to apply request items and memory size: -``` - // Extract limiter provider from middlewares. - s.limiterProvider, err = limiterhelper.MiddlewaresToLimiterWrapperProvider( - cfg.Middlewares) - if err != nil { ... } - - // Here get a limiter-wrapped pipeline and a combination of weight-specific - // limiters for MustDeny() functionality. - s.anyLimiter, s.nextMetrics, err = limiterhelper.NewLimitedMetrics( - s.nextMetrics, limiterhelper.StandardNotMiddlewareKeys(), s.limiterProvider) - if err != nil { ... } +```golang + // Extract limiter provider from middlewares. + s.limiterProvider, err = limiterhelper.MiddlewaresToLimiterWrapperProvider( + cfg.Middlewares) + if err != nil { ... } + + // Here get a limiter-wrapped pipeline and a combination of weight-specific + // limiters for MustDeny() functionality. + s.anyLimiter, s.nextMetrics, err = limiterhelper.NewLimitedMetrics( + s.nextMetrics, limiterhelper.StandardNotMiddlewareKeys(), s.limiterProvider) + if err != nil { ... } ``` In the scraper loop, use `MustDeny` before starting a scrape: -``` +```golang func (s *scraper) scrapeOnce(ctx context.Context) error { if err := s.anyLimiter.MustDeny(ctx); err != nil { - return err - } - - // Network bytes and request count limits are applied in middleware. - // before this returns: - data, err := s.getData(ctx) - if err != nil { - return err - } - - // Request items and memory size are applied in the pipeline. - return s.nextMetrics.ConsumeMetrics(ctx, data) + return err + } + + // Network bytes and request count limits are applied in middleware. + // before this returns: + data, err := s.getData(ctx) + if err != nil { + return err + } + + // Request items and memory size are applied in the pipeline. + return s.nextMetrics.ConsumeMetrics(ctx, data) } ``` @@ -220,16 +219,16 @@ func (s *scraper) scrapeOnce(ctx context.Context) error { A gRPC streaming receiver that holds memory across its allocated in `Send()` and does not release it until after a corresponding `Recv()` -requires use of the lower-level `ResourceLimiter` interface. +requires use of the lower-level `ResourceLimiter` interface. The gRPC config object's `middlewares` field automatically configures network bytes and request count limits: -``` +```yaml receivers: streamer: grpc: middlewares: - - ratelimiter/streamer + - ratelimiter/streamer ``` The receiver will check `s.anyLimiter.MustDeny()` as above. In a @@ -239,43 +238,43 @@ wish to return from `Send()` to continue accepting new requests while the consumer works in a separate goroutine. The limit will be released after the consumer returns. -``` +```golang func (s *scraper) LogsStream(ctx context.Context, stream *Stream) error { for { - // Check saturation for all limiters. - err := s.anyLimiter.MustDeny(ctx) - if err != nil { ... } + // Check saturation for all limiters. + err := s.anyLimiter.MustDeny(ctx) + if err != nil { ... } // The network bytes and request count are applied in middleware. - req, err := stream.Recv() - if err != nil { ... } - - // Allocate memory objects. - data, err := s.getLogs(ctx, req) - if err != nil { ... } - - release, err := s.memorySizeLimiter.Acquire(ctx, pdataSize(data)) - if err != nil { ... } - - go func() { - // Request items limit is applied in the pipeline consumer - err := s.nextMetrics.ConsumeMetrics(ctx, data) - - // Release the memory. - release() - - // Reply to the caller. - stream.Send(streamResponseFromConsumerError(err)) - } - } + req, err := stream.Recv() + if err != nil { ... } + + // Allocate memory objects. + data, err := s.getLogs(ctx, req) + if err != nil { ... } + + release, err := s.memorySizeLimiter.Acquire(ctx, pdataSize(data)) + if err != nil { ... } + + go func() { + // Request items limit is applied in the pipeline consumer + err := s.nextMetrics.ConsumeMetrics(ctx, data) + + // Release the memory. + release() + + // Reply to the caller. + stream.Send(streamResponseFromConsumerError(err)) + } + } } ``` #### Data-dependent limiter processor -**NOTE: This is not implemented.** An `Option` type has been added as -a placeholder in the provider interfaces to support adding this -feature. +An `Option` type has been added as a placeholder in the provider +interfaces to support adding this feature. **NOTE: This is not +implemented.** The provider interfaces can be extended to accept a `map[string]string` that identify limiter instances based on @@ -287,20 +286,20 @@ Limiter implementations would support options, likely assisted by `limiterhelper` features to configure them, for configuring metadata-specific limits. -``` +```golang func handleRequest(ctx context.Context, req *Request) error { - // Get a data-specific limiter: - md := metadataFromRequest(req) - lim, err := s.limiterProvider.LimiterWrapper(weightKey, md) - if err != nil { ... } - - if err = lim.MustDeny(ctx); err != nil { ... } - - // Calculate the data and its weight. - data := dataFromReq(req) - weight := getWeight(data) - - return lim.LimitCall(ctx, weight, func(ctx context.Context) error { - return s.nextLogs.ConsumeLogs(ctx, data) - }) + // Get a data-specific limiter: + md := metadataFromRequest(req) + lim, err := s.limiterProvider.LimiterWrapper(weightKey, md) + if err != nil { ... } + + if err = lim.MustDeny(ctx); err != nil { ... } + + // Calculate the data and its weight. + data := dataFromReq(req) + weight := getWeight(data) + + return lim.LimitCall(ctx, weight, func(ctx context.Context) error { + return s.nextLogs.ConsumeLogs(ctx, data) + }) ``` From b8ed41d5c6b7b7de95d687ff59cd38b47832522a Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Mon, 5 May 2025 16:57:14 -0700 Subject: [PATCH 08/14] wip split Checker (was Limiter) --- .../extensionlimiter/extensionlimiter.go | 29 +++++--- .../limiterhelper/consumer.go | 28 ++++---- .../limiterhelper/middleware.go | 2 +- extension/extensionlimiter/rate.go | 38 ++-------- extension/extensionlimiter/resource.go | 58 ++++----------- extension/extensionlimiter/wrapper.go | 70 ++++++++++++------- 6 files changed, 97 insertions(+), 128 deletions(-) diff --git a/extension/extensionlimiter/extensionlimiter.go b/extension/extensionlimiter/extensionlimiter.go index 7ada56bc045..6d85503dd67 100644 --- a/extension/extensionlimiter/extensionlimiter.go +++ b/extension/extensionlimiter/extensionlimiter.go @@ -7,6 +7,11 @@ import ( "context" ) +// Defines: +// - Option +// - Checker +// - CheckerFunc + // Option is passed to limiter providers. // // NOTE: For data-specific or tenant-specific limits we will extend @@ -17,11 +22,10 @@ type Option interface { apply() } -// Limiter is the common functionality implemented by LimiterWrapper, -// RateLimiter, and ResourceLimiter. This can be called prior to the -// start of work to check for limiter saturation. -type Limiter interface { - // Must deny is the logical equivalent of Acquire(0). If the +// Checker is for checking when a limit is saturated. This can be +// called prior to the start of work to check for limiter saturation. +type Checker interface { + // MustDeny is the logical equivalent of Acquire(0). If the // Acquire would fail even for 0 units of a rate, the // caller must deny the request. Implementations are // encouraged to ensure that when MustDeny() is false, @@ -31,15 +35,20 @@ type Limiter interface { MustDeny(context.Context) error } -// LimiterFunc is a functional way to build MustDeny functions. -type LimiterFunc func(context.Context) error +// CheckerFunc is a functional way to build Checker implementations. +type CheckerFunc func(context.Context) error -var _ Limiter = LimiterFunc(nil) +var _ Checker = CheckerFunc(nil) -// MustDeny implements Limiter. -func (f LimiterFunc) MustDeny(ctx context.Context) error { +// MustDeny implements Checker. +func (f CheckerFunc) MustDeny(ctx context.Context) error { if f == nil { return nil } return f(ctx) } + +// PassThroughChecker returns a Checker that never denies. +func PassThroughChecker() Checker { + return CheckerFunc(nil) +} diff --git a/extension/extensionlimiter/limiterhelper/consumer.go b/extension/extensionlimiter/limiterhelper/consumer.go index e56e24233db..f6810f51e7c 100644 --- a/extension/extensionlimiter/limiterhelper/consumer.go +++ b/extension/extensionlimiter/limiterhelper/consumer.go @@ -17,13 +17,13 @@ import ( "go.opentelemetry.io/collector/pdata/ptrace" ) -// MultiLimiter returns MustDeny when any element returns MustDeny. -type MultiLimiter []extensionlimiter.Limiter +// MultiChecker returns MustDeny when any element returns MustDeny. +type MultiChecker []extensionlimiter.Checker -var _ extensionlimiter.Limiter = MultiLimiter{} +var _ extensionlimiter.Checker = MultiChecker{} -// MustDeny implements Limiter. -func (ls MultiLimiter) MustDeny(ctx context.Context) error { +// MustDeny implements Checker. +func (ls MultiChecker) MustDeny(ctx context.Context) error { for _, lim := range ls { if lim == nil { continue @@ -143,7 +143,7 @@ func limitOne[P any, C any]( key extensionlimiter.WeightKey, opts []consumer.Option, quantify func(P) uint64, -) (extensionlimiter.Limiter, C, error) { +) (extensionlimiter.Checker, C, error) { if !slices.Contains(keys, key) { return nil, next, nil } @@ -159,7 +159,7 @@ func limitOne[P any, C any]( return m.consume(ctx, data, next) }) }, opts...) - return lim, con, err + return NewLimiterWrapperChecker(lim), con, err } // newLimited is signal-generic limiting logic. @@ -169,11 +169,11 @@ func newLimited[P any, C any]( provider extensionlimiter.LimiterWrapperProvider, m traits[P, C], opts ...consumer.Option, -) (extensionlimiter.Limiter, C, error) { +) (extensionlimiter.Checker, C, error) { if provider == nil { return nil, next, nil } - var lim1, lim2, lim3 extensionlimiter.Limiter + var lim1, lim2, lim3 extensionlimiter.Checker var err1, err2, err3 error // Note: reverse order of evaluation cost => least-cost applied first. lim1, next, err1 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyMemorySize, opts, @@ -189,29 +189,29 @@ func newLimited[P any, C any]( func(_ P) uint64 { return 1 }) - return MultiLimiter{lim1, lim2, lim3}, next, errors.Join(err1, err2, err3) + return MultiChecker{lim1, lim2, lim3}, next, errors.Join(err1, err2, err3) } // NewLimitedTraces applies a limiter using the provider over keys before calling next. -func NewLimitedTraces(next consumer.Traces, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (extensionlimiter.Limiter, consumer.Traces, error) { +func NewLimitedTraces(next consumer.Traces, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (extensionlimiter.Checker, consumer.Traces, error) { return newLimited(next, keys, provider, traceTraits{}, consumer.WithCapabilities(next.Capabilities())) } // NewLimitedLogs applies a limiter using the provider over keys before calling next. -func NewLimitedLogs(next consumer.Logs, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (extensionlimiter.Limiter, consumer.Logs, error) { +func NewLimitedLogs(next consumer.Logs, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (extensionlimiter.Checker, consumer.Logs, error) { return newLimited(next, keys, provider, logTraits{}, consumer.WithCapabilities(next.Capabilities())) } // NewLimitedMetrics applies a limiter using the provider over keys before calling next. -func NewLimitedMetrics(next consumer.Metrics, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (extensionlimiter.Limiter, consumer.Metrics, error) { +func NewLimitedMetrics(next consumer.Metrics, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (extensionlimiter.Checker, consumer.Metrics, error) { return newLimited(next, keys, provider, metricTraits{}, consumer.WithCapabilities(next.Capabilities())) } // NewLimitedProfiles applies a limiter using the provider over keys before calling next. -func NewLimitedProfiles(next xconsumer.Profiles, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (extensionlimiter.Limiter, xconsumer.Profiles, error) { +func NewLimitedProfiles(next xconsumer.Profiles, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (extensionlimiter.Checker, xconsumer.Profiles, error) { return newLimited(next, keys, provider, profileTraits{}, consumer.WithCapabilities(next.Capabilities())) } diff --git a/extension/extensionlimiter/limiterhelper/middleware.go b/extension/extensionlimiter/limiterhelper/middleware.go index 533327479d6..b203194cacd 100644 --- a/extension/extensionlimiter/limiterhelper/middleware.go +++ b/extension/extensionlimiter/limiterhelper/middleware.go @@ -110,7 +110,7 @@ var _ extensionlimiter.LimiterWrapperProvider = MultiLimiterWrapperProvider{} // LimiterWrapper implements LimiterWrapperProvider. func (ps MultiLimiterWrapperProvider) LimiterWrapper(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (extensionlimiter.LimiterWrapper, error) { if len(ps) == 0 { - return extensionlimiter.PassThrough(), nil + return extensionlimiter.PassThroughWrapper(), nil } // Map provider list to limiter list. diff --git a/extension/extensionlimiter/rate.go b/extension/extensionlimiter/rate.go index a294bb84cb6..a02e9c2c5aa 100644 --- a/extension/extensionlimiter/rate.go +++ b/extension/extensionlimiter/rate.go @@ -7,6 +7,12 @@ import ( "context" ) +// Defines: +// - RateLimiterProvider +// - RateLimiterProviderFunc +// - RateLimiter +// - RateLimiterFunc + // RateLimiterProvider is a provider for rate limiters. // // Limiter implementations will implement this or the @@ -40,9 +46,6 @@ func (f RateLimiterProviderFunc) RateLimiter(key WeightKey, opts ...Option) (Rat // // See the README for more recommendations. type RateLimiter interface { - // Limiter includes MustDeny(). - Limiter - // Limit attempts to apply rate limiting with the provided // weight, based on the key that was given to the provider. // @@ -57,11 +60,6 @@ type RateLimiterFunc func(ctx context.Context, value uint64) error var _ RateLimiter = RateLimiterFunc(nil) -// MustDeny implements RateLimiter. -func (f RateLimiterFunc) MustDeny(ctx context.Context) error { - return f.Limit(ctx, 0) -} - // Limit implements RateLimiter. func (f RateLimiterFunc) Limit(ctx context.Context, value uint64) error { if f == nil { @@ -69,27 +67,3 @@ func (f RateLimiterFunc) Limit(ctx context.Context, value uint64) error { } return f(ctx, value) } - -// NewRateLimiterWrapper returns a LimiterWrapper from a RateLimiter. -func NewRateLimiterWrapper(limiter RateLimiter) LimiterWrapper { - return rateLimiterWrapper{limiter: limiter} -} - -type rateLimiterWrapper struct { - limiter RateLimiter -} - -var _ LimiterWrapper = rateLimiterWrapper{} - -// MustDeny implements LimiterWrapper. -func (w rateLimiterWrapper) MustDeny(ctx context.Context) error { - return w.limiter.MustDeny(ctx) -} - -// LimitCall implements LimiterWrapper. -func (w rateLimiterWrapper) LimitCall(ctx context.Context, value uint64, call func(context.Context) error) error { - if err := w.limiter.Limit(ctx, value); err != nil { - return err - } - return call(ctx) -} diff --git a/extension/extensionlimiter/resource.go b/extension/extensionlimiter/resource.go index 2bd62df41f2..c919fc55979 100644 --- a/extension/extensionlimiter/resource.go +++ b/extension/extensionlimiter/resource.go @@ -7,6 +7,13 @@ import ( "context" ) +// Defines: +// - ResourceLimiterProvider +// - ResourceLimiterProviderFunc +// - ResourceLimiter +// - ResourceLimiterFunc +// - ReleaseFunc + // ResourceLimiterProvider is a provider for resource limiters. // // Limiter implementations will implement this or the @@ -41,9 +48,6 @@ func (f ResourceLimiterProviderFunc) ResourceLimiter(key WeightKey, opts ...Opti // // See the README for more recommendations. type ResourceLimiter interface { - // Limiter includes MustDeny(). - Limiter - // Acquire attempts to acquire a quantified resource with the // provided weight, based on the key that was given to the // provider. The caller has these options: @@ -56,18 +60,17 @@ type ResourceLimiter interface { // See the README for more recommendations. // // On success, it returns a ReleaseFunc that should be called - // when the resources are no longer needed. - // - // Implementations are not required to call a release func - // when Acquire(0) is called, because there is nothing to - // release. Acquire(0) the equivalent of MustDeny(). + // after the resources is no longer in use. Acquire(ctx context.Context, value uint64) (ReleaseFunc, error) } -// ReleaseFunc is called when resources should be released after limiting. +// ReleaseFunc is called when resources have been released after use. // // RelaseFunc values are never nil values, even in the error case, for -// safety. Users should unconditionally defer these. +// safety. Users may unconditionally defer these. +// +// Implementations are not required to call a release func after +// Acquire(0) is called, since there is nothing to release. type ReleaseFunc func() // ResourceLimiterFunc is a functional way to construct ResourceLimiters. @@ -75,12 +78,6 @@ type ResourceLimiterFunc func(ctx context.Context, value uint64) (ReleaseFunc, e var _ ResourceLimiter = ResourceLimiterFunc(nil) -// MustDeny implements ResourceLimiter -func (f ResourceLimiterFunc) MustDeny(ctx context.Context) error { - _, err := f.Acquire(ctx, 0) - return err -} - // Acquire implements ResourceLimiter func (f ResourceLimiterFunc) Acquire(ctx context.Context, value uint64) (ReleaseFunc, error) { if f == nil { @@ -88,32 +85,3 @@ func (f ResourceLimiterFunc) Acquire(ctx context.Context, value uint64) (Release } return f(ctx, value) } - -// NewResourceLimiterWrapper returns a LimiterWrapper from a ResourceLimiter. -func NewResourceLimiterWrapper(limiter ResourceLimiter) LimiterWrapper { - return resourceLimiterWrapper{limiter: limiter} -} - -type resourceLimiterWrapper struct { - limiter ResourceLimiter -} - -var _ LimiterWrapper = resourceLimiterWrapper{} - -// MustDeny implements LimiterWrapper. -func (w resourceLimiterWrapper) MustDeny(ctx context.Context) error { - if w.limiter == nil { - return nil - } - return w.limiter.MustDeny(ctx) -} - -// LimitCall implements LimiterWrapper. -func (w resourceLimiterWrapper) LimitCall(ctx context.Context, value uint64, call func(context.Context) error) error { - release, err := w.limiter.Acquire(ctx, value) - if err != nil { - return err - } - defer release() - return call(ctx) -} diff --git a/extension/extensionlimiter/wrapper.go b/extension/extensionlimiter/wrapper.go index cd521f4487a..0e4f69206f8 100644 --- a/extension/extensionlimiter/wrapper.go +++ b/extension/extensionlimiter/wrapper.go @@ -15,44 +15,32 @@ type LimiterWrapperProvider interface { LimiterWrapper(WeightKey, ...Option) (LimiterWrapper, error) } -// LimiterWrapperFunc is a functional way to build LimiterWrappers. -type LimiterWrapperFunc func(context.Context, uint64, func(ctx context.Context) error) error - -var _ LimiterWrapper = LimiterWrapperFunc(nil) - // LimiterWrapper is a general-purpose interface for limiter consumers // to limit resources with use of a callback. This is the simplest // form of rate limiting interface from a callers perspective. If the // caller is a pipeline component, consider using a consumer-oriented -// limiterhelper (e.g., limiterhelper.NewLimitedLogs) to apply a list of +// limiterhelper (e.g., limiterhelper.NewLimitedLogs) to simplify +// construction of this interface. // -// Limiter implementions are meant to implement either the RateLimiter -// or ResourceLimiter interfaces. LimiterWrappers can be constructed -// from either of the underlying limiters and their corresponding -// providers. Usually configmiddleware or limiterhelper is responsible -// for constructing the correct wrapper from these two kinds of limiter; -// users will use this interface consistently. +// A wrapped limiter is either a RateLimiter or ResourceLimiter +// interface. LimiterWrappers can be constructed from either of the +// underlying limiters and their corresponding providers. Usually +// configmiddleware or limiterhelper is responsible for constructing +// the correct wrapper from these two kinds of limiter; users will use +// this interface consistently. type LimiterWrapper interface { - // Limiter includes MustDeny(). - Limiter - // LimitCall applies the limiter and with the rate or resource // granted makes a scoped call, returning success or an error // from either the limiter or the enclosed callback. - LimitCall(context.Context, uint64, func(ctx context.Context) error) error + // + // The `call` parameter must be non-nil. + LimitCall(ctx context.Context, weight uint64, call func(ctx context.Context) error) error } -// PassThrough returns a LimiterWrapper that imposes no limit. -func PassThrough() LimiterWrapper { - return LimiterWrapperFunc(nil) -} +// LimiterWrapperFunc is a functional way to build LimiterWrappers. +type LimiterWrapperFunc func(context.Context, uint64, func(ctx context.Context) error) error -// MustDeny implements LimiterWrapper. -func (f LimiterWrapperFunc) MustDeny(ctx context.Context) error { - return f.LimitCall(ctx, 0, func(_ context.Context) error { - return nil - }) -} +var _ LimiterWrapper = LimiterWrapperFunc(nil) // LimitCall implements LimiterWrapper. func (f LimiterWrapperFunc) LimitCall(ctx context.Context, value uint64, call func(ctx context.Context) error) error { @@ -62,6 +50,11 @@ func (f LimiterWrapperFunc) LimitCall(ctx context.Context, value uint64, call fu return f(ctx, value, call) } +// PassThroughWrapper returns a LimiterWrapper that imposes no limit. +func PassThroughWrapper() LimiterWrapper { + return LimiterWrapperFunc(nil) +} + // LimiterWrapperProviderFunc is a functional way to build LimiterWrappers. type LimiterWrapperProviderFunc func(WeightKey, ...Option) (LimiterWrapper, error) @@ -69,6 +62,9 @@ var _ LimiterWrapperProvider = LimiterWrapperProviderFunc(nil) // LimiterWrapper implements LimiterWrapperProvider. func (f LimiterWrapperProviderFunc) LimiterWrapper(key WeightKey, opts ...Option) (LimiterWrapper, error) { + if f == nil { + return PassThroughWrapper(), nil + } return f(key, opts...) } @@ -95,3 +91,25 @@ func NewRateLimiterWrapperProvider(rp RateLimiterProvider) LimiterWrapperProvide return NewRateLimiterWrapper(lim), err }) } + +// NewRateLimiterWrapper returns a LimiterWrapper from a RateLimiter. +func NewRateLimiterWrapper(limiter RateLimiter) LimiterWrapper { + return LimiterWrapperFunc(func(ctx context.Context, value uint64, call func(context.Context) error) error { + if err := limiter.Limit(ctx, value); err != nil { + return err + } + return call(ctx) + }) +} + +// NewResourceLimiterWrapper returns a LimiterWrapper from a ResourceLimiter. +func NewResourceLimiterWrapper(limiter ResourceLimiter) LimiterWrapper { + return LimiterWrapperFunc(func(ctx context.Context, value uint64, call func(context.Context) error) error { + release, err := limiter.Acquire(ctx, value) + if err != nil { + return err + } + defer release() + return call(ctx) + }) +} From d07da61ea168e07096c2aa3ca2d4644f559c5cdd Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Mon, 5 May 2025 16:59:21 -0700 Subject: [PATCH 09/14] rename --- extension/extensionlimiter/{ => limiterhelper}/wrapper.go | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename extension/extensionlimiter/{ => limiterhelper}/wrapper.go (100%) diff --git a/extension/extensionlimiter/wrapper.go b/extension/extensionlimiter/limiterhelper/wrapper.go similarity index 100% rename from extension/extensionlimiter/wrapper.go rename to extension/extensionlimiter/limiterhelper/wrapper.go From f3a2f2f213be1060f10f29b771ee833b9b6ec574 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Mon, 5 May 2025 17:09:25 -0700 Subject: [PATCH 10/14] move wrapper into limiterhelper --- .../extensionlimiter/extensionlimiter.go | 5 -- .../extensionlimiter/limiterhelper/checker.go | 50 +++++++++++++++++++ .../limiterhelper/consumer.go | 30 +++-------- .../limiterhelper/middleware.go | 26 +++++----- .../extensionlimiter/limiterhelper/wrapper.go | 44 ++++++++-------- extension/extensionlimiter/rate.go | 6 --- extension/extensionlimiter/resource.go | 7 --- 7 files changed, 92 insertions(+), 76 deletions(-) create mode 100644 extension/extensionlimiter/limiterhelper/checker.go diff --git a/extension/extensionlimiter/extensionlimiter.go b/extension/extensionlimiter/extensionlimiter.go index 6d85503dd67..e2faec9e03e 100644 --- a/extension/extensionlimiter/extensionlimiter.go +++ b/extension/extensionlimiter/extensionlimiter.go @@ -7,11 +7,6 @@ import ( "context" ) -// Defines: -// - Option -// - Checker -// - CheckerFunc - // Option is passed to limiter providers. // // NOTE: For data-specific or tenant-specific limits we will extend diff --git a/extension/extensionlimiter/limiterhelper/checker.go b/extension/extensionlimiter/limiterhelper/checker.go new file mode 100644 index 00000000000..bc3fb8c6f49 --- /dev/null +++ b/extension/extensionlimiter/limiterhelper/checker.go @@ -0,0 +1,50 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package limiterhelper // import "go.opentelemetry.io/collector/extension/extensionlimiter/limiterhelper" + +import ( + "context" + + "go.opentelemetry.io/collector/extension/extensionlimiter" +) + +// MultiChecker returns MustDeny when any element returns MustDeny. +type MultiChecker []extensionlimiter.Checker + +var _ extensionlimiter.Checker = MultiChecker{} + +// MustDeny implements Checker. +func (ls MultiChecker) MustDeny(ctx context.Context) error { + for _, lim := range ls { + if lim == nil { + continue + } + if err := lim.MustDeny(ctx); err != nil { + return err + } + } + return nil +} + +// NewLimiterWrapperChecker returns a Checker for a LimiterWrapper. +func NewLimiterWrapperChecker(limiter LimiterWrapper) extensionlimiter.Checker { + return extensionlimiter.CheckerFunc(func(ctx context.Context) error { + return limiter.LimitCall(ctx, 0, func(_ context.Context) error { return nil }) + }) +} + +// NewRateLimiterChecker returns a Checker for a RateLimiter. +func NewRateLimiterChecker(limiter extensionlimiter.RateLimiter) extensionlimiter.Checker { + return extensionlimiter.CheckerFunc(func(ctx context.Context) error { + return limiter.Limit(ctx, 0) + }) +} + +// NewResourceLimiterChecker returns a Checker for ResourceLimiter. +func NewResourceLimiterChecker(limiter extensionlimiter.ResourceLimiter) extensionlimiter.Checker { + return extensionlimiter.CheckerFunc(func(ctx context.Context) error { + _, err := limiter.Acquire(ctx, 0) + return err + }) +} diff --git a/extension/extensionlimiter/limiterhelper/consumer.go b/extension/extensionlimiter/limiterhelper/consumer.go index f6810f51e7c..18d05346f65 100644 --- a/extension/extensionlimiter/limiterhelper/consumer.go +++ b/extension/extensionlimiter/limiterhelper/consumer.go @@ -17,24 +17,6 @@ import ( "go.opentelemetry.io/collector/pdata/ptrace" ) -// MultiChecker returns MustDeny when any element returns MustDeny. -type MultiChecker []extensionlimiter.Checker - -var _ extensionlimiter.Checker = MultiChecker{} - -// MustDeny implements Checker. -func (ls MultiChecker) MustDeny(ctx context.Context) error { - for _, lim := range ls { - if lim == nil { - continue - } - if err := lim.MustDeny(ctx); err != nil { - return err - } - } - return nil -} - // Traits object interface is generalized by P the pipeline data type // (e.g., ptrace.Traces) and C the consumer type (e.g., // consumer.Traces) @@ -138,7 +120,7 @@ func (profileTraits) consume(ctx context.Context, data pprofile.Profiles, next x func limitOne[P any, C any]( next C, keys []extensionlimiter.WeightKey, - provider extensionlimiter.LimiterWrapperProvider, + provider LimiterWrapperProvider, m traits[P, C], key extensionlimiter.WeightKey, opts []consumer.Option, @@ -166,7 +148,7 @@ func limitOne[P any, C any]( func newLimited[P any, C any]( next C, keys []extensionlimiter.WeightKey, - provider extensionlimiter.LimiterWrapperProvider, + provider LimiterWrapperProvider, m traits[P, C], opts ...consumer.Option, ) (extensionlimiter.Checker, C, error) { @@ -193,25 +175,25 @@ func newLimited[P any, C any]( } // NewLimitedTraces applies a limiter using the provider over keys before calling next. -func NewLimitedTraces(next consumer.Traces, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (extensionlimiter.Checker, consumer.Traces, error) { +func NewLimitedTraces(next consumer.Traces, keys []extensionlimiter.WeightKey, provider LimiterWrapperProvider) (extensionlimiter.Checker, consumer.Traces, error) { return newLimited(next, keys, provider, traceTraits{}, consumer.WithCapabilities(next.Capabilities())) } // NewLimitedLogs applies a limiter using the provider over keys before calling next. -func NewLimitedLogs(next consumer.Logs, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (extensionlimiter.Checker, consumer.Logs, error) { +func NewLimitedLogs(next consumer.Logs, keys []extensionlimiter.WeightKey, provider LimiterWrapperProvider) (extensionlimiter.Checker, consumer.Logs, error) { return newLimited(next, keys, provider, logTraits{}, consumer.WithCapabilities(next.Capabilities())) } // NewLimitedMetrics applies a limiter using the provider over keys before calling next. -func NewLimitedMetrics(next consumer.Metrics, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (extensionlimiter.Checker, consumer.Metrics, error) { +func NewLimitedMetrics(next consumer.Metrics, keys []extensionlimiter.WeightKey, provider LimiterWrapperProvider) (extensionlimiter.Checker, consumer.Metrics, error) { return newLimited(next, keys, provider, metricTraits{}, consumer.WithCapabilities(next.Capabilities())) } // NewLimitedProfiles applies a limiter using the provider over keys before calling next. -func NewLimitedProfiles(next xconsumer.Profiles, keys []extensionlimiter.WeightKey, provider extensionlimiter.LimiterWrapperProvider) (extensionlimiter.Checker, xconsumer.Profiles, error) { +func NewLimitedProfiles(next xconsumer.Profiles, keys []extensionlimiter.WeightKey, provider LimiterWrapperProvider) (extensionlimiter.Checker, xconsumer.Profiles, error) { return newLimited(next, keys, provider, profileTraits{}, consumer.WithCapabilities(next.Capabilities())) } diff --git a/extension/extensionlimiter/limiterhelper/middleware.go b/extension/extensionlimiter/limiterhelper/middleware.go index b203194cacd..95570d9b7c4 100644 --- a/extension/extensionlimiter/limiterhelper/middleware.go +++ b/extension/extensionlimiter/limiterhelper/middleware.go @@ -36,9 +36,9 @@ func MiddlewareIsLimiter(host component.Host, middleware configmiddleware.Config // When no limiters are found (with no errors), the returned provider // is nil. When a nil is passed to the consumer helpers (e.g., // NewLimitedLogs) it will pass-through when the limiter is nil. -func MiddlewaresToLimiterWrapperProvider(host component.Host, middleware []configmiddleware.Config) (extensionlimiter.LimiterWrapperProvider, error) { +func MiddlewaresToLimiterWrapperProvider(host component.Host, middleware []configmiddleware.Config) (LimiterWrapperProvider, error) { var retErr error - var providers []extensionlimiter.LimiterWrapperProvider + var providers []LimiterWrapperProvider for _, mid := range middleware { ok, err := MiddlewareIsLimiter(host, mid) retErr = errors.Join(retErr, err) @@ -63,17 +63,17 @@ func MiddlewaresToLimiterWrapperProvider(host component.Host, middleware []confi // provider from middleware. Returns a package-level error if the // middleware does not implement exactly one of the limiter // interfaces (i.e., rate or resource). -func MiddlewareToLimiterWrapperProvider(host component.Host, middleware configmiddleware.Config) (extensionlimiter.LimiterWrapperProvider, error) { +func MiddlewareToLimiterWrapperProvider(host component.Host, middleware configmiddleware.Config) (LimiterWrapperProvider, error) { ext, ok, err := middlewareIsLimiter(host, middleware) if err != nil { return nil, err } if ok { if lim, ok := ext.(extensionlimiter.ResourceLimiterProvider); ok { - return extensionlimiter.NewResourceLimiterWrapperProvider(lim), nil + return NewResourceLimiterWrapperProvider(lim), nil } if lim, ok := ext.(extensionlimiter.RateLimiterProvider); ok { - return extensionlimiter.NewRateLimiterWrapperProvider(lim), nil + return NewRateLimiterWrapperProvider(lim), nil } } return nil, fmt.Errorf("%w: %s", ErrNotALimiter, ext) @@ -103,18 +103,18 @@ func middlewareIsLimiter(host component.Host, middleware configmiddleware.Config // MultiLimiterWrapperProvider combines multiple limiter wrappers // providers into a single provider by sequencing wrapped limiters. // Returns errors from the underlying LimiterWrapper() calls, if any. -type MultiLimiterWrapperProvider []extensionlimiter.LimiterWrapperProvider +type MultiLimiterWrapperProvider []LimiterWrapperProvider -var _ extensionlimiter.LimiterWrapperProvider = MultiLimiterWrapperProvider{} +var _ LimiterWrapperProvider = MultiLimiterWrapperProvider{} // LimiterWrapper implements LimiterWrapperProvider. -func (ps MultiLimiterWrapperProvider) LimiterWrapper(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (extensionlimiter.LimiterWrapper, error) { +func (ps MultiLimiterWrapperProvider) LimiterWrapper(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { if len(ps) == 0 { - return extensionlimiter.PassThroughWrapper(), nil + return PassThroughWrapper(), nil } // Map provider list to limiter list. - var lims []extensionlimiter.LimiterWrapper + var lims []LimiterWrapper for _, provider := range ps { lim, err := provider.LimiterWrapper(key, opts...) @@ -128,15 +128,15 @@ func (ps MultiLimiterWrapperProvider) LimiterWrapper(key extensionlimiter.Weight return sequenceLimiters(lims), nil } -func sequenceLimiters(lims []extensionlimiter.LimiterWrapper) extensionlimiter.LimiterWrapper { +func sequenceLimiters(lims []LimiterWrapper) LimiterWrapper { if len(lims) == 1 { return lims[0] } return composeLimiters(lims[0], sequenceLimiters(lims[1:])) } -func composeLimiters(first, second extensionlimiter.LimiterWrapper) extensionlimiter.LimiterWrapper { - return extensionlimiter.LimiterWrapperFunc(func(ctx context.Context, value uint64, call func(ctx context.Context) error) error { +func composeLimiters(first, second LimiterWrapper) LimiterWrapper { + return LimiterWrapperFunc(func(ctx context.Context, value uint64, call func(ctx context.Context) error) error { return first.LimitCall(ctx, value, func(ctx context.Context) error { return second.LimitCall(ctx, value, call) }) diff --git a/extension/extensionlimiter/limiterhelper/wrapper.go b/extension/extensionlimiter/limiterhelper/wrapper.go index 0e4f69206f8..26ec8ab94ac 100644 --- a/extension/extensionlimiter/limiterhelper/wrapper.go +++ b/extension/extensionlimiter/limiterhelper/wrapper.go @@ -1,10 +1,12 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package extensionlimiter // import "go.opentelemetry.io/collector/extension/extensionlimiter" +package limiterhelper // import "go.opentelemetry.io/collector/extension/extensionlimiter/limiterhelper" import ( "context" + + "go.opentelemetry.io/collector/extension/extensionlimiter" ) // LimiterWrapperProvider provides access to LimiterWrappers, which is @@ -12,7 +14,20 @@ import ( // function call, because for wrapped calls there is no distinction // between rate limiters and resource limiters. type LimiterWrapperProvider interface { - LimiterWrapper(WeightKey, ...Option) (LimiterWrapper, error) + LimiterWrapper(extensionlimiter.WeightKey, ...extensionlimiter.Option) (LimiterWrapper, error) +} + +// LimiterWrapperProviderFunc is a functional way to build LimiterWrappers. +type LimiterWrapperProviderFunc func(extensionlimiter.WeightKey, ...extensionlimiter.Option) (LimiterWrapper, error) + +var _ LimiterWrapperProvider = LimiterWrapperProviderFunc(nil) + +// LimiterWrapper implements LimiterWrapperProvider. +func (f LimiterWrapperProviderFunc) LimiterWrapper(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { + if f == nil { + return PassThroughWrapper(), nil + } + return f(key, opts...) } // LimiterWrapper is a general-purpose interface for limiter consumers @@ -55,23 +70,10 @@ func PassThroughWrapper() LimiterWrapper { return LimiterWrapperFunc(nil) } -// LimiterWrapperProviderFunc is a functional way to build LimiterWrappers. -type LimiterWrapperProviderFunc func(WeightKey, ...Option) (LimiterWrapper, error) - -var _ LimiterWrapperProvider = LimiterWrapperProviderFunc(nil) - -// LimiterWrapper implements LimiterWrapperProvider. -func (f LimiterWrapperProviderFunc) LimiterWrapper(key WeightKey, opts ...Option) (LimiterWrapper, error) { - if f == nil { - return PassThroughWrapper(), nil - } - return f(key, opts...) -} - // NewResourceLimiterWrapperProvider constructs a // LimiterWrapperProvider for a resource limiter extension. -func NewResourceLimiterWrapperProvider(rp ResourceLimiterProvider) LimiterWrapperProvider { - return LimiterWrapperProviderFunc(func(key WeightKey, opts ...Option) (LimiterWrapper, error) { +func NewResourceLimiterWrapperProvider(rp extensionlimiter.ResourceLimiterProvider) LimiterWrapperProvider { + return LimiterWrapperProviderFunc(func(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { lim, err := rp.ResourceLimiter(key, opts...) if err == nil { return nil, err @@ -82,8 +84,8 @@ func NewResourceLimiterWrapperProvider(rp ResourceLimiterProvider) LimiterWrappe // NewRateLimiterWrapperProvider constructs a LimiterWrapperProvider // for a rate limiter extension. -func NewRateLimiterWrapperProvider(rp RateLimiterProvider) LimiterWrapperProvider { - return LimiterWrapperProviderFunc(func(key WeightKey, opts ...Option) (LimiterWrapper, error) { +func NewRateLimiterWrapperProvider(rp extensionlimiter.RateLimiterProvider) LimiterWrapperProvider { + return LimiterWrapperProviderFunc(func(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { lim, err := rp.RateLimiter(key, opts...) if err == nil { return nil, err @@ -93,7 +95,7 @@ func NewRateLimiterWrapperProvider(rp RateLimiterProvider) LimiterWrapperProvide } // NewRateLimiterWrapper returns a LimiterWrapper from a RateLimiter. -func NewRateLimiterWrapper(limiter RateLimiter) LimiterWrapper { +func NewRateLimiterWrapper(limiter extensionlimiter.RateLimiter) LimiterWrapper { return LimiterWrapperFunc(func(ctx context.Context, value uint64, call func(context.Context) error) error { if err := limiter.Limit(ctx, value); err != nil { return err @@ -103,7 +105,7 @@ func NewRateLimiterWrapper(limiter RateLimiter) LimiterWrapper { } // NewResourceLimiterWrapper returns a LimiterWrapper from a ResourceLimiter. -func NewResourceLimiterWrapper(limiter ResourceLimiter) LimiterWrapper { +func NewResourceLimiterWrapper(limiter extensionlimiter.ResourceLimiter) LimiterWrapper { return LimiterWrapperFunc(func(ctx context.Context, value uint64, call func(context.Context) error) error { release, err := limiter.Acquire(ctx, value) if err != nil { diff --git a/extension/extensionlimiter/rate.go b/extension/extensionlimiter/rate.go index a02e9c2c5aa..f6c0cd49079 100644 --- a/extension/extensionlimiter/rate.go +++ b/extension/extensionlimiter/rate.go @@ -7,12 +7,6 @@ import ( "context" ) -// Defines: -// - RateLimiterProvider -// - RateLimiterProviderFunc -// - RateLimiter -// - RateLimiterFunc - // RateLimiterProvider is a provider for rate limiters. // // Limiter implementations will implement this or the diff --git a/extension/extensionlimiter/resource.go b/extension/extensionlimiter/resource.go index c919fc55979..e23e38a49ca 100644 --- a/extension/extensionlimiter/resource.go +++ b/extension/extensionlimiter/resource.go @@ -7,13 +7,6 @@ import ( "context" ) -// Defines: -// - ResourceLimiterProvider -// - ResourceLimiterProviderFunc -// - ResourceLimiter -// - ResourceLimiterFunc -// - ReleaseFunc - // ResourceLimiterProvider is a provider for resource limiters. // // Limiter implementations will implement this or the From 8b31cd61d1bf925e7196f5509257503ebfb76f6f Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Tue, 6 May 2025 08:46:41 -0700 Subject: [PATCH 11/14] style --- .../extensionlimiter/extensionlimiter.go | 25 ++++++-------- .../extensionlimiter/limiterhelper/checker.go | 6 ++-- .../extensionlimiter/limiterhelper/wrapper.go | 4 +-- extension/extensionlimiter/rate.go | 34 ++++++++++++------- extension/extensionlimiter/resource.go | 34 ++++++++++++------- 5 files changed, 60 insertions(+), 43 deletions(-) diff --git a/extension/extensionlimiter/extensionlimiter.go b/extension/extensionlimiter/extensionlimiter.go index e2faec9e03e..580d1623848 100644 --- a/extension/extensionlimiter/extensionlimiter.go +++ b/extension/extensionlimiter/extensionlimiter.go @@ -20,30 +20,27 @@ type Option interface { // Checker is for checking when a limit is saturated. This can be // called prior to the start of work to check for limiter saturation. type Checker interface { - // MustDeny is the logical equivalent of Acquire(0). If the - // Acquire would fail even for 0 units of a rate, the - // caller must deny the request. Implementations are - // encouraged to ensure that when MustDeny() is false, - // Acquire(0) is also false, however callers could use a - // faster code path to implement MustDeny() since it does not - // depend on the value. + // MustDeny is a request to apply a hard limit. If this + // returns non-nil, the caller must not begin new work in this + // context. MustDeny(context.Context) error } -// CheckerFunc is a functional way to build Checker implementations. -type CheckerFunc func(context.Context) error +// MustDenyFunc is a functional way to build MustDeny functions. +type MustDenyFunc func(context.Context) error -var _ Checker = CheckerFunc(nil) +// A MustDeny function is a complete Checker. +var _ Checker = MustDenyFunc(nil) // MustDeny implements Checker. -func (f CheckerFunc) MustDeny(ctx context.Context) error { +func (f MustDenyFunc) MustDeny(ctx context.Context) error { if f == nil { return nil } return f(ctx) } -// PassThroughChecker returns a Checker that never denies. -func PassThroughChecker() Checker { - return CheckerFunc(nil) +// NeverDeny returns a Checker that never denies. +func NeverDeny() Checker { + return MustDenyFunc(nil) } diff --git a/extension/extensionlimiter/limiterhelper/checker.go b/extension/extensionlimiter/limiterhelper/checker.go index bc3fb8c6f49..e5f92ca2e4b 100644 --- a/extension/extensionlimiter/limiterhelper/checker.go +++ b/extension/extensionlimiter/limiterhelper/checker.go @@ -29,21 +29,21 @@ func (ls MultiChecker) MustDeny(ctx context.Context) error { // NewLimiterWrapperChecker returns a Checker for a LimiterWrapper. func NewLimiterWrapperChecker(limiter LimiterWrapper) extensionlimiter.Checker { - return extensionlimiter.CheckerFunc(func(ctx context.Context) error { + return extensionlimiter.MustDenyFunc(func(ctx context.Context) error { return limiter.LimitCall(ctx, 0, func(_ context.Context) error { return nil }) }) } // NewRateLimiterChecker returns a Checker for a RateLimiter. func NewRateLimiterChecker(limiter extensionlimiter.RateLimiter) extensionlimiter.Checker { - return extensionlimiter.CheckerFunc(func(ctx context.Context) error { + return extensionlimiter.MustDenyFunc(func(ctx context.Context) error { return limiter.Limit(ctx, 0) }) } // NewResourceLimiterChecker returns a Checker for ResourceLimiter. func NewResourceLimiterChecker(limiter extensionlimiter.ResourceLimiter) extensionlimiter.Checker { - return extensionlimiter.CheckerFunc(func(ctx context.Context) error { + return extensionlimiter.MustDenyFunc(func(ctx context.Context) error { _, err := limiter.Acquire(ctx, 0) return err }) diff --git a/extension/extensionlimiter/limiterhelper/wrapper.go b/extension/extensionlimiter/limiterhelper/wrapper.go index 26ec8ab94ac..35c7b213c5b 100644 --- a/extension/extensionlimiter/limiterhelper/wrapper.go +++ b/extension/extensionlimiter/limiterhelper/wrapper.go @@ -74,7 +74,7 @@ func PassThroughWrapper() LimiterWrapper { // LimiterWrapperProvider for a resource limiter extension. func NewResourceLimiterWrapperProvider(rp extensionlimiter.ResourceLimiterProvider) LimiterWrapperProvider { return LimiterWrapperProviderFunc(func(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { - lim, err := rp.ResourceLimiter(key, opts...) + lim, err := rp.GetResourceLimiter(key, opts...) if err == nil { return nil, err } @@ -86,7 +86,7 @@ func NewResourceLimiterWrapperProvider(rp extensionlimiter.ResourceLimiterProvid // for a rate limiter extension. func NewRateLimiterWrapperProvider(rp extensionlimiter.RateLimiterProvider) LimiterWrapperProvider { return LimiterWrapperProviderFunc(func(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { - lim, err := rp.RateLimiter(key, opts...) + lim, err := rp.GetRateLimiter(key, opts...) if err == nil { return nil, err } diff --git a/extension/extensionlimiter/rate.go b/extension/extensionlimiter/rate.go index f6c0cd49079..b8748281da6 100644 --- a/extension/extensionlimiter/rate.go +++ b/extension/extensionlimiter/rate.go @@ -14,17 +14,21 @@ import ( // Limiters are covered by configmiddleware configuration, which is // able to construct LimiterWrappers from these providers. type RateLimiterProvider interface { - // RateLimiter returns a provider for rate limiters. - RateLimiter(WeightKey, ...Option) (RateLimiter, error) + // GetRateLimiter returns a provider for rate limiters. + GetRateLimiter(WeightKey, ...Option) (RateLimiter, error) } -// RateLimiterProviderFunc is a functional way to build RateLimters. -type RateLimiterProviderFunc func(WeightKey, ...Option) (RateLimiter, error) +// RateLimiterFunc is a functional way to construct GetRateLimiter +// functions. +type GetRateLimiterFunc func(WeightKey, ...Option) (RateLimiter, error) -var _ RateLimiterProvider = RateLimiterProviderFunc(nil) +var _ RateLimiterProvider = GetRateLimiterFunc(nil) // RateLimiter implements RateLimiterProvider. -func (f RateLimiterProviderFunc) RateLimiter(key WeightKey, opts ...Option) (RateLimiter, error) { +func (f GetRateLimiterFunc) GetRateLimiter(key WeightKey, opts ...Option) (RateLimiter, error) { + if f == nil { + return nil, nil + } return f(key, opts...) } @@ -40,6 +44,8 @@ func (f RateLimiterProviderFunc) RateLimiter(key WeightKey, opts ...Option) (Rat // // See the README for more recommendations. type RateLimiter interface { + Checker + // Limit attempts to apply rate limiting with the provided // weight, based on the key that was given to the provider. // @@ -49,15 +55,19 @@ type RateLimiter interface { Limit(ctx context.Context, value uint64) error } -// RateLimiterFunc is an easy way to construct RateLimiters. -type RateLimiterFunc func(ctx context.Context, value uint64) error +// LimitFunc is a functional way to construct Limit functions. +type LimitFunc func(ctx context.Context, value uint64) error -var _ RateLimiter = RateLimiterFunc(nil) - -// Limit implements RateLimiter. -func (f RateLimiterFunc) Limit(ctx context.Context, value uint64) error { +// Limit implements part of the RateLimiter interface. +func (f LimitFunc) Limit(ctx context.Context, value uint64) error { if f == nil { return nil } return f(ctx, value) } + +// Verify that a rate limiter is constructed of two functions. +var _ RateLimiter = struct { + MustDenyFunc + LimitFunc +}{} diff --git a/extension/extensionlimiter/resource.go b/extension/extensionlimiter/resource.go index e23e38a49ca..2baf417243d 100644 --- a/extension/extensionlimiter/resource.go +++ b/extension/extensionlimiter/resource.go @@ -14,16 +14,20 @@ import ( // Limiters are covered by configmiddleware configuration, which // is able to construct LimiterWrappers from these providers. type ResourceLimiterProvider interface { - ResourceLimiter(WeightKey, ...Option) (ResourceLimiter, error) + GetResourceLimiter(WeightKey, ...Option) (ResourceLimiter, error) } -// ResourceLimiterProviderFunc is a functional way to build ResourceLimters. -type ResourceLimiterProviderFunc func(WeightKey, ...Option) (ResourceLimiter, error) +// GetResourceLimiterFunc is a functional way to construct +// GetResourceLimiter functions. +type GetResourceLimiterFunc func(WeightKey, ...Option) (ResourceLimiter, error) -var _ ResourceLimiterProvider = ResourceLimiterProviderFunc(nil) +var _ ResourceLimiterProvider = GetResourceLimiterFunc(nil) -// ResourceLimiter implements ResourceLimiterProvider. -func (f ResourceLimiterProviderFunc) ResourceLimiter(key WeightKey, opts ...Option) (ResourceLimiter, error) { +// GetResourceLimiter implements part of ResourceLimiterProvider. +func (f GetResourceLimiterFunc) GetResourceLimiter(key WeightKey, opts ...Option) (ResourceLimiter, error) { + if f == nil { + return nil, nil + } return f(key, opts...) } @@ -41,6 +45,8 @@ func (f ResourceLimiterProviderFunc) ResourceLimiter(key WeightKey, opts ...Opti // // See the README for more recommendations. type ResourceLimiter interface { + Checker + // Acquire attempts to acquire a quantified resource with the // provided weight, based on the key that was given to the // provider. The caller has these options: @@ -66,15 +72,19 @@ type ResourceLimiter interface { // Acquire(0) is called, since there is nothing to release. type ReleaseFunc func() -// ResourceLimiterFunc is a functional way to construct ResourceLimiters. -type ResourceLimiterFunc func(ctx context.Context, value uint64) (ReleaseFunc, error) +// AcquireFunc is a functional way to construct Acquire functions. +type AcquireFunc func(ctx context.Context, value uint64) (ReleaseFunc, error) -var _ ResourceLimiter = ResourceLimiterFunc(nil) - -// Acquire implements ResourceLimiter -func (f ResourceLimiterFunc) Acquire(ctx context.Context, value uint64) (ReleaseFunc, error) { +// Acquire implements part of ResourceLimiter. +func (f AcquireFunc) Acquire(ctx context.Context, value uint64) (ReleaseFunc, error) { if f == nil { return func() {}, nil } return f(ctx, value) } + +// Verify that a rate limiter is constructed of two functions. +var _ ResourceLimiter = struct { + MustDenyFunc + AcquireFunc +}{} From 40aee984eebd3330f9f54ca8b776528eb43bc1c0 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Tue, 6 May 2025 11:05:28 -0700 Subject: [PATCH 12/14] call checker once --- .../extensionlimiter/extensionlimiter.go | 19 ++++ .../extensionlimiter/limiterhelper/checker.go | 30 +----- .../limiterhelper/consumer.go | 67 ++++++++----- .../limiterhelper/middleware.go | 34 +++++-- .../extensionlimiter/limiterhelper/wrapper.go | 94 ++++++++++--------- extension/extensionlimiter/rate.go | 21 ++--- extension/extensionlimiter/resource.go | 17 ++-- extension/extensionlimiter/weight.go | 9 +- receiver/otlpreceiver/otlp.go | 16 ++-- 9 files changed, 179 insertions(+), 128 deletions(-) diff --git a/extension/extensionlimiter/extensionlimiter.go b/extension/extensionlimiter/extensionlimiter.go index 580d1623848..2a8659c0b9e 100644 --- a/extension/extensionlimiter/extensionlimiter.go +++ b/extension/extensionlimiter/extensionlimiter.go @@ -44,3 +44,22 @@ func (f MustDenyFunc) MustDeny(ctx context.Context) error { func NeverDeny() Checker { return MustDenyFunc(nil) } + +// CheckerProvider is an interface to obtain checkers for a group of +// weight keys. +type CheckerProvider interface { + // GetChecker returns a checker for a group of weight keys. + GetChecker(WeightSet, ...Option) (Checker, error) +} + +// GetCheckerFunc is a functional way to construct GetChecker +// functions, used in limiter providers. +type GetCheckerFunc func(WeightSet, ...Option) (Checker, error) + +// Checker implements CheckerProvider. +func (f GetCheckerFunc) GetChecker(keys WeightSet, opts ...Option) (Checker, error) { + if f == nil { + return nil, nil + } + return f(keys, opts...) +} diff --git a/extension/extensionlimiter/limiterhelper/checker.go b/extension/extensionlimiter/limiterhelper/checker.go index e5f92ca2e4b..115c8603d52 100644 --- a/extension/extensionlimiter/limiterhelper/checker.go +++ b/extension/extensionlimiter/limiterhelper/checker.go @@ -5,6 +5,7 @@ package limiterhelper // import "go.opentelemetry.io/collector/extension/extensi import ( "context" + "errors" "go.opentelemetry.io/collector/extension/extensionlimiter" ) @@ -16,35 +17,12 @@ var _ extensionlimiter.Checker = MultiChecker{} // MustDeny implements Checker. func (ls MultiChecker) MustDeny(ctx context.Context) error { + var err error for _, lim := range ls { if lim == nil { continue } - if err := lim.MustDeny(ctx); err != nil { - return err - } + err = errors.Join(err, lim.MustDeny(ctx)) } - return nil -} - -// NewLimiterWrapperChecker returns a Checker for a LimiterWrapper. -func NewLimiterWrapperChecker(limiter LimiterWrapper) extensionlimiter.Checker { - return extensionlimiter.MustDenyFunc(func(ctx context.Context) error { - return limiter.LimitCall(ctx, 0, func(_ context.Context) error { return nil }) - }) -} - -// NewRateLimiterChecker returns a Checker for a RateLimiter. -func NewRateLimiterChecker(limiter extensionlimiter.RateLimiter) extensionlimiter.Checker { - return extensionlimiter.MustDenyFunc(func(ctx context.Context) error { - return limiter.Limit(ctx, 0) - }) -} - -// NewResourceLimiterChecker returns a Checker for ResourceLimiter. -func NewResourceLimiterChecker(limiter extensionlimiter.ResourceLimiter) extensionlimiter.Checker { - return extensionlimiter.MustDenyFunc(func(ctx context.Context) error { - _, err := limiter.Acquire(ctx, 0) - return err - }) + return err } diff --git a/extension/extensionlimiter/limiterhelper/consumer.go b/extension/extensionlimiter/limiterhelper/consumer.go index 18d05346f65..1db74c5fc16 100644 --- a/extension/extensionlimiter/limiterhelper/consumer.go +++ b/extension/extensionlimiter/limiterhelper/consumer.go @@ -6,7 +6,6 @@ package limiterhelper // import "go.opentelemetry.io/collector/extension/extensi import ( "context" "errors" - "slices" "go.opentelemetry.io/collector/consumer" "go.opentelemetry.io/collector/consumer/xconsumer" @@ -119,81 +118,101 @@ func (profileTraits) consume(ctx context.Context, data pprofile.Profiles, next x // limitOne obtains a LimiterWrapper and applies a single weight limit. func limitOne[P any, C any]( next C, - keys []extensionlimiter.WeightKey, + keys extensionlimiter.WeightSet, provider LimiterWrapperProvider, m traits[P, C], key extensionlimiter.WeightKey, opts []consumer.Option, quantify func(P) uint64, -) (extensionlimiter.Checker, C, error) { - if !slices.Contains(keys, key) { - return nil, next, nil +) (C, error) { + if !keys.Contains(key) { + return next, nil } - lim, err := provider.LimiterWrapper(key) + lim, err := provider.GetLimiterWrapper(key) if err != nil { - return nil, next, err + return next, err } if lim == nil { - return nil, next, nil + return next, nil } - con, err := m.create(func(ctx context.Context, data P) error { + return m.create(func(ctx context.Context, data P) error { return lim.LimitCall(ctx, quantify(data), func(ctx context.Context) error { return m.consume(ctx, data, next) }) }, opts...) - return NewLimiterWrapperChecker(lim), con, err +} + +// applyChecker gets a Checker and wraps the pipeline in a MustDeny +// check. +func applyChecker[P any, C any]( + next C, + keys extensionlimiter.WeightSet, + provider LimiterWrapperProvider, + m traits[P, C], + opts []consumer.Option, +) (C, error) { + // Apply the Checker. + ck, err := provider.GetChecker(keys) + if err != nil { + return next, err + } + return m.create(func(ctx context.Context, data P) error { + if err := ck.MustDeny(ctx); err != nil { + return err + } + return m.consume(ctx, data, next) + }, opts...) } // newLimited is signal-generic limiting logic. func newLimited[P any, C any]( next C, - keys []extensionlimiter.WeightKey, + keys extensionlimiter.WeightSet, provider LimiterWrapperProvider, m traits[P, C], opts ...consumer.Option, -) (extensionlimiter.Checker, C, error) { +) (C, error) { if provider == nil { - return nil, next, nil + return next, nil } - var lim1, lim2, lim3 extensionlimiter.Checker - var err1, err2, err3 error + var err1, err2, err3, err4 error // Note: reverse order of evaluation cost => least-cost applied first. - lim1, next, err1 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyMemorySize, opts, - + next, err1 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyMemorySize, opts, func(data P) uint64 { return m.memorySize(data) }) - lim2, next, err2 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyRequestItems, opts, + next, err2 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyRequestItems, opts, func(data P) uint64 { return m.itemCount(data) }) - lim3, next, err3 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyRequestCount, opts, + next, err3 = limitOne(next, keys, provider, m, extensionlimiter.WeightKeyRequestCount, opts, func(_ P) uint64 { return 1 }) - return MultiChecker{lim1, lim2, lim3}, next, errors.Join(err1, err2, err3) + next, err4 = applyChecker(next, keys, provider, m, opts) + return next, errors.Join(err1, err2, err3, err4) } // NewLimitedTraces applies a limiter using the provider over keys before calling next. -func NewLimitedTraces(next consumer.Traces, keys []extensionlimiter.WeightKey, provider LimiterWrapperProvider) (extensionlimiter.Checker, consumer.Traces, error) { +func NewLimitedTraces(next consumer.Traces, keys extensionlimiter.WeightSet, provider LimiterWrapperProvider) (consumer.Traces, error) { return newLimited(next, keys, provider, traceTraits{}, consumer.WithCapabilities(next.Capabilities())) } // NewLimitedLogs applies a limiter using the provider over keys before calling next. -func NewLimitedLogs(next consumer.Logs, keys []extensionlimiter.WeightKey, provider LimiterWrapperProvider) (extensionlimiter.Checker, consumer.Logs, error) { +func NewLimitedLogs(next consumer.Logs, keys extensionlimiter.WeightSet, provider LimiterWrapperProvider) (consumer.Logs, error) { return newLimited(next, keys, provider, logTraits{}, consumer.WithCapabilities(next.Capabilities())) } // NewLimitedMetrics applies a limiter using the provider over keys before calling next. -func NewLimitedMetrics(next consumer.Metrics, keys []extensionlimiter.WeightKey, provider LimiterWrapperProvider) (extensionlimiter.Checker, consumer.Metrics, error) { +func NewLimitedMetrics(next consumer.Metrics, keys extensionlimiter.WeightSet, provider LimiterWrapperProvider) (consumer.Metrics, error) { return newLimited(next, keys, provider, metricTraits{}, consumer.WithCapabilities(next.Capabilities())) } // NewLimitedProfiles applies a limiter using the provider over keys before calling next. -func NewLimitedProfiles(next xconsumer.Profiles, keys []extensionlimiter.WeightKey, provider LimiterWrapperProvider) (extensionlimiter.Checker, xconsumer.Profiles, error) { +func NewLimitedProfiles(next xconsumer.Profiles, keys extensionlimiter.WeightSet, provider LimiterWrapperProvider) (xconsumer.Profiles, error) { return newLimited(next, keys, provider, profileTraits{}, consumer.WithCapabilities(next.Capabilities())) } diff --git a/extension/extensionlimiter/limiterhelper/middleware.go b/extension/extensionlimiter/limiterhelper/middleware.go index 95570d9b7c4..fc284f47f83 100644 --- a/extension/extensionlimiter/limiterhelper/middleware.go +++ b/extension/extensionlimiter/limiterhelper/middleware.go @@ -107,24 +107,46 @@ type MultiLimiterWrapperProvider []LimiterWrapperProvider var _ LimiterWrapperProvider = MultiLimiterWrapperProvider{} -// LimiterWrapper implements LimiterWrapperProvider. -func (ps MultiLimiterWrapperProvider) LimiterWrapper(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { - if len(ps) == 0 { - return PassThroughWrapper(), nil +// GetLimiterWrapper implements LimiterWrapperProvider, combining +// checkers for all wrappers in a sequence. +func (ps MultiLimiterWrapperProvider) GetChecker(keys extensionlimiter.WeightSet, opts ...extensionlimiter.Option) (extensionlimiter.Checker, error) { + var retErr error + var cks MultiChecker + for _, provider := range ps { + ck, err := provider.GetChecker(keys, opts...) + retErr = errors.Join(retErr, err) + if ck == nil { + continue + } + cks = append(cks, ck) + } + if len(cks) == 0 { + return extensionlimiter.NeverDeny(), retErr } + return cks, retErr +} +// GetLimiterWrapper implements LimiterWrapperProvider, calling +// wrappers in a sequence. +func (ps MultiLimiterWrapperProvider) GetLimiterWrapper(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { // Map provider list to limiter list. var lims []LimiterWrapper for _, provider := range ps { - lim, err := provider.LimiterWrapper(key, opts...) + lim, err := provider.GetLimiterWrapper(key, opts...) if err == nil { return nil, err } + if lim == nil { + continue + } lims = append(lims, lim) } - // Compose limiters in sequence. + if len(lims) == 0 { + return PassThroughWrapper(), nil + } + return sequenceLimiters(lims), nil } diff --git a/extension/extensionlimiter/limiterhelper/wrapper.go b/extension/extensionlimiter/limiterhelper/wrapper.go index 35c7b213c5b..f3b1e2c7f3e 100644 --- a/extension/extensionlimiter/limiterhelper/wrapper.go +++ b/extension/extensionlimiter/limiterhelper/wrapper.go @@ -14,22 +14,27 @@ import ( // function call, because for wrapped calls there is no distinction // between rate limiters and resource limiters. type LimiterWrapperProvider interface { - LimiterWrapper(extensionlimiter.WeightKey, ...extensionlimiter.Option) (LimiterWrapper, error) -} + extensionlimiter.CheckerProvider -// LimiterWrapperProviderFunc is a functional way to build LimiterWrappers. -type LimiterWrapperProviderFunc func(extensionlimiter.WeightKey, ...extensionlimiter.Option) (LimiterWrapper, error) + GetLimiterWrapper(extensionlimiter.WeightKey, ...extensionlimiter.Option) (LimiterWrapper, error) +} -var _ LimiterWrapperProvider = LimiterWrapperProviderFunc(nil) +// GetLimiterWrapperFunc is an easy way to build GetLimiterWrapper functions. +type GetLimiterWrapperFunc func(extensionlimiter.WeightKey, ...extensionlimiter.Option) (LimiterWrapper, error) -// LimiterWrapper implements LimiterWrapperProvider. -func (f LimiterWrapperProviderFunc) LimiterWrapper(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { +// GetLimiterWrapper implements LimiterWrapperProvider. +func (f GetLimiterWrapperFunc) GetLimiterWrapper(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { if f == nil { return PassThroughWrapper(), nil } return f(key, opts...) } +var _ LimiterWrapperProvider = struct { + GetLimiterWrapperFunc + extensionlimiter.GetCheckerFunc +}{} + // LimiterWrapper is a general-purpose interface for limiter consumers // to limit resources with use of a callback. This is the simplest // form of rate limiting interface from a callers perspective. If the @@ -70,48 +75,51 @@ func PassThroughWrapper() LimiterWrapper { return LimiterWrapperFunc(nil) } +// wrapperProvider is a combinator for building wrapper providers from +// the underlying limter types. +type wrapperProvider struct { + GetLimiterWrapperFunc + extensionlimiter.GetCheckerFunc +} + // NewResourceLimiterWrapperProvider constructs a // LimiterWrapperProvider for a resource limiter extension. func NewResourceLimiterWrapperProvider(rp extensionlimiter.ResourceLimiterProvider) LimiterWrapperProvider { - return LimiterWrapperProviderFunc(func(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { - lim, err := rp.GetResourceLimiter(key, opts...) - if err == nil { - return nil, err - } - return NewResourceLimiterWrapper(lim), err - }) + return wrapperProvider{ + GetCheckerFunc: rp.GetChecker, + GetLimiterWrapperFunc: func(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { + lim, err := rp.GetResourceLimiter(key, opts...) + if err == nil { + return nil, err + } + return LimiterWrapperFunc(func(ctx context.Context, value uint64, call func(context.Context) error) error { + release, err := lim.Acquire(ctx, value) + if err != nil { + return err + } + defer release() + return call(ctx) + }), err + }, + } } // NewRateLimiterWrapperProvider constructs a LimiterWrapperProvider // for a rate limiter extension. func NewRateLimiterWrapperProvider(rp extensionlimiter.RateLimiterProvider) LimiterWrapperProvider { - return LimiterWrapperProviderFunc(func(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { - lim, err := rp.GetRateLimiter(key, opts...) - if err == nil { - return nil, err - } - return NewRateLimiterWrapper(lim), err - }) -} - -// NewRateLimiterWrapper returns a LimiterWrapper from a RateLimiter. -func NewRateLimiterWrapper(limiter extensionlimiter.RateLimiter) LimiterWrapper { - return LimiterWrapperFunc(func(ctx context.Context, value uint64, call func(context.Context) error) error { - if err := limiter.Limit(ctx, value); err != nil { - return err - } - return call(ctx) - }) -} - -// NewResourceLimiterWrapper returns a LimiterWrapper from a ResourceLimiter. -func NewResourceLimiterWrapper(limiter extensionlimiter.ResourceLimiter) LimiterWrapper { - return LimiterWrapperFunc(func(ctx context.Context, value uint64, call func(context.Context) error) error { - release, err := limiter.Acquire(ctx, value) - if err != nil { - return err - } - defer release() - return call(ctx) - }) + return wrapperProvider{ + GetCheckerFunc: rp.GetChecker, + GetLimiterWrapperFunc: func(key extensionlimiter.WeightKey, opts ...extensionlimiter.Option) (LimiterWrapper, error) { + lim, err := rp.GetRateLimiter(key, opts...) + if err == nil { + return nil, err + } + return LimiterWrapperFunc(func(ctx context.Context, value uint64, call func(context.Context) error) error { + if err := lim.Limit(ctx, value); err != nil { + return err + } + return call(ctx) + }), err + }, + } } diff --git a/extension/extensionlimiter/rate.go b/extension/extensionlimiter/rate.go index b8748281da6..dcc8d1cc4af 100644 --- a/extension/extensionlimiter/rate.go +++ b/extension/extensionlimiter/rate.go @@ -14,16 +14,16 @@ import ( // Limiters are covered by configmiddleware configuration, which is // able to construct LimiterWrappers from these providers. type RateLimiterProvider interface { - // GetRateLimiter returns a provider for rate limiters. + CheckerProvider + + // GetRateLimiter returns a rate limiter for a weight key. GetRateLimiter(WeightKey, ...Option) (RateLimiter, error) } -// RateLimiterFunc is a functional way to construct GetRateLimiter +// GetRateLimiterFunc is a functional way to construct GetRateLimiter // functions. type GetRateLimiterFunc func(WeightKey, ...Option) (RateLimiter, error) -var _ RateLimiterProvider = GetRateLimiterFunc(nil) - // RateLimiter implements RateLimiterProvider. func (f GetRateLimiterFunc) GetRateLimiter(key WeightKey, opts ...Option) (RateLimiter, error) { if f == nil { @@ -32,6 +32,11 @@ func (f GetRateLimiterFunc) GetRateLimiter(key WeightKey, opts ...Option) (RateL return f(key, opts...) } +var _ RateLimiterProvider = struct { + GetRateLimiterFunc + GetCheckerFunc +}{} + // RateLimiter is an interface that an implementation makes available // to apply time-based limits on quantities such as the number of // bytes or items per second. @@ -44,8 +49,6 @@ func (f GetRateLimiterFunc) GetRateLimiter(key WeightKey, opts ...Option) (RateL // // See the README for more recommendations. type RateLimiter interface { - Checker - // Limit attempts to apply rate limiting with the provided // weight, based on the key that was given to the provider. // @@ -66,8 +69,4 @@ func (f LimitFunc) Limit(ctx context.Context, value uint64) error { return f(ctx, value) } -// Verify that a rate limiter is constructed of two functions. -var _ RateLimiter = struct { - MustDenyFunc - LimitFunc -}{} +var _ RateLimiter = LimitFunc(nil) diff --git a/extension/extensionlimiter/resource.go b/extension/extensionlimiter/resource.go index 2baf417243d..98ba253accf 100644 --- a/extension/extensionlimiter/resource.go +++ b/extension/extensionlimiter/resource.go @@ -14,6 +14,8 @@ import ( // Limiters are covered by configmiddleware configuration, which // is able to construct LimiterWrappers from these providers. type ResourceLimiterProvider interface { + CheckerProvider + GetResourceLimiter(WeightKey, ...Option) (ResourceLimiter, error) } @@ -21,8 +23,6 @@ type ResourceLimiterProvider interface { // GetResourceLimiter functions. type GetResourceLimiterFunc func(WeightKey, ...Option) (ResourceLimiter, error) -var _ ResourceLimiterProvider = GetResourceLimiterFunc(nil) - // GetResourceLimiter implements part of ResourceLimiterProvider. func (f GetResourceLimiterFunc) GetResourceLimiter(key WeightKey, opts ...Option) (ResourceLimiter, error) { if f == nil { @@ -31,6 +31,11 @@ func (f GetResourceLimiterFunc) GetResourceLimiter(key WeightKey, opts ...Option return f(key, opts...) } +var _ ResourceLimiterProvider = struct { + GetResourceLimiterFunc + GetCheckerFunc +}{} + // ResourceLimiter is an interface that an implementation makes // available to apply physical limits on quantities such as the number // of concurrent requests or amount of memory in use. @@ -45,8 +50,6 @@ func (f GetResourceLimiterFunc) GetResourceLimiter(key WeightKey, opts ...Option // // See the README for more recommendations. type ResourceLimiter interface { - Checker - // Acquire attempts to acquire a quantified resource with the // provided weight, based on the key that was given to the // provider. The caller has these options: @@ -83,8 +86,4 @@ func (f AcquireFunc) Acquire(ctx context.Context, value uint64) (ReleaseFunc, er return f(ctx, value) } -// Verify that a rate limiter is constructed of two functions. -var _ ResourceLimiter = struct { - MustDenyFunc - AcquireFunc -}{} +var _ ResourceLimiter = AcquireFunc(nil) diff --git a/extension/extensionlimiter/weight.go b/extension/extensionlimiter/weight.go index 16d5c8c4ec9..47a8d7a4828 100644 --- a/extension/extensionlimiter/weight.go +++ b/extension/extensionlimiter/weight.go @@ -3,7 +3,7 @@ package extensionlimiter // import "go.opentelemetry.io/collector/extension/extensionlimiter" -// WeightKey is an enum type for common rate limits. The +import "slices" // WeightKey is an enum type for common rate limits. The // StandardAllKeys, StandardMiddlewareKeys, and // StandardNotMiddlewareKeys methods return the list of middleware // keys that can be automatically configured through middleware and @@ -38,6 +38,13 @@ const ( WeightKeyMemorySize WeightKey = "memory_size" ) +// WeightSet are a group of weights to be tested. +type WeightSet []WeightKey + +func (ws WeightSet) Contains(w WeightKey) bool { + return slices.Contains(ws, w) +} + // StandardAllKeys is all the keys that can be automatically // implemented by middleware and/or limiterhelper. func StandardAllKeys() []WeightKey { diff --git a/receiver/otlpreceiver/otlp.go b/receiver/otlpreceiver/otlp.go index 4b2a52e565f..288be4c3e7f 100644 --- a/receiver/otlpreceiver/otlp.go +++ b/receiver/otlpreceiver/otlp.go @@ -107,7 +107,7 @@ func (r *otlpReceiver) startGRPCServer(host component.Host) error { if r.nextTraces != nil { var next consumer.Traces - _, next, err = limiterhelper.NewLimitedTraces(r.nextTraces, limitKeys, limiterProvider) + next, err = limiterhelper.NewLimitedTraces(r.nextTraces, limitKeys, limiterProvider) if err != nil { return err } @@ -116,7 +116,7 @@ func (r *otlpReceiver) startGRPCServer(host component.Host) error { if r.nextMetrics != nil { var next consumer.Metrics - _, next, err = limiterhelper.NewLimitedMetrics(r.nextMetrics, limitKeys, limiterProvider) + next, err = limiterhelper.NewLimitedMetrics(r.nextMetrics, limitKeys, limiterProvider) if err != nil { return err } @@ -125,7 +125,7 @@ func (r *otlpReceiver) startGRPCServer(host component.Host) error { if r.nextLogs != nil { var next consumer.Logs - _, next, err = limiterhelper.NewLimitedLogs(r.nextLogs, limitKeys, limiterProvider) + next, err = limiterhelper.NewLimitedLogs(r.nextLogs, limitKeys, limiterProvider) if err != nil { return err } @@ -134,7 +134,7 @@ func (r *otlpReceiver) startGRPCServer(host component.Host) error { if r.nextProfiles != nil { var next xconsumer.Profiles - _, next, err = limiterhelper.NewLimitedProfiles(r.nextProfiles, limitKeys, limiterProvider) + next, err = limiterhelper.NewLimitedProfiles(r.nextProfiles, limitKeys, limiterProvider) if err != nil { return err } @@ -172,7 +172,7 @@ func (r *otlpReceiver) startHTTPServer(ctx context.Context, host component.Host) httpMux := http.NewServeMux() if r.nextTraces != nil { - _, next, err := limiterhelper.NewLimitedTraces(r.nextTraces, limitKeys, limiterProvider) + next, err := limiterhelper.NewLimitedTraces(r.nextTraces, limitKeys, limiterProvider) if err != nil { return err } @@ -183,7 +183,7 @@ func (r *otlpReceiver) startHTTPServer(ctx context.Context, host component.Host) } if r.nextMetrics != nil { - _, next, err := limiterhelper.NewLimitedMetrics(r.nextMetrics, limitKeys, limiterProvider) + next, err := limiterhelper.NewLimitedMetrics(r.nextMetrics, limitKeys, limiterProvider) if err != nil { return err } @@ -194,7 +194,7 @@ func (r *otlpReceiver) startHTTPServer(ctx context.Context, host component.Host) } if r.nextLogs != nil { - _, next, err := limiterhelper.NewLimitedLogs(r.nextLogs, limitKeys, limiterProvider) + next, err := limiterhelper.NewLimitedLogs(r.nextLogs, limitKeys, limiterProvider) if err != nil { return err } @@ -205,7 +205,7 @@ func (r *otlpReceiver) startHTTPServer(ctx context.Context, host component.Host) } if r.nextProfiles != nil { - _, next, err := limiterhelper.NewLimitedProfiles(r.nextProfiles, limitKeys, limiterProvider) + next, err := limiterhelper.NewLimitedProfiles(r.nextProfiles, limitKeys, limiterProvider) if err != nil { return err } From 137be274e2c7f3108339e3f3393da8d666706e0c Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Tue, 6 May 2025 13:25:56 -0700 Subject: [PATCH 13/14] checker all keys --- extension/extensionlimiter/README.md | 57 +++++++++++-------- .../extensionlimiter/extensionlimiter.go | 13 ++--- .../extensionlimiter/limiterhelper/checker.go | 5 ++ .../limiterhelper/consumer.go | 3 +- .../limiterhelper/middleware.go | 6 +- extension/extensionlimiter/weight.go | 18 +++--- 6 files changed, 56 insertions(+), 46 deletions(-) diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index 51cd0efecc6..fbeb44a0f18 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -22,17 +22,17 @@ which simplifies consumers in most cases by providing a consistent `LimitCall` interface. A limiter is **saturated** by definition when a limit is completely -overloaded, generally it means a limit request of any size would fail -at that moment and should be taken as a strong signal to stop -accepting requests. +overloaded in at least one weight, generally it means callers should +immediately deny work to continue on the request. Each kind of limiter as well as the wrapper type have corresponding **provider** interfaces that return a limiter instance based on a -weight key or keys. +weight keys. Weight keys describe the standard limiting dimensions. There are currently four standard weight keys: network bytes, request count, -request items, and memory size. +request items, and memory size. Callers use the `Checker` interface +to check whether any weight keys (from a set) are saturated. ## Key Interfaces @@ -44,7 +44,7 @@ request items, and memory size. - `ResourceLimiter`: Manages physical resource limits, has an `Acquire` method and a corresponding `ReleaseFunc`, plus a provider type. -- `Limiter`: Any of the above, has a `MustDeny` method. +- `Checker`: Has a `MustDeny` method. ### Limiter helpers @@ -91,15 +91,17 @@ becomes available, they should return a standard overload signal. ### Limiter saturation -All limiters feature a `MustDeny` method which is made available for -applications to test when a limit is fully saturated. This special -limit request is defined as the equivalent of passing a zero value to -the limiter. +Rate and resource limiter providers have a `GetChecker` method to +provide a `Checker`, featuring a `MustDeny` method which is made +available for applications to test when any limit is fully +saturated that would eventually deny the request. -Limiters SHOULD treat a request for zero units of the limit as a -special case, used for indicating when non-zero limit requests are -likely to fail. This is not an exact requirement; implementations are -free to define their own saturation parameters. +The `Checker` is consulted at least once and applies to all weight +keys. Because a `Checker` can be consulted more than once by a +receiver and/or middleware, it is possible for requests to be denied +over the saturation of limits they were already granted. Users should +configure external load balancers and/or horizontal scaling policies +to avoid cases of limiter saturation. ### Limit before or after use @@ -187,10 +189,14 @@ apply request items and memory size: s.limiterProvider, err = limiterhelper.MiddlewaresToLimiterWrapperProvider( cfg.Middlewares) if err != nil { ... } + + // Extract a checker from the provider + s.checker, err = s.limiterProvider.GetChecker() + if err != nil { ... } // Here get a limiter-wrapped pipeline and a combination of weight-specific // limiters for MustDeny() functionality. - s.anyLimiter, s.nextMetrics, err = limiterhelper.NewLimitedMetrics( + s.nextMetrics, err = limiterhelper.NewLimitedMetrics( s.nextMetrics, limiterhelper.StandardNotMiddlewareKeys(), s.limiterProvider) if err != nil { ... } ``` @@ -199,7 +205,7 @@ In the scraper loop, use `MustDeny` before starting a scrape: ```golang func (s *scraper) scrapeOnce(ctx context.Context) error { - if err := s.anyLimiter.MustDeny(ctx); err != nil { + if err := s.checker.MustDeny(ctx); err != nil { return err } @@ -231,21 +237,22 @@ receivers: - ratelimiter/streamer ``` -The receiver will check `s.anyLimiter.MustDeny()` as above. In a -stream, limiters are expected to block the stream until limit requests -succeed, however after the limit requests succeed, the receiver may -wish to return from `Send()` to continue accepting new requests while -the consumer works in a separate goroutine. The limit will be released -after the consumer returns. +The receiver will create with `extensionlimiter.StandardAllKeys()` and +check `s.checker.MustDeny()` as above. In a stream, limiters are +expected to block the stream until limit requests succeed, however +after the limit requests succeed, the receiver may wish to return from +`Send()` to continue accepting new requests while the consumer works +in a separate goroutine. The limit will be released after the consumer +returns. ```golang func (s *scraper) LogsStream(ctx context.Context, stream *Stream) error { for { - // Check saturation for all limiters. - err := s.anyLimiter.MustDeny(ctx) + // Check saturation for all limiters, all keys. + err := s.checker.MustDeny(ctx) if err != nil { ... } - // The network bytes and request count are applied in middleware. + // The network bytes and request count limits are applied in middleware. req, err := stream.Recv() if err != nil { ... } diff --git a/extension/extensionlimiter/extensionlimiter.go b/extension/extensionlimiter/extensionlimiter.go index 2a8659c0b9e..04789e79dab 100644 --- a/extension/extensionlimiter/extensionlimiter.go +++ b/extension/extensionlimiter/extensionlimiter.go @@ -40,26 +40,21 @@ func (f MustDenyFunc) MustDeny(ctx context.Context) error { return f(ctx) } -// NeverDeny returns a Checker that never denies. -func NeverDeny() Checker { - return MustDenyFunc(nil) -} - // CheckerProvider is an interface to obtain checkers for a group of // weight keys. type CheckerProvider interface { // GetChecker returns a checker for a group of weight keys. - GetChecker(WeightSet, ...Option) (Checker, error) + GetChecker(...Option) (Checker, error) } // GetCheckerFunc is a functional way to construct GetChecker // functions, used in limiter providers. -type GetCheckerFunc func(WeightSet, ...Option) (Checker, error) +type GetCheckerFunc func(...Option) (Checker, error) // Checker implements CheckerProvider. -func (f GetCheckerFunc) GetChecker(keys WeightSet, opts ...Option) (Checker, error) { +func (f GetCheckerFunc) GetChecker(opts ...Option) (Checker, error) { if f == nil { return nil, nil } - return f(keys, opts...) + return f(opts...) } diff --git a/extension/extensionlimiter/limiterhelper/checker.go b/extension/extensionlimiter/limiterhelper/checker.go index 115c8603d52..fcad04b4c18 100644 --- a/extension/extensionlimiter/limiterhelper/checker.go +++ b/extension/extensionlimiter/limiterhelper/checker.go @@ -26,3 +26,8 @@ func (ls MultiChecker) MustDeny(ctx context.Context) error { } return err } + +// NeverDeny returns a Checker that never denies. +func NeverDeny() extensionlimiter.Checker { + return extensionlimiter.MustDenyFunc(nil) +} diff --git a/extension/extensionlimiter/limiterhelper/consumer.go b/extension/extensionlimiter/limiterhelper/consumer.go index 1db74c5fc16..c4eb3aa7d56 100644 --- a/extension/extensionlimiter/limiterhelper/consumer.go +++ b/extension/extensionlimiter/limiterhelper/consumer.go @@ -151,8 +151,7 @@ func applyChecker[P any, C any]( m traits[P, C], opts []consumer.Option, ) (C, error) { - // Apply the Checker. - ck, err := provider.GetChecker(keys) + ck, err := provider.GetChecker() if err != nil { return next, err } diff --git a/extension/extensionlimiter/limiterhelper/middleware.go b/extension/extensionlimiter/limiterhelper/middleware.go index fc284f47f83..aabcc175aa3 100644 --- a/extension/extensionlimiter/limiterhelper/middleware.go +++ b/extension/extensionlimiter/limiterhelper/middleware.go @@ -109,11 +109,11 @@ var _ LimiterWrapperProvider = MultiLimiterWrapperProvider{} // GetLimiterWrapper implements LimiterWrapperProvider, combining // checkers for all wrappers in a sequence. -func (ps MultiLimiterWrapperProvider) GetChecker(keys extensionlimiter.WeightSet, opts ...extensionlimiter.Option) (extensionlimiter.Checker, error) { +func (ps MultiLimiterWrapperProvider) GetChecker(opts ...extensionlimiter.Option) (extensionlimiter.Checker, error) { var retErr error var cks MultiChecker for _, provider := range ps { - ck, err := provider.GetChecker(keys, opts...) + ck, err := provider.GetChecker(opts...) retErr = errors.Join(retErr, err) if ck == nil { continue @@ -121,7 +121,7 @@ func (ps MultiLimiterWrapperProvider) GetChecker(keys extensionlimiter.WeightSet cks = append(cks, ck) } if len(cks) == 0 { - return extensionlimiter.NeverDeny(), retErr + return NeverDeny(), retErr } return cks, retErr } diff --git a/extension/extensionlimiter/weight.go b/extension/extensionlimiter/weight.go index 47a8d7a4828..28fcfcf0b9b 100644 --- a/extension/extensionlimiter/weight.go +++ b/extension/extensionlimiter/weight.go @@ -38,7 +38,11 @@ const ( WeightKeyMemorySize WeightKey = "memory_size" ) -// WeightSet are a group of weights to be tested. +// WeightSet are a group of weights to be tested. The purpose of this +// type is to be explicit about a group of weights that have to be +// checked at a certain stage. The receiver and middleware can both +// be responsible for applying limits, and this type helps ensure +// limits are applied only across cooperating sub-components. type WeightSet []WeightKey func (ws WeightSet) Contains(w WeightKey) bool { @@ -47,8 +51,8 @@ func (ws WeightSet) Contains(w WeightKey) bool { // StandardAllKeys is all the keys that can be automatically // implemented by middleware and/or limiterhelper. -func StandardAllKeys() []WeightKey { - return []WeightKey{ +func StandardAllKeys() WeightSet { + return WeightSet{ WeightKeyNetworkBytes, WeightKeyRequestCount, WeightKeyRequestItems, @@ -60,8 +64,8 @@ func StandardAllKeys() []WeightKey { // protocols that support it. Receivers should be careful not to // re-apply these limits, especially not to twice-limit by // WeightKeyRequestItems. -func StandardMiddlewareKeys() []WeightKey { - return []WeightKey{ +func StandardMiddlewareKeys() WeightSet { + return WeightSet{ WeightKeyNetworkBytes, WeightKeyRequestCount, } @@ -70,8 +74,8 @@ func StandardMiddlewareKeys() []WeightKey { // StandardNotMiddlewareKeys are the keys that are typically not // handled through middlware because they are protocol specific and // generally easier to handle after the input has become pdata. -func StandardNotMiddlewareKeys() []WeightKey { - return []WeightKey{ +func StandardNotMiddlewareKeys() WeightSet { + return WeightSet{ WeightKeyRequestItems, WeightKeyMemorySize, } From 4a44264c5772673cc1a17b689d531237ebf68b53 Mon Sep 17 00:00:00 2001 From: Joshua MacDonald Date: Tue, 6 May 2025 15:13:10 -0700 Subject: [PATCH 14/14] readme --- extension/extensionlimiter/README.md | 102 +++++++++++++++++++-------- 1 file changed, 71 insertions(+), 31 deletions(-) diff --git a/extension/extensionlimiter/README.md b/extension/extensionlimiter/README.md index fbeb44a0f18..4656ebece4f 100644 --- a/extension/extensionlimiter/README.md +++ b/extension/extensionlimiter/README.md @@ -237,13 +237,12 @@ receivers: - ratelimiter/streamer ``` -The receiver will create with `extensionlimiter.StandardAllKeys()` and -check `s.checker.MustDeny()` as above. In a stream, limiters are -expected to block the stream until limit requests succeed, however -after the limit requests succeed, the receiver may wish to return from -`Send()` to continue accepting new requests while the consumer works -in a separate goroutine. The limit will be released after the consumer -returns. +The receiver will check `s.checker.MustDeny()` as above. In a stream, +limiters are expected to block the stream until limit requests +succeed, however after the limit requests succeed, the receiver may +wish to return from `Send()` to continue accepting new requests while +the consumer works in a separate goroutine. The limit will be released +after the consumer returns. ```golang func (s *scraper) LogsStream(ctx context.Context, stream *Stream) error { @@ -277,36 +276,77 @@ func (s *scraper) LogsStream(ctx context.Context, stream *Stream) error { } ``` -#### Data-dependent limiter processor +#### Open questions + +##### Middleware implementation details + +Details are +important. [#12700](https://github.com/open-telemetry/opentelemetry-collector/pull/12700) +contained a `limitermiddleware` implementation which was a middleware +that called a limiter for HTTP and gRPC. Roughly the same code will be +used, and the details will come out. + +##### Provider options An `Option` type has been added as a placeholder in the provider -interfaces to support adding this feature. **NOTE: This is not -implemented.** +interfaces. **NOTE: No options are implemented.** Potential options: -The provider interfaces can be extended to accept a -`map[string]string` that identify limiter instances based on -additional metadata, such as tenant information. Since the limits are -data specific, the limiter will be computed for each request and for -each specific weight key. +- The protocol name +- The signal kind +- The caller's component ID -Limiter implementations would support options, likely assisted by -`limiterhelper` features to configure them, for configuring -metadata-specific limits. +Because the set of each of these is small, it is possible to +pre-compute limiter instances for the cross product of configurations. -```golang -func handleRequest(ctx context.Context, req *Request) error { - // Get a data-specific limiter: - md := metadataFromRequest(req) - lim, err := s.limiterProvider.LimiterWrapper(weightKey, md) - if err != nil { ... } +##### Context-dependent limits + +Client metadata (i.e., headers) may be used in the context to make +limiter decisions. These details are automatically extracted from the +Context passed to `MustDeny`, `Limit`, `Acquire`, and `LimitCall` +functions. No examples are provided. How will limiters configure, for +example, tenant-specific limits? - if err = lim.MustDeny(ctx); err != nil { ... } +##### Data-dependent limits - // Calculate the data and its weight. - data := dataFromReq(req) - weight := getWeight(data) +When a single unit of data contains limits that are assignable to +multiple distinct limiters, one option available to users is to split +requests and add to their context and run them concurrently through +context-dependent limiters. See +[#39199](https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/39199). - return lim.LimitCall(ctx, weight, func(ctx context.Context) error { - return s.nextLogs.ConsumeLogs(ctx, data) - }) +Another option is to add support for non-blocking limit requests. For +example, to apply limits using information derived from the +OpenTelemetry resource, we might do something like this pseudo-code: + +``` +func (p *processor) limitLogs(ctx context.Context, logsData plog.Logs) (plog.Logs, extensionlimiter.ReleaseFunc, error) { + var rels extensionlimiter.ReleaseFuncs + logsData.ResourceLogs().RemoveIf(func(rl plog.ResourceLogs) bool { + md := resourceToMetadata(rl.Resource()) + rel, err := p.nonBlockingLimiter.TryLimitOrAcquire(withMetadata(ctx, md)) + if err != nil { + return false + } + rels = append(rels, rel) + return true + }) + if logsData.ResourceLogs().Len() == 0 { + return logsData, func() {}, processorhelper.ErrSkipProcessingData + } + return logsData, rels.Release, nil +} + +func (p *processor) ConsumeLogs(ctx context.Context, logsData plog.Logs) error { + logsData, release, err = limitLogs(ctx, logsData) + if err != nil { + return err + } + defer release() + return p.nextLogs.ConsumeLogs(ctx, logsData) +} ``` + +Here, the release a new `TryLimitOrAcquire` function abstracts the +form of a non-blocking call to either form of limiter. If the +underyling limiter is a rate limiter, the release function will be a +no-op.