Pyroscope: Annotation support for series queries (#104130)

* Pyroscope: Add annotations frame to series response

* Adapt to API change, add tests

* Run make lint-go

* Fix conflicts after rebase

* Add annotation via a separate data frame

* Process annotations fully at the datasource

* Add mod owner for go-humanize

* Pyroscope: Annotations in Query Response can be optional

---------

Co-authored-by: Piotr Jamróz <pm.jamroz@gmail.com>
This commit is contained in:
Aleksandar Petrov
2025-05-28 10:42:19 +02:00
committed by GitHub
co-authored by Piotr Jamróz
parent ea0e49a6e6
commit 0b8252fd7c
11 changed files with 535 additions and 17 deletions
+44 -4
View File
@@ -14,6 +14,7 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana-plugin-sdk-go/live"
"github.com/grafana/grafana/pkg/tsdb/grafana-pyroscope-datasource/kinds/dataquery"
typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1"
"github.com/xlab/treeprint"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
@@ -94,7 +95,15 @@ func (d *PyroscopeDatasource) query(ctx context.Context, pCtx backend.PluginCont
}
// add the frames to the response.
responseMutex.Lock()
response.Frames = append(response.Frames, seriesToDataFrames(seriesResp)...)
withAnnotations := qm.Annotations != nil && *qm.Annotations
frames, err := seriesToDataFrames(seriesResp, withAnnotations)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
logger.Error("Querying SelectSeries()", "err", err, "function", logEntrypoint())
return err
}
response.Frames = append(response.Frames, frames...)
responseMutex.Unlock()
return nil
})
@@ -411,8 +420,22 @@ func walkTree(tree *ProfileTree, fn func(tree *ProfileTree)) {
}
}
func seriesToDataFrames(resp *SeriesResponse) []*data.Frame {
type TimedAnnotation struct {
Timestamp int64 `json:"timestamp"`
Annotation *typesv1.ProfileAnnotation `json:"annotation"`
}
func (ta *TimedAnnotation) getKey() string {
return ta.Annotation.Key
}
func (ta *TimedAnnotation) getValue() string {
return ta.Annotation.Value
}
func seriesToDataFrames(resp *SeriesResponse, withAnnotations bool) ([]*data.Frame, error) {
frames := make([]*data.Frame, 0, len(resp.Series))
annotations := make([]*TimedAnnotation, 0)
for _, series := range resp.Series {
// We create separate data frames as the series may not have the same length
@@ -430,15 +453,32 @@ func seriesToDataFrames(resp *SeriesResponse) []*data.Frame {
valueField := data.NewField(resp.Label, labels, []float64{})
valueField.Config = &data.FieldConfig{Unit: resp.Units}
fields = append(fields, valueField)
for _, point := range series.Points {
timeField.Append(time.UnixMilli(point.Timestamp))
valueField.Append(point.Value)
if withAnnotations {
for _, a := range point.Annotations {
annotations = append(annotations, &TimedAnnotation{
Timestamp: point.Timestamp,
Annotation: a,
})
}
}
}
fields = append(fields, valueField)
frame.Fields = fields
frames = append(frames, frame)
}
return frames
if len(annotations) > 0 {
frame, err := createAnnotationFrame(annotations)
if err != nil {
return nil, err
}
frames = append(frames, frame)
}
return frames, nil
}