Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 32 additions & 1 deletion internal/component/loki/secretfilter/secretfilter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand All @@ -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})
Expand All @@ -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
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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
}

Expand Down Expand Up @@ -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
}

Expand Down
40 changes: 40 additions & 0 deletions internal/component/loki/secretfilter/secretfilter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading