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