mirror of https://github.com/grafana/loki
Querier/Ruler: query blocker (#7785)
Block malicious or expensive queries using a per-tenant runtime configuration.pull/7791/head
parent
c6b55e7512
commit
a63ad06509
@ -0,0 +1,47 @@ |
||||
--- |
||||
title: Blocking Queries |
||||
weight: 60 |
||||
--- |
||||
# Blocking Queries |
||||
|
||||
In certain situations, you may not be able to control the queries being sent to your Loki installation. These queries |
||||
may be intentionally or unintentionally expensive to run, and they may affect the overall stability or cost of running |
||||
your service. |
||||
|
||||
You can block queries using [per-tenant overrides](../configuration/#runtime-configuration-file), like so: |
||||
|
||||
```yaml |
||||
overrides: |
||||
"tenant-id": |
||||
blocked_queries: |
||||
# block this query exactly |
||||
- pattern: 'sum(rate({env="prod"}[1m]))' |
||||
|
||||
# block any query matching this regex pattern |
||||
- pattern: '.*prod.*' |
||||
regex: true |
||||
|
||||
# block all metric queries |
||||
- types: metric |
||||
|
||||
# block any filter or limited queries matching this regex pattern |
||||
- pattern: '.*prod.*' |
||||
regex: true |
||||
types: filter,limited |
||||
``` |
||||
|
||||
The available query types are: |
||||
|
||||
- `metric`: a query with an aggregation, e.g. `sum(rate({env="prod"}[1m]))` |
||||
- `filter`: a query with a log filter, e.g. `{env="prod"} |= "error"` |
||||
- `limited`: a query without a filter or a metric aggregation |
||||
|
||||
**Note:** the order of patterns is preserved, so the first matching pattern will be used |
||||
|
||||
## Observing blocked queries |
||||
|
||||
Blocked queries are logged, as well as counted in the `loki_blocked_queries` metric on a per-tenant basis. |
||||
|
||||
## Scope |
||||
|
||||
Queries received via the API and executed as [alerting/recording rules](../rules/) will be blocked. |
||||
@ -0,0 +1,94 @@ |
||||
package logql |
||||
|
||||
import ( |
||||
"context" |
||||
"strings" |
||||
|
||||
"github.com/go-kit/log" |
||||
"github.com/go-kit/log/level" |
||||
"github.com/grafana/regexp" |
||||
|
||||
logutil "github.com/grafana/loki/pkg/util/log" |
||||
"github.com/grafana/loki/pkg/util/validation" |
||||
) |
||||
|
||||
type queryBlocker struct { |
||||
ctx context.Context |
||||
q *query |
||||
logger log.Logger |
||||
} |
||||
|
||||
func newQueryBlocker(ctx context.Context, q *query) *queryBlocker { |
||||
return &queryBlocker{ |
||||
ctx: ctx, |
||||
q: q, |
||||
logger: logutil.WithContext(ctx, q.logger), |
||||
} |
||||
} |
||||
|
||||
func (qb *queryBlocker) isBlocked(tenant string) bool { |
||||
patterns := qb.q.limits.BlockedQueries(tenant) |
||||
if len(patterns) <= 0 { |
||||
return false |
||||
} |
||||
|
||||
typ, err := QueryType(qb.q.params.Query()) |
||||
if err != nil { |
||||
typ = "unknown" |
||||
} |
||||
|
||||
logger := log.With(qb.logger, "user", tenant, "type", typ) |
||||
|
||||
query := qb.q.params.Query() |
||||
for _, p := range patterns { |
||||
|
||||
// if no pattern is given, assume we want to match all queries
|
||||
if p.Pattern == "" { |
||||
p.Pattern = ".*" |
||||
p.Regex = true |
||||
} |
||||
|
||||
if strings.TrimSpace(p.Pattern) == strings.TrimSpace(query) { |
||||
level.Warn(logger).Log("msg", "query blocker matched with exact match policy", "query", query) |
||||
return qb.block(p, typ, logger) |
||||
} |
||||
|
||||
if p.Regex { |
||||
r, err := regexp.Compile(p.Pattern) |
||||
if err != nil { |
||||
level.Error(logger).Log("msg", "query blocker regex does not compile", "pattern", p.Pattern, "err", err) |
||||
continue |
||||
} |
||||
|
||||
if r.MatchString(query) { |
||||
level.Warn(logger).Log("msg", "query blocker matched with regex policy", "pattern", p.Pattern, "query", query) |
||||
return qb.block(p, typ, logger) |
||||
} |
||||
} |
||||
} |
||||
|
||||
return false |
||||
} |
||||
|
||||
func (qb *queryBlocker) block(q *validation.BlockedQuery, typ string, logger log.Logger) bool { |
||||
// no specific types to validate against, so query is blocked
|
||||
if len(q.Types) == 0 { |
||||
return true |
||||
} |
||||
|
||||
matched := false |
||||
for _, qt := range q.Types { |
||||
if qt == typ { |
||||
matched = true |
||||
break |
||||
} |
||||
} |
||||
|
||||
// query would be blocked, but it didn't match specified types
|
||||
if !matched { |
||||
level.Debug(logger).Log("msg", "query blocker matched pattern, but not specified types", "pattern", q.Pattern, "types", q.Types.String(), "queryType", typ) |
||||
return false |
||||
} |
||||
|
||||
return true |
||||
} |
||||
@ -0,0 +1,150 @@ |
||||
package logql |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/go-kit/log" |
||||
"github.com/stretchr/testify/require" |
||||
"github.com/weaveworks/common/user" |
||||
|
||||
"github.com/grafana/loki/pkg/logproto" |
||||
"github.com/grafana/loki/pkg/logqlmodel" |
||||
"github.com/grafana/loki/pkg/util/validation" |
||||
) |
||||
|
||||
func TestEngine_ExecWithBlockedQueries(t *testing.T) { |
||||
limits := &fakeLimits{maxSeries: 10} |
||||
eng := NewEngine(EngineOpts{}, getLocalQuerier(100000), limits, log.NewNopLogger()) |
||||
|
||||
defaultQuery := `topk(1,rate(({app=~"foo|bar"})[1m]))` |
||||
for _, test := range []struct { |
||||
name string |
||||
q string |
||||
blocked []*validation.BlockedQuery |
||||
expectedErr error |
||||
}{ |
||||
{ |
||||
"exact match all types", |
||||
defaultQuery, []*validation.BlockedQuery{ |
||||
{ |
||||
Pattern: defaultQuery, |
||||
}, |
||||
}, logqlmodel.ErrBlocked, |
||||
}, |
||||
{ |
||||
"exact match all types with surrounding whitespace trimmed", |
||||
defaultQuery, []*validation.BlockedQuery{ |
||||
{ |
||||
Pattern: fmt.Sprintf(" %s ", defaultQuery), |
||||
}, |
||||
}, logqlmodel.ErrBlocked, |
||||
}, |
||||
{ |
||||
"exact match filter type only", |
||||
`{app=~"foo|bar"} |= "baz"`, []*validation.BlockedQuery{ |
||||
{ |
||||
Pattern: `{app=~"foo|bar"} |= "baz"`, |
||||
Types: []string{QueryTypeFilter}, |
||||
}, |
||||
}, logqlmodel.ErrBlocked, |
||||
}, |
||||
{ |
||||
"match from multiple patterns", |
||||
`{app=~"foo|bar"} |= "baz"`, []*validation.BlockedQuery{ |
||||
// won't match
|
||||
{ |
||||
Pattern: `.*"buzz".*`, |
||||
Regex: true, |
||||
}, |
||||
// will match
|
||||
{ |
||||
Pattern: `{app=~"foo|bar"} |= "baz"`, |
||||
Types: []string{QueryTypeFilter}, |
||||
}, |
||||
}, logqlmodel.ErrBlocked, |
||||
}, |
||||
{ |
||||
"no block: exact match not matching filter type", |
||||
`{app=~"foo|bar"} | json`, []*validation.BlockedQuery{ |
||||
{ |
||||
Pattern: `{app=~"foo|bar"} | json`, // "limited" query
|
||||
Types: []string{QueryTypeFilter}, |
||||
}, |
||||
}, nil, |
||||
}, |
||||
{ |
||||
"regex match all types", |
||||
defaultQuery, []*validation.BlockedQuery{ |
||||
{ |
||||
Pattern: ".*foo.*", |
||||
Regex: true, |
||||
}, |
||||
}, logqlmodel.ErrBlocked, |
||||
}, |
||||
{ |
||||
"regex match multiple types", |
||||
defaultQuery, []*validation.BlockedQuery{ |
||||
{ |
||||
Pattern: ".*foo.*", |
||||
Regex: true, |
||||
Types: []string{QueryTypeFilter, QueryTypeMetric}, |
||||
}, |
||||
}, logqlmodel.ErrBlocked, |
||||
}, |
||||
{ |
||||
"match all queries by type", |
||||
defaultQuery, []*validation.BlockedQuery{ |
||||
{ |
||||
Types: []string{QueryTypeFilter, QueryTypeMetric}, |
||||
}, |
||||
}, logqlmodel.ErrBlocked, |
||||
}, |
||||
{ |
||||
"no block: match all queries by type", |
||||
defaultQuery, []*validation.BlockedQuery{ |
||||
{ |
||||
Types: []string{QueryTypeLimited}, |
||||
}, |
||||
}, nil, |
||||
}, |
||||
{ |
||||
"regex does not compile", |
||||
defaultQuery, []*validation.BlockedQuery{ |
||||
{ |
||||
Pattern: "[.*", |
||||
Regex: true, |
||||
Types: []string{QueryTypeFilter, QueryTypeMetric}, |
||||
}, |
||||
}, nil, |
||||
}, |
||||
{ |
||||
"no blocked queries", |
||||
defaultQuery, []*validation.BlockedQuery{}, nil, |
||||
}, |
||||
} { |
||||
t.Run(test.name, func(t *testing.T) { |
||||
limits.blockedQueries = test.blocked |
||||
|
||||
q := eng.Query(LiteralParams{ |
||||
qs: test.q, |
||||
start: time.Unix(0, 0), |
||||
end: time.Unix(100000, 0), |
||||
step: 60 * time.Second, |
||||
direction: logproto.FORWARD, |
||||
limit: 1000, |
||||
}) |
||||
_, err := q.Exec(user.InjectOrgID(context.Background(), "fake")) |
||||
|
||||
if test.expectedErr == nil { |
||||
require.NoError(t, err) |
||||
return |
||||
} |
||||
|
||||
require.Error(t, err) |
||||
require.Equal(t, err.Error(), test.expectedErr.Error()) |
||||
}) |
||||
} |
||||
} |
||||
@ -0,0 +1,9 @@ |
||||
package validation |
||||
|
||||
import "github.com/grafana/dskit/flagext" |
||||
|
||||
type BlockedQuery struct { |
||||
Pattern string `yaml:"pattern"` |
||||
Regex bool `yaml:"regex"` |
||||
Types flagext.StringSliceCSV `yaml:"types"` |
||||
} |
||||
Loading…
Reference in new issue