e3f5a65372
* pyroscope: process sampling annotations * Enable annotations in classic explore * Run prettier * Revert unneeded change to plugin.json * Tweak wording in sampling annotation * Fix test * Disable annotations by default
124 lines
3.6 KiB
Go
124 lines
3.6 KiB
Go
package annotation
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
|
typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1"
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
type profileAnnotationKey string
|
|
|
|
const (
|
|
// ProfileAnnotationKeyThrottled is an identifier for throttling annotations
|
|
ProfileAnnotationKeyThrottled profileAnnotationKey = "pyroscope.ingest.throttled"
|
|
// ProfileAnnotationKeySampled is an identifier for sampling annotations
|
|
ProfileAnnotationKeySampled profileAnnotationKey = "pyroscope.ingest.sampled"
|
|
)
|
|
|
|
type processedProfileAnnotation struct {
|
|
id string
|
|
text string
|
|
time int64
|
|
timeEnd int64
|
|
isRegion bool
|
|
}
|
|
|
|
type grafanaAnnotationData struct {
|
|
ids []string
|
|
times []time.Time
|
|
timeEnds []time.Time
|
|
texts []string
|
|
isRegions []bool
|
|
}
|
|
|
|
func (ga *grafanaAnnotationData) add(a *processedProfileAnnotation) {
|
|
// simple de-duplication, assuming annotations are ordered
|
|
if len(ga.ids) > 0 {
|
|
lastIdx := len(ga.ids) - 1
|
|
if a.id == ga.ids[lastIdx] {
|
|
// duplicate annotation, extend the previous annotation and discard the rest
|
|
ga.timeEnds[lastIdx] = time.UnixMilli(a.timeEnd)
|
|
return
|
|
}
|
|
}
|
|
ga.ids = append(ga.ids, a.id)
|
|
ga.times = append(ga.times, time.UnixMilli(a.time))
|
|
ga.timeEnds = append(ga.timeEnds, time.UnixMilli(a.timeEnd))
|
|
ga.isRegions = append(ga.isRegions, a.isRegion)
|
|
ga.texts = append(ga.texts, a.text)
|
|
}
|
|
|
|
// convertAnnotation converts a Pyroscope profile annotation into a Grafana annotation
|
|
func convertAnnotation(timedAnnotation *TimedAnnotation) (*processedProfileAnnotation, error) {
|
|
switch timedAnnotation.getKey() {
|
|
case string(ProfileAnnotationKeySampled):
|
|
return convertSamplingAnnotation(timedAnnotation.getValue(), timedAnnotation.Timestamp)
|
|
case string(ProfileAnnotationKeyThrottled):
|
|
return convertThrottlingAnnotation(timedAnnotation.getValue(), timedAnnotation.Timestamp)
|
|
default:
|
|
// Currently, we only support throttling and sampling annotations
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
func processAnnotations(timedAnnotations []*TimedAnnotation) (*grafanaAnnotationData, error) {
|
|
result := &grafanaAnnotationData{
|
|
times: []time.Time{},
|
|
timeEnds: []time.Time{},
|
|
texts: []string{},
|
|
isRegions: []bool{},
|
|
}
|
|
|
|
for _, timedAnnotation := range timedAnnotations {
|
|
if timedAnnotation == nil || timedAnnotation.Annotation == nil {
|
|
continue
|
|
}
|
|
processed, err := convertAnnotation(timedAnnotation)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if processed != nil {
|
|
result.add(processed)
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// CreateAnnotationFrame creates a Grafana data frame from annotation data
|
|
func CreateAnnotationFrame(annotations []*TimedAnnotation) (*data.Frame, error) {
|
|
annotationData, err := processAnnotations(annotations)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
timeField := data.NewField("time", nil, annotationData.times)
|
|
timeEndField := data.NewField("timeEnd", nil, annotationData.timeEnds)
|
|
textField := data.NewField("text", nil, annotationData.texts)
|
|
isRegionField := data.NewField("isRegion", nil, annotationData.isRegions)
|
|
colorField := data.NewField("color", nil, make([]string, len(annotationData.times)))
|
|
|
|
frame := data.NewFrame("annotations")
|
|
frame.Fields = data.Fields{timeField, timeEndField, textField, isRegionField, colorField}
|
|
frame.SetMeta(&data.FrameMeta{
|
|
DataTopic: data.DataTopicAnnotations,
|
|
})
|
|
|
|
return frame, nil
|
|
}
|