-
Notifications
You must be signed in to change notification settings - Fork 3.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Store structured metadata with patterns #12936
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,9 +4,12 @@ import ( | |
"sort" | ||
"time" | ||
|
||
"github.com/grafana/loki/pkg/push" | ||
"github.com/prometheus/common/model" | ||
"github.com/prometheus/prometheus/model/labels" | ||
|
||
"github.com/grafana/loki/v3/pkg/logproto" | ||
"github.com/grafana/loki/v3/pkg/logql/log" | ||
"github.com/grafana/loki/v3/pkg/pattern/iter" | ||
) | ||
|
||
|
@@ -21,16 +24,20 @@ const ( | |
type Chunks []Chunk | ||
|
||
type Chunk struct { | ||
Samples []logproto.PatternSample | ||
Samples []logproto.PatternSample | ||
StructuredMetadata map[string]string | ||
} | ||
|
||
func newChunk(ts model.Time) Chunk { | ||
func newChunk(ts model.Time, structuredMetadata push.LabelsAdapter) Chunk { | ||
maxSize := int(maxChunkTime.Nanoseconds()/TimeResolution.UnixNano()) + 1 | ||
v := Chunk{Samples: make([]logproto.PatternSample, 1, maxSize)} | ||
v := Chunk{Samples: make([]logproto.PatternSample, 1, maxSize), StructuredMetadata: make(map[string]string, 32)} | ||
v.Samples[0] = logproto.PatternSample{ | ||
Timestamp: ts, | ||
Value: 1, | ||
} | ||
for _, lbl := range structuredMetadata { | ||
v.StructuredMetadata[lbl.Name] = lbl.Value | ||
} | ||
return v | ||
} | ||
|
||
|
@@ -94,31 +101,50 @@ func (c Chunk) ForRange(start, end, step model.Time) []logproto.PatternSample { | |
return aggregatedSamples | ||
} | ||
|
||
func (c *Chunks) Add(ts model.Time) { | ||
func (c *Chunks) Add(ts model.Time, metadata push.LabelsAdapter) { | ||
t := truncateTimestamp(ts, TimeResolution) | ||
|
||
if len(*c) == 0 { | ||
*c = append(*c, newChunk(t)) | ||
*c = append(*c, newChunk(t, metadata)) | ||
return | ||
} | ||
last := &(*c)[len(*c)-1] | ||
if last.Samples[len(last.Samples)-1].Timestamp == t { | ||
last.Samples[len(last.Samples)-1].Value++ | ||
for _, md := range metadata { | ||
last.StructuredMetadata[md.Name] = md.Value | ||
} | ||
return | ||
} | ||
if !last.spaceFor(t) { | ||
*c = append(*c, newChunk(t)) | ||
*c = append(*c, newChunk(t, metadata)) | ||
return | ||
} | ||
last.Samples = append(last.Samples, logproto.PatternSample{ | ||
Timestamp: t, | ||
Value: 1, | ||
}) | ||
for _, md := range metadata { | ||
last.StructuredMetadata[md.Name] = md.Value | ||
} | ||
} | ||
|
||
func (c Chunks) Iterator(pattern string, from, through, step model.Time) iter.Iterator { | ||
func (c Chunks) Iterator(pattern string, from, through, step model.Time, labelFilters log.StreamPipeline) iter.Iterator { | ||
iters := make([]iter.Iterator, 0, len(c)) | ||
var emptyLine []byte | ||
for _, chunk := range c { | ||
chunkMetadata := make([]labels.Label, 0, len(chunk.StructuredMetadata)) | ||
for k, v := range chunk.StructuredMetadata { | ||
chunkMetadata = append(chunkMetadata, labels.Label{ | ||
Name: k, | ||
Value: v, | ||
}) | ||
} | ||
_, _, matches := labelFilters.Process(0, emptyLine, chunkMetadata...) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This feels a little hacky since I'm passing empty TS and Line, but it enables re-use of the existing LogQL logic to implement the matching so I think its the best option. |
||
if !matches { | ||
continue | ||
} | ||
|
||
samples := chunk.ForRange(from, through, step) | ||
if len(samples) == 0 { | ||
continue | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ import ( | |
"github.com/go-kit/log" | ||
"github.com/grafana/dskit/httpgrpc" | ||
"github.com/grafana/dskit/ring" | ||
"github.com/pkg/errors" | ||
"github.com/prometheus/client_golang/prometheus" | ||
|
||
"github.com/grafana/loki/v3/pkg/logproto" | ||
|
@@ -44,10 +45,24 @@ func NewIngesterQuerier( | |
} | ||
|
||
func (q *IngesterQuerier) Patterns(ctx context.Context, req *logproto.QueryPatternsRequest) (*logproto.QueryPatternsResponse, error) { | ||
_, err := syntax.ParseMatchers(req.Query, true) | ||
expr, err := syntax.ParseLogSelector(req.Query, true) | ||
if err != nil { | ||
return nil, httpgrpc.Errorf(http.StatusBadRequest, err.Error()) | ||
} | ||
var queryErr error | ||
expr.Walk(func(treeExpr syntax.Expr) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I put this here to guard against anyone passing in more complex queries to this API. Only allowing a subset of LogQL is a bit of an odd use case so I'd appreciate any input on whether there's a better way to do this. |
||
switch treeExpr.(type) { | ||
case *syntax.MatchersExpr: // Permit | ||
case *syntax.PipelineExpr: // Permit | ||
case *syntax.LabelFilterExpr: // Permit | ||
default: | ||
queryErr = errors.New("only label filters are allowed") | ||
} | ||
}) | ||
if queryErr != nil { | ||
return nil, httpgrpc.Errorf(http.StatusBadRequest, err.Error()) | ||
} | ||
|
||
resps, err := q.forAllIngesters(ctx, func(_ context.Context, client logproto.PatternClient) (interface{}, error) { | ||
return client.Query(ctx, req) | ||
}) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not confident I am using the right types to pass the labels through from the request - any recommendations here?