update openapi

This commit is contained in:
Ryan McKinley
2025-07-01 09:14:01 -07:00
110 changed files with 4160 additions and 1131 deletions
+2
View File
@@ -59,7 +59,9 @@ type SecureValueSpec struct {
// The raw value is only valid for write. Read/List will always be empty.
// There is no support for mixing `value` and `ref`, you can't create a secret in a third-party keeper with a specified `ref`.
// Minimum and maximum lengths in bytes.
// +k8s:validation:minLength=1
// +k8s:validation:maxLength=24576
Value ExposedSecureValue `json:"value,omitempty"`
// When using a third-party keeper, the `ref` is used to reference a value inside the remote storage.
@@ -641,8 +641,9 @@ func schema_pkg_apis_secret_v0alpha1_SecureValueSpec(ref common.ReferenceCallbac
},
"value": {
SchemaProps: spec.SchemaProps{
Description: "The raw value is only valid for write. Read/List will always be empty. There is no support for mixing `value` and `ref`, you can't create a secret in a third-party keeper with a specified `ref`.",
Description: "The raw value is only valid for write. Read/List will always be empty. There is no support for mixing `value` and `ref`, you can't create a secret in a third-party keeper with a specified `ref`. Minimum and maximum lengths in bytes.",
MinLength: ptr.To[int64](1),
MaxLength: ptr.To[int64](24576),
Type: []string{"string"},
Format: "",
},
+18 -18
View File
@@ -4,25 +4,25 @@ const (
// All includes all modules necessary for Grafana to run as a standalone server
All string = "all"
Core string = "core"
MemberlistKV string = "memberlistkv"
GrafanaAPIServer string = "grafana-apiserver"
StorageRing string = "storage-ring"
Distributor string = "distributor"
StorageServer string = "storage-server"
ZanzanaServer string = "zanzana-server"
InstrumentationServer string = "instrumentation-server"
FrontendServer string = "frontend-server"
Core string = "core"
MemberlistKV string = "memberlistkv"
GrafanaAPIServer string = "grafana-apiserver"
SearchServerRing string = "search-server-ring"
SearchServerDistributor string = "search-server-distributor"
StorageServer string = "storage-server"
ZanzanaServer string = "zanzana-server"
InstrumentationServer string = "instrumentation-server"
FrontendServer string = "frontend-server"
)
var dependencyMap = map[string][]string{
MemberlistKV: {InstrumentationServer},
StorageRing: {InstrumentationServer, MemberlistKV},
GrafanaAPIServer: {InstrumentationServer},
StorageServer: {InstrumentationServer, StorageRing},
ZanzanaServer: {InstrumentationServer},
Distributor: {InstrumentationServer, MemberlistKV, StorageRing},
Core: {},
All: {Core},
FrontendServer: {},
MemberlistKV: {InstrumentationServer},
SearchServerRing: {InstrumentationServer, MemberlistKV},
GrafanaAPIServer: {InstrumentationServer},
StorageServer: {InstrumentationServer, SearchServerRing},
ZanzanaServer: {InstrumentationServer},
SearchServerDistributor: {InstrumentationServer, MemberlistKV, SearchServerRing},
Core: {},
All: {Core},
FrontendServer: {},
}
+6 -5
View File
@@ -100,7 +100,7 @@ func (c *Client) SendReq(ctx context.Context, url *url.URL, compatOpts CompatOpt
return io.ReadAll(bodyReader)
}
func (c *Client) downloadFile(ctx context.Context, tmpFile *os.File, pluginURL, checksum string, compatOpts CompatOpts) (err error) {
func (c *Client) downloadFile(ctx context.Context, tmpFile *os.File, pluginURL, expectedChecksum string, compatOpts CompatOpts) (err error) {
// Try handling URL as a local file path first
if _, err := os.Stat(pluginURL); err == nil {
// TODO re-verify
@@ -136,7 +136,7 @@ func (c *Client) downloadFile(ctx context.Context, tmpFile *os.File, pluginURL,
if err != nil {
return
}
err = c.downloadFile(ctx, tmpFile, pluginURL, checksum, compatOpts)
err = c.downloadFile(ctx, tmpFile, pluginURL, expectedChecksum, compatOpts)
} else {
c.retryCount = 0
failure := fmt.Sprintf("%v", r)
@@ -169,7 +169,7 @@ func (c *Client) downloadFile(ctx context.Context, tmpFile *os.File, pluginURL,
if c.retryCount < 3 {
c.retryCount++
c.log.Debug("Failed downloading. Will retry.")
err = c.downloadFile(ctx, tmpFile, pluginURL, checksum, compatOpts)
err = c.downloadFile(ctx, tmpFile, pluginURL, expectedChecksum, compatOpts)
}
return err
}
@@ -187,8 +187,9 @@ func (c *Client) downloadFile(ctx context.Context, tmpFile *os.File, pluginURL,
if err = w.Flush(); err != nil {
return fmt.Errorf("failed to write to %q: %w", tmpFile.Name(), err)
}
if len(checksum) > 0 && checksum != fmt.Sprintf("%x", h.Sum(nil)) {
return ErrChecksumMismatch(pluginURL)
computedChecksum := fmt.Sprintf("%x", h.Sum(nil))
if len(expectedChecksum) > 0 && expectedChecksum != computedChecksum {
return ErrChecksumMismatch(pluginURL, expectedChecksum, computedChecksum)
}
c.retryCount = 0
+3 -3
View File
@@ -60,7 +60,7 @@ var (
ErrArcNotFoundBase = errutil.NotFound("plugin.archNotFound").
MustTemplate(ErrArcNotFoundMsg, errutil.WithPublic(ErrArcNotFoundMsg))
ErrChecksumMismatchMsg = "expected SHA256 checksum does not match the downloaded archive ({{.Public.ArchiveURL}}) - please contact security@grafana.com"
ErrChecksumMismatchMsg = "expected SHA256 checksum ({{.Public.ExpectedSHA256}}) does not match the downloaded archive ({{.Public.ArchiveURL}}) computed SHA256 checksum ({{.Public.ComputedSHA256}}) - please contact security@grafana.com"
ErrChecksumMismatchBase = errutil.UnprocessableEntity("plugin.checksumMismatch").
MustTemplate(ErrChecksumMismatchMsg, errutil.WithPublic(ErrChecksumMismatchMsg))
@@ -85,8 +85,8 @@ func ErrArcNotFound(pluginID, systemInfo string) error {
return ErrArcNotFoundBase.Build(errutil.TemplateData{Public: map[string]any{"PluginID": pluginID, "SysInfo": systemInfo}})
}
func ErrChecksumMismatch(archiveURL string) error {
return ErrChecksumMismatchBase.Build(errutil.TemplateData{Public: map[string]any{"ArchiveURL": archiveURL}})
func ErrChecksumMismatch(archiveURL, expectedSHA256, computedSHA256 string) error {
return ErrChecksumMismatchBase.Build(errutil.TemplateData{Public: map[string]any{"ArchiveURL": archiveURL, "ExpectedSHA256": expectedSHA256, "ComputedSHA256": computedSHA256}})
}
func ErrCorePlugin(pluginID string) error {
+4 -2
View File
@@ -49,11 +49,13 @@ func TestErrorTemplates(t *testing.T) {
require.Equal(t, "plugin.archNotFound", base.Public().MessageID)
require.Equal(t, "grafana-test-app is not compatible with your system architecture: darwin-amd64", base.Public().Message)
err = ErrChecksumMismatch("http://localhost:6481/grafana-test-app/versions/1.0.0/download")
expectedChecksum := "abcdef1234567890"
computedChecksum := "abcdef0987654321"
err = ErrChecksumMismatch("http://localhost:6481/grafana-test-app/versions/1.0.0/download", expectedChecksum, computedChecksum)
require.True(t, errors.As(err, base))
require.Equal(t, http.StatusUnprocessableEntity, base.Public().StatusCode)
require.Equal(t, "plugin.checksumMismatch", base.Public().MessageID)
require.Equal(t, "expected SHA256 checksum does not match the downloaded archive (http://localhost:6481/grafana-test-app/versions/1.0.0/download) - please contact security@grafana.com", base.Public().Message)
require.Equal(t, "expected SHA256 checksum (abcdef1234567890) does not match the downloaded archive (http://localhost:6481/grafana-test-app/versions/1.0.0/download) computed SHA256 checksum (abcdef0987654321) - please contact security@grafana.com", base.Public().Message)
err = ErrCorePlugin("grafana-test-app")
require.True(t, errors.As(err, base))
+20 -9
View File
@@ -6,6 +6,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
"github.com/grafana/grafana/pkg/components/simplejson"
@@ -91,11 +92,16 @@ func (r *converter) toAddCommand(ds *v0alpha1.GenericDataSource) (*datasources.A
if r.group != "" && !strings.HasPrefix(ds.APIVersion, r.group) {
return nil, fmt.Errorf("expecting APIGroup: %s", r.group)
}
info, err := types.ParseNamespace(ds.Namespace)
if err != nil {
return nil, err
}
cmd := &datasources.AddDataSourceCommand{
Name: ds.Spec.Title,
UID: ds.Name,
Type: r.dstype,
Name: ds.Spec.Title,
UID: ds.Name,
OrgID: info.OrgID,
Type: r.dstype,
Access: datasources.DsAccess(ds.Spec.Access),
URL: ds.Spec.URL,
@@ -121,11 +127,16 @@ func (r *converter) toUpdateCommand(ds *v0alpha1.GenericDataSource) (*datasource
if r.group != "" && !strings.HasPrefix(ds.APIVersion, r.group) {
return nil, fmt.Errorf("expecting APIGroup: %s", r.group)
}
info, err := types.ParseNamespace(ds.Namespace)
if err != nil {
return nil, err
}
cmd := &datasources.UpdateDataSourceCommand{
Name: ds.Spec.Title,
UID: ds.Name,
Type: r.dstype,
Name: ds.Spec.Title,
UID: ds.Name,
OrgID: info.OrgID,
Type: r.dstype,
Access: datasources.DsAccess(ds.Spec.Access),
URL: ds.Spec.URL,
@@ -136,15 +147,15 @@ func (r *converter) toUpdateCommand(ds *v0alpha1.GenericDataSource) (*datasource
WithCredentials: ds.Spec.WithCredentials,
IsDefault: ds.Spec.IsDefault,
ReadOnly: ds.Spec.ReadOnly,
// The only field different than add
Version: int(ds.Generation),
}
if len(ds.Spec.JsonData.Object) > 0 {
cmd.JsonData = simplejson.NewFromAny(ds.Spec.JsonData.Object)
}
cmd.SecureJsonData = toSecureJsonData(ds)
// The only thing differnet from the add command???
cmd.Version = int(ds.Generation)
return cmd, nil
}
@@ -8,6 +8,9 @@ import (
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
)
// The maximum size of a secure value in bytes when written as raw input.
const SECURE_VALUE_RAW_INPUT_MAX_SIZE_BYTES = 24576 // 24 KiB
type DecryptSecureValue struct {
Keeper *string
Ref string
@@ -13,6 +13,7 @@ import (
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/endpoints/request"
@@ -245,6 +246,13 @@ func ValidateSecureValue(sv, oldSv *secretv0alpha1.SecureValue, operation admiss
}
// General validations.
if len(sv.Spec.Value) > contracts.SECURE_VALUE_RAW_INPUT_MAX_SIZE_BYTES {
errs = append(
errs,
field.TooLong(field.NewPath("spec", "value"), len(sv.Spec.Value), contracts.SECURE_VALUE_RAW_INPUT_MAX_SIZE_BYTES),
)
}
if errs := validateDecrypters(sv.Spec.Decrypters, decryptersAllowList); len(errs) > 0 {
return errs
}
@@ -301,7 +309,7 @@ func validateSecureValueUpdate(sv, oldSv *secretv0alpha1.SecureValue) field.Erro
return errs
}
// validateDecrypters validates that (if populated) the `decrypters` must match "actor_{name}" and must be unique.
// validateDecrypters validates that (if populated) the `decrypters` must be unique.
func validateDecrypters(decrypters []string, decryptersAllowList map[string]struct{}) field.ErrorList {
errs := make(field.ErrorList, 0)
@@ -319,8 +327,17 @@ func validateDecrypters(decrypters []string, decryptersAllowList map[string]stru
decrypterNames := make(map[string]struct{}, 0)
for i, decrypter := range decrypters {
decrypter = strings.TrimSpace(decrypter)
if decrypter == "" {
errs = append(
errs,
field.Invalid(field.NewPath("spec", "decrypters", "["+strconv.Itoa(i)+"]"), decrypter, "decrypters cannot be empty if specified"),
)
continue
}
// Allow List: decrypters must match exactly and be in the allowed list to be able to decrypt.
// This means an allow list item should have the format "actor_{name}" and not just "{name}".
if len(decryptersAllowList) > 0 {
if _, exists := decryptersAllowList[decrypter]; !exists {
errs = append(
@@ -334,17 +351,19 @@ func validateDecrypters(decrypters []string, decryptersAllowList map[string]stru
continue
}
actor, name, found := strings.Cut(strings.TrimSpace(decrypter), "_")
if !found || actor != "actor" || name == "" {
errs = append(
errs,
field.Invalid(field.NewPath("spec", "decrypters", "["+strconv.Itoa(i)+"]"), decrypter, "a decrypter must have the format `actor_{name}`"),
)
// Use the same validation as labels for the decrypters.
if verrs := validation.IsValidLabelValue(decrypter); len(verrs) > 0 {
for _, verr := range verrs {
errs = append(
errs,
field.Invalid(field.NewPath("spec", "decrypters", "["+strconv.Itoa(i)+"]"), decrypter, verr),
)
}
continue
}
if _, exists := decrypterNames[name]; exists {
if _, exists := decrypterNames[decrypter]; exists {
errs = append(
errs,
field.Invalid(field.NewPath("spec", "decrypters", "["+strconv.Itoa(i)+"]"), decrypter, "decrypters must be unique"),
@@ -353,7 +372,7 @@ func validateDecrypters(decrypters []string, decryptersAllowList map[string]stru
continue
}
decrypterNames[name] = struct{}{}
decrypterNames[decrypter] = struct{}{}
}
return errs
@@ -4,12 +4,14 @@ import (
"fmt"
"maps"
"slices"
"strings"
"testing"
"github.com/stretchr/testify/require"
"k8s.io/apiserver/pkg/admission"
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
)
func TestValidateSecureValue(t *testing.T) {
@@ -20,7 +22,7 @@ func TestValidateSecureValue(t *testing.T) {
Description: "description",
Value: "value",
Keeper: &keeper,
Decrypters: []string{"actor_app1", "actor_app2"},
Decrypters: []string{"app1", "app2"},
},
}
@@ -50,6 +52,16 @@ func TestValidateSecureValue(t *testing.T) {
require.Len(t, errs, 1)
require.Equal(t, "spec", errs[0].Field)
})
t.Run("`value` cannot exceed 24576 bytes", func(t *testing.T) {
sv := validSecureValue.DeepCopy()
sv.Spec.Value = secretv0alpha1.NewExposedSecureValue(strings.Repeat("a", contracts.SECURE_VALUE_RAW_INPUT_MAX_SIZE_BYTES+1))
sv.Spec.Ref = nil
errs := ValidateSecureValue(sv, nil, admission.Create, nil)
require.Len(t, errs, 1)
require.Equal(t, "spec.value", errs[0].Field)
})
})
t.Run("when updating a securevalue", func(t *testing.T) {
@@ -175,8 +187,8 @@ func TestValidateSecureValue(t *testing.T) {
Description: "description", Ref: &ref,
Decrypters: []string{
"actor_app1",
"actor_app1",
"app1",
"app1",
},
},
}
@@ -186,33 +198,8 @@ func TestValidateSecureValue(t *testing.T) {
require.Equal(t, "spec.decrypters.[1]", errs[0].Field)
})
t.Run("`decrypters` must match the expected format", func(t *testing.T) {
ref := "ref"
sv := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{
Description: "description", Ref: &ref,
Decrypters: []string{
"app1",
"_app1",
"actr_app1",
"actor_ ",
"actor_",
},
},
}
errs := ValidateSecureValue(sv, nil, admission.Create, nil)
require.Len(t, errs, len(sv.Spec.Decrypters))
for i, err := range errs {
require.Equal(t, fmt.Sprintf("spec.decrypters.[%d]", i), err.Field)
require.Contains(t, err.Error(), "a decrypter must have the format `actor_{name}`")
}
})
t.Run("when set, the `decrypters` must be one of the allowed in the allow list", func(t *testing.T) {
allowList := map[string]struct{}{"actor_app1": {}, "actor_app2": {}}
allowList := map[string]struct{}{"app1": {}, "app2": {}}
decrypters := slices.Collect(maps.Keys(allowList))
t.Run("no matches, returns an error", func(t *testing.T) {
@@ -221,7 +208,7 @@ func TestValidateSecureValue(t *testing.T) {
Spec: secretv0alpha1.SecureValueSpec{
Description: "description", Ref: &ref,
Decrypters: []string{"actor_app3"},
Decrypters: []string{"app3"},
},
}
@@ -272,10 +259,37 @@ func TestValidateSecureValue(t *testing.T) {
})
})
t.Run("`decrypters` must be a valid label value", func(t *testing.T) {
decrypters := []string{
"", // invalid
"is/this/valid", // invalid
"is this valid", // invalid
"is.this.valid",
"is-this-valid",
"is_this_valid",
"0isthisvalid9",
"isthisvalid9",
"0isthisvalid",
"isthisvalid",
}
ref := "ref"
sv := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{
Description: "description", Ref: &ref,
Decrypters: decrypters,
},
}
errs := ValidateSecureValue(sv, nil, admission.Create, nil)
require.Len(t, errs, 3)
})
t.Run("`decrypters` cannot have more than 64 items", func(t *testing.T) {
decrypters := make([]string, 0, 64+1)
for i := 0; i < 64+1; i++ {
decrypters = append(decrypters, fmt.Sprintf("actor_app%d", i))
decrypters = append(decrypters, fmt.Sprintf("app%d", i))
}
ref := "ref"
+17 -8
View File
@@ -53,7 +53,16 @@ func NewModule(opts Options,
return s, nil
}
func newModuleServer(opts Options, apiOpts api.ServerOptions, features featuremgmt.FeatureToggles, cfg *setting.Cfg, storageMetrics *resource.StorageMetrics, indexMetrics *resource.BleveIndexMetrics, reg prometheus.Registerer, promGatherer prometheus.Gatherer, license licensing.Licensing) (*ModuleServer, error) {
func newModuleServer(opts Options,
apiOpts api.ServerOptions,
features featuremgmt.FeatureToggles,
cfg *setting.Cfg,
storageMetrics *resource.StorageMetrics,
indexMetrics *resource.BleveIndexMetrics,
reg prometheus.Registerer,
promGatherer prometheus.Gatherer,
license licensing.Licensing,
) (*ModuleServer, error) {
rootCtx, shutdownFn := context.WithCancel(context.Background())
s := &ModuleServer{
@@ -107,10 +116,10 @@ type ModuleServer struct {
promGatherer prometheus.Gatherer
registerer prometheus.Registerer
MemberlistKVConfig kv.Config
httpServerRouter *mux.Router
storageRing *ring.Ring
storageRingClientPool *ringclient.Pool
MemberlistKVConfig kv.Config
httpServerRouter *mux.Router
searchServerRing *ring.Ring
searchServerRingClientPool *ringclient.Pool
}
// init initializes the server and its services.
@@ -153,8 +162,8 @@ func (s *ModuleServer) Run() error {
})
m.RegisterModule(modules.MemberlistKV, s.initMemberlistKV)
m.RegisterModule(modules.StorageRing, s.initRing)
m.RegisterModule(modules.Distributor, s.initDistributor)
m.RegisterModule(modules.SearchServerRing, s.initSearchServerRing)
m.RegisterModule(modules.SearchServerDistributor, s.initSearchServerDistributor)
m.RegisterModule(modules.Core, func() (services.Service, error) {
return NewService(s.cfg, s.opts, s.apiOpts)
@@ -174,7 +183,7 @@ func (s *ModuleServer) Run() error {
if err != nil {
return nil, err
}
return sql.ProvideUnifiedStorageGrpcService(s.cfg, s.features, nil, s.log, s.registerer, docBuilders, s.storageMetrics, s.indexMetrics, s.storageRing, s.MemberlistKVConfig)
return sql.ProvideUnifiedStorageGrpcService(s.cfg, s.features, nil, s.log, s.registerer, docBuilders, s.storageMetrics, s.indexMetrics, s.searchServerRing, s.MemberlistKVConfig)
})
m.RegisterModule(modules.ZanzanaServer, func() (services.Service, error) {
+7 -7
View File
@@ -25,7 +25,7 @@ import (
var metricsPrefix = resource.RingName + "_"
func (ms *ModuleServer) initRing() (services.Service, error) {
func (ms *ModuleServer) initSearchServerRing() (services.Service, error) {
if !ms.cfg.EnableSharding {
return nil, nil
}
@@ -48,7 +48,7 @@ func (ms *ModuleServer) initRing() (services.Service, error) {
return nil, fmt.Errorf("failed to create KV store client: %s", err)
}
storageRing, err := ring.NewWithStoreClientAndStrategy(
searchServerRing, err := ring.NewWithStoreClientAndStrategy(
toRingConfig(ms.cfg, ms.MemberlistKVConfig),
resource.RingName,
resource.RingKey,
@@ -58,11 +58,11 @@ func (ms *ModuleServer) initRing() (services.Service, error) {
logger,
)
if err != nil {
return nil, fmt.Errorf("failed to initialize storage-ring ring: %s", err)
return nil, fmt.Errorf("failed to initialize index-server-ring ring: %s", err)
}
startFn := func(ctx context.Context) error {
err = storageRing.StartAsync(ctx)
err = searchServerRing.StartAsync(ctx)
if err != nil {
return fmt.Errorf("failed to start the ring: %s", err)
}
@@ -74,10 +74,10 @@ func (ms *ModuleServer) initRing() (services.Service, error) {
return nil
}
ms.storageRing = storageRing
ms.storageRingClientPool = pool
ms.searchServerRing = searchServerRing
ms.searchServerRingClientPool = pool
ms.httpServerRouter.Path("/ring").Methods("GET", "POST").Handler(storageRing)
ms.httpServerRouter.Path("/ring").Methods("GET", "POST").Handler(searchServerRing)
svc := services.NewIdleService(startFn, nil)
@@ -10,18 +10,18 @@ import (
"go.opentelemetry.io/otel"
)
func (ms *ModuleServer) initDistributor() (services.Service, error) {
func (ms *ModuleServer) initSearchServerDistributor() (services.Service, error) {
var (
distributor = &distributorService{}
tracer = otel.Tracer("unified-storage-distributor")
tracer = otel.Tracer("index-server-distributor")
err error
)
distributor.grpcHandler, err = resource.ProvideDistributorServer(ms.cfg, ms.features, ms.registerer, tracer, ms.storageRing, ms.storageRingClientPool)
distributor.grpcHandler, err = resource.ProvideSearchDistributorServer(ms.cfg, ms.features, ms.registerer, tracer, ms.searchServerRing, ms.searchServerRingClientPool)
if err != nil {
return nil, err
}
return services.NewBasicService(nil, distributor.running, nil).WithName(modules.Distributor), nil
return services.NewBasicService(nil, distributor.running, nil).WithName(modules.SearchServerDistributor), nil
}
type distributorService struct {
@@ -273,7 +273,7 @@ func initDistributorServerForTest(t *testing.T, memberlistPort int) testModuleSe
cfg.MemberlistJoinMember = "127.0.0.1:" + strconv.Itoa(memberlistPort)
cfg.MemberlistAdvertiseAddr = "127.0.0.1"
cfg.MemberlistAdvertisePort = memberlistPort
cfg.Target = []string{modules.Distributor}
cfg.Target = []string{modules.SearchServerDistributor}
cfg.InstanceID = "distributor" // does nothing for the distributor but may be useful to debug tests
conn, err := grpc.NewClient(cfg.GRPCServer.Address,
@@ -352,7 +352,18 @@ func createBaselineServer(t *testing.T, dbType, dbConnStr string, testNamespaces
require.NoError(t, err)
searchOpts, err := search.NewSearchOptions(features, cfg, tracer, docBuilders, nil)
require.NoError(t, err)
server, err := sql.NewResourceServer(nil, cfg, tracer, nil, nil, searchOpts, nil, nil, features)
server, err := sql.NewResourceServer(sql.ServerOptions{
DB: nil,
Cfg: cfg,
Tracer: tracer,
Reg: nil,
AccessClient: nil,
SearchOptions: searchOpts,
StorageMetrics: nil,
IndexMetrics: nil,
Features: features,
QOSQueue: nil,
})
require.NoError(t, err)
testUserA := &identity.StaticRequester{
File diff suppressed because one or more lines are too long
+4 -18
View File
@@ -697,13 +697,6 @@ var (
Stage: FeatureStageExperimental,
Owner: grafanaDatavizSquad,
},
{
Name: "regressionTransformation",
Description: "Enables regression analysis transformation",
Stage: FeatureStagePublicPreview,
FrontendOnly: true,
Owner: grafanaDatavizSquad,
},
{
// this is mainly used as a way to quickly disable query hints as a safeguard for our infrastructure
Name: "lokiQueryHints",
@@ -1145,7 +1138,8 @@ var (
{
Name: "improvedExternalSessionHandling",
Description: "Enables improved support for OAuth external sessions. After enabling this feature, users might need to re-authenticate themselves.",
Stage: FeatureStagePublicPreview,
Stage: FeatureStageGeneralAvailability,
Expression: "true", // enabled by default
Owner: identityAccessTeam,
AllowSelfServe: true,
},
@@ -1367,7 +1361,8 @@ var (
{
Name: "improvedExternalSessionHandlingSAML",
Description: "Enables improved support for SAML external sessions. Ensure the NameID format is correctly configured in Grafana for SAML Single Logout to function properly.",
Stage: FeatureStagePublicPreview,
Stage: FeatureStageGeneralAvailability,
Expression: "true", // enabled by default
Owner: identityAccessTeam,
AllowSelfServe: true,
},
@@ -1675,15 +1670,6 @@ var (
HideFromDocs: true,
Expression: "true", // enabled by default
},
{
Name: "extensionsReadOnlyProxy",
Description: "Use proxy-based read-only objects for plugin extensions instead of deep cloning",
Stage: FeatureStageExperimental,
Owner: grafanaPluginsPlatformSquad,
HideFromAdminPage: true,
HideFromDocs: true,
FrontendOnly: true,
},
{
Name: "kubernetesAuthzApis",
Description: "Registers AuthZ /apis endpoint",
+2 -4
View File
@@ -92,7 +92,6 @@ logsInfiniteScrolling,GA,@grafana/observability-logs,false,false,true
logRowsPopoverMenu,GA,@grafana/observability-logs,false,false,true
pluginsSkipHostEnvVars,experimental,@grafana/plugins-platform-backend,false,false,false
tableSharedCrosshair,experimental,@grafana/dataviz-squad,false,false,true
regressionTransformation,preview,@grafana/dataviz-squad,false,false,true
lokiQueryHints,GA,@grafana/observability-logs,false,false,true
kubernetesFeatureToggles,experimental,@grafana/grafana-operator-experience-squad,false,false,true
cloudRBACRoles,preview,@grafana/identity-access-team,false,true,false
@@ -148,7 +147,7 @@ exploreLogsLimitedTimeRange,experimental,@grafana/observability-logs,false,false
appPlatformGrpcClientAuth,experimental,@grafana/identity-access-team,false,false,false
groupAttributeSync,privatePreview,@grafana/identity-access-team,false,false,false
alertingQueryAndExpressionsStepMode,GA,@grafana/alerting-squad,false,false,true
improvedExternalSessionHandling,preview,@grafana/identity-access-team,false,false,false
improvedExternalSessionHandling,GA,@grafana/identity-access-team,false,false,false
useSessionStorageForRedirection,GA,@grafana/identity-access-team,false,false,false
rolePickerDrawer,experimental,@grafana/identity-access-team,false,false,false
unifiedStorageSearch,experimental,@grafana/search-and-storage,false,false,false
@@ -179,7 +178,7 @@ lokiLabelNamesQueryApi,GA,@grafana/observability-logs,false,false,false
investigationsBackend,experimental,@grafana/grafana-app-platform-squad,false,false,false
k8SFolderCounts,experimental,@grafana/search-and-storage,false,false,false
k8SFolderMove,experimental,@grafana/search-and-storage,false,false,false
improvedExternalSessionHandlingSAML,preview,@grafana/identity-access-team,false,false,false
improvedExternalSessionHandlingSAML,GA,@grafana/identity-access-team,false,false,false
teamHttpHeadersMimir,GA,@grafana/identity-access-team,false,false,false
teamHttpHeadersTempo,experimental,@grafana/identity-access-team,false,false,false
templateVariablesUsesCombobox,experimental,@grafana/grafana-frontend-platform,false,false,true
@@ -219,7 +218,6 @@ multiTenantFrontend,experimental,@grafana/grafana-frontend-platform,false,false,
alertingListViewV2PreviewToggle,privatePreview,@grafana/alerting-squad,false,false,true
alertRuleUseFiredAtForStartsAt,experimental,@grafana/alerting-squad,false,false,false
alertingBulkActionsInUI,GA,@grafana/alerting-squad,false,false,true
extensionsReadOnlyProxy,experimental,@grafana/plugins-platform-backend,false,false,true
kubernetesAuthzApis,experimental,@grafana/identity-access-team,false,false,false
restoreDashboards,experimental,@grafana/grafana-frontend-platform,false,false,false
skipTokenRotationIfRecent,privatePreview,@grafana/identity-access-team,false,false,false
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
92 logRowsPopoverMenu GA @grafana/observability-logs false false true
93 pluginsSkipHostEnvVars experimental @grafana/plugins-platform-backend false false false
94 tableSharedCrosshair experimental @grafana/dataviz-squad false false true
regressionTransformation preview @grafana/dataviz-squad false false true
95 lokiQueryHints GA @grafana/observability-logs false false true
96 kubernetesFeatureToggles experimental @grafana/grafana-operator-experience-squad false false true
97 cloudRBACRoles preview @grafana/identity-access-team false true false
147 appPlatformGrpcClientAuth experimental @grafana/identity-access-team false false false
148 groupAttributeSync privatePreview @grafana/identity-access-team false false false
149 alertingQueryAndExpressionsStepMode GA @grafana/alerting-squad false false true
150 improvedExternalSessionHandling preview GA @grafana/identity-access-team false false false
151 useSessionStorageForRedirection GA @grafana/identity-access-team false false false
152 rolePickerDrawer experimental @grafana/identity-access-team false false false
153 unifiedStorageSearch experimental @grafana/search-and-storage false false false
178 investigationsBackend experimental @grafana/grafana-app-platform-squad false false false
179 k8SFolderCounts experimental @grafana/search-and-storage false false false
180 k8SFolderMove experimental @grafana/search-and-storage false false false
181 improvedExternalSessionHandlingSAML preview GA @grafana/identity-access-team false false false
182 teamHttpHeadersMimir GA @grafana/identity-access-team false false false
183 teamHttpHeadersTempo experimental @grafana/identity-access-team false false false
184 templateVariablesUsesCombobox experimental @grafana/grafana-frontend-platform false false true
218 alertingListViewV2PreviewToggle privatePreview @grafana/alerting-squad false false true
219 alertRuleUseFiredAtForStartsAt experimental @grafana/alerting-squad false false false
220 alertingBulkActionsInUI GA @grafana/alerting-squad false false true
extensionsReadOnlyProxy experimental @grafana/plugins-platform-backend false false true
221 kubernetesAuthzApis experimental @grafana/identity-access-team false false false
222 restoreDashboards experimental @grafana/grafana-frontend-platform false false false
223 skipTokenRotationIfRecent privatePreview @grafana/identity-access-team false false false
-8
View File
@@ -379,10 +379,6 @@ const (
// Enables shared crosshair in table panel
FlagTableSharedCrosshair = "tableSharedCrosshair"
// FlagRegressionTransformation
// Enables regression analysis transformation
FlagRegressionTransformation = "regressionTransformation"
// FlagLokiQueryHints
// Enables query hints for Loki
FlagLokiQueryHints = "lokiQueryHints"
@@ -887,10 +883,6 @@ const (
// Enables the alerting bulk actions in the UI
FlagAlertingBulkActionsInUI = "alertingBulkActionsInUI"
// FlagExtensionsReadOnlyProxy
// Use proxy-based read-only objects for plugin extensions instead of deep cloning
FlagExtensionsReadOnlyProxy = "extensionsReadOnlyProxy"
// FlagKubernetesAuthzApis
// Registers AuthZ /apis endpoint
FlagKubernetesAuthzApis = "kubernetesAuthzApis"
+20 -10
View File
@@ -1110,7 +1110,8 @@
"metadata": {
"name": "extensionsReadOnlyProxy",
"resourceVersion": "1750434297879",
"creationTimestamp": "2025-05-06T04:55:23Z"
"creationTimestamp": "2025-05-06T04:55:23Z",
"deletionTimestamp": "2025-06-30T08:24:11Z"
},
"spec": {
"description": "Use proxy-based read-only objects for plugin extensions instead of deep cloning",
@@ -1398,27 +1399,35 @@
{
"metadata": {
"name": "improvedExternalSessionHandling",
"resourceVersion": "1750434297879",
"creationTimestamp": "2024-09-17T10:54:39Z"
"resourceVersion": "1751355094344",
"creationTimestamp": "2024-09-17T10:54:39Z",
"annotations": {
"grafana.app/updatedTimestamp": "2025-07-01 07:31:34.344238 +0000 UTC"
}
},
"spec": {
"description": "Enables improved support for OAuth external sessions. After enabling this feature, users might need to re-authenticate themselves.",
"stage": "preview",
"stage": "GA",
"codeowner": "@grafana/identity-access-team",
"allowSelfServe": true
"allowSelfServe": true,
"expression": "true"
}
},
{
"metadata": {
"name": "improvedExternalSessionHandlingSAML",
"resourceVersion": "1750434297879",
"creationTimestamp": "2025-01-09T17:02:49Z"
"resourceVersion": "1751355094344",
"creationTimestamp": "2025-01-09T17:02:49Z",
"annotations": {
"grafana.app/updatedTimestamp": "2025-07-01 07:31:34.344238 +0000 UTC"
}
},
"spec": {
"description": "Enables improved support for SAML external sessions. Ensure the NameID format is correctly configured in Grafana for SAML Single Logout to function properly.",
"stage": "preview",
"stage": "GA",
"codeowner": "@grafana/identity-access-team",
"allowSelfServe": true
"allowSelfServe": true,
"expression": "true"
}
},
{
@@ -2606,7 +2615,8 @@
"metadata": {
"name": "regressionTransformation",
"resourceVersion": "1750434297879",
"creationTimestamp": "2023-11-24T14:49:16Z"
"creationTimestamp": "2023-11-24T14:49:16Z",
"deletionTimestamp": "2025-07-01T13:24:02Z"
},
"spec": {
"description": "Enables regression analysis transformation",
+3
View File
@@ -567,6 +567,9 @@ type Cfg struct {
IndexRebuildInterval time.Duration
IndexCacheTTL time.Duration
EnableSharding bool
QOSEnabled bool
QOSNumberWorker int
QOSMaxSizePerTenant int
MemberlistBindAddr string
MemberlistAdvertiseAddr string
MemberlistAdvertisePort int
+4 -1
View File
@@ -49,13 +49,16 @@ func (cfg *Cfg) setUnifiedStorageConfig() {
}
cfg.UnifiedStorage = storageConfig
// Set indexer config for unified storaae
// Set indexer config for unified storage
section := cfg.Raw.Section("unified_storage")
cfg.MaxPageSizeBytes = section.Key("max_page_size_bytes").MustInt(0)
cfg.IndexPath = section.Key("index_path").String()
cfg.IndexWorkers = section.Key("index_workers").MustInt(10)
cfg.IndexMaxBatchSize = section.Key("index_max_batch_size").MustInt(100)
cfg.EnableSharding = section.Key("enable_sharding").MustBool(false)
cfg.QOSEnabled = section.Key("qos_enabled").MustBool(false)
cfg.QOSNumberWorker = section.Key("qos_num_worker").MustInt(16)
cfg.QOSMaxSizePerTenant = section.Key("qos_max_size_per_tenant").MustInt(1000)
cfg.MemberlistBindAddr = section.Key("memberlist_bind_addr").String()
cfg.MemberlistAdvertiseAddr = section.Key("memberlist_advertise_addr").String()
cfg.MemberlistAdvertisePort = section.Key("memberlist_advertise_port").MustInt(7946)
+46 -3
View File
@@ -20,6 +20,7 @@ import (
"github.com/grafana/dskit/flagext"
"github.com/grafana/dskit/grpcclient"
"github.com/grafana/dskit/middleware"
"github.com/grafana/dskit/services"
infraDB "github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/tracing"
@@ -31,6 +32,7 @@ import (
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/search"
"github.com/grafana/grafana/pkg/storage/unified/sql"
"github.com/grafana/grafana/pkg/util/scheduler"
)
type Options struct {
@@ -49,7 +51,10 @@ type clientMetrics struct {
}
// This adds a UnifiedStorage client into the wire dependency tree
func ProvideUnifiedStorageClient(opts *Options, storageMetrics *resource.StorageMetrics, indexMetrics *resource.BleveIndexMetrics) (resource.ResourceClient, error) {
func ProvideUnifiedStorageClient(opts *Options,
storageMetrics *resource.StorageMetrics,
indexMetrics *resource.BleveIndexMetrics,
) (resource.ResourceClient, error) {
// See: apiserver.applyAPIServerConfig(cfg, features, o)
apiserverCfg := opts.Cfg.SectionWithEnvOverrides("grafana-apiserver")
client, err := newClient(options.StorageOptions{
@@ -83,6 +88,7 @@ func newClient(opts options.StorageOptions,
indexMetrics *resource.BleveIndexMetrics,
) (resource.ResourceClient, error) {
ctx := context.Background()
switch opts.StorageType {
case options.StorageTypeFile:
if opts.DataPath == "" {
@@ -146,13 +152,50 @@ func newClient(opts options.StorageOptions,
}
return client, nil
// Use the local SQL
default:
searchOptions, err := search.NewSearchOptions(features, cfg, tracer, docs, indexMetrics)
if err != nil {
return nil, err
}
server, err := sql.NewResourceServer(db, cfg, tracer, reg, authzc, searchOptions, storageMetrics, indexMetrics, features)
serverOptions := sql.ServerOptions{
DB: db,
Cfg: cfg,
Tracer: tracer,
Reg: reg,
AccessClient: authzc,
SearchOptions: searchOptions,
StorageMetrics: storageMetrics,
IndexMetrics: indexMetrics,
Features: features,
}
if cfg.QOSEnabled {
qosReg := prometheus.WrapRegistererWithPrefix("resource_server_qos_", reg)
queue := scheduler.NewQueue(&scheduler.QueueOptions{
MaxSizePerTenant: cfg.QOSMaxSizePerTenant,
Registerer: qosReg,
Logger: cfg.Logger,
})
if err := services.StartAndAwaitRunning(ctx, queue); err != nil {
return nil, fmt.Errorf("failed to start queue: %w", err)
}
scheduler, err := scheduler.NewScheduler(queue, &scheduler.Config{
NumWorkers: cfg.QOSNumberWorker,
Logger: cfg.Logger,
})
if err != nil {
return nil, fmt.Errorf("failed to create scheduler: %w", err)
}
err = services.StartAndAwaitRunning(ctx, scheduler)
if err != nil {
return nil, fmt.Errorf("failed to start scheduler: %w", err)
}
serverOptions.QOSQueue = queue
}
server, err := sql.NewResourceServer(serverOptions)
if err != nil {
return nil, err
}
+16
View File
@@ -12,6 +12,7 @@ import (
grpcstatus "google.golang.org/grpc/status"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
"github.com/grafana/grafana/pkg/util/scheduler"
)
// Package-level errors.
@@ -50,6 +51,14 @@ func NewNotFoundError(key *resourcepb.ResourceKey) *resourcepb.ErrorResult {
}
}
func NewTooManyRequestsError(msg string) *resourcepb.ErrorResult {
return &resourcepb.ErrorResult{
Message: msg,
Code: http.StatusTooManyRequests,
Reason: string(metav1.StatusReasonTooManyRequests),
}
}
// Convert golang errors to status result errors that can be returned to a client
func AsErrorResult(err error) *resourcepb.ErrorResult {
if err == nil {
@@ -125,3 +134,10 @@ func GetError(res *resourcepb.ErrorResult) error {
}
return status
}
func HandleQueueError[T any](err error, makeResp func(*resourcepb.ErrorResult) *T) (*T, error) {
if errors.Is(err, scheduler.ErrTenantQueueFull) {
return makeResp(NewTooManyRequestsError("tenant queue is full, please try again later")), nil
}
return makeResp(AsErrorResult(err)), nil
}
@@ -21,7 +21,7 @@ import (
"google.golang.org/grpc/metadata"
)
func ProvideDistributorServer(cfg *setting.Cfg, features featuremgmt.FeatureToggles, registerer prometheus.Registerer, tracer trace.Tracer, ring *ring.Ring, ringClientPool *ringclient.Pool) (grpcserver.Provider, error) {
func ProvideSearchDistributorServer(cfg *setting.Cfg, features featuremgmt.FeatureToggles, registerer prometheus.Registerer, tracer trace.Tracer, ring *ring.Ring, ringClientPool *ringclient.Pool) (grpcserver.Provider, error) {
var err error
grpcHandler, err := grpcserver.ProvideService(cfg, features, nil, tracer, registerer)
if err != nil {
@@ -29,7 +29,7 @@ func ProvideDistributorServer(cfg *setting.Cfg, features featuremgmt.FeatureTogg
}
distributorServer := &distributorServer{
log: log.New("unified-storage-distributor"),
log: log.New("index-server-distributor"),
ring: ring,
clientPool: ringClientPool,
}
@@ -73,8 +73,8 @@ func (c *RingClient) RemoteAddress() string {
return c.Conn.Target()
}
const RingKey = "unified-storage-ring"
const RingName = "unified_storage_ring"
const RingKey = "search-server-ring"
const RingName = "search_server_ring"
const RingHeartbeatTimeout = time.Minute
const RingNumTokens = 128
+139
View File
@@ -19,9 +19,20 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
claims "github.com/grafana/authlib/types"
"github.com/grafana/dskit/backoff"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
"github.com/grafana/grafana/pkg/util/scheduler"
)
const (
// DefaultMaxBackoff is the default maximum backoff duration for enqueue operations.
DefaultMaxBackoff = 1 * time.Second
// DefaultMinBackoff is the default minimum backoff duration for enqueue operations.
DefaultMinBackoff = 100 * time.Millisecond
// DefaultMaxRetries is the default maximum number of retries for enqueue operations.
DefaultMaxRetries = 3
)
// ResourceServer implements all gRPC services
@@ -134,6 +145,10 @@ type BlobSupport interface {
// TODO? List+Delete? This is for admin access
}
type QOSEnqueuer interface {
Enqueue(ctx context.Context, tenantID string, runnable func(ctx context.Context)) error
}
type BlobConfig struct {
// The CDK configuration URL
URL string
@@ -203,7 +218,11 @@ type ResourceServerOptions struct {
IndexMetrics *BleveIndexMetrics
// MaxPageSizeBytes is the maximum size of a page in bytes.
MaxPageSizeBytes int
// QOSQueue is the quality of service queue used to enqueue
QOSQueue QOSEnqueuer
}
func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) {
@@ -222,6 +241,7 @@ func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) {
if opts.Diagnostics == nil {
opts.Diagnostics = &noopService{}
}
if opts.Now == nil {
opts.Now = func() int64 {
return time.Now().UnixMilli()
@@ -233,6 +253,10 @@ func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) {
opts.MaxPageSizeBytes = 1024 * 1024 * 2
}
if opts.QOSQueue == nil {
opts.QOSQueue = scheduler.NewNoopQueue()
}
// Initialize the blob storage
blobstore := opts.Blob.Backend
if blobstore == nil {
@@ -275,6 +299,8 @@ func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) {
storageMetrics: opts.storageMetrics,
indexMetrics: opts.IndexMetrics,
maxPageSizeBytes: opts.MaxPageSizeBytes,
reg: opts.Reg,
queue: opts.QOSQueue,
}
if opts.Search.Resources != nil {
@@ -321,6 +347,8 @@ type server struct {
initErr error
maxPageSizeBytes int
reg prometheus.Registerer
queue QOSEnqueuer
}
// Init implements ResourceServer.
@@ -570,6 +598,25 @@ func (s *server) Create(ctx context.Context, req *resourcepb.CreateRequest) (*re
return rsp, nil
}
var (
res *resourcepb.CreateResponse
err error
)
runErr := s.runInQueue(ctx, req.Key.Namespace, func(ctx context.Context) {
res, err = s.create(ctx, user, req)
})
if runErr != nil {
return HandleQueueError(runErr, func(e *resourcepb.ErrorResult) *resourcepb.CreateResponse {
return &resourcepb.CreateResponse{Error: e}
})
}
return res, err
}
func (s *server) create(ctx context.Context, user claims.AuthInfo, req *resourcepb.CreateRequest) (*resourcepb.CreateResponse, error) {
rsp := &resourcepb.CreateResponse{}
event, e := s.newEvent(ctx, user, req.Key, req.Value, nil)
if e != nil {
rsp.Error = e
@@ -605,6 +652,24 @@ func (s *server) Update(ctx context.Context, req *resourcepb.UpdateRequest) (*re
return rsp, nil
}
var (
res *resourcepb.UpdateResponse
err error
)
runErr := s.runInQueue(ctx, req.Key.Namespace, func(ctx context.Context) {
res, err = s.update(ctx, user, req)
})
if runErr != nil {
return HandleQueueError(runErr, func(e *resourcepb.ErrorResult) *resourcepb.UpdateResponse {
return &resourcepb.UpdateResponse{Error: e}
})
}
return res, err
}
func (s *server) update(ctx context.Context, user claims.AuthInfo, req *resourcepb.UpdateRequest) (*resourcepb.UpdateResponse, error) {
rsp := &resourcepb.UpdateResponse{}
latest := s.backend.ReadResource(ctx, &resourcepb.ReadRequest{
Key: req.Key,
})
@@ -654,6 +719,25 @@ func (s *server) Delete(ctx context.Context, req *resourcepb.DeleteRequest) (*re
return rsp, nil
}
var (
res *resourcepb.DeleteResponse
err error
)
runErr := s.runInQueue(ctx, req.Key.Namespace, func(ctx context.Context) {
res, err = s.delete(ctx, user, req)
})
if runErr != nil {
return HandleQueueError(runErr, func(e *resourcepb.ErrorResult) *resourcepb.DeleteResponse {
return &resourcepb.DeleteResponse{Error: e}
})
}
return res, err
}
func (s *server) delete(ctx context.Context, user claims.AuthInfo, req *resourcepb.DeleteRequest) (*resourcepb.DeleteResponse, error) {
rsp := &resourcepb.DeleteResponse{}
latest := s.backend.ReadResource(ctx, &resourcepb.ReadRequest{
Key: req.Key,
})
@@ -744,6 +828,23 @@ func (s *server) Read(ctx context.Context, req *resourcepb.ReadRequest) (*resour
return &resourcepb.ReadResponse{Error: NewBadRequestError("missing resource")}, nil
}
var (
res *resourcepb.ReadResponse
err error
)
runErr := s.runInQueue(ctx, req.Key.Namespace, func(ctx context.Context) {
res, err = s.read(ctx, user, req)
})
if runErr != nil {
return HandleQueueError(runErr, func(e *resourcepb.ErrorResult) *resourcepb.ReadResponse {
return &resourcepb.ReadResponse{Error: e}
})
}
return res, err
}
func (s *server) read(ctx context.Context, user claims.AuthInfo, req *resourcepb.ReadRequest) (*resourcepb.ReadResponse, error) {
rsp := s.backend.ReadResource(ctx, req)
if rsp.Error != nil && rsp.Error.Code == http.StatusNotFound {
return &resourcepb.ReadResponse{Error: rsp.Error}, nil
@@ -1237,3 +1338,41 @@ func (s *server) GetBlob(ctx context.Context, req *resourcepb.GetBlobRequest) (*
}
return rsp, nil
}
func (s *server) runInQueue(ctx context.Context, tenantID string, runnable func(ctx context.Context)) error {
boff := backoff.New(ctx, backoff.Config{
MinBackoff: DefaultMinBackoff,
MaxBackoff: DefaultMaxBackoff,
MaxRetries: DefaultMaxRetries,
})
var (
wg sync.WaitGroup
err error
)
wg.Add(1)
wrapped := func(ctx context.Context) {
runnable(ctx)
wg.Done()
}
for boff.Ongoing() {
err = s.queue.Enqueue(ctx, tenantID, wrapped)
if err == nil {
break
}
s.log.Warn("failed to enqueue runnable, retrying",
"maxRetries", DefaultMaxRetries,
"tenantID", tenantID,
"error", err)
boff.Wait()
}
if err != nil {
s.log.Error("failed to enqueue runnable",
"maxRetries", DefaultMaxRetries,
"tenantID", tenantID,
"error", err)
return fmt.Errorf("failed to enqueue runnable for tenant %s: %w", tenantID, err)
}
wg.Wait()
return nil
}
@@ -87,9 +87,10 @@ func getEngineMySQL(getter confGetter) (*xorm.Engine, error) {
return nil, fmt.Errorf("open database: %w", err)
}
engine.SetMaxOpenConns(0)
engine.SetMaxIdleConns(2)
engine.SetConnMaxLifetime(4 * time.Hour)
engine.SetMaxOpenConns(getter.Int("max_open_conn", 0))
engine.SetMaxIdleConns(getter.Int("max_idle_conn", 4))
maxLifetime := time.Duration(getter.Int("conn_max_lifetime", 14400)) * time.Second
engine.SetConnMaxLifetime(maxLifetime)
return engine, nil
}
@@ -188,5 +189,10 @@ func getEnginePostgres(getter confGetter) (*xorm.Engine, error) {
return nil, fmt.Errorf("open database: %w", err)
}
engine.SetMaxOpenConns(getter.Int("max_open_conn", 0))
engine.SetMaxIdleConns(getter.Int("max_idle_conn", 4))
maxLifetime := time.Duration(getter.Int("conn_max_lifetime", 14400)) * time.Second
engine.SetConnMaxLifetime(maxLifetime)
return engine, nil
}
@@ -18,6 +18,7 @@ type confGetter interface {
Err() error
Bool(key string) bool
String(key string) string
Int(key string, def int) int
}
func newConfGetter(ds *setting.DynamicSection, keyPrefix string) confGetter {
@@ -52,6 +53,10 @@ func (g *sectionGetter) String(key string) string {
return v
}
func (g *sectionGetter) Int(key string, def int) int {
return g.ds.Key(g.keyPrefix + key).MustInt(def)
}
// MakeDSN creates a DSN from the given key/value pair. It validates the strings
// form valid UTF-8 sequences and escapes values if needed.
func MakeDSN(m map[string]string) (string, error) {
+29 -5
View File
@@ -28,11 +28,13 @@ func TestSectionGetter(t *testing.T) {
t.Parallel()
var (
key = "the key"
keyBoolTrue = "I'm true"
keyBoolFalse = "not me!"
prefix = "this is some prefix"
val = string(invalidUTF8ByteSequence)
key = "the key"
keyBoolTrue = "I'm true"
keyBoolFalse = "not me!"
keyIntValid = "valid_int"
keyIntMissing = "missing_int"
prefix = "this is some prefix"
val = string(invalidUTF8ByteSequence)
)
t.Run("with prefix", func(t *testing.T) {
@@ -42,6 +44,8 @@ func TestSectionGetter(t *testing.T) {
prefix + key: val,
prefix + keyBoolTrue: "YES",
prefix + keyBoolFalse: "0",
prefix + keyIntValid: "42",
// Note: keyIntMissing is intentionally not included to test default behavior
}, prefix)
require.False(t, g.Bool("whatever bool"))
@@ -53,6 +57,15 @@ func TestSectionGetter(t *testing.T) {
require.True(t, g.Bool(keyBoolTrue))
require.NoError(t, g.Err())
require.Equal(t, 999, g.Int("whatever int", 999))
require.NoError(t, g.Err())
require.Equal(t, 42, g.Int(keyIntValid, 100))
require.NoError(t, g.Err())
require.Equal(t, 200, g.Int(keyIntMissing, 200))
require.NoError(t, g.Err())
require.Empty(t, g.String("whatever string"))
require.NoError(t, g.Err())
@@ -68,6 +81,8 @@ func TestSectionGetter(t *testing.T) {
key: val,
keyBoolTrue: "true",
keyBoolFalse: "f",
keyIntValid: "123",
// Note: keyIntMissing is intentionally not included to test default behavior
}, "")
require.False(t, g.Bool("whatever bool"))
@@ -79,6 +94,15 @@ func TestSectionGetter(t *testing.T) {
require.True(t, g.Bool(keyBoolTrue))
require.NoError(t, g.Err())
require.Equal(t, 500, g.Int("whatever int", 500))
require.NoError(t, g.Err())
require.Equal(t, 123, g.Int(keyIntValid, 0))
require.NoError(t, g.Err())
require.Equal(t, 300, g.Int(keyIntMissing, 300))
require.NoError(t, g.Err())
require.Empty(t, g.String("whatever string"))
require.NoError(t, g.Err())
+50 -33
View File
@@ -1,6 +1,7 @@
package sql
import (
"context"
"os"
"strings"
@@ -8,6 +9,7 @@ import (
"go.opentelemetry.io/otel/trace"
"github.com/grafana/authlib/types"
"github.com/grafana/dskit/services"
infraDB "github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/featuremgmt"
@@ -17,70 +19,85 @@ import (
"github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl"
)
type QOSEnqueueDequeuer interface {
services.Service
Enqueue(ctx context.Context, tenantID string, runnable func(ctx context.Context)) error
Dequeue(ctx context.Context) (func(ctx context.Context), error)
}
// ServerOptions contains the options for creating a new ResourceServer
type ServerOptions struct {
DB infraDB.DB
Cfg *setting.Cfg
Tracer trace.Tracer
Reg prometheus.Registerer
AccessClient types.AccessClient
SearchOptions resource.SearchOptions
StorageMetrics *resource.StorageMetrics
IndexMetrics *resource.BleveIndexMetrics
Features featuremgmt.FeatureToggles
QOSQueue QOSEnqueueDequeuer
}
// Creates a new ResourceServer
func NewResourceServer(db infraDB.DB, cfg *setting.Cfg,
tracer trace.Tracer, reg prometheus.Registerer, ac types.AccessClient,
searchOptions resource.SearchOptions, storageMetrics *resource.StorageMetrics,
indexMetrics *resource.BleveIndexMetrics, features featuremgmt.FeatureToggles) (resource.ResourceServer, error) {
apiserverCfg := cfg.SectionWithEnvOverrides("grafana-apiserver")
opts := resource.ResourceServerOptions{
Tracer: tracer,
func NewResourceServer(
opts ServerOptions,
) (resource.ResourceServer, error) {
apiserverCfg := opts.Cfg.SectionWithEnvOverrides("grafana-apiserver")
serverOptions := resource.ResourceServerOptions{
Tracer: opts.Tracer,
Blob: resource.BlobConfig{
URL: apiserverCfg.Key("blob_url").MustString(""),
},
Reg: reg,
Reg: opts.Reg,
}
if ac != nil {
opts.AccessClient = resource.NewAuthzLimitedClient(ac, resource.AuthzOptions{Tracer: tracer, Registry: reg})
if opts.AccessClient != nil {
serverOptions.AccessClient = resource.NewAuthzLimitedClient(opts.AccessClient, resource.AuthzOptions{Tracer: opts.Tracer, Registry: opts.Reg})
}
// Support local file blob
if strings.HasPrefix(opts.Blob.URL, "./data/") {
dir := strings.Replace(opts.Blob.URL, "./data", cfg.DataPath, 1)
if strings.HasPrefix(serverOptions.Blob.URL, "./data/") {
dir := strings.Replace(serverOptions.Blob.URL, "./data", opts.Cfg.DataPath, 1)
err := os.MkdirAll(dir, 0700)
if err != nil {
return nil, err
}
opts.Blob.URL = "file:///" + dir
serverOptions.Blob.URL = "file:///" + dir
}
// This is mostly for testing, being able to influence when we paginate
// based on the page size during tests.
unifiedStorageCfg := cfg.SectionWithEnvOverrides("unified_storage")
unifiedStorageCfg := opts.Cfg.SectionWithEnvOverrides("unified_storage")
maxPageSizeBytes := unifiedStorageCfg.Key("max_page_size_bytes")
opts.MaxPageSizeBytes = maxPageSizeBytes.MustInt(0)
serverOptions.MaxPageSizeBytes = maxPageSizeBytes.MustInt(0)
eDB, err := dbimpl.ProvideResourceDB(db, cfg, tracer)
eDB, err := dbimpl.ProvideResourceDB(opts.DB, opts.Cfg, opts.Tracer)
if err != nil {
return nil, err
}
isHA := isHighAvailabilityEnabled(cfg.SectionWithEnvOverrides("database"),
cfg.SectionWithEnvOverrides("resource_api"))
withPruner := features.IsEnabledGlobally(featuremgmt.FlagUnifiedStorageHistoryPruner)
isHA := isHighAvailabilityEnabled(opts.Cfg.SectionWithEnvOverrides("database"),
opts.Cfg.SectionWithEnvOverrides("resource_api"))
withPruner := opts.Features.IsEnabledGlobally(featuremgmt.FlagUnifiedStorageHistoryPruner)
store, err := NewBackend(BackendOptions{
DBProvider: eDB,
Tracer: tracer,
Reg: reg,
Tracer: opts.Tracer,
Reg: opts.Reg,
IsHA: isHA,
withPruner: withPruner,
storageMetrics: storageMetrics,
storageMetrics: opts.StorageMetrics,
})
if err != nil {
return nil, err
}
opts.Backend = store
opts.Diagnostics = store
opts.Lifecycle = store
opts.Search = searchOptions
opts.IndexMetrics = indexMetrics
serverOptions.Backend = store
serverOptions.Diagnostics = store
serverOptions.Lifecycle = store
serverOptions.Search = opts.SearchOptions
serverOptions.IndexMetrics = opts.IndexMetrics
serverOptions.QOSQueue = opts.QOSQueue
rs, err := resource.NewResourceServer(opts)
if err != nil {
return nil, err
}
return rs, nil
return resource.NewResourceServer(serverOptions)
}
// isHighAvailabilityEnabled determines if high availability mode should
+87 -29
View File
@@ -34,6 +34,7 @@ import (
"github.com/grafana/grafana/pkg/storage/unified/resource/grpc"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
"github.com/grafana/grafana/pkg/storage/unified/search"
"github.com/grafana/grafana/pkg/util/scheduler"
)
var (
@@ -50,6 +51,11 @@ type UnifiedStorageGrpcService interface {
type service struct {
*services.BasicService
// Subservices manager
subservices *services.Manager
subservicesWatcher *services.FailureWatcher
hasSubservices bool
cfg *setting.Cfg
features featuremgmt.FeatureToggles
db infraDB.DB
@@ -71,6 +77,9 @@ type service struct {
storageRing *ring.Ring
lifecycler *ring.BasicLifecycler
queue QOSEnqueueDequeuer
scheduler *scheduler.Scheduler
}
func ProvideUnifiedStorageGrpcService(
@@ -85,6 +94,7 @@ func ProvideUnifiedStorageGrpcService(
storageRing *ring.Ring,
memberlistKVConfig kv.Config,
) (UnifiedStorageGrpcService, error) {
var err error
tracer := otel.Tracer("unified-storage")
// FIXME: This is a temporary solution while we are migrating to the new authn interceptor
@@ -95,20 +105,22 @@ func ProvideUnifiedStorageGrpcService(
})
s := &service{
cfg: cfg,
features: features,
stopCh: make(chan struct{}),
authenticator: authn,
tracing: tracer,
db: db,
log: log,
reg: reg,
docBuilders: docBuilders,
storageMetrics: storageMetrics,
indexMetrics: indexMetrics,
storageRing: storageRing,
cfg: cfg,
features: features,
stopCh: make(chan struct{}),
authenticator: authn,
tracing: tracer,
db: db,
log: log,
reg: reg,
docBuilders: docBuilders,
storageMetrics: storageMetrics,
indexMetrics: indexMetrics,
storageRing: storageRing,
subservicesWatcher: services.NewFailureWatcher(),
}
subservices := []services.Service{}
if cfg.EnableSharding {
ringStore, err := kv.NewClient(
memberlistKVConfig,
@@ -143,15 +155,50 @@ func ProvideUnifiedStorageGrpcService(
if err != nil {
return nil, fmt.Errorf("failed to initialize storage-ring lifecycler: %s", err)
}
subservices = append(subservices, s.lifecycler)
}
if cfg.QOSEnabled {
qosReg := prometheus.WrapRegistererWithPrefix("resource_server_qos_", reg)
queue := scheduler.NewQueue(&scheduler.QueueOptions{
MaxSizePerTenant: cfg.QOSMaxSizePerTenant,
Registerer: qosReg,
})
scheduler, err := scheduler.NewScheduler(queue, &scheduler.Config{
NumWorkers: cfg.QOSNumberWorker,
Logger: log,
})
if err != nil {
return nil, fmt.Errorf("failed to create qos scheduler: %s", err)
}
s.queue = queue
s.scheduler = scheduler
subservices = append(subservices, s.queue, s.scheduler)
}
if len(subservices) > 0 {
s.hasSubservices = true
s.subservices, err = services.NewManager(subservices...)
if err != nil {
return nil, fmt.Errorf("failed to create subservices manager: %w", err)
}
}
// This will be used when running as a dskit service
s.BasicService = services.NewBasicService(s.start, s.running, s.stopping).WithName(modules.StorageServer)
s.BasicService = services.NewBasicService(s.starting, s.running, s.stopping).WithName(modules.StorageServer)
return s, nil
}
func (s *service) start(ctx context.Context) error {
func (s *service) starting(ctx context.Context) error {
if s.hasSubservices {
s.subservicesWatcher.WatchManager(s.subservices)
if err := services.StartManagerAndAwaitHealthy(ctx, s.subservices); err != nil {
return fmt.Errorf("failed to start subservices: %w", err)
}
}
authzClient, err := authz.ProvideStandaloneAuthZClient(s.cfg, s.features, s.tracing)
if err != nil {
return err
@@ -162,7 +209,19 @@ func (s *service) start(ctx context.Context) error {
return err
}
server, err := NewResourceServer(s.db, s.cfg, s.tracing, s.reg, authzClient, searchOptions, s.storageMetrics, s.indexMetrics, s.features)
serverOptions := ServerOptions{
DB: s.db,
Cfg: s.cfg,
Tracer: s.tracing,
Reg: s.reg,
AccessClient: authzClient,
SearchOptions: searchOptions,
StorageMetrics: s.storageMetrics,
IndexMetrics: s.indexMetrics,
Features: s.features,
QOSQueue: s.queue,
}
server, err := NewResourceServer(serverOptions)
if err != nil {
return err
}
@@ -192,11 +251,6 @@ func (s *service) start(ctx context.Context) error {
}
if s.cfg.EnableSharding {
err = s.lifecycler.StartAsync(ctx)
if err != nil {
return fmt.Errorf("failed to start the lifecycler: %s", err)
}
s.log.Info("waiting until resource server is JOINING in the ring")
lfcCtx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
@@ -231,15 +285,27 @@ func (s *service) GetAddress() string {
func (s *service) running(ctx context.Context) error {
select {
case err := <-s.stoppedCh:
if err != nil {
if err != nil && !errors.Is(err, context.Canceled) {
return err
}
case err := <-s.subservicesWatcher.Chan():
return fmt.Errorf("subservice failure: %w", err)
case <-ctx.Done():
close(s.stopCh)
}
return nil
}
func (s *service) stopping(_ error) error {
if s.hasSubservices {
err := services.StopManagerAndAwaitStopped(context.Background(), s.subservices)
if err != nil {
return fmt.Errorf("failed to stop subservices: %w", err)
}
}
return nil
}
type authenticatorWithFallback struct {
authenticator func(ctx context.Context) (context.Context, error)
fallback func(ctx context.Context) (context.Context, error)
@@ -309,14 +375,6 @@ func NewAuthenticatorWithFallback(cfg *setting.Cfg, reg prometheus.Registerer, t
}
}
func (s *service) stopping(err error) error {
if err != nil && !errors.Is(err, context.Canceled) {
s.log.Error("stopping unified storage grpc service", "error", err)
return err
}
return nil
}
func toLifecyclerConfig(cfg *setting.Cfg, logger log.Logger) (ring.BasicLifecyclerConfig, error) {
instanceAddr, err := ring.GetInstanceAddr(cfg.MemberlistBindAddr, netutil.PrivateNetworkInterfacesWithFallback([]string{"eth0", "en0"}, logger), logger, true)
if err != nil {
@@ -1851,6 +1851,10 @@
]
}
},
"moreInfo": {
"description": "More information about the failure, not meant to be displayed to the user. Used for LLM suggestions.",
"type": "string"
},
"severity": {
"description": "Severity of the failure",
"type": "string",
@@ -11,5 +11,5 @@ spec:
description: This is a secret
value: this is super duper secure
decrypters:
- actor_k6
- actor_synthetic-monitoring
- k6
- synthetic-monitoring
+2 -2
View File
@@ -12,5 +12,5 @@ spec:
keeper: my-keeper-1
value: super duper secure
decrypters:
- actor_k6
- actor_synthetic-monitoring
- k6
- synthetic-monitoring
+3 -4
View File
@@ -8,10 +8,9 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
"github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter"
"github.com/grafana/grafana/pkg/infra/httpclient"
)
var logger = backend.NewLoggerWith("logger", "tsdb.jaeger")
@@ -20,7 +19,7 @@ type Service struct {
im instancemgmt.InstanceManager
}
func ProvideService(httpClientProvider httpclient.Provider) *Service {
func ProvideService(httpClientProvider *httpclient.Provider) *Service {
return &Service{
im: datasource.NewInstanceManager(newInstanceSettings(httpClientProvider)),
}
@@ -36,7 +35,7 @@ type datasourceJSONData struct {
} `json:"traceIdTimeParams"`
}
func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.InstanceFactoryFunc {
func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.InstanceFactoryFunc {
return func(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
httpClientOptions, err := settings.HTTPClientOptions(ctx)
if err != nil {
+39
View File
@@ -0,0 +1,39 @@
package main
import (
"context"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
jaeger "github.com/grafana/grafana/pkg/tsdb/jaeger"
)
var (
_ backend.QueryDataHandler = (*Datasource)(nil)
_ backend.CheckHealthHandler = (*Datasource)(nil)
_ backend.CallResourceHandler = (*Datasource)(nil)
)
func NewDatasource(context.Context, backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return &Datasource{
Service: jaeger.ProvideService(httpclient.NewProvider()),
}, nil
}
type Datasource struct {
Service *jaeger.Service
}
func (d *Datasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
return d.Service.QueryData(ctx, req)
}
func (d *Datasource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
return d.Service.CallResource(ctx, req, sender)
}
func (d *Datasource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
return d.Service.CheckHealth(ctx, req)
}
+23
View File
@@ -0,0 +1,23 @@
package main
import (
"os"
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
)
func main() {
// Start listening to requests sent from Grafana. This call is blocking so
// it won't finish until Grafana shuts down the process or the plugin choose
// to exit by itself using os.Exit. Manage automatically manages life cycle
// of datasource instances. It accepts datasource instance factory as first
// argument. This factory will be automatically called on incoming request
// from Grafana to create different instances of SampleDatasource (per datasource
// ID). When datasource configuration changed Dispose method will be called and
// new datasource instance created using NewSampleDatasource factory.
if err := datasource.Manage("jaeger", NewDatasource, datasource.ManageOpts{}); err != nil {
log.DefaultLogger.Error(err.Error())
os.Exit(1)
}
}
+39
View File
@@ -0,0 +1,39 @@
package main
import (
"context"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
"github.com/grafana/grafana/pkg/tsdb/zipkin"
)
var (
_ backend.QueryDataHandler = (*Datasource)(nil)
_ backend.CheckHealthHandler = (*Datasource)(nil)
_ backend.CallResourceHandler = (*Datasource)(nil)
)
func NewDatasource(context.Context, backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return &Datasource{
Service: zipkin.ProvideService(httpclient.NewProvider()),
}, nil
}
type Datasource struct {
Service *zipkin.Service
}
func (d *Datasource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
return d.Service.QueryData(ctx, req)
}
func (d *Datasource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
return d.Service.CallResource(ctx, req, sender)
}
func (d *Datasource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
return d.Service.CheckHealth(ctx, req)
}
+23
View File
@@ -0,0 +1,23 @@
package main
import (
"os"
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
)
func main() {
// Start listening to requests sent from Grafana. This call is blocking so
// it won't finish until Grafana shuts down the process or the plugin choose
// to exit by itself using os.Exit. Manage automatically manages life cycle
// of datasource instances. It accepts datasource instance factory as first
// argument. This factory will be automatically called on incoming request
// from Grafana to create different instances of SampleDatasource (per datasource
// ID). When datasource configuration changed Dispose method will be called and
// new datasource instance created using NewSampleDatasource factory.
if err := datasource.Manage("zipkin", NewDatasource, datasource.ManageOpts{}); err != nil {
log.DefaultLogger.Error(err.Error())
os.Exit(1)
}
}
+3 -4
View File
@@ -7,10 +7,9 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
"github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter"
"github.com/grafana/grafana/pkg/infra/httpclient"
)
var logger = backend.NewLoggerWith("logger", "tsdb.zipkin")
@@ -19,7 +18,7 @@ type Service struct {
im instancemgmt.InstanceManager
}
func ProvideService(httpClientProvider httpclient.Provider) *Service {
func ProvideService(httpClientProvider *httpclient.Provider) *Service {
return &Service{
im: datasource.NewInstanceManager(newInstanceSettings(httpClientProvider)),
}
@@ -29,7 +28,7 @@ type datasourceInfo struct {
ZipkinClient ZipkinClient
}
func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.InstanceFactoryFunc {
func newInstanceSettings(httpClientProvider *httpclient.Provider) datasource.InstanceFactoryFunc {
return func(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
httpClientOptions, err := settings.HTTPClientOptions(ctx)
if err != nil {
+18 -1
View File
@@ -9,6 +9,8 @@ import (
"github.com/grafana/dskit/services"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/grafana/grafana/pkg/infra/log"
)
const (
@@ -82,6 +84,8 @@ func NewNoopQueue() *NoopQueue {
type Queue struct {
services.Service
logger log.Logger
enqueueChan chan enqueueRequest
dequeueChan chan dequeueRequest
lenChan chan lenRequest
@@ -108,6 +112,7 @@ type Queue struct {
type QueueOptions struct {
MaxSizePerTenant int
Registerer prometheus.Registerer
Logger log.Logger
}
// NewQueue creates a new Queue and starts its dispatcher goroutine.
@@ -116,7 +121,13 @@ func NewQueue(opts *QueueOptions) *Queue {
opts.MaxSizePerTenant = DefaultMaxSizePerTenant
}
if opts.Logger == nil {
opts.Logger = log.NewNopLogger()
}
q := &Queue{
logger: opts.Logger,
enqueueChan: make(chan enqueueRequest),
dequeueChan: make(chan dequeueRequest),
lenChan: make(chan lenRequest),
@@ -226,6 +237,8 @@ func (q *Queue) handleLenRequest(req lenRequest) {
func (q *Queue) dispatcherLoop(ctx context.Context) error {
defer close(q.dispatcherStoppedChan)
q.logger.Info("queue running", "maxSizePerTenant", q.maxSizePerTenant)
for {
q.scheduleRoundRobin()
@@ -275,7 +288,6 @@ func (q *Queue) Enqueue(ctx context.Context, tenantID string, runnable func(ctx
select {
case q.enqueueChan <- req:
err = <-respChan
q.enqueueDuration.Observe(time.Since(start).Seconds())
case <-q.dispatcherStoppedChan:
q.discardedRequests.WithLabelValues(tenantID, "dispatcher_stopped").Inc()
err = ErrQueueClosed
@@ -283,6 +295,7 @@ func (q *Queue) Enqueue(ctx context.Context, tenantID string, runnable func(ctx
q.discardedRequests.WithLabelValues(tenantID, "context_canceled").Inc()
err = ctx.Err()
}
q.enqueueDuration.Observe(time.Since(start).Seconds())
return err
}
@@ -352,6 +365,8 @@ func (q *Queue) ActiveTenantsLen() int {
}
func (q *Queue) stopping(_ error) error {
q.logger.Info("queue stopping")
q.queueLength.Reset()
q.discardedRequests.Reset()
for _, tq := range q.tenantQueues {
@@ -359,5 +374,7 @@ func (q *Queue) stopping(_ error) error {
}
q.activeTenants.Init()
q.pendingDequeueRequests.Init()
q.logger.Info("queue stopped")
return nil
}
+4
View File
@@ -11,6 +11,7 @@ import (
"time"
"github.com/grafana/dskit/services"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/require"
)
@@ -25,6 +26,9 @@ func QueueOptionsWithDefaults(opts *QueueOptions) *QueueOptions {
if opts.Registerer == nil {
opts.Registerer = prometheus.NewRegistry()
}
if opts.Logger == nil {
opts.Logger = log.New("qos.test")
}
return opts
}
+8 -4
View File
@@ -2,6 +2,7 @@ package scheduler
import (
"context"
"fmt"
"sync"
"sync/atomic"
"testing"
@@ -130,16 +131,16 @@ func TestScheduler(t *testing.T) {
t.Run("ProcessItems", func(t *testing.T) {
t.Parallel()
q := NewQueue(QueueOptionsWithDefaults(nil))
q := NewQueue(QueueOptionsWithDefaults(&QueueOptions{MaxSizePerTenant: 1000}))
require.NoError(t, services.StartAndAwaitRunning(context.Background(), q))
const itemCount = 10
const itemCount = 1000
var processed sync.Map
var wg sync.WaitGroup
wg.Add(itemCount)
scheduler, err := NewScheduler(q, &Config{
NumWorkers: 2,
NumWorkers: 10,
MaxBackoff: 100 * time.Millisecond,
Logger: log.New("qos.test"),
})
@@ -148,8 +149,11 @@ func TestScheduler(t *testing.T) {
for i := 0; i < itemCount; i++ {
itemID := i
require.NoError(t, q.Enqueue(context.Background(), "tenant-1", func(_ context.Context) {
tenantIndex := itemID % 10
tenantID := fmt.Sprintf("tenant-%d", tenantIndex)
require.NoError(t, q.Enqueue(context.Background(), tenantID, func(_ context.Context) {
processed.Store(itemID, true)
time.Sleep(10 * time.Millisecond)
wg.Done()
}))
}