mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Pyroscope: Remove support for old pyroscope (#74683)
This commit is contained in:
parent
8b4d167de5
commit
f7aab06e23
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -194,7 +194,6 @@
|
||||
/devenv/docker/blocks/mysql_opendata/ @grafana/oss-big-tent
|
||||
/devenv/docker/blocks/mysql_tests/ @grafana/oss-big-tent
|
||||
/devenv/docker/blocks/opentsdb/ @grafana/observability-metrics
|
||||
/devenv/docker/blocks/phlare/ @grafana/observability-traces-and-profiling
|
||||
/devenv/docker/blocks/postgres/ @grafana/oss-big-tent
|
||||
/devenv/docker/blocks/postgres_tests/ @grafana/oss-big-tent
|
||||
/devenv/docker/blocks/prometheus/ @grafana/observability-metrics
|
||||
|
@ -1,8 +0,0 @@
|
||||
phlare:
|
||||
image: grafana/phlare:latest
|
||||
command:
|
||||
- --config.file=/etc/phlare.yaml
|
||||
ports:
|
||||
- 4100:4100
|
||||
volumes:
|
||||
- ./docker/blocks/phlare/phlare.yaml:/etc/phlare.yaml
|
@ -1,5 +0,0 @@
|
||||
scrape_configs:
|
||||
- job_name: "default"
|
||||
scrape_interval: "15s"
|
||||
static_configs:
|
||||
- targets: ["127.0.0.1:4100"]
|
@ -1,6 +1,4 @@
|
||||
pyroscope:
|
||||
image: "pyroscope/pyroscope:latest"
|
||||
command:
|
||||
- "server"
|
||||
image: "grafana/pyroscope:latest"
|
||||
ports:
|
||||
- "4040:4040"
|
||||
|
@ -21,7 +21,7 @@ weight: 1150
|
||||
|
||||
# Grafana Pyroscope data source
|
||||
|
||||
Formerly Phlare data source, it supports both Phlare and Pyroscope, a horizontally scalable, highly-available, multi-tenant, OSS, continuous profiling aggregation systems. Add it as a data source, and you are ready to query your profiles in [Explore][explore].
|
||||
Formerly Phlare data source, now Grafana Pyroscope, a horizontally scalable, highly-available, multi-tenant, OSS, continuous profiling aggregation system. Add it as a data source, and you are ready to query your profiles in [Explore][explore].
|
||||
|
||||
## Configure the Grafana Pyroscope data source
|
||||
|
||||
@ -45,7 +45,6 @@ To configure basic settings for the data source, complete the following steps:
|
||||
| `User` | User name for basic authentication. |
|
||||
| `Password` | Password for basic authentication. |
|
||||
| `Minimal step` | Used for queries returning timeseries data. Phlare backend, similar to Prometheus, scrapes profiles at certain intervals. To prevent querying at smaller interval use Minimal step same or higher than your Phlare scrape interval. For Pyroscope backend this prevents returning too many data points to the front end. |
|
||||
| `Backend type` | Select a backend type between Phlare and Pyroscope. It is autodetected if not set but once set you have to change it manually. |
|
||||
|
||||
## Querying
|
||||
|
||||
@ -55,13 +54,13 @@ To configure basic settings for the data source, complete the following steps:
|
||||
|
||||
Query editor gives you access to a profile type selector, a label selector, and collapsible options.
|
||||
|
||||

|
||||

|
||||
|
||||
Select a profile type or app from the drop-down menu. While the label selector can be left empty to query all profiles without filtering by labels, the profile type or app must be selected for the query to be valid. Grafana does not show any data if the profile type or app isn’t selected when a query is run.
|
||||
Select a profile type from the drop-down menu. While the label selector can be left empty to query all profiles without filtering by labels, the profile type or app must be selected for the query to be valid. Grafana does not show any data if the profile type or app isn’t selected when a query is run.
|
||||
|
||||

|
||||
|
||||
Use the labels selector input to filter by labels. Phlare and Pyroscope uses similar syntax to Prometheus to filter labels. Refer to [Phlare documentation](https://grafana.com/docs/phlare/latest/) for available operators and syntax.
|
||||
Use the labels selector input to filter by labels. Pyroscope uses similar syntax to Prometheus to filter labels. Refer to [Pyroscope documentation](https://grafana.com/docs/pyroscope/latest/) for available operators and syntax.
|
||||
|
||||

|
||||
|
||||
@ -77,7 +76,7 @@ Profiles can be visualized in a flame graph. See the [Flame Graph documentation]
|
||||
|
||||

|
||||
|
||||
Phlare and Pyroscope returns profiles aggregated over a selected time range, and the absolute values in the flame graph grow as the time range gets bigger while keeping the relative values meaningful. You can zoom in on the time range to get a higher granularity profile up to the point of a single scrape interval.
|
||||
Pyroscope returns profiles aggregated over a selected time range, and the absolute values in the flame graph grow as the time range gets bigger while keeping the relative values meaningful. You can zoom in on the time range to get a higher granularity profile up to the point of a single scrape interval.
|
||||
|
||||
### Metrics query results
|
||||
|
||||
@ -98,11 +97,10 @@ apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Grafana Pyroscope
|
||||
type: phlare
|
||||
url: http://localhost:4100
|
||||
type: grafana-pyroscope-datasource
|
||||
url: http://localhost:4040
|
||||
jsonData:
|
||||
minStep: '15s'
|
||||
backendType: 'pyroscope'
|
||||
```
|
||||
|
||||
{{% docs/reference %}}
|
||||
|
@ -671,7 +671,7 @@
|
||||
"name": "Grafana Labs",
|
||||
"url": "https://www.grafana.com"
|
||||
},
|
||||
"description": "Supports Phlare and Pyroscope backends, horizontally-scalable, highly-available, multi-tenant continuous profiling aggregation systems.",
|
||||
"description": "Data source for Grafana Pyroscope, horizontally-scalable, highly-available, multi-tenant continuous profiling aggregation system.",
|
||||
"links": [
|
||||
{
|
||||
"name": "GitHub Project",
|
||||
@ -1896,4 +1896,4 @@
|
||||
"signatureOrg": "",
|
||||
"angularDetected": false
|
||||
}
|
||||
]
|
||||
]
|
@ -1,66 +0,0 @@
|
||||
package phlare
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type ProfilingClient interface {
|
||||
ProfileTypes(context.Context) ([]*ProfileType, error)
|
||||
LabelNames(ctx context.Context, query string, start int64, end int64) ([]string, error)
|
||||
LabelValues(ctx context.Context, query string, label string, start int64, end int64) ([]string, error)
|
||||
GetSeries(ctx context.Context, profileTypeID string, labelSelector string, start int64, end int64, groupBy []string, step float64) (*SeriesResponse, error)
|
||||
GetProfile(ctx context.Context, profileTypeID string, labelSelector string, start int64, end int64, maxNodes *int64) (*ProfileResponse, error)
|
||||
}
|
||||
|
||||
type ProfileType struct {
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
func getClient(backendType string, httpClient *http.Client, url string) ProfilingClient {
|
||||
if backendType == "pyroscope" {
|
||||
return NewPyroscopeClient(httpClient, url)
|
||||
}
|
||||
|
||||
// We treat unset value as phlare
|
||||
return NewPhlareClient(httpClient, url)
|
||||
}
|
||||
|
||||
type Flamebearer struct {
|
||||
Names []string
|
||||
Levels []*Level
|
||||
Total int64
|
||||
MaxSelf int64
|
||||
}
|
||||
|
||||
type Level struct {
|
||||
Values []int64
|
||||
}
|
||||
|
||||
type Series struct {
|
||||
Labels []*LabelPair
|
||||
Points []*Point
|
||||
}
|
||||
|
||||
type LabelPair struct {
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
type Point struct {
|
||||
Value float64
|
||||
// Milliseconds unix timestamp
|
||||
Timestamp int64
|
||||
}
|
||||
|
||||
type ProfileResponse struct {
|
||||
Flamebearer *Flamebearer
|
||||
Units string
|
||||
}
|
||||
|
||||
type SeriesResponse struct {
|
||||
Series []*Series
|
||||
Units string
|
||||
Label string
|
||||
}
|
@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
@ -14,8 +13,6 @@ import (
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/grafana/grafana/pkg/infra/httpclient"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -25,6 +22,14 @@ var (
|
||||
_ backend.StreamHandler = (*PhlareDatasource)(nil)
|
||||
)
|
||||
|
||||
type ProfilingClient interface {
|
||||
ProfileTypes(context.Context) ([]*ProfileType, error)
|
||||
LabelNames(ctx context.Context) ([]string, error)
|
||||
LabelValues(ctx context.Context, label string) ([]string, error)
|
||||
GetSeries(ctx context.Context, profileTypeID string, labelSelector string, start int64, end int64, groupBy []string, step float64) (*SeriesResponse, error)
|
||||
GetProfile(ctx context.Context, profileTypeID string, labelSelector string, start int64, end int64, maxNodes *int64) (*ProfileResponse, error)
|
||||
}
|
||||
|
||||
// PhlareDatasource is a datasource for querying application performance profiles.
|
||||
type PhlareDatasource struct {
|
||||
httpClient *http.Client
|
||||
@ -33,10 +38,6 @@ type PhlareDatasource struct {
|
||||
ac accesscontrol.AccessControl
|
||||
}
|
||||
|
||||
type JsonData struct {
|
||||
BackendType string `json:"backendType"`
|
||||
}
|
||||
|
||||
// NewPhlareDatasource creates a new datasource instance.
|
||||
func NewPhlareDatasource(httpClientProvider httpclient.Provider, settings backend.DataSourceInstanceSettings, ac accesscontrol.AccessControl) (instancemgmt.Instance, error) {
|
||||
opt, err := settings.HTTPClientOptions()
|
||||
@ -48,15 +49,9 @@ func NewPhlareDatasource(httpClientProvider httpclient.Provider, settings backen
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var jsonData *JsonData
|
||||
err = json.Unmarshal(settings.JSONData, &jsonData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &PhlareDatasource{
|
||||
httpClient: httpClient,
|
||||
client: getClient(jsonData.BackendType, httpClient, settings.URL),
|
||||
client: NewPhlareClient(httpClient, settings.URL),
|
||||
settings: settings,
|
||||
ac: ac,
|
||||
}, nil
|
||||
@ -73,9 +68,6 @@ func (d *PhlareDatasource) CallResource(ctx context.Context, req *backend.CallRe
|
||||
if req.Path == "labelValues" {
|
||||
return d.labelValues(ctx, req, sender)
|
||||
}
|
||||
if req.Path == "backendType" {
|
||||
return d.backendType(ctx, req, sender)
|
||||
}
|
||||
return sender.Send(&backend.CallResourceResponse{
|
||||
Status: 404,
|
||||
})
|
||||
@ -98,21 +90,7 @@ func (d *PhlareDatasource) profileTypes(ctx context.Context, req *backend.CallRe
|
||||
}
|
||||
|
||||
func (d *PhlareDatasource) labelNames(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
|
||||
u, err := url.Parse(req.URL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
query := u.Query()
|
||||
start, err := strconv.ParseInt(query["start"][0], 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
end, err := strconv.ParseInt(query["end"][0], 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := d.client.LabelNames(ctx, query["query"][0], start, end)
|
||||
res, err := d.client.LabelNames(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error calling LabelNames: %v", err)
|
||||
}
|
||||
@ -140,16 +118,8 @@ func (d *PhlareDatasource) labelValues(ctx context.Context, req *backend.CallRes
|
||||
return err
|
||||
}
|
||||
query := u.Query()
|
||||
start, err := strconv.ParseInt(query["start"][0], 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
end, err := strconv.ParseInt(query["end"][0], 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := d.client.LabelValues(ctx, query["query"][0], query["label"][0], start, end)
|
||||
res, err := d.client.LabelValues(ctx, query["label"][0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("error calling LabelValues: %v", err)
|
||||
}
|
||||
@ -164,74 +134,6 @@ func (d *PhlareDatasource) labelValues(ctx context.Context, req *backend.CallRes
|
||||
return nil
|
||||
}
|
||||
|
||||
type BackendTypeRespBody struct {
|
||||
BackendType string `json:"backendType"` // "phlare" or "pyroscope"
|
||||
}
|
||||
|
||||
// backendType is a simplistic test to figure out if we are speaking to phlare or pyroscope backend
|
||||
func (d *PhlareDatasource) backendType(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
|
||||
// To prevent any user sending arbitrary URL for us to test with we allow this only for users who can edit the datasource
|
||||
// as config page is where this is meant to be used.
|
||||
ok, err := d.isUserAllowedToEditDatasource(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return sender.Send(&backend.CallResourceResponse{Headers: req.Headers, Status: 401})
|
||||
}
|
||||
|
||||
u, err := url.Parse(req.URL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
query := u.Query()
|
||||
body := &BackendTypeRespBody{BackendType: "unknown"}
|
||||
|
||||
// We take the url from the request query because the data source may not yet be saved in DB with the URL we want
|
||||
// to test with (like when filling in the confgi page for the first time)
|
||||
url := query["url"][0]
|
||||
|
||||
pyroClient := getClient("pyroscope", d.httpClient, url)
|
||||
_, err = pyroClient.ProfileTypes(ctx)
|
||||
|
||||
if err == nil {
|
||||
body.BackendType = "pyroscope"
|
||||
} else {
|
||||
phlareClient := getClient("phlare", d.httpClient, url)
|
||||
_, err := phlareClient.ProfileTypes(ctx)
|
||||
if err == nil {
|
||||
body.BackendType = "phlare"
|
||||
}
|
||||
}
|
||||
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sender.Send(&backend.CallResourceResponse{Body: data, Headers: req.Headers, Status: 200})
|
||||
}
|
||||
|
||||
func (d *PhlareDatasource) isUserAllowedToEditDatasource(ctx context.Context) (bool, error) {
|
||||
reqCtx := contexthandler.FromContext(ctx)
|
||||
uidScope := datasources.ScopeProvider.GetResourceScopeUID(accesscontrol.Parameter(":uid"))
|
||||
|
||||
if reqCtx == nil || reqCtx.SignedInUser == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
ok, err := d.ac.Evaluate(ctx, reqCtx.SignedInUser, accesscontrol.EvalPermission(datasources.ActionWrite, uidScope))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// QueryData handles multiple queries and returns multiple responses.
|
||||
// req contains the queries []DataQuery (where each query contains RefID as a unique identifier).
|
||||
// The QueryDataResponse contains a map of RefID to the response for each query, and each response
|
||||
|
@ -11,6 +11,49 @@ import (
|
||||
"github.com/grafana/phlare/api/gen/proto/go/querier/v1/querierv1connect"
|
||||
)
|
||||
|
||||
type ProfileType struct {
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
type Flamebearer struct {
|
||||
Names []string
|
||||
Levels []*Level
|
||||
Total int64
|
||||
MaxSelf int64
|
||||
}
|
||||
|
||||
type Level struct {
|
||||
Values []int64
|
||||
}
|
||||
|
||||
type Series struct {
|
||||
Labels []*LabelPair
|
||||
Points []*Point
|
||||
}
|
||||
|
||||
type LabelPair struct {
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
type Point struct {
|
||||
Value float64
|
||||
// Milliseconds unix timestamp
|
||||
Timestamp int64
|
||||
}
|
||||
|
||||
type ProfileResponse struct {
|
||||
Flamebearer *Flamebearer
|
||||
Units string
|
||||
}
|
||||
|
||||
type SeriesResponse struct {
|
||||
Series []*Series
|
||||
Units string
|
||||
Label string
|
||||
}
|
||||
|
||||
type PhlareClient struct {
|
||||
connectClient querierv1connect.QuerierServiceClient
|
||||
}
|
||||
@ -136,7 +179,7 @@ func getUnits(profileTypeID string) string {
|
||||
return unit
|
||||
}
|
||||
|
||||
func (c *PhlareClient) LabelNames(ctx context.Context, query string, start int64, end int64) ([]string, error) {
|
||||
func (c *PhlareClient) LabelNames(ctx context.Context) ([]string, error) {
|
||||
resp, err := c.connectClient.LabelNames(ctx, connect.NewRequest(&querierv1.LabelNamesRequest{}))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error seding LabelNames request %v", err)
|
||||
@ -152,7 +195,7 @@ func (c *PhlareClient) LabelNames(ctx context.Context, query string, start int64
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
func (c *PhlareClient) LabelValues(ctx context.Context, query string, label string, start int64, end int64) ([]string, error) {
|
||||
func (c *PhlareClient) LabelValues(ctx context.Context, label string) ([]string, error) {
|
||||
resp, err := c.connectClient.LabelValues(ctx, connect.NewRequest(&querierv1.LabelValuesRequest{Name: label}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -1,282 +0,0 @@
|
||||
package phlare
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type PyroscopeClient struct {
|
||||
httpClient *http.Client
|
||||
URL string
|
||||
}
|
||||
|
||||
type App struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func NewPyroscopeClient(httpClient *http.Client, url string) *PyroscopeClient {
|
||||
return &PyroscopeClient{
|
||||
httpClient: httpClient,
|
||||
URL: url,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *PyroscopeClient) ProfileTypes(ctx context.Context) ([]*ProfileType, error) {
|
||||
resp, err := c.httpClient.Get(c.URL + "/api/apps")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
logger.Error("Failed to close response body", "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var apps []App
|
||||
|
||||
err = json.Unmarshal(body, &apps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var profileTypes []*ProfileType
|
||||
for _, app := range apps {
|
||||
profileTypes = append(profileTypes, &ProfileType{
|
||||
ID: app.Name,
|
||||
Label: app.Name,
|
||||
})
|
||||
}
|
||||
|
||||
return profileTypes, nil
|
||||
}
|
||||
|
||||
type PyroscopeProfileResponse struct {
|
||||
Flamebearer *PyroFlamebearer `json:"flamebearer"`
|
||||
Metadata *Metadata `json:"metadata"`
|
||||
Groups map[string]*Group `json:"groups"`
|
||||
}
|
||||
|
||||
type Metadata struct {
|
||||
Units string `json:"units"`
|
||||
}
|
||||
|
||||
type Group struct {
|
||||
StartTime int64 `json:"startTime"`
|
||||
Samples []int64 `json:"samples"`
|
||||
DurationDelta int64 `json:"durationDelta"`
|
||||
}
|
||||
|
||||
type PyroFlamebearer struct {
|
||||
Levels [][]int64 `json:"levels"`
|
||||
MaxSelf int64 `json:"maxSelf"`
|
||||
NumTicks int64 `json:"numTicks"`
|
||||
Names []string `json:"names"`
|
||||
}
|
||||
|
||||
func (c *PyroscopeClient) getProfileData(ctx context.Context, profileTypeID, labelSelector string, start, end int64, maxNodes *int64, groupBy []string) (*PyroscopeProfileResponse, error) {
|
||||
params := url.Values{}
|
||||
params.Add("from", strconv.FormatInt(start, 10))
|
||||
params.Add("until", strconv.FormatInt(end, 10))
|
||||
params.Add("query", profileTypeID+labelSelector)
|
||||
if maxNodes != nil {
|
||||
params.Add("maxNodes", strconv.FormatInt(*maxNodes, 10))
|
||||
}
|
||||
params.Add("format", "json")
|
||||
if len(groupBy) > 0 {
|
||||
params.Add("groupBy", groupBy[0])
|
||||
}
|
||||
|
||||
url := c.URL + "/render?" + params.Encode()
|
||||
logger.Debug("Calling /render", "url", url)
|
||||
|
||||
resp, err := c.httpClient.Get(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error calling /render api: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
logger.Error("Failed to close response body", "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
var respData *PyroscopeProfileResponse
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading response body: %v", err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &respData)
|
||||
if err != nil {
|
||||
logger.Debug("Flamegraph data", "body", string(body))
|
||||
return nil, fmt.Errorf("error decoding flamegraph data: %v", err)
|
||||
}
|
||||
|
||||
return respData, nil
|
||||
}
|
||||
|
||||
func (c *PyroscopeClient) GetProfile(ctx context.Context, profileTypeID, labelSelector string, start, end int64, maxNodes *int64) (*ProfileResponse, error) {
|
||||
respData, err := c.getProfileData(ctx, profileTypeID, labelSelector, start, end, maxNodes, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mappedLevels := make([]*Level, len(respData.Flamebearer.Levels))
|
||||
for i, level := range respData.Flamebearer.Levels {
|
||||
mappedLevels[i] = &Level{
|
||||
Values: level,
|
||||
}
|
||||
}
|
||||
|
||||
units := "short"
|
||||
if respData.Metadata.Units == "bytes" {
|
||||
units = "bytes"
|
||||
}
|
||||
if respData.Metadata.Units == "samples" {
|
||||
units = "ms"
|
||||
}
|
||||
|
||||
return &ProfileResponse{
|
||||
Flamebearer: &Flamebearer{
|
||||
Names: respData.Flamebearer.Names,
|
||||
Levels: mappedLevels,
|
||||
Total: respData.Flamebearer.NumTicks,
|
||||
MaxSelf: respData.Flamebearer.MaxSelf,
|
||||
},
|
||||
Units: units,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *PyroscopeClient) GetSeries(ctx context.Context, profileTypeID string, labelSelector string, start, end int64, groupBy []string, step float64) (*SeriesResponse, error) {
|
||||
// This is super ineffective at the moment. We need 2 different APIs one for profile one for series (timeline) data
|
||||
// but Pyro returns all in single response. This currently does the simplest thing and calls the same API 2 times
|
||||
// and gets the part of the response it needs.
|
||||
respData, err := c.getProfileData(ctx, profileTypeID, labelSelector, start, end, nil, groupBy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stepMillis := int64(step * 1000)
|
||||
var series []*Series
|
||||
|
||||
if len(respData.Groups) == 1 {
|
||||
series = []*Series{processGroup(respData.Groups["*"], stepMillis, nil)}
|
||||
} else {
|
||||
for key, val := range respData.Groups {
|
||||
// If we have a group by, we don't want the * group
|
||||
if key != "*" {
|
||||
label := &LabelPair{
|
||||
Name: groupBy[0],
|
||||
Value: key,
|
||||
}
|
||||
|
||||
series = append(series, processGroup(val, stepMillis, label))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &SeriesResponse{Series: series}, nil
|
||||
}
|
||||
|
||||
// processGroup turns group timeline data into the Series format. Pyro does not seem to have a way to define step, so we
|
||||
// always get the data in specific step, and we have to aggregate a bit into s step size we need.
|
||||
func processGroup(group *Group, step int64, label *LabelPair) *Series {
|
||||
series := &Series{}
|
||||
if label != nil {
|
||||
series.Labels = []*LabelPair{label}
|
||||
}
|
||||
|
||||
durationDeltaMillis := group.DurationDelta * 1000
|
||||
timestamp := group.StartTime * 1000
|
||||
value := int64(0)
|
||||
|
||||
for i, sample := range group.Samples {
|
||||
pointsLen := int64(len(series.Points))
|
||||
// Check if the timestamp of the sample is more than next timestamp in the series. If so we create a new point
|
||||
// with the value we have so far.
|
||||
if int64(i)*durationDeltaMillis > step*pointsLen+1 {
|
||||
series.Points = append(series.Points, &Point{
|
||||
Value: float64(value),
|
||||
Timestamp: timestamp + step*pointsLen,
|
||||
})
|
||||
value = 0
|
||||
}
|
||||
|
||||
value += sample
|
||||
}
|
||||
|
||||
return series
|
||||
}
|
||||
|
||||
func (c *PyroscopeClient) LabelNames(ctx context.Context, query string, start int64, end int64) ([]string, error) {
|
||||
params := url.Values{}
|
||||
// Seems like this should be seconds instead of millis for other endpoints
|
||||
params.Add("from", strconv.FormatInt(start/1000, 10))
|
||||
params.Add("until", strconv.FormatInt(end/1000, 10))
|
||||
params.Add("query", query)
|
||||
resp, err := c.httpClient.Get(c.URL + "/labels?" + params.Encode())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
logger.Error("Failed to close response body", "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
var names []string
|
||||
err = json.NewDecoder(resp.Body).Decode(&names)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var filtered []string
|
||||
for _, label := range names {
|
||||
// Using the same func from Phlare client, works but should do separate one probably
|
||||
if !isPrivateLabel(label) {
|
||||
filtered = append(filtered, label)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
func (c *PyroscopeClient) LabelValues(ctx context.Context, query string, label string, start int64, end int64) ([]string, error) {
|
||||
params := url.Values{}
|
||||
// Seems like this should be seconds instead of millis for other endpoints
|
||||
params.Add("from", strconv.FormatInt(start/1000, 10))
|
||||
params.Add("until", strconv.FormatInt(end/1000, 10))
|
||||
params.Add("label", label)
|
||||
params.Add("query", query)
|
||||
resp, err := c.httpClient.Get(c.URL + "/labels?" + params.Encode())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
logger.Error("Failed to close response body", "err", err)
|
||||
}
|
||||
}()
|
||||
var values []string
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = json.Unmarshal(body, &values)
|
||||
if err != nil {
|
||||
logger.Debug("Response", "body", string(body))
|
||||
return nil, fmt.Errorf("error unmarshaling response %v", err)
|
||||
}
|
||||
|
||||
return values, nil
|
||||
}
|
@ -288,11 +288,11 @@ func (f *FakeClient) ProfileTypes(ctx context.Context) ([]*ProfileType, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *FakeClient) LabelValues(ctx context.Context, query string, label string, start int64, end int64) ([]string, error) {
|
||||
func (f *FakeClient) LabelValues(ctx context.Context, label string) ([]string, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (f *FakeClient) LabelNames(ctx context.Context, query string, start int64, end int64) ([]string, error) {
|
||||
func (f *FakeClient) LabelNames(ctx context.Context) ([]string, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
|
@ -1,57 +1,20 @@
|
||||
import React from 'react';
|
||||
import { useAsyncFn, useDebounce } from 'react-use';
|
||||
|
||||
import { DataSourcePluginOptionsEditorProps, SelectableValue } from '@grafana/data';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { Alert, DataSourceHttpSettings, EventsWithValidation, LegacyForms, regexValidation } from '@grafana/ui';
|
||||
import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
|
||||
import { DataSourceHttpSettings, EventsWithValidation, LegacyForms, regexValidation } from '@grafana/ui';
|
||||
import { config } from 'app/core/config';
|
||||
|
||||
import { PhlareDataSource } from './datasource';
|
||||
import { BackendType, PhlareDataSourceOptions } from './types';
|
||||
import { PhlareDataSourceOptions } from './types';
|
||||
|
||||
interface Props extends DataSourcePluginOptionsEditorProps<PhlareDataSourceOptions> {}
|
||||
|
||||
export const ConfigEditor = (props: Props) => {
|
||||
const { options, onOptionsChange } = props;
|
||||
const [mismatchedBackendType, setMismatchedBackendType] = React.useState<BackendType | undefined>();
|
||||
|
||||
const dataSourceSrv = getDataSourceSrv();
|
||||
|
||||
const [, getBackendType] = useAsyncFn(async () => {
|
||||
if (!options.url) {
|
||||
return;
|
||||
}
|
||||
const ds = await dataSourceSrv.get({ type: options.type, uid: options.uid });
|
||||
if (!(ds instanceof PhlareDataSource)) {
|
||||
// Should not happen, makes TS happy
|
||||
throw new Error('Datasource is not a PhlareDataSource');
|
||||
}
|
||||
|
||||
const { backendType } = await ds.getBackendType(options.url);
|
||||
if (backendType === 'unknown') {
|
||||
setMismatchedBackendType(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
// If user already has something selected don't overwrite but show warning.
|
||||
if (options.jsonData.backendType) {
|
||||
if (backendType !== options.jsonData.backendType) {
|
||||
setMismatchedBackendType(backendType);
|
||||
} else {
|
||||
setMismatchedBackendType(undefined);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
onOptionsChange({ ...options, jsonData: { ...options.jsonData, backendType } });
|
||||
}, [options]);
|
||||
|
||||
useDebounce(getBackendType, 500, [options]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataSourceHttpSettings
|
||||
defaultUrl={'http://localhost:4100'}
|
||||
defaultUrl={'http://localhost:4040'}
|
||||
dataSourceConfig={options}
|
||||
showAccessOptions={false}
|
||||
onChange={onOptionsChange}
|
||||
@ -94,50 +57,7 @@ export const ConfigEditor = (props: Props) => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<LegacyForms.FormField
|
||||
label="Backend type"
|
||||
labelWidth={13}
|
||||
inputEl={
|
||||
<LegacyForms.Select<BackendType>
|
||||
allowCustomValue={false}
|
||||
value={options.jsonData.backendType ? backendTypeOptions[options.jsonData.backendType] : undefined}
|
||||
options={Object.values(backendTypeOptions)}
|
||||
onChange={(option) => {
|
||||
onOptionsChange({
|
||||
...options,
|
||||
jsonData: {
|
||||
...options.jsonData,
|
||||
backendType: option.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
}
|
||||
tooltip="Select what type of backend you use. This datasource supports both Phlare and Pyroscope backends."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{mismatchedBackendType && (
|
||||
<Alert
|
||||
title={`"${options.jsonData.backendType}" option is selected but it seems like you are using "${mismatchedBackendType}" backend.`}
|
||||
severity="warning"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const backendTypeOptions: Record<BackendType, SelectableValue<BackendType>> = {
|
||||
phlare: {
|
||||
label: 'Phlare',
|
||||
value: 'phlare',
|
||||
},
|
||||
pyroscope: {
|
||||
label: 'Pyroscope',
|
||||
value: 'pyroscope',
|
||||
},
|
||||
};
|
||||
|
@ -69,12 +69,7 @@ describe('VariableQueryEditor', () => {
|
||||
});
|
||||
|
||||
function getMockDatasource() {
|
||||
const ds = new PhlareDataSource(
|
||||
{
|
||||
jsonData: { backendType: 'phlare' },
|
||||
} as DataSourceInstanceSettings<PhlareDataSourceOptions>,
|
||||
new TemplateSrv()
|
||||
);
|
||||
const ds = new PhlareDataSource({} as DataSourceInstanceSettings<PhlareDataSourceOptions>, new TemplateSrv());
|
||||
ds.getResource = jest.fn();
|
||||
(ds.getResource as jest.Mock).mockImplementation(async (type: string) => {
|
||||
if (type === 'profileTypes') {
|
||||
|
@ -133,19 +133,13 @@ function ProfileTypeRow(props: {
|
||||
initialValue?: string;
|
||||
}) {
|
||||
const profileTypes = useProfileTypes(props.datasource);
|
||||
const label = props.datasource.backendType === 'phlare' ? 'Profile type' : 'Application';
|
||||
return (
|
||||
<InlineFieldRow>
|
||||
<InlineField
|
||||
label={label}
|
||||
aria-label={label}
|
||||
label={'Profile type'}
|
||||
aria-label={'Profile type'}
|
||||
labelWidth={20}
|
||||
tooltip={
|
||||
<div>
|
||||
Select {props.datasource.backendType === 'phlare' ? 'profile type' : 'application'} for which to retrieve
|
||||
available labels
|
||||
</div>
|
||||
}
|
||||
tooltip={<div>Select profile type for which to retrieve available labels</div>}
|
||||
>
|
||||
{profileTypes ? (
|
||||
<ProfileTypesCascader
|
||||
|
@ -15,17 +15,14 @@ import { extractLabelMatchers, toPromLikeExpr } from '../prometheus/language_uti
|
||||
|
||||
import { VariableSupport } from './VariableSupport';
|
||||
import { defaultGrafanaPyroscope, defaultPhlareQueryType } from './dataquery.gen';
|
||||
import { PhlareDataSourceOptions, Query, ProfileTypeMessage, BackendType } from './types';
|
||||
import { PhlareDataSourceOptions, Query, ProfileTypeMessage } from './types';
|
||||
|
||||
export class PhlareDataSource extends DataSourceWithBackend<Query, PhlareDataSourceOptions> {
|
||||
backendType: BackendType;
|
||||
|
||||
constructor(
|
||||
instanceSettings: DataSourceInstanceSettings<PhlareDataSourceOptions>,
|
||||
private readonly templateSrv: TemplateSrv = getTemplateSrv()
|
||||
) {
|
||||
super(instanceSettings);
|
||||
this.backendType = instanceSettings.jsonData.backendType ?? 'phlare';
|
||||
this.variables = new VariableSupport(this);
|
||||
}
|
||||
|
||||
@ -68,11 +65,6 @@ export class PhlareDataSource extends DataSourceWithBackend<Query, PhlareDataSou
|
||||
});
|
||||
}
|
||||
|
||||
// We need the URL here because it may not be saved on the backend yet when used from config page.
|
||||
async getBackendType(url: string): Promise<{ backendType: BackendType | 'unknown' }> {
|
||||
return await super.getResource('backendType', { url });
|
||||
}
|
||||
|
||||
applyTemplateVariables(query: Query, scopedVars: ScopedVars): Query {
|
||||
return {
|
||||
...query,
|
||||
|
@ -13,7 +13,7 @@
|
||||
"backend": true,
|
||||
|
||||
"info": {
|
||||
"description": "Supports Phlare and Pyroscope backends, horizontally-scalable, highly-available, multi-tenant continuous profiling aggregation systems.",
|
||||
"description": "Data source for Grafana Pyroscope, horizontally-scalable, highly-available, multi-tenant continuous profiling aggregation system.",
|
||||
"author": {
|
||||
"name": "Grafana Labs",
|
||||
"url": "https://www.grafana.com"
|
||||
|
@ -16,11 +16,8 @@ export interface ProfileTypeMessage {
|
||||
*/
|
||||
export interface PhlareDataSourceOptions extends DataSourceJsonData {
|
||||
minStep?: string;
|
||||
backendType?: BackendType; // if not set we assume it's phlare
|
||||
}
|
||||
|
||||
export type BackendType = 'phlare' | 'pyroscope';
|
||||
|
||||
export type ProfileTypeQuery = {
|
||||
type: 'profileType';
|
||||
refId: string;
|
||||
|
Loading…
Reference in New Issue
Block a user