diff --git a/internal/component/loki/secretfilter/secretfilter.go b/internal/component/loki/secretfilter/secretfilter.go index a0427de23ff..98fb586566f 100644 --- a/internal/component/loki/secretfilter/secretfilter.go +++ b/internal/component/loki/secretfilter/secretfilter.go @@ -10,9 +10,11 @@ import ( "sync" "time" + "github.com/go-kit/log" "github.com/grafana/alloy/internal/component" "github.com/grafana/alloy/internal/component/common/loki" "github.com/grafana/alloy/internal/featuregate" + "github.com/grafana/alloy/internal/runtime/logging/level" "github.com/grafana/alloy/internal/sampling" "github.com/grafana/alloy/internal/service/livedebugging" "github.com/grafana/alloy/internal/util" @@ -90,6 +92,7 @@ var ( // Component implements the loki.secretfilter component. type Component struct { opts component.Options + log log.Logger mut sync.RWMutex args Arguments @@ -257,6 +260,7 @@ func New(o component.Options, args Arguments) (*Component, error) { c := &Component{ opts: o, + log: o.Logger, receiver: loki.NewLogsReceiver(loki.WithComponentID(o.ID)), detector: detector, metrics: newMetrics(o.Registerer, args.OriginLabel), @@ -268,6 +272,17 @@ func New(o component.Options, args Arguments) (*Component, error) { return nil, err } + level.Debug(c.log).Log( + "msg", "loki.secretfilter initialized", + "origin_label", args.OriginLabel, + "redact_with", args.RedactWith, + "redact_percent", c.redactPercent, + "gitleaks_config", args.GitleaksConfig, + "rate", args.Rate, + "processing_timeout", args.ProcessingTimeout, + "drop_on_timeout", args.DropOnTimeout, + ) + // Immediately export the receiver which remains the same for the component // lifetime. o.OnStateChange(Exports{Receiver: c.receiver}) @@ -288,8 +303,10 @@ func (c *Component) Run(ctx context.Context) error { var newEntry loki.Entry if c.shouldProcessEntry() { - newEntry, dropped := c.processEntry(ctx, entry) + var dropped bool + newEntry, dropped = c.processEntry(ctx, entry) if dropped { + level.Debug(c.log).Log("msg", "entry dropped", "reason", "processing_timeout") c.mut.RUnlock() continue } @@ -304,6 +321,7 @@ func (c *Component) Run(ctx context.Context) error { } else { newEntry = entry c.metrics.entriesBypassedTotal.Inc() + level.Debug(c.log).Log("msg", "entry bypassed by sampling", "rate", c.args.Rate) } for _, f := range c.fanout { @@ -349,6 +367,7 @@ func (c *Component) processEntry(ctx context.Context, entry loki.Entry) (loki.En if ctx.Err() != nil { c.metrics.linesTimedOutTotal.Inc() + level.Debug(c.log).Log("msg", "processing timeout exceeded", "drop_on_timeout", c.args.DropOnTimeout, "partial_findings", len(findings)) if c.args.DropOnTimeout { c.metrics.linesDroppedTotal.Inc() return loki.Entry{}, true @@ -364,6 +383,7 @@ func (c *Component) processEntry(ctx context.Context, entry loki.Entry) (loki.En if len(findings) == 0 { return entry, false } + level.Debug(c.log).Log("msg", "secrets detected in line", "findings", len(findings)) return c.redactLine(entry, findings), false } @@ -438,6 +458,17 @@ func (c *Component) Update(args component.Arguments) error { } c.metrics = newMetrics(c.opts.Registerer, newArgs.OriginLabel) + level.Debug(c.log).Log( + "msg", "loki.secretfilter config updated", + "origin_label", newArgs.OriginLabel, + "redact_with", newArgs.RedactWith, + "redact_percent", c.redactPercent, + "gitleaks_config", newArgs.GitleaksConfig, + "rate", newArgs.Rate, + "processing_timeout", newArgs.ProcessingTimeout, + "drop_on_timeout", newArgs.DropOnTimeout, + ) + return nil } diff --git a/internal/component/loki/secretfilter/secretfilter_test.go b/internal/component/loki/secretfilter/secretfilter_test.go index 6f53612ca62..60339a8a8a0 100644 --- a/internal/component/loki/secretfilter/secretfilter_test.go +++ b/internal/component/loki/secretfilter/secretfilter_test.go @@ -102,6 +102,46 @@ func TestRate_ZeroBypassesAll(t *testing.T) { require.Equal(t, float64(1), testutil.ToFloat64(c.metrics.entriesBypassedTotal)) } +// TestRate_OneForwardsProcessedEntry verifies that when rate=1 (all entries processed), the +// entry forwarded to downstream is the processed (redacted) entry, not an empty or zero value. +// This guards against bugs where the Run loop assigns to a shadowed variable and forwards +// the wrong value. +func TestRate_OneForwardsProcessedEntry(t *testing.T) { + registry := prometheus.NewRegistry() + downstream := loki.NewLogsReceiver() + args := Arguments{ + ForwardTo: []loki.LogsReceiver{downstream}, + Rate: 1, + RedactPercent: 100, + } + opts := component.Options{ + Logger: util.TestLogger(t), + OnStateChange: func(e component.Exports) {}, + GetServiceData: testhelper.GetServiceData, + Registerer: registry, + } + c, err := New(opts, args) + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go func() { _ = c.Run(ctx) }() + + secret := testhelper.FakeSecrets["grafana-api-key"].Value + lineWithSecret := "log with secret " + secret + " end" + entry := loki.Entry{ + Labels: model.LabelSet{}, + Entry: push.Entry{Timestamp: time.Now(), Line: lineWithSecret}, + } + c.receiver.Chan() <- entry + received := <-downstream.Chan() + + require.NotEmpty(t, received.Line, "processed entry must not be empty when forwarded") + require.NotContains(t, received.Line, secret, "forwarded entry must contain redacted content, not the raw secret") + require.Contains(t, received.Line, "REDACTED", "forwarded entry should contain redaction placeholder") + require.Equal(t, float64(0), testutil.ToFloat64(c.metrics.entriesBypassedTotal), "no entries should be bypassed when rate=1") +} + // TestRate_Half approximates that with rate=0.5 about half of entries are processed and half bypassed. func TestRate_Half(t *testing.T) { registry := prometheus.NewRegistry()