Skip to content

Commit 0be39a0

Browse files
authored
feat(policies): Log when multiple policies match the same stream (#16321)
1 parent 4a674b2 commit 0be39a0

File tree

5 files changed

+118
-30
lines changed

5 files changed

+118
-30
lines changed

‎pkg/distributor/distributor.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ func New(
226226

227227
policyResolver := push.PolicyResolver(func(userID string, lbs labels.Labels) string {
228228
mappings := overrides.PoliciesStreamMapping(userID)
229-
return mappings.PolicyFor(lbs)
229+
return getPolicy(userID, lbs, mappings, logger)
230230
})
231231

232232
validator, err := NewValidator(overrides, usageTracker)
@@ -1197,18 +1197,18 @@ func (d *Distributor) parseStreamLabels(vContext validationContext, key string,
11971197
mapping := d.validator.Limits.PoliciesStreamMapping(vContext.userID)
11981198
if val, ok := d.labelCache.Get(key); ok {
11991199
retentionHours := d.tenantsRetention.RetentionHoursFor(vContext.userID, val.ls)
1200-
policy := mapping.PolicyFor(val.ls)
1200+
policy := getPolicy(vContext.userID, val.ls, mapping, d.logger)
12011201
return val.ls, val.ls.String(), val.hash, retentionHours, policy, nil
12021202
}
12031203

12041204
ls, err := syntax.ParseLabels(key)
12051205
if err != nil {
12061206
retentionHours := d.tenantsRetention.RetentionHoursFor(vContext.userID, nil)
12071207
// TODO: check for global policy.
1208-
return nil, "", 0, retentionHours, mapping.PolicyFor(nil), fmt.Errorf(validation.InvalidLabelsErrorMsg, key, err)
1208+
return nil, "", 0, retentionHours, "", fmt.Errorf(validation.InvalidLabelsErrorMsg, key, err)
12091209
}
12101210

1211-
policy := mapping.PolicyFor(ls)
1211+
policy := getPolicy(vContext.userID, ls, mapping, d.logger)
12121212
retentionHours := d.tenantsRetention.RetentionHoursFor(vContext.userID, ls)
12131213

12141214
if err := d.validator.ValidateLabels(vContext, ls, stream, retentionHours, policy); err != nil {
@@ -1304,3 +1304,24 @@ func newRingAndLifecycler(cfg RingConfig, instanceCount *atomic.Uint32, logger l
13041304
func (d *Distributor) HealthyInstancesCount() int {
13051305
return int(d.healthyInstancesCount.Load())
13061306
}
1307+
1308+
func getPolicy(userID string, lbs labels.Labels, mapping validation.PolicyStreamMapping, logger log.Logger) string {
1309+
policies := mapping.PolicyFor(lbs)
1310+
1311+
var policy string
1312+
if len(policies) > 0 {
1313+
policy = policies[0]
1314+
if len(policies) > 1 {
1315+
level.Warn(logger).Log(
1316+
"msg", "multiple policies matched for the same stream",
1317+
"org_id", userID,
1318+
"stream", lbs.String(),
1319+
"policy", policy,
1320+
"policies", strings.Join(policies, ","),
1321+
"insight", "true",
1322+
)
1323+
}
1324+
}
1325+
1326+
return policy
1327+
}

‎pkg/ingester/instance.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"math"
88
"net/http"
99
"os"
10+
"strings"
1011
"sync"
1112
"syscall"
1213
"time"
@@ -298,11 +299,27 @@ func (i *instance) createStream(ctx context.Context, pushReqStream logproto.Stre
298299

299300
retentionHours := util.RetentionHours(i.tenantsRetention.RetentionPeriodFor(i.instanceID, labels))
300301
mapping := i.limiter.limits.PoliciesStreamMapping(i.instanceID)
301-
policy := mapping.PolicyFor(labels)
302+
policies := mapping.PolicyFor(labels)
302303
if record != nil {
303304
err = i.streamCountLimiter.AssertNewStreamAllowed(i.instanceID)
304305
}
305306

307+
// NOTE: We previously resolved the policy on distributors and logged when multiple policies were matched.
308+
// As on distributors, we use the first policy by alphabetical order.
309+
var policy string
310+
if len(policies) > 0 {
311+
policy = policies[0]
312+
if len(policies) > 1 {
313+
level.Warn(util_log.Logger).Log(
314+
"msg", "multiple policies matched for the same stream",
315+
"org_id", i.instanceID,
316+
"stream", pushReqStream.Labels,
317+
"policy", policy,
318+
"policies", strings.Join(policies, ","),
319+
)
320+
}
321+
}
322+
306323
if err != nil {
307324
return i.onStreamCreationError(ctx, pushReqStream, err, labels, retentionHours, policy)
308325
}

‎pkg/validation/ingestion_policies.go

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
package validation
22

3-
import "github.com/prometheus/prometheus/model/labels"
3+
import (
4+
"fmt"
5+
"slices"
6+
7+
"github.com/prometheus/prometheus/model/labels"
8+
9+
"github.com/grafana/loki/v3/pkg/logql/syntax"
10+
)
411

512
type PriorityStream struct {
613
Priority int `yaml:"priority" json:"priority" doc:"description=The larger the value, the higher the priority."`
@@ -19,29 +26,65 @@ func (p *PriorityStream) Matches(lbs labels.Labels) bool {
1926

2027
type PolicyStreamMapping map[string][]*PriorityStream
2128

22-
func (p *PolicyStreamMapping) PolicyFor(lbs labels.Labels) string {
29+
func (p *PolicyStreamMapping) Validate() error {
30+
for policyName, policyStreams := range *p {
31+
for idx, policyStream := range policyStreams {
32+
matchers, err := syntax.ParseMatchers(policyStream.Selector, true)
33+
if err != nil {
34+
return fmt.Errorf("invalid labels matchers for policy stream mapping: %w", err)
35+
}
36+
(*p)[policyName][idx].Matchers = matchers
37+
}
38+
39+
// Sort the mappings by priority. Higher priority mappings come first.
40+
slices.SortFunc(policyStreams, func(a, b *PriorityStream) int {
41+
return b.Priority - a.Priority
42+
})
43+
}
44+
45+
return nil
46+
}
47+
48+
// PolicyFor returns all the policies that matches the given labels with the highest priority.
49+
// Note that this method will return multiple policies if two different policies match the same labels
50+
// with the same priority.
51+
// Returned policies are sorted alphabetically.
52+
// If no policies match, it returns an empty slice.
53+
func (p *PolicyStreamMapping) PolicyFor(lbs labels.Labels) []string {
2354
var (
24-
matchedPolicy *PriorityStream
25-
found bool
26-
matchedPolicyName string
55+
found bool
56+
highestPriority int
57+
matchedPolicies = make(map[string]int, len(*p))
2758
)
2859

2960
for policyName, policyStreams := range *p {
3061
for _, policyStream := range policyStreams {
31-
if found && policyStream.Priority <= matchedPolicy.Priority {
32-
// Even if a match occurs it won't have a higher priority than the current matched policy.
33-
continue
62+
// Mappings are sorted by priority (see PolicyStreamMapping.Validate at this file).
63+
// So we can break early if the current policy has a lower priority than the highest priority matched policy.
64+
if found && policyStream.Priority < highestPriority {
65+
break
3466
}
3567

3668
if !policyStream.Matches(lbs) {
3769
continue
3870
}
3971

4072
found = true
41-
matchedPolicy = policyStream
42-
matchedPolicyName = policyName
73+
highestPriority = policyStream.Priority
74+
matchedPolicies[policyName] = policyStream.Priority
4375
}
4476
}
4577

46-
return matchedPolicyName
78+
// Stick with only the highest priority policies.
79+
policies := make([]string, 0, len(matchedPolicies))
80+
for policyName, priority := range matchedPolicies {
81+
if priority == highestPriority {
82+
policies = append(policies, policyName)
83+
}
84+
}
85+
86+
// Sort the policies alphabetically.
87+
slices.Sort(policies)
88+
89+
return policies
4790
}

‎pkg/validation/ingestion_policies_test.go

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,17 +89,30 @@ func Test_PolicyStreamMapping_PolicyFor(t *testing.T) {
8989
},
9090
},
9191
},
92+
"policy8": []*PriorityStream{
93+
{
94+
Selector: `{env=~"prod|test"}`,
95+
Priority: 3,
96+
Matchers: []*labels.Matcher{
97+
labels.MustNewMatcher(labels.MatchRegexp, "env", "prod|test"),
98+
},
99+
},
100+
},
92101
}
93102

94-
require.Equal(t, "policy1", mapping.PolicyFor(labels.FromStrings("foo", "bar")))
103+
require.NoError(t, mapping.Validate())
104+
105+
require.Equal(t, []string{"policy1"}, mapping.PolicyFor(labels.FromStrings("foo", "bar")))
95106
// matches both policy2 and policy1 but policy1 has higher priority.
96-
require.Equal(t, "policy1", mapping.PolicyFor(labels.FromStrings("foo", "bar", "daz", "baz")))
107+
require.Equal(t, []string{"policy1"}, mapping.PolicyFor(labels.FromStrings("foo", "bar", "daz", "baz")))
97108
// matches policy3 and policy4 but policy3 has higher priority..
98-
require.Equal(t, "policy3", mapping.PolicyFor(labels.FromStrings("qyx", "qzx", "qox", "qox")))
109+
require.Equal(t, []string{"policy3"}, mapping.PolicyFor(labels.FromStrings("qyx", "qzx", "qox", "qox")))
99110
// matches no policy.
100-
require.Equal(t, "", mapping.PolicyFor(labels.FromStrings("foo", "fooz", "daz", "qux", "quux", "corge")))
111+
require.Empty(t, mapping.PolicyFor(labels.FromStrings("foo", "fooz", "daz", "qux", "quux", "corge")))
101112
// matches policy5 through regex.
102-
require.Equal(t, "policy5", mapping.PolicyFor(labels.FromStrings("qab", "qzxqox")))
113+
require.Equal(t, []string{"policy5"}, mapping.PolicyFor(labels.FromStrings("qab", "qzxqox")))
103114

104-
require.Equal(t, "policy6", mapping.PolicyFor(labels.FromStrings("env", "prod", "team", "finance")))
115+
require.Equal(t, []string{"policy6"}, mapping.PolicyFor(labels.FromStrings("env", "prod", "team", "finance")))
116+
// Matches policy7 and policy8 which have the same priority.
117+
require.Equal(t, []string{"policy7", "policy8"}, mapping.PolicyFor(labels.FromStrings("env", "prod")))
105118
}

‎pkg/validation/limits.go

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -516,14 +516,8 @@ func (l *Limits) Validate() error {
516516
}
517517

518518
if l.PolicyStreamMapping != nil {
519-
for policyName, policyStreams := range l.PolicyStreamMapping {
520-
for idx, policyStream := range policyStreams {
521-
matchers, err := syntax.ParseMatchers(policyStream.Selector, true)
522-
if err != nil {
523-
return fmt.Errorf("invalid labels matchers for policy stream mapping: %w", err)
524-
}
525-
l.PolicyStreamMapping[policyName][idx].Matchers = matchers
526-
}
519+
if err := l.PolicyStreamMapping.Validate(); err != nil {
520+
return err
527521
}
528522
}
529523

0 commit comments

Comments
 (0)