Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
aaaaf85
feat(logging): add rate_limiting config options (slog-sampling variant)
dehaansa Jul 29, 2026
0223df0
feat(logging): slog-sampling dependency, matcher, metric, buildRoot
dehaansa Jul 29, 2026
519ea7f
feat(logging): samplingInjector with versioned replay and empty-messa…
dehaansa Jul 29, 2026
50cbc4d
feat(logging): wire slog-sampling rate limiter into Logger (live-reco…
dehaansa Jul 29, 2026
9e8f147
docs(logging): document rate_limiting block (slog-sampling variant)
dehaansa Jul 29, 2026
19bc417
Minor changes
dehaansa Jul 29, 2026
00eee0d
fix(logging): key rate limiter on component_id+path; validate before …
dehaansa Jul 30, 2026
0aebe02
fix(logging): rebuild sampler only on rate_limiting change; guard Upd…
dehaansa Jul 30, 2026
955bc0d
test(logging): add rate limiter benchmarks (slog-sampling variant)
dehaansa Jul 30, 2026
4354927
perf(logging): route Enabled to bare terminal and cache component con…
dehaansa Jul 30, 2026
f42803e
docs(logging): simplify code comments to ASD-STE100
dehaansa Aug 3, 2026
fd655d3
fix(logging): address review — drop rlVersion, rename InitRateLimitMe…
dehaansa Aug 3, 2026
ac70e41
docs(logging): fix stale rlVersion reference in comment
dehaansa Aug 3, 2026
ed65cdd
docs(logging): fix rate_limiting arg table alphabetical order (thresh…
dehaansa Aug 3, 2026
f2ade66
docs(logging): trim rlHolder comment per review
dehaansa Aug 3, 2026
82bef69
refactor(logging): simplify rate limiter — merge handler-tag type, va…
dehaansa Aug 3, 2026
30ad419
refactor(logging): revert rlApplied to nilable pointer per review
dehaansa Aug 3, 2026
d9b3c8f
feat(logging): default rate_limiting tick to 10s to catch sustained r…
dehaansa Aug 3, 2026
0a79b30
fix(logging): live-publish suppressed metric, key controller_path, re…
dehaansa Aug 3, 2026
5b4e653
PR feedback
dehaansa Aug 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions collector/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bitfield/gotestdox v0.2.2 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/bluele/gcache v0.0.2 // indirect
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
Comment on lines 352 to 356
github.com/bodgit/plumbing v1.3.0 // indirect
github.com/bodgit/sevenzip v1.6.1 // indirect
Expand Down Expand Up @@ -386,6 +387,7 @@ require (
github.com/coreos/go-oidc/v3 v3.18.0 // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.7.0 // indirect
github.com/cornelk/hashmap v1.0.8 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/danieljoos/wincred v1.2.3 // indirect
github.com/databricks/databricks-sql-go v1.11.0 // indirect
Expand Down Expand Up @@ -845,6 +847,9 @@ require (
github.com/safchain/ethtool v0.7.0 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/samber/lo v1.53.0 // indirect
github.com/samber/slog-common v0.21.0 // indirect
github.com/samber/slog-multi v1.8.0 // indirect
github.com/samber/slog-sampling v1.6.0 // indirect
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 // indirect
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
Expand Down
10 changes: 10 additions & 0 deletions collector/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,8 @@ github.com/bitfield/gotestdox v0.2.2 h1:x6RcPAbBbErKLnapz1QeAlf3ospg8efBsedU93CD
github.com/bitfield/gotestdox v0.2.2/go.mod h1:D+gwtS0urjBrzguAkTM2wodsTQYFHdpx8eqRJ3N+9pY=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0=
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
Expand Down Expand Up @@ -737,6 +739,8 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7
github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cornelk/hashmap v1.0.8 h1:nv0AWgw02n+iDcawr5It4CjQIAcdMMKRrs10HOJYlrc=
github.com/cornelk/hashmap v1.0.8/go.mod h1:RfZb7JO3RviW/rT6emczVuC/oxpdz4UsSB2LJSclR1k=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
Expand Down Expand Up @@ -2237,6 +2241,12 @@ github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDc
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/samber/slog-common v0.21.0 h1:Wo2hTly1Br5RjYqX/BTWJJeDnTE85oWk/7vqlpZuAUc=
github.com/samber/slog-common v0.21.0/go.mod h1:d/6OaSlzdkl9PFpfRLgn8FwY1OW6EFmPtBpsHX4MrU0=
github.com/samber/slog-multi v1.8.0 h1:E05c1wnQ+8M58oQDBABlJ4TEIJWssNgtckso3zlaLlI=
github.com/samber/slog-multi v1.8.0/go.mod h1:6+3j/ILxDvAcLD75YdQAm6iKWu6AmwlohLgQxL/2aiI=
github.com/samber/slog-sampling v1.6.0 h1:ODK16Wse1139eo25P+APfYKQqLYE79LMnnTBcUe+OCA=
github.com/samber/slog-sampling v1.6.0/go.mod h1:2vMB0an9YwqxtBdzmcBOpO5ZoL6LI74ghC3jC4sJuk4=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da h1:p3Vo3i64TCLY7gIfzeQaUJ+kppEO5WQG3cL8iE8tGHU=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
Expand Down
35 changes: 35 additions & 0 deletions docs/sources/reference/config-blocks/logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,41 @@ Otherwise, `destination` defaults to `"stderr"`.

{{< param "PRODUCT_NAME" >}} fails to start if `destination` is set to `"windows_event_log"` and {{< param "PRODUCT_NAME" >}} is not running on Windows.

## Blocks

You can use the following blocks with `logging`:

| Block | Description | Required |
| ----------------- | ---------------------------------------------- | -------- |
| [`rate_limiting`][rate_limiting] | Configure per-message rate limiting and sampling. | no |

### `rate_limiting`

The `rate_limiting` block enables per-message rate limiting and sampling of repeated log lines.

| Name | Type | Description | Default | Required |
|------|------|-------------|---------|----------|
| `enabled` | `bool` | Enable per-message rate limiting. | `true` | no |
| `max_signatures` | `number` | Distinct signatures tracked; least recently used is evicted when full. | `1000` | no |
| `rate` | `number` | Fraction (0-1) of the over-threshold tail still admitted; `0` drops all excess. | `0` | no |
| `threshold` | `number` | Identical lines admitted per (component, level, message) per tick before sampling. | `10` | no |
| `tick` | `duration` | Sampling window. | `"10s"` | no |

Rate limiting keys on the component, the log level, and the log message text (not attributes/fields).
Only identical repeated lines from the same component at the same level are throttled; distinct components/messages are independent (LRU-bounded by `max_signatures`).

Log lines that share a constant message but differ only in attributes are treated as the same signature and throttled together.

Log lines with an empty message, such as some `go-kit`-style logs emitted without a `msg` or `message` field, bypass rate limiting entirely and are always written.

After suppression begins, the first admitted line of each new window carries a `slog_sampling.dropped_count` attribute.

Dropped lines are counted by the `alloy_logging_suppressed_lines_total` metric (labeled by `level` and `component_id`).

Set `enabled = false` to disable. Omitting the `rate_limiting` block leaves limiting enabled with defaults.

[rate_limiting]: #rate_limiting

## Retrieve logs

You can retrieve the logs in different ways depending on your platform and installation method:
Expand Down
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -969,6 +969,7 @@ require (
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/translator/splunk v0.153.0
github.com/open-telemetry/opentelemetry-collector-contrib/processor/redactionprocessor v0.153.0
github.com/open-telemetry/opentelemetry-collector-contrib/receiver/nginxreceiver v0.153.0
github.com/samber/slog-sampling v1.6.0
github.com/spf13/viper v1.21.0
github.com/vektah/gqlparser/v2 v2.5.36
github.com/zricethezav/gitleaks/v8 v8.30.1
Expand Down Expand Up @@ -1027,6 +1028,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/rds v1.118.2 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.2.2 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/bluele/gcache v0.0.2 // indirect
github.com/bodgit/plumbing v1.3.0 // indirect
github.com/bodgit/sevenzip v1.6.1 // indirect
github.com/bodgit/windows v1.0.1 // indirect
Expand All @@ -1045,6 +1047,7 @@ require (
github.com/coder/websocket v1.8.14 // indirect
github.com/containerd/containerd/api v1.9.0 // indirect
github.com/containerd/typeurl/v2 v2.2.3 // indirect
github.com/cornelk/hashmap v1.0.8 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/fatih/semgroup v1.2.0 // indirect
Expand Down Expand Up @@ -1079,6 +1082,8 @@ require (
github.com/puzpuzpuz/xsync/v4 v4.5.0 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/samber/slog-common v0.21.0 // indirect
github.com/samber/slog-multi v1.8.0 // indirect
github.com/sijms/go-ora/v2 v2.9.0 // indirect
github.com/sorairolake/lzip-go v0.3.8 // indirect
github.com/sosodev/duration v1.4.0 // indirect
Expand Down
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,8 @@ github.com/bitfield/gotestdox v0.2.2 h1:x6RcPAbBbErKLnapz1QeAlf3ospg8efBsedU93CD
github.com/bitfield/gotestdox v0.2.2/go.mod h1:D+gwtS0urjBrzguAkTM2wodsTQYFHdpx8eqRJ3N+9pY=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0=
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
Expand Down Expand Up @@ -775,6 +777,8 @@ github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cornelk/hashmap v1.0.8 h1:nv0AWgw02n+iDcawr5It4CjQIAcdMMKRrs10HOJYlrc=
github.com/cornelk/hashmap v1.0.8/go.mod h1:RfZb7JO3RviW/rT6emczVuC/oxpdz4UsSB2LJSclR1k=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
Expand Down Expand Up @@ -2259,6 +2263,12 @@ github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDc
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/samber/slog-common v0.21.0 h1:Wo2hTly1Br5RjYqX/BTWJJeDnTE85oWk/7vqlpZuAUc=
github.com/samber/slog-common v0.21.0/go.mod h1:d/6OaSlzdkl9PFpfRLgn8FwY1OW6EFmPtBpsHX4MrU0=
github.com/samber/slog-multi v1.8.0 h1:E05c1wnQ+8M58oQDBABlJ4TEIJWssNgtckso3zlaLlI=
github.com/samber/slog-multi v1.8.0/go.mod h1:6+3j/ILxDvAcLD75YdQAm6iKWu6AmwlohLgQxL/2aiI=
github.com/samber/slog-sampling v1.6.0 h1:ODK16Wse1139eo25P+APfYKQqLYE79LMnnTBcUe+OCA=
github.com/samber/slog-sampling v1.6.0/go.mod h1:2vMB0an9YwqxtBdzmcBOpO5ZoL6LI74ghC3jC4sJuk4=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da h1:p3Vo3i64TCLY7gIfzeQaUJ+kppEO5WQG3cL8iE8tGHU=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
Expand Down
6 changes: 3 additions & 3 deletions internal/runtime/internal/controller/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ import (
"github.com/grafana/alloy/internal/runtime/internal/worker"
"github.com/grafana/alloy/internal/runtime/tracing"
"github.com/grafana/alloy/internal/service"
"github.com/grafana/alloy/internal/util"
astutil "github.com/grafana/alloy/internal/util/ast"
"github.com/grafana/alloy/internal/util/metricsutil"
"github.com/grafana/alloy/syntax/ast"
"github.com/grafana/alloy/syntax/diag"
"github.com/grafana/alloy/syntax/vm"
Expand Down Expand Up @@ -120,12 +120,12 @@ func NewLoader(opts LoaderOptions) (*Loader, error) {
// These metrics already being registered indicates there's already a loader which exists for this controller.
// Creating duplicate loaders should only happen in error states where we should not proceed further. One know
// case of this is when remotecfg loads an invalid config and attempts to reload the prior config.
existing := util.MustRegisterOrReturnExisting(globals.Registerer, l.cc)
existing := metricsutil.MustRegisterOrReturnExisting(globals.Registerer, l.cc)
if existing != nil {
return nil, fmt.Errorf("a loader exists already exists for %q", globals.ControllerID)
}

existing = util.MustRegisterOrReturnExisting(globals.Registerer, l.cm)
existing = metricsutil.MustRegisterOrReturnExisting(globals.Registerer, l.cm)
if existing != nil {
return nil, fmt.Errorf("a loader exists already exists for %q", globals.ControllerID)
}
Expand Down
2 changes: 2 additions & 0 deletions internal/runtime/internal/controller/node_config_logging.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type LoggingConfigNode struct {
// NewLoggingConfigNode creates a new LoggingConfigNode from an initial ast.BlockStmt.
// The underlying config isn't applied until Evaluate is called.
func NewLoggingConfigNode(block *ast.BlockStmt, globals ComponentGlobals) *LoggingConfigNode {
globals.Logger.InitRateLimitMetrics(globals.Registerer)
return &LoggingConfigNode{
nodeID: BlockComponentID(block).String(),
componentName: block.GetBlockName(),
Expand All @@ -38,6 +39,7 @@ func NewLoggingConfigNode(block *ast.BlockStmt, globals ComponentGlobals) *Loggi
// NewDefaultLoggingConfigNode creates a new LoggingConfigNode with nil block and eval.
// This will force evaluate to use the default logging options for this node.
func NewDefaultLoggingConfigNode(globals ComponentGlobals) *LoggingConfigNode {
globals.Logger.InitRateLimitMetrics(globals.Registerer)
return &LoggingConfigNode{
nodeID: loggingBlockID,
componentName: loggingBlockID,
Expand Down
9 changes: 7 additions & 2 deletions internal/runtime/logging/deferred_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,14 @@ func (d *deferredSlogHandler) buildHandlers(parent slog.Handler) {
d.mut.Lock()
defer d.mut.Unlock()

// Root node will not have attrs or groups.
// The root node has no attrs or groups. Route it through the Logger's
// persistent rootInjector, so the shared root handler (l.rlHolder),
// which may be rate-limited, sits between component loggers and the
// terminal handler. Reusing rootInjector, instead of building a new
// samplingInjector on every Update, avoids an allocation on config
// reloads that do not touch rate limiting.
if parent == nil {
d.handle = d.l.handler
d.handle = d.l.rootInjector
} else {
if d.group != "" {
d.handle = parent.WithGroup(d.group)
Expand Down
74 changes: 74 additions & 0 deletions internal/runtime/logging/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import (
"sync"
"time"

"go.uber.org/atomic"

"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"

"github.com/grafana/alloy/internal/component/common/loki"
Expand Down Expand Up @@ -36,6 +39,42 @@ type Logger struct {
// the optional Windows Event Log.
handler *handler
deferredSlog *deferredSlogHandler // Buffers slog output until config is loaded, then delegates to handler.

// rootInjector is the samplingInjector at the root of the deferred
// handler tree. buildHandlers reuses this single instance on every
// Update, instead of building a fresh one, because the root injector
// needs only rlHolder and handler, both stable for the life of the
// Logger. Config changes reach it through rlHolder's version, not
// through rebuilding the injector itself.
rootInjector *samplingInjector

// rlHolder holds the current root handler used by the samplingInjector
// in the deferred handler tree. This handler may be wrapped for
// rate-limit sampling. It starts as the bare terminal handler (rate
// limiting off), and Update swaps it atomically. The stored version
// increases on each swap, so samplingInjector instances know to rebuild
// their cached, per-component replay of the root handler.
rlHolder atomic.Pointer[versionedHandler]
// rlMetrics holds the suppressed-lines metric, or nil before
// InitRateLimitMetrics runs. buildRoot's OnDropped closure reads this
// pointer live on every drop, so it is an atomic.Pointer rather than a
// plain field: InitRateLimitMetrics can set it at any time, even after
// an earlier Update already built the root handler, and drops that
// follow will still be counted.
rlMetrics atomic.Pointer[rateLimitMetrics]

// rlMut guards the rate-limiting block in Update (the rlApplied check,
// buildRoot call, version increase, and rlHolder store, which must
// happen as one unit) and InitRateLimitMetrics's one-time set of
// rlMetrics. Reading rlMetrics is lock-free (atomic); rlMut is not
// needed for that.
rlMut sync.Mutex
// rlApplied is the RateLimitingOptions last used to build the current
// rlHolder root. It is nil until the first Update runs. Update rebuilds
// the sampler, and bumps the stored version, only when rlApplied is nil
// or the new options differ from it. This way, a config reload that does
// not change rate limiting does not reset rate-limit budgets already in use.
rlApplied *RateLimitingOptions
}

var _ EnabledAware = (*Logger)(nil)
Expand Down Expand Up @@ -94,6 +133,11 @@ func NewDeferred(w io.Writer) (*Logger, error) {
writer: writer,
handler: bh,
}
// Rate limiting starts disabled: the injector's root is the bare
// terminal handler, so logging works as it did before rate limiting
// existed, until the first config Update enables it.
l.rlHolder.Store(&versionedHandler{version: 0, h: bh})
l.rootInjector = newSamplingInjector(&l.rlHolder, l.handler)
l.deferredSlog = newDeferredHandler(l)

return l, nil
Expand All @@ -115,6 +159,14 @@ func (l *Logger) Update(o Options) error {
return fmt.Errorf("unrecognized log format %q", o.Format)
}

rlOpts := defaultRateLimitingOptions()
if o.RateLimiting != nil {
rlOpts = *o.RateLimiting
}
if err := rlOpts.Validate(); err != nil {
return err
}

l.bufferMut.Lock()
l.level.Set(slogLevel(o.Level).Level())
l.format.Set(o.Format)
Expand All @@ -125,6 +177,16 @@ func (l *Logger) Update(o Options) error {
l.writer.SetLokiWriter(o.WriteTo)
l.bufferMut.Unlock()

l.rlMut.Lock()
if l.rlApplied == nil || *l.rlApplied != rlOpts {
root := buildRoot(rlOpts, l.handler, &l.rlMetrics)
next := l.rlHolder.Load().version + 1
l.rlHolder.Store(&versionedHandler{version: next, h: root})
applied := rlOpts
l.rlApplied = &applied
}
l.rlMut.Unlock()

// Rebuild deferred slog handlers outside bufferMut to avoid a deadlock
// with concurrent Handle() calls (they hold a child handler's RLock
// while waiting for bufferMut via addRecord).
Expand Down Expand Up @@ -157,6 +219,18 @@ func (l *Logger) flushBuffer() {
}
}

// InitRateLimitMetrics sets up the suppressed-lines metric. Call this once.
// It takes effect right away, including for a logger that already has an
// Update call behind it: buildRoot's OnDropped closure reads rlMetrics live,
// so InitRateLimitMetrics does not need to run before the first Update.
func (l *Logger) InitRateLimitMetrics(reg prometheus.Registerer) {
l.rlMut.Lock()
defer l.rlMut.Unlock()
if l.rlMetrics.Load() == nil {
l.rlMetrics.Store(newRateLimitMetrics(reg))
}
}

func (l *Logger) SetTemporaryWriter(w io.Writer) {
l.writer.SetTemporaryWriter(w)
}
Expand Down
Loading
Loading