Prometheus: Configuration page overhaul (#66198)

* organize layout, make design uniform, add doc link

* fix e2e test

* move overhauled config parts into prometheus code

* update tooltips with doc links

* clean component styles for section padding, top and bottom 32px

* make additional settings subsection headers h6

* use secondary gray for section descriptions

* fix merge issues

* change inlineswitch to switch only in prom settings because the other components are shared

* remove legacy formfield and input, replace with inlinefield and input from grafana-ui

* find more formfield and UI design fixes like changing inlineformlabel to inlinefield

* remove unused inline form label

* replace legacy duration validations with <FieldValidationMessage>

* fix styles secondary gray from theme

* change language, headings and datasource -> data source

* update alert setting styles with new component

* update prom url heading and tooltip

* update default editor tooltip and set builder as default editor

* update interval tooltip

* update prom type tooltip

* update custom query params tooltip

* update exemplar internal link tooltip

* fix inline form styling inconsistency

* allow for using the DataSourceHTTPSettings component without the connection string

* remove overhaul component, re-use dshtttps comp, and use connection input in config editor

* make tooltips interactive to click links

* consistent label width across the elements we can control for now, fix exemplar switch

* make connection url a component

* refactor onBlur validation

* remove unused component

* add tests for config validations

* add more meaningful health check

* fix e2e test

* fix e2e test

* fix e2e test

* add error handling for more url errors

* remove unnecessary conversion

* health check tests

* Clean up the health check

* health check unit tests

* health check unit tests improved

* make pretty for drone

* lint check go

* lint check go

* add required attr to connection component

* Update public/app/plugins/datasource/prometheus/configuration/Connection.tsx

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>

* fix read only issue for provisioned datasources

* validate multiple durations for incremental query setting

* use sentence case for headers

* use className consistently for styles

* add tests for url regex

* remove console logs

* only use query as healthcheck as the healthy api is not supported by Mimir

* fix exemplar e2e test

* remove overhaul prop from custom headers setting component

* remove connection section and use DatasourceHttpSettings connection with custom label and interactive tooltip

* add spaces back

* spaces

---------

Co-authored-by: ismail simsek <ismailsimsek09@gmail.com>
Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
Brendan O'Handley
2023-04-27 09:43:54 -04:00
committed by GitHub
co-authored by Torkel Ödegaard ismail simsek
parent 5c32925f9f
commit 0a9240aeba
17 changed files with 818 additions and 345 deletions
+93
View File
@@ -0,0 +1,93 @@
package prometheus
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/kindsys"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/tsdb/prometheus/kinds/dataquery"
"github.com/grafana/grafana/pkg/tsdb/prometheus/models"
)
const (
refID = "__healthcheck__"
)
var logger log.Logger = log.New("tsdb.prometheus")
func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult,
error) {
logger := logger.FromContext(ctx)
ds, err := s.getInstance(req.PluginContext)
// check that the datasource exists
if err != nil {
return getHealthCheckMessage(logger, "error getting datasource info", err)
}
if ds == nil {
return getHealthCheckMessage(logger, "", errors.New("invalid datasource info received"))
}
return healthcheck(ctx, req, ds)
}
func healthcheck(ctx context.Context, req *backend.CheckHealthRequest, i *instance) (*backend.CheckHealthResult, error) {
qm := models.QueryModel{
LegendFormat: "",
UtcOffsetSec: 0,
PrometheusDataQuery: dataquery.PrometheusDataQuery{
Expr: "1+1",
Instant: kindsys.Ptr(true),
RefId: refID,
},
}
b, _ := json.Marshal(&qm)
query := backend.DataQuery{
RefID: refID,
TimeRange: backend.TimeRange{
From: time.Unix(1, 0).UTC(),
To: time.Unix(4, 0).UTC(),
},
JSON: b,
}
resp, err := i.queryData.Execute(ctx, &backend.QueryDataRequest{
PluginContext: req.PluginContext,
Queries: []backend.DataQuery{query},
})
if err != nil {
return getHealthCheckMessage(logger, "Your configuration has been saved but there is an error.", err)
}
if resp.Responses[refID].Error != nil {
return getHealthCheckMessage(logger, "Your configuration has been saved but there is an error.",
errors.New(resp.Responses[refID].Error.Error()))
}
return getHealthCheckMessage(logger, "Successfully saved the configuration and queried the Prometheus API.", nil)
}
func getHealthCheckMessage(logger log.Logger, message string, err error) (*backend.CheckHealthResult, error) {
if err == nil {
return &backend.CheckHealthResult{
Status: backend.HealthStatusOk,
Message: message,
}, nil
}
logger.Warn("error performing prometheus healthcheck", "err", err.Error())
errorMessage := fmt.Sprintf("%s - %s", err.Error(), message)
return &backend.CheckHealthResult{
Status: backend.HealthStatusError,
Message: errorMessage,
}, nil
}
+125
View File
@@ -0,0 +1,125 @@
package prometheus
import (
"context"
"net/http"
"testing"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
sdkHttpClient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana/pkg/infra/httpclient"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
)
type healthCheckProvider[T http.RoundTripper] struct {
httpclient.Provider
RoundTripper *T
}
type healthCheckSuccessRoundTripper struct {
}
type healthCheckFailRoundTripper struct {
}
func (rt *healthCheckSuccessRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return &http.Response{
Status: "200",
StatusCode: 200,
Header: nil,
Body: nil,
ContentLength: 0,
Request: req,
}, nil
}
func (rt *healthCheckFailRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return &http.Response{
Status: "400",
StatusCode: 400,
Header: nil,
Body: nil,
ContentLength: 0,
Request: req,
}, nil
}
func (provider *healthCheckProvider[T]) New(opts ...sdkHttpClient.Options) (*http.Client, error) {
client := &http.Client{}
provider.RoundTripper = new(T)
client.Transport = *provider.RoundTripper
return client, nil
}
func (provider *healthCheckProvider[T]) GetTransport(opts ...sdkHttpClient.Options) (http.RoundTripper, error) {
return *new(T), nil
}
func getMockProvider[T http.RoundTripper]() *healthCheckProvider[T] {
return &healthCheckProvider[T]{
RoundTripper: new(T),
}
}
func Test_healthcheck(t *testing.T) {
t.Run("should do a successful health check", func(t *testing.T) {
httpProvider := getMockProvider[*healthCheckSuccessRoundTripper]()
s := &Service{
im: datasource.NewInstanceManager(newInstanceSettings(httpProvider, &setting.Cfg{}, &featuremgmt.FeatureManager{}, nil)),
}
req := &backend.CheckHealthRequest{
PluginContext: getPluginContext(),
Headers: nil,
}
res, err := s.CheckHealth(context.Background(), req)
assert.NoError(t, err)
assert.Equal(t, backend.HealthStatusOk, res.Status)
})
t.Run("should return an error for an unsuccessful health check", func(t *testing.T) {
httpProvider := getMockProvider[*healthCheckFailRoundTripper]()
s := &Service{
im: datasource.NewInstanceManager(newInstanceSettings(httpProvider, &setting.Cfg{}, &featuremgmt.FeatureManager{}, nil)),
}
req := &backend.CheckHealthRequest{
PluginContext: getPluginContext(),
Headers: nil,
}
res, err := s.CheckHealth(context.Background(), req)
assert.NoError(t, err)
assert.Equal(t, backend.HealthStatusError, res.Status)
})
}
func getPluginContext() backend.PluginContext {
return backend.PluginContext{
OrgID: 0,
PluginID: "prometheus",
User: nil,
AppInstanceSettings: nil,
DataSourceInstanceSettings: getPromInstanceSettings(),
}
}
func getPromInstanceSettings() *backend.DataSourceInstanceSettings {
return &backend.DataSourceInstanceSettings{
ID: 0,
UID: "",
Type: "prometheus",
Name: "test-prometheus",
URL: "http://promurl:9090",
User: "",
Database: "",
BasicAuthEnabled: true,
BasicAuthUser: "admin",
JSONData: []byte("{}"),
DecryptedSecureJSONData: map[string]string{},
Updated: time.Time{},
}
}
+7
View File
@@ -179,6 +179,13 @@ func (s *QueryData) instantQuery(ctx context.Context, c *client.Client, q *model
}
}
// This is only for health check fall back scenario
if res.StatusCode != 200 && q.RefId == "__healthcheck__" {
return backend.DataResponse{
Error: fmt.Errorf(res.Status),
}
}
defer func() {
err := res.Body.Close()
if err != nil {