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
31 changes: 20 additions & 11 deletions docs/sources/reference/components/loki/loki.secretfilter.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,13 @@ You can use the following arguments with `loki.secretfilter`:
| Name | Type | Description | Default | Required |
| -------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------- | ------- | -------- |
| `forward_to` | `list(LogsReceiver)` | List of receivers to send log entries to. | | yes |
| `drop_on_timeout` | `bool` | When true, drop entries that exceed `processing_timeout` instead of forwarding them unredacted. | `false` | no |
| `gitleaks_config` | `string` | Path to a custom Gitleaks TOML config file. If empty, the default Gitleaks config is used. | `""` | no |
| `origin_label` | `string` | Loki label to use for the `secrets_redacted_by_origin` metric. If empty, that metric is not registered. | `""` | no |
| `processing_timeout` | `duration` | Maximum time allowed to process a single log entry. `0` disables the timeout. | `0` | no |
| `redact_percent` | `uint` | When `redact_with` is not set: percent of the secret to redact (1–100), where 100 is full redaction. | `80` | no |
| `rate` | `float` | Entry sampling rate in `[0.0, 1.0]` where `1` processes all entries. Unsampled entries are forwarded unchanged. | `1.0` | no |
| `redact_with` | `string` | Template for the redaction placeholder. Use `$SECRET_NAME` and `$SECRET_HASH`, for example, `"<$SECRET_NAME:$SECRET_HASH>"` | `""` | no |
| `redact_percent` | `uint` | When `redact_with` is not set: percent of the secret to redact (1–100), where 100 is full redaction. | `80` | no |
| `drop_on_timeout` | `bool` | When true, drop entries that exceed `processing_timeout` instead of forwarding them unredacted. | `false` | no |
| `processing_timeout` | `duration` | Maximum time allowed to process a single log entry. `0` disables the timeout. | `0` | no |

The `gitleaks_config` argument is the path to a custom [Gitleaks TOML config file][gitleaks-config].
The file supports the standard Gitleaks structure (rules, allowlists, and `[extend]` to extend the default config).
Expand All @@ -75,6 +76,11 @@ For consistent behavior, use an external configuration file via `gitleaks_config
`100` replaces the entire secret with `"REDACTED"`.
When `redact_percent` is 0 or unset, 80% redaction is used.

**Sampling:** The `rate` argument controls what fraction of log entries are processed by the secret filter.
Entries that {{< param "PRODUCT_NAME" >}} does not select based on the sampling rate pass through unchanged, with no detection or redaction applied.
Use a value below `1.0`, for example, `0.1` for 10%, to reduce CPU usage when processing high-volume logs.
Monitor `loki_secretfilter_entries_bypassed_total` to observe how many entries were skipped.

**Origin metric:** The `origin_label` argument specifies which Loki label to use for the `secrets_redacted_by_origin` metric, so you can track how many secrets were redacted per source or environment.

**Processing timeout:** The `processing_timeout` argument sets a maximum duration for processing each log entry.
Expand Down Expand Up @@ -110,14 +116,16 @@ The following fields are exported and can be referenced by other components:

`loki.secretfilter` exposes the following Prometheus metrics:

| Name | Type | Description |
| -------------------------------------------------- | ------- | --------------------------------------------------------------------------------------------------- |
| `loki_secretfilter_lines_dropped_total` | Counter | Total number of log lines dropped due to processing timeout, when `drop_on_timeout` is `true`. |
| `loki_secretfilter_lines_timed_out_total` | Counter | Total number of log lines that exceeded the processing timeout, whether dropped or forwarded. |
| `loki_secretfilter_processing_duration_seconds` | Summary | Time taken to process and redact logs, in seconds. |
| `loki_secretfilter_secrets_redacted_total` | Counter | Total number of secrets redacted. |
| `loki_secretfilter_secrets_redacted_by_rule_total` | Counter | Number of secrets redacted, partitioned by rule name. |
| `loki_secretfilter_secrets_redacted_by_origin` | Counter | Number of secrets redacted, partitioned by origin label, when `origin_label` is set. |
| Name | Type | Description |
| -------------------------------------------------- | ------- | ---------------------------------------------------------------------------------------------- |
| `loki_secretfilter_entries_bypassed_total` | Counter | Total number of entries forwarded without processing due to sampling. |
| `loki_secretfilter_lines_dropped_total` | Counter | Total number of log lines dropped due to processing timeout, when `drop_on_timeout` is `true`. |
| `loki_secretfilter_lines_timed_out_total` | Counter | Total number of log lines that exceeded the processing timeout, whether dropped or forwarded. |
| `loki_secretfilter_processing_duration_seconds` | Summary | Time taken to process and redact logs, in seconds. |
| `loki_secretfilter_secrets_redacted_total` | Counter | Total number of secrets redacted. |
| `loki_secretfilter_secrets_redacted_by_rule_total` | Counter | Number of secrets redacted, partitioned by rule name. |
| `loki_secretfilter_secrets_redacted_by_origin` | Counter | Number of secrets redacted, partitioned by origin label, when `origin_label` is set. |


## Example

Expand All @@ -128,6 +136,7 @@ Alternatively, you can:
- Omit `redact_with` to use percentage-based redaction, which defaults to 80% redacted.
- Set `redact_percent` to `100` for full redaction.
- Set `gitleaks_config` to point to a custom Gitleaks TOML configuration file.
- Set `rate` to a value below `1.0` to sample entries and reduce CPU usage; entries not selected are forwarded unchanged.

```alloy
local.file_match "local_logs" {
Expand Down
48 changes: 48 additions & 0 deletions internal/component/loki/secretfilter/extend/extend_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package extend

import (
"fmt"
"os"
"path/filepath"
"runtime"
"testing"

"github.com/grafana/alloy/internal/component/loki/secretfilter"
)

// This test file is used to test the secretfilter component with a custom gitleaks config file.
// The reason why these tests are in a separate package is that the gitleaks library uses global variables
// when loading configs with [extend] useDefault = true. If we ran these
// tests in the same package as the main secretfilter tests, the extend logic does not work correctly.
// putting the extend tests in a separate package, "go test ./..." runs them in a
// different process, so they get a clean viper and gitleaks global state.

// gitleaksConfigPath returns the path to gitleaks.toml in the extend package directory.
func gitleaksConfigPath(t *testing.T) string {
t.Helper()
_, filename, _, _ := runtime.Caller(0)
dir := filepath.Dir(filename)
return filepath.Join(dir, "gitleaks.toml")
}

func TestSecretFiltering_WithGitleaksConfigFile(t *testing.T) {
configPath := gitleaksConfigPath(t)
if _, err := os.Stat(configPath); err != nil {
t.Skipf("gitleaks.toml not found at %s: %v", configPath, err)
}

config := fmt.Sprintf(`
forward_to = []
gitleaks_config = %q
`, configPath)

// Same cases as default, plus the UUID allowlist case (generic-api-key when secret is a UUID).
cases := secretfilter.DefaultTestCases()
cases = append(cases, secretfilter.TestCase{
Name: "generic_api_key_uuid_allowlisted",
InputLog: `{"message": "audit tokenID=550e8400-e29b-41d4-a716-446655440000 resourceID=6ba7b810-9dad-11d1-80b4-00c04fd430c8"}`,
ShouldRedact: false,
})

secretfilter.RunTestCases(t, config, cases)
}
14 changes: 14 additions & 0 deletions internal/component/loki/secretfilter/extend/gitleaks.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
title = "gitleakstests extended config"

[extend]
useDefault = true

# allowlist by secret – ignore when the “secret” is a UUID.
[[rules]]
id = "generic-api-key"
[[rules.allowlists]]
description = "ignore when extracted secret is a UUID"
regexTarget = "secret"
regexes = [
'''^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$''',
]
68 changes: 68 additions & 0 deletions internal/component/loki/secretfilter/run_test_cases.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// run_test_cases.go provides RunTestCases for use by secretfilter tests and the
// extend package. It lives in the secretfilter package so it can use
// secretfilter.Arguments/Exports and avoids testhelper importing secretfilter.

package secretfilter

import (
"context"
"testing"
"time"

"github.com/grafana/alloy/internal/component"
"github.com/grafana/alloy/internal/component/common/loki"
"github.com/grafana/alloy/internal/component/loki/secretfilter/testhelper"
"github.com/grafana/alloy/internal/util"
"github.com/grafana/alloy/syntax"
"github.com/grafana/loki/pkg/push"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
)

// TestCase is a single test case for RunTestCases (name, log line, expected redaction).
type TestCase struct {
Comment thread
kalleep marked this conversation as resolved.
Name string
InputLog string
ShouldRedact bool
}

// RunTestCases runs all cases through a single component (one config load).
// It builds the component once and calls processEntry for each case.
func RunTestCases(t *testing.T, config string, cases []TestCase) {
t.Helper()
var args Arguments
require.NoError(t, syntax.Unmarshal([]byte(config), &args))
args.ForwardTo = []loki.LogsReceiver{loki.NewLogsReceiver()}

opts := component.Options{
Logger: util.TestLogger(t),
OnStateChange: func(component.Exports) {},
GetServiceData: testhelper.GetServiceData,
Registerer: prometheus.NewRegistry(),
}
c, err := New(opts, args)
require.NoError(t, err)

for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
require.NotEmpty(t, tc.InputLog)
entry := loki.Entry{Labels: model.LabelSet{}, Entry: push.Entry{Timestamp: time.Now(), Line: tc.InputLog}}
got, _ := c.processEntry(context.Background(), entry)
if tc.ShouldRedact {
require.NotEqual(t, tc.InputLog, got.Line, "Expected log to be redacted but it was not")
} else {
require.Equal(t, tc.InputLog, got.Line, "Expected log to remain unchanged but it was modified")
}
})
}
}

// DefaultTestCases returns the standard 7 cases from testhelper as []TestCase.
func DefaultTestCases() []TestCase {
out := make([]TestCase, 0, len(testhelper.DefaultCases))
for _, c := range testhelper.DefaultCases {
out = append(out, TestCase{Name: c.Name, InputLog: c.InputLog, ShouldRedact: c.ShouldRedact})
}
return out
}
Loading
Loading