Merge remote-tracking branch 'origin/main' into grafana-git-ui-sync

This commit is contained in:
Ryan McKinley committed 2025-01-16 11:12:20 +03:00
commit 0bef58898e
59 files changed
+2250 -1802

No files matched your search

+7
View File
@@ -6377,6 +6377,9 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "7"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "8"]
],
"public/app/features/trails/MetricSelect/NativeHistogramBadge.tsx:5381": [
[0, 0, 0, "\'@grafana/ui/src/utils/i18n\' import is restricted from being used by a pattern. Import from the public export instead.", "0"]
],
"public/app/features/trails/MetricsHeader.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
@@ -6389,6 +6392,10 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"]
],
"public/app/features/trails/banners/NativeHistogramBanner.tsx:5381": [
[0, 0, 0, "\'@grafana/ui/src/utils/i18n\' import is restricted from being used by a pattern. Import from the public export instead.", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"]
],
"public/app/features/transformers/FilterByValueTransformer/FilterByValueFilterEditor.tsx:5381": [
[0, 0, 0, "\'@grafana/data/src/transformations/transformers/filterByValue\' import is restricted from being used by a pattern. Import from the public export instead.", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
@@ -233,6 +233,7 @@ Experimental features might be changed or removed without prior notice.
| `k8SFolderMove` | Enable folder's api server move |
| `teamHttpHeadersMimir` | Enables LBAC for datasources for Mimir to apply LBAC filtering of metrics to the client requests for users in teams |
| `queryLibraryDashboards` | Enables Query Library feature in Dashboards |
| `elasticsearchImprovedParsing` | Enables less memory intensive Elasticsearch result parsing |
## Development feature toggles
@@ -253,4 +253,5 @@ export interface FeatureToggles {
ABTestFeatureToggleA?: boolean;
ABTestFeatureToggleB?: boolean;
queryLibraryDashboards?: boolean;
elasticsearchImprovedParsing?: boolean;
}
@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.35.2
// protoc-gen-go v1.36.1
// protoc (unknown)
// source: rendererv2.proto
@@ -21,11 +21,10 @@ const (
)
type StringList struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Values []string `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"`
unknownFields protoimpl.UnknownFields
Values []string `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *StringList) Reset() {
@@ -66,10 +65,7 @@ func (x *StringList) GetValues() []string {
}
type RenderRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
state protoimpl.MessageState `protogen:"open.v1"`
Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
Width int32 `protobuf:"varint,2,opt,name=width,proto3" json:"width,omitempty"`
Height int32 `protobuf:"varint,3,opt,name=height,proto3" json:"height,omitempty"`
@@ -79,9 +75,11 @@ type RenderRequest struct {
Domain string `protobuf:"bytes,7,opt,name=domain,proto3" json:"domain,omitempty"`
Timeout int32 `protobuf:"varint,8,opt,name=timeout,proto3" json:"timeout,omitempty"`
Timezone string `protobuf:"bytes,9,opt,name=timezone,proto3" json:"timezone,omitempty"`
Headers map[string]*StringList `protobuf:"bytes,10,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
Headers map[string]*StringList `protobuf:"bytes,10,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
AuthToken string `protobuf:"bytes,11,opt,name=authToken,proto3" json:"authToken,omitempty"`
Encoding string `protobuf:"bytes,12,opt,name=encoding,proto3" json:"encoding,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RenderRequest) Reset() {
@@ -199,11 +197,10 @@ func (x *RenderRequest) GetEncoding() string {
}
type RenderResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"`
unknownFields protoimpl.UnknownFields
Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *RenderResponse) Reset() {
@@ -244,18 +241,17 @@ func (x *RenderResponse) GetError() string {
}
type RenderCSVRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
FilePath string `protobuf:"bytes,2,opt,name=filePath,proto3" json:"filePath,omitempty"`
RenderKey string `protobuf:"bytes,3,opt,name=renderKey,proto3" json:"renderKey,omitempty"`
Domain string `protobuf:"bytes,4,opt,name=domain,proto3" json:"domain,omitempty"`
Timeout int32 `protobuf:"varint,5,opt,name=timeout,proto3" json:"timeout,omitempty"`
Timezone string `protobuf:"bytes,6,opt,name=timezone,proto3" json:"timezone,omitempty"`
Headers map[string]*StringList `protobuf:"bytes,7,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
AuthToken string `protobuf:"bytes,8,opt,name=authToken,proto3" json:"authToken,omitempty"`
unknownFields protoimpl.UnknownFields
Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
FilePath string `protobuf:"bytes,2,opt,name=filePath,proto3" json:"filePath,omitempty"`
RenderKey string `protobuf:"bytes,3,opt,name=renderKey,proto3" json:"renderKey,omitempty"`
Domain string `protobuf:"bytes,4,opt,name=domain,proto3" json:"domain,omitempty"`
Timeout int32 `protobuf:"varint,5,opt,name=timeout,proto3" json:"timeout,omitempty"`
Timezone string `protobuf:"bytes,6,opt,name=timezone,proto3" json:"timezone,omitempty"`
Headers map[string]*StringList `protobuf:"bytes,7,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
AuthToken string `protobuf:"bytes,8,opt,name=authToken,proto3" json:"authToken,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *RenderCSVRequest) Reset() {
@@ -345,12 +341,11 @@ func (x *RenderCSVRequest) GetAuthToken() string {
}
type RenderCSVResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"`
FileName string `protobuf:"bytes,2,opt,name=fileName,proto3" json:"fileName,omitempty"`
unknownFields protoimpl.UnknownFields
Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"`
FileName string `protobuf:"bytes,2,opt,name=fileName,proto3" json:"fileName,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *RenderCSVResponse) Reset() {
@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.35.2
// protoc-gen-go v1.36.1
// protoc (unknown)
// source: sanitizer.proto
@@ -21,14 +21,13 @@ const (
)
type SanitizeRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Filename string `protobuf:"bytes,1,opt,name=filename,proto3" json:"filename,omitempty"`
Content []byte `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"`
ConfigType string `protobuf:"bytes,3,opt,name=configType,proto3" json:"configType,omitempty"` // DOMPurify, ...
Config []byte `protobuf:"bytes,4,opt,name=config,proto3" json:"config,omitempty"`
unknownFields protoimpl.UnknownFields
Filename string `protobuf:"bytes,1,opt,name=filename,proto3" json:"filename,omitempty"`
Content []byte `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"`
ConfigType string `protobuf:"bytes,3,opt,name=configType,proto3" json:"configType,omitempty"` // DOMPurify, ...
Config []byte `protobuf:"bytes,4,opt,name=config,proto3" json:"config,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *SanitizeRequest) Reset() {
@@ -90,12 +89,11 @@ func (x *SanitizeRequest) GetConfig() []byte {
}
type SanitizeResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"`
Sanitized []byte `protobuf:"bytes,2,opt,name=sanitized,proto3" json:"sanitized,omitempty"`
unknownFields protoimpl.UnknownFields
Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"`
Sanitized []byte `protobuf:"bytes,2,opt,name=sanitized,proto3" json:"sanitized,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *SanitizeResponse) Reset() {
@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.35.2
// protoc-gen-go v1.36.1
// protoc (unknown)
// source: secretsmanager.proto
@@ -21,13 +21,12 @@ const (
)
type Key struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
OrgId int64 `protobuf:"varint,1,opt,name=orgId,proto3" json:"orgId,omitempty"`
Namespace string `protobuf:"bytes,2,opt,name=namespace,proto3" json:"namespace,omitempty"`
Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"`
unknownFields protoimpl.UnknownFields
OrgId int64 `protobuf:"varint,1,opt,name=orgId,proto3" json:"orgId,omitempty"`
Namespace string `protobuf:"bytes,2,opt,name=namespace,proto3" json:"namespace,omitempty"`
Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *Key) Reset() {
@@ -82,12 +81,11 @@ func (x *Key) GetType() string {
}
type Item struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Key *Key `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
unknownFields protoimpl.UnknownFields
Key *Key `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *Item) Reset() {
@@ -135,11 +133,10 @@ func (x *Item) GetValue() string {
}
type GetSecretRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
unknownFields protoimpl.UnknownFields
KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *GetSecretRequest) Reset() {
@@ -180,13 +177,12 @@ func (x *GetSecretRequest) GetKeyDescriptor() *Key {
}
type GetSecretResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"`
DecryptedValue string `protobuf:"bytes,2,opt,name=decryptedValue,proto3" json:"decryptedValue,omitempty"`
Exists bool `protobuf:"varint,3,opt,name=exists,proto3" json:"exists,omitempty"`
state protoimpl.MessageState `protogen:"open.v1"`
UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"`
DecryptedValue string `protobuf:"bytes,2,opt,name=decryptedValue,proto3" json:"decryptedValue,omitempty"`
Exists bool `protobuf:"varint,3,opt,name=exists,proto3" json:"exists,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetSecretResponse) Reset() {
@@ -241,12 +237,11 @@ func (x *GetSecretResponse) GetExists() bool {
}
type SetSecretRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
unknownFields protoimpl.UnknownFields
KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *SetSecretRequest) Reset() {
@@ -294,11 +289,10 @@ func (x *SetSecretRequest) GetValue() string {
}
type SetSecretResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"`
state protoimpl.MessageState `protogen:"open.v1"`
UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SetSecretResponse) Reset() {
@@ -339,11 +333,10 @@ func (x *SetSecretResponse) GetUserFriendlyError() string {
}
type DeleteSecretRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
unknownFields protoimpl.UnknownFields
KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *DeleteSecretRequest) Reset() {
@@ -384,11 +377,10 @@ func (x *DeleteSecretRequest) GetKeyDescriptor() *Key {
}
type DeleteSecretResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"`
state protoimpl.MessageState `protogen:"open.v1"`
UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeleteSecretResponse) Reset() {
@@ -429,12 +421,11 @@ func (x *DeleteSecretResponse) GetUserFriendlyError() string {
}
type ListSecretsRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
AllOrganizations bool `protobuf:"varint,2,opt,name=allOrganizations,proto3" json:"allOrganizations,omitempty"`
state protoimpl.MessageState `protogen:"open.v1"`
KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
AllOrganizations bool `protobuf:"varint,2,opt,name=allOrganizations,proto3" json:"allOrganizations,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListSecretsRequest) Reset() {
@@ -482,12 +473,11 @@ func (x *ListSecretsRequest) GetAllOrganizations() bool {
}
type ListSecretsResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"`
Keys []*Key `protobuf:"bytes,2,rep,name=keys,proto3" json:"keys,omitempty"`
state protoimpl.MessageState `protogen:"open.v1"`
UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"`
Keys []*Key `protobuf:"bytes,2,rep,name=keys,proto3" json:"keys,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListSecretsResponse) Reset() {
@@ -535,9 +525,9 @@ func (x *ListSecretsResponse) GetKeys() []*Key {
}
type GetAllSecretsRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetAllSecretsRequest) Reset() {
@@ -571,12 +561,11 @@ func (*GetAllSecretsRequest) Descriptor() ([]byte, []int) {
}
type GetAllSecretsResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"`
Items []*Item `protobuf:"bytes,2,rep,name=items,proto3" json:"items,omitempty"`
state protoimpl.MessageState `protogen:"open.v1"`
UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"`
Items []*Item `protobuf:"bytes,2,rep,name=items,proto3" json:"items,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetAllSecretsResponse) Reset() {
@@ -624,12 +613,11 @@ func (x *GetAllSecretsResponse) GetItems() []*Item {
}
type RenameSecretRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
NewNamespace string `protobuf:"bytes,2,opt,name=newNamespace,proto3" json:"newNamespace,omitempty"`
unknownFields protoimpl.UnknownFields
KeyDescriptor *Key `protobuf:"bytes,1,opt,name=keyDescriptor,proto3" json:"keyDescriptor,omitempty"`
NewNamespace string `protobuf:"bytes,2,opt,name=newNamespace,proto3" json:"newNamespace,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *RenameSecretRequest) Reset() {
@@ -677,11 +665,10 @@ func (x *RenameSecretRequest) GetNewNamespace() string {
}
type RenameSecretResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"`
state protoimpl.MessageState `protogen:"open.v1"`
UserFriendlyError string `protobuf:"bytes,1,opt,name=userFriendlyError,proto3" json:"userFriendlyError,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RenameSecretResponse) Reset() {
@@ -42,11 +42,6 @@ func (d *directResourceClient) GetStats(ctx context.Context, in *resource.Resour
return d.server.GetStats(ctx, in)
}
// History implements ResourceClient.
func (d *directResourceClient) History(ctx context.Context, in *resource.HistoryRequest, opts ...grpc.CallOption) (*resource.HistoryResponse, error) {
return d.server.History(ctx, in)
}
// IsHealthy implements ResourceClient.
func (d *directResourceClient) IsHealthy(ctx context.Context, in *resource.HealthCheckRequest, opts ...grpc.CallOption) (*resource.HealthCheckResponse, error) {
return d.server.IsHealthy(ctx, in)
@@ -7,8 +7,6 @@ import (
"net/http"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/grafana/authlib/claims"
"github.com/grafana/grafana/pkg/apimachinery/utils"
dashboard "github.com/grafana/grafana/pkg/apis/dashboard"
@@ -251,6 +249,7 @@ func (a *dashboardSqlAccess) Search(ctx context.Context, req *resource.ResourceS
return nil, fmt.Errorf("not yet (filter)")
}
/**
func (a *dashboardSqlAccess) History(ctx context.Context, req *resource.HistoryRequest) (*resource.HistoryResponse, error) {
info, err := claims.ParseNamespace(req.Key.Namespace)
if err == nil {
@@ -329,6 +328,7 @@ func (a *dashboardSqlAccess) History(ctx context.Context, req *resource.HistoryR
}
return list, err
}
**/
func (a *dashboardSqlAccess) ListRepositoryObjects(ctx context.Context, req *resource.ListRepositoryObjectsRequest) (*resource.ListRepositoryObjectsResponse, error) {
return nil, fmt.Errorf("not implemented")
@@ -94,7 +94,3 @@ func (m *MockClient) Search(ctx context.Context, in *resource.ResourceSearchRequ
func (m *MockClient) GetStats(ctx context.Context, in *resource.ResourceStatsRequest, opts ...grpc.CallOption) (*resource.ResourceStatsResponse, error) {
return nil, nil
}
func (m *MockClient) History(ctx context.Context, in *resource.HistoryRequest, opts ...grpc.CallOption) (*resource.HistoryResponse, error) {
return nil, nil
}
@@ -2,7 +2,6 @@ package v0alpha1
import (
"context"
"errors"
"fmt"
"github.com/prometheus/client_golang/prometheus"
@@ -146,14 +145,6 @@ func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver
storage := map[string]rest.Storage{}
storage[dash.StoragePath()] = legacyStore
storage[dash.StoragePath("history")] = apistore.NewHistoryConnector(
b.legacy.Server, // as client???
dashboardv0alpha1.DashboardResourceInfo.GroupResource(),
)
if optsGetter == nil {
return errors.New("missing RESTOptionsGetter")
}
// Dual writes if a RESTOptionsGetter is provided
if dualWriteBuilder != nil {
@@ -142,13 +142,9 @@ func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver
storage := map[string]rest.Storage{}
storage[dash.StoragePath()] = legacyStore
storage[dash.StoragePath("history")] = apistore.NewHistoryConnector(
b.legacy.Server, // as client???
dashboardv1alpha1.DashboardResourceInfo.GroupResource(),
)
// Dual writes if a RESTOptionsGetter is provided
if optsGetter != nil && dualWriteBuilder != nil {
if dualWriteBuilder != nil {
store, err := grafanaregistry.NewRegistryStore(scheme, dash, optsGetter)
if err != nil {
return err
@@ -143,13 +143,9 @@ func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver
storage := map[string]rest.Storage{}
storage[dash.StoragePath()] = legacyStore
storage[dash.StoragePath("history")] = apistore.NewHistoryConnector(
b.legacy.Server, // as client???
dashboardv2alpha1.DashboardResourceInfo.GroupResource(),
)
// Dual writes if a RESTOptionsGetter is provided
if optsGetter != nil && dualWriteBuilder != nil {
if dualWriteBuilder != nil {
store, err := grafanaregistry.NewRegistryStore(scheme, dash, optsGetter)
if err != nil {
return err
+1 -8
View File
@@ -127,13 +127,6 @@ func (s *ExtendedJWT) authenticateAsUser(
return nil, errExtJWTInvalidSubject.Errorf("unexpected identity: %s", idTokenClaims.Subject)
}
// For use in service layer, allow higher privilege
namespace := accessTokenClaims.Rest.Namespace
if len(s.cfg.StackID) > 0 {
// For single-tenant cloud use, choose the lower of the two (id token will always have the specific namespace)
namespace = idTokenClaims.Rest.Namespace
}
return &authn.Identity{
ID: id,
Type: t,
@@ -142,7 +135,7 @@ func (s *ExtendedJWT) authenticateAsUser(
IDTokenClaims: &idTokenClaims,
AuthenticatedBy: login.ExtendedJWTModule,
AuthID: accessTokenClaims.Subject,
Namespace: namespace,
Namespace: idTokenClaims.Rest.Namespace,
ClientParams: authn.ClientParams{
SyncPermissions: true,
FetchPermissionsParams: authn.FetchPermissionsParams{
+2 -2
View File
@@ -283,7 +283,7 @@ func TestExtendedJWT_Authenticate(t *testing.T) {
},
},
{
name: "should authenticate as user using wildcard namespace for access token",
name: "should authenticate as user in the user namespace",
accessToken: &validAccessTokenClaimsWildcard,
idToken: &validIDTokenClaims,
orgID: 1,
@@ -293,7 +293,7 @@ func TestExtendedJWT_Authenticate(t *testing.T) {
OrgID: 1,
AccessTokenClaims: &validAccessTokenClaimsWildcard,
IDTokenClaims: &validIDTokenClaims,
Namespace: "*",
Namespace: "default",
AuthenticatedBy: "extendedjwt",
AuthID: "access-policy:this-uid",
ClientParams: authn.ClientParams{
+65 -79
View File
@@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.35.2
// protoc-gen-go v1.36.1
// protoc (unknown)
// source: extention.proto
@@ -24,14 +24,13 @@ const (
)
type TupleKey struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
User string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
Relation string `protobuf:"bytes,2,opt,name=relation,proto3" json:"relation,omitempty"`
Object string `protobuf:"bytes,3,opt,name=object,proto3" json:"object,omitempty"`
Condition *RelationshipCondition `protobuf:"bytes,4,opt,name=condition,proto3" json:"condition,omitempty"`
unknownFields protoimpl.UnknownFields
User string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
Relation string `protobuf:"bytes,2,opt,name=relation,proto3" json:"relation,omitempty"`
Object string `protobuf:"bytes,3,opt,name=object,proto3" json:"object,omitempty"`
Condition *RelationshipCondition `protobuf:"bytes,4,opt,name=condition,proto3" json:"condition,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *TupleKey) Reset() {
@@ -93,12 +92,11 @@ func (x *TupleKey) GetCondition() *RelationshipCondition {
}
type Tuple struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Key *TupleKey `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
Timestamp *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
unknownFields protoimpl.UnknownFields
Key *TupleKey `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
Timestamp *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *Tuple) Reset() {
@@ -146,13 +144,12 @@ func (x *Tuple) GetTimestamp() *timestamppb.Timestamp {
}
type TupleKeyWithoutCondition struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
User string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
Relation string `protobuf:"bytes,2,opt,name=relation,proto3" json:"relation,omitempty"`
Object string `protobuf:"bytes,3,opt,name=object,proto3" json:"object,omitempty"`
unknownFields protoimpl.UnknownFields
User string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
Relation string `protobuf:"bytes,2,opt,name=relation,proto3" json:"relation,omitempty"`
Object string `protobuf:"bytes,3,opt,name=object,proto3" json:"object,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *TupleKeyWithoutCondition) Reset() {
@@ -207,12 +204,11 @@ func (x *TupleKeyWithoutCondition) GetObject() string {
}
type RelationshipCondition struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Context *structpb.Struct `protobuf:"bytes,2,opt,name=context,proto3" json:"context,omitempty"`
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Context *structpb.Struct `protobuf:"bytes,2,opt,name=context,proto3" json:"context,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *RelationshipCondition) Reset() {
@@ -260,14 +256,13 @@ func (x *RelationshipCondition) GetContext() *structpb.Struct {
}
type ReadRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
state protoimpl.MessageState `protogen:"open.v1"`
Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"`
TupleKey *ReadRequestTupleKey `protobuf:"bytes,2,opt,name=tuple_key,json=tupleKey,proto3" json:"tuple_key,omitempty"`
PageSize *wrapperspb.Int32Value `protobuf:"bytes,3,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"`
ContinuationToken string `protobuf:"bytes,4,opt,name=continuation_token,json=continuationToken,proto3" json:"continuation_token,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ReadRequest) Reset() {
@@ -329,13 +324,12 @@ func (x *ReadRequest) GetContinuationToken() string {
}
type ReadRequestTupleKey struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
User string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
Relation string `protobuf:"bytes,2,opt,name=relation,proto3" json:"relation,omitempty"`
Object string `protobuf:"bytes,3,opt,name=object,proto3" json:"object,omitempty"`
unknownFields protoimpl.UnknownFields
User string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"`
Relation string `protobuf:"bytes,2,opt,name=relation,proto3" json:"relation,omitempty"`
Object string `protobuf:"bytes,3,opt,name=object,proto3" json:"object,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *ReadRequestTupleKey) Reset() {
@@ -390,12 +384,11 @@ func (x *ReadRequestTupleKey) GetObject() string {
}
type ReadResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Tuples []*Tuple `protobuf:"bytes,1,rep,name=tuples,proto3" json:"tuples,omitempty"`
ContinuationToken string `protobuf:"bytes,2,opt,name=continuation_token,json=continuationToken,proto3" json:"continuation_token,omitempty"`
state protoimpl.MessageState `protogen:"open.v1"`
Tuples []*Tuple `protobuf:"bytes,1,rep,name=tuples,proto3" json:"tuples,omitempty"`
ContinuationToken string `protobuf:"bytes,2,opt,name=continuation_token,json=continuationToken,proto3" json:"continuation_token,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ReadResponse) Reset() {
@@ -443,11 +436,10 @@ func (x *ReadResponse) GetContinuationToken() string {
}
type WriteRequestWrites struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
TupleKeys []*TupleKey `protobuf:"bytes,1,rep,name=tuple_keys,json=tupleKeys,proto3" json:"tuple_keys,omitempty"`
unknownFields protoimpl.UnknownFields
TupleKeys []*TupleKey `protobuf:"bytes,1,rep,name=tuple_keys,json=tupleKeys,proto3" json:"tuple_keys,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *WriteRequestWrites) Reset() {
@@ -488,11 +480,10 @@ func (x *WriteRequestWrites) GetTupleKeys() []*TupleKey {
}
type WriteRequestDeletes struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
TupleKeys []*TupleKeyWithoutCondition `protobuf:"bytes,1,rep,name=tuple_keys,json=tupleKeys,proto3" json:"tuple_keys,omitempty"`
unknownFields protoimpl.UnknownFields
TupleKeys []*TupleKeyWithoutCondition `protobuf:"bytes,1,rep,name=tuple_keys,json=tupleKeys,proto3" json:"tuple_keys,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *WriteRequestDeletes) Reset() {
@@ -533,13 +524,12 @@ func (x *WriteRequestDeletes) GetTupleKeys() []*TupleKeyWithoutCondition {
}
type WriteRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"`
Writes *WriteRequestWrites `protobuf:"bytes,2,opt,name=writes,proto3" json:"writes,omitempty"`
Deletes *WriteRequestDeletes `protobuf:"bytes,3,opt,name=deletes,proto3" json:"deletes,omitempty"`
unknownFields protoimpl.UnknownFields
Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"`
Writes *WriteRequestWrites `protobuf:"bytes,2,opt,name=writes,proto3" json:"writes,omitempty"`
Deletes *WriteRequestDeletes `protobuf:"bytes,3,opt,name=deletes,proto3" json:"deletes,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *WriteRequest) Reset() {
@@ -594,9 +584,9 @@ func (x *WriteRequest) GetDeletes() *WriteRequestDeletes {
}
type WriteResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *WriteResponse) Reset() {
@@ -630,13 +620,12 @@ func (*WriteResponse) Descriptor() ([]byte, []int) {
}
type BatchCheckRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Subject string `protobuf:"bytes,1,opt,name=subject,proto3" json:"subject,omitempty"`
Namespace string `protobuf:"bytes,2,opt,name=namespace,proto3" json:"namespace,omitempty"`
Items []*BatchCheckItem `protobuf:"bytes,3,rep,name=items,proto3" json:"items,omitempty"`
unknownFields protoimpl.UnknownFields
Subject string `protobuf:"bytes,1,opt,name=subject,proto3" json:"subject,omitempty"`
Namespace string `protobuf:"bytes,2,opt,name=namespace,proto3" json:"namespace,omitempty"`
Items []*BatchCheckItem `protobuf:"bytes,3,rep,name=items,proto3" json:"items,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *BatchCheckRequest) Reset() {
@@ -691,16 +680,15 @@ func (x *BatchCheckRequest) GetItems() []*BatchCheckItem {
}
type BatchCheckItem struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Verb string `protobuf:"bytes,1,opt,name=verb,proto3" json:"verb,omitempty"`
Group string `protobuf:"bytes,2,opt,name=group,proto3" json:"group,omitempty"`
Resource string `protobuf:"bytes,3,opt,name=resource,proto3" json:"resource,omitempty"`
Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"`
Subresource string `protobuf:"bytes,5,opt,name=subresource,proto3" json:"subresource,omitempty"`
Folder string `protobuf:"bytes,6,opt,name=folder,proto3" json:"folder,omitempty"`
unknownFields protoimpl.UnknownFields
Verb string `protobuf:"bytes,1,opt,name=verb,proto3" json:"verb,omitempty"`
Group string `protobuf:"bytes,2,opt,name=group,proto3" json:"group,omitempty"`
Resource string `protobuf:"bytes,3,opt,name=resource,proto3" json:"resource,omitempty"`
Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"`
Subresource string `protobuf:"bytes,5,opt,name=subresource,proto3" json:"subresource,omitempty"`
Folder string `protobuf:"bytes,6,opt,name=folder,proto3" json:"folder,omitempty"`
sizeCache protoimpl.SizeCache
}
func (x *BatchCheckItem) Reset() {
@@ -776,11 +764,10 @@ func (x *BatchCheckItem) GetFolder() string {
}
type BatchCheckResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Groups map[string]*BatchCheckGroupResource `protobuf:"bytes,1,rep,name=groups,proto3" json:"groups,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
unknownFields protoimpl.UnknownFields
Groups map[string]*BatchCheckGroupResource `protobuf:"bytes,1,rep,name=groups,proto3" json:"groups,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
sizeCache protoimpl.SizeCache
}
func (x *BatchCheckResponse) Reset() {
@@ -821,11 +808,10 @@ func (x *BatchCheckResponse) GetGroups() map[string]*BatchCheckGroupResource {
}
type BatchCheckGroupResource struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Items map[string]bool `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"varint,2,opt,name=value"`
unknownFields protoimpl.UnknownFields
Items map[string]bool `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3"`
sizeCache protoimpl.SizeCache
}
func (x *BatchCheckGroupResource) Reset() {
+6
View File
@@ -1750,6 +1750,12 @@ var (
Owner: grafanaFrontendPlatformSquad,
AllowSelfServe: false,
},
{
Name: "elasticsearchImprovedParsing",
Description: "Enables less memory intensive Elasticsearch result parsing",
Stage: FeatureStageExperimental,
Owner: awsDatasourcesSquad,
},
}
)
+1
View File
@@ -234,3 +234,4 @@ teamHttpHeadersMimir,experimental,@grafana/identity-access-team,false,false,fals
ABTestFeatureToggleA,experimental,@grafana/sharing-squad,false,false,false
ABTestFeatureToggleB,experimental,@grafana/sharing-squad,false,false,false
queryLibraryDashboards,experimental,@grafana/grafana-frontend-platform,false,false,false
elasticsearchImprovedParsing,experimental,@grafana/aws-datasources,false,false,false
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
234 ABTestFeatureToggleA experimental @grafana/sharing-squad false false false
235 ABTestFeatureToggleB experimental @grafana/sharing-squad false false false
236 queryLibraryDashboards experimental @grafana/grafana-frontend-platform false false false
237 elasticsearchImprovedParsing experimental @grafana/aws-datasources false false false
+4
View File
@@ -946,4 +946,8 @@ const (
// FlagQueryLibraryDashboards
// Enables Query Library feature in Dashboards
FlagQueryLibraryDashboards = "queryLibraryDashboards"
// FlagElasticsearchImprovedParsing
// Enables less memory intensive Elasticsearch result parsing
FlagElasticsearchImprovedParsing = "elasticsearchImprovedParsing"
)
+15
View File
@@ -1321,6 +1321,21 @@
"codeowner": "@grafana/aws-datasources"
}
},
{
"metadata": {
"name": "elasticsearchImprovedParsing",
"resourceVersion": "1736808262603",
"creationTimestamp": "2025-01-13T20:32:35Z",
"annotations": {
"grafana.app/updatedTimestamp": "2025-01-13 22:44:22.603729 +0000 UTC"
}
},
"spec": {
"description": "Enables less memory intensive Elasticsearch result parsing",
"stage": "experimental",
"codeowner": "@grafana/aws-datasources"
}
},
{
"metadata": {
"name": "enableDatagridEditing",
+2 -1
View File
@@ -11,6 +11,7 @@ import (
alertingImages "github.com/grafana/alerting/images"
alertingModels "github.com/grafana/alerting/models"
alertingNotify "github.com/grafana/alerting/notify"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/store"
@@ -32,7 +33,7 @@ func (i imageProvider) GetImage(ctx context.Context, uri string) (*alertingImage
image, err := i.getImageFromURI(ctx, uri)
if err != nil {
if errors.Is(err, models.ErrImageNotFound) {
i.logger.Info("Image not found in database")
i.logger.Info("Image not found in database", "uri", uri)
return nil, alertingImages.ErrImageNotFound
}
return nil, err
+13 -11
View File
@@ -24,6 +24,8 @@ var (
ResendDelay = 30 * time.Second
)
type takeImageFn func(reason string) *ngModels.Image
// AlertInstanceManager defines the interface for querying the current alert instances.
type AlertInstanceManager interface {
GetAll(orgID int64) []*State
@@ -327,21 +329,21 @@ func (st *Manager) ProcessEvalResults(
logger := st.log.FromContext(ctx)
// lazy evaluation of takeImage only once and only if it is requested.
var fn func() *ngModels.Image
var fn takeImageFn
{
var image *ngModels.Image
var imageTaken bool
fn = func() *ngModels.Image {
fn = func(reason string) *ngModels.Image {
if imageTaken {
return image
}
logger.Debug("Taking image", "dashboard", alertRule.GetDashboardUID(), "panel", alertRule.GetPanelID())
logger.Debug("Taking image", "dashboard", alertRule.GetDashboardUID(), "panel", alertRule.GetPanelID(), "reason", reason)
img, err := takeImage(ctx, st.images, alertRule)
imageTaken = true
if err != nil {
logger.Warn("Failed to take an image",
"dashboard", alertRule.GetDashboardUID(),
"panel", alertRule.GetPanelID(),
"panel", alertRule.GetPanelID(), "reason", reason,
"error", err)
return nil
}
@@ -395,7 +397,7 @@ func (st *Manager) updateLastSentAt(states StateTransitions, evaluatedAt time.Ti
return result
}
func (st *Manager) setNextStateForRule(ctx context.Context, alertRule *ngModels.AlertRule, results eval.Results, extraLabels data.Labels, logger log.Logger, takeImageFn func() *ngModels.Image) []StateTransition {
func (st *Manager) setNextStateForRule(ctx context.Context, alertRule *ngModels.AlertRule, results eval.Results, extraLabels data.Labels, logger log.Logger, takeImageFn takeImageFn) []StateTransition {
if st.applyNoDataAndErrorToAllStates && results.IsNoData() && (alertRule.NoDataState == ngModels.Alerting || alertRule.NoDataState == ngModels.OK || alertRule.NoDataState == ngModels.KeepLast) { // If it is no data, check the mapping and switch all results to the new state
// aggregate UID of datasources that returned NoData into one and provide as auxiliary info via annotationa. See: https://github.com/grafana/grafana/issues/88184
var refIds strings.Builder
@@ -445,7 +447,7 @@ func (st *Manager) setNextStateForRule(ctx context.Context, alertRule *ngModels.
return transitions
}
func (st *Manager) setNextStateForAll(alertRule *ngModels.AlertRule, result eval.Result, logger log.Logger, extraAnnotations data.Labels, takeImageFn func() *ngModels.Image) []StateTransition {
func (st *Manager) setNextStateForAll(alertRule *ngModels.AlertRule, result eval.Result, logger log.Logger, extraAnnotations data.Labels, takeImageFn takeImageFn) []StateTransition {
currentStates := st.cache.getStatesForRuleUID(alertRule.OrgID, alertRule.UID, false)
transitions := make([]StateTransition, 0, len(currentStates))
updated := ruleStates{
@@ -462,7 +464,7 @@ func (st *Manager) setNextStateForAll(alertRule *ngModels.AlertRule, result eval
}
// Set the current state based on evaluation results
func (st *Manager) setNextState(alertRule *ngModels.AlertRule, currentState *State, result eval.Result, extraAnnotations data.Labels, logger log.Logger, takeImageFn func() *ngModels.Image) StateTransition {
func (st *Manager) setNextState(alertRule *ngModels.AlertRule, currentState *State, result eval.Result, extraAnnotations data.Labels, logger log.Logger, takeImageFn takeImageFn) StateTransition {
start := st.clock.Now()
currentState.LastEvaluationTime = result.EvaluatedAt
@@ -538,8 +540,8 @@ func (st *Manager) setNextState(alertRule *ngModels.AlertRule, currentState *Sta
currentState.ResolvedAt = nil
}
if shouldTakeImage(currentState.State, oldState, currentState.Image, newlyResolved) {
image := takeImageFn()
if reason := shouldTakeImage(currentState.State, oldState, currentState.Image, newlyResolved); reason != "" {
image := takeImageFn(reason)
if image != nil {
currentState.Image = image
}
@@ -606,7 +608,7 @@ func translateInstanceState(state ngModels.InstanceStateType) eval.State {
}
}
func (st *Manager) deleteStaleStatesFromCache(logger log.Logger, evaluatedAt time.Time, alertRule *ngModels.AlertRule, takeImageFn func() *ngModels.Image) []StateTransition {
func (st *Manager) deleteStaleStatesFromCache(logger log.Logger, evaluatedAt time.Time, alertRule *ngModels.AlertRule, takeImageFn takeImageFn) []StateTransition {
// If we are removing two or more stale series it makes sense to share the resolved image as the alert rule is the same.
// TODO: We will need to change this when we support images without screenshots as each series will have a different image
staleStates := st.cache.deleteRuleStates(alertRule.GetKey(), func(s *State) bool {
@@ -626,7 +628,7 @@ func (st *Manager) deleteStaleStatesFromCache(logger log.Logger, evaluatedAt tim
if oldState == eval.Alerting {
s.ResolvedAt = &evaluatedAt
image := takeImageFn()
image := takeImageFn("stale state")
if image != nil {
s.Image = image
}
+18 -7
View File
@@ -575,13 +575,24 @@ func (a *State) IsStale() bool {
return a.StateReason == models.StateReasonMissingSeries
}
// shouldTakeImage returns true if the state just has transitioned to alerting from another state,
// transitioned to alerting in a previous evaluation but does not have a screenshot, or has just
// been resolved.
func shouldTakeImage(state, previousState eval.State, previousImage *models.Image, resolved bool) bool {
return resolved ||
state == eval.Alerting && previousState != eval.Alerting ||
state == eval.Alerting && previousImage == nil
// shouldTakeImage determines whether a new image should be taken for a given transition. This should return true when
// newly transitioning to an alerting state, when no valid image exists, or when the alert has been resolved.
func shouldTakeImage(state, previousState eval.State, previousImage *models.Image, resolved bool) string {
if resolved {
return "resolved"
}
if state == eval.Alerting {
if previousState != eval.Alerting {
return "transition to alerting"
}
if previousImage == nil {
return "no image"
}
if previousImage.HasExpired() {
return "expired image"
}
}
return ""
}
// takeImage takes an image for the alert rule. It returns nil if screenshots are disabled or
+9 -3
View File
@@ -602,15 +602,21 @@ func TestShouldTakeImage(t *testing.T) {
state: eval.Pending,
previousState: eval.Normal,
}, {
name: "should not take image for alerting state with image",
name: "should not take image for alerting state with valid image",
state: eval.Alerting,
previousState: eval.Alerting,
previousImage: &ngmodels.Image{URL: "https://example.com/foo.png"},
previousImage: &ngmodels.Image{URL: "https://example.com/foo.png", ExpiresAt: time.Now().Add(time.Hour)},
}, {
name: "should take image for alerting state with expired image",
state: eval.Alerting,
previousState: eval.Alerting,
previousImage: &ngmodels.Image{URL: "https://example.com/foo.png", ExpiresAt: time.Now().Add(-time.Hour)},
expected: true,
}}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, shouldTakeImage(test.state, test.previousState, test.previousImage, test.resolved))
assert.Equal(t, test.expected, shouldTakeImage(test.state, test.previousState, test.previousImage, test.resolved) != "")
})
}
}
-114
View File
@@ -1,114 +0,0 @@
package apistore
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
"github.com/grafana/authlib/claims"
"github.com/grafana/grafana/pkg/storage/unified/resource"
)
type HistoryConnector interface {
rest.Storage
rest.Connecter
rest.StorageMetadata
}
func NewHistoryConnector(index resource.ResourceIndexServer, gr schema.GroupResource) HistoryConnector {
return &historyREST{
index: index,
gr: gr,
}
}
type historyREST struct {
index resource.ResourceIndexServer // should be a client!
gr schema.GroupResource
}
func (r *historyREST) New() runtime.Object {
return &metav1.PartialObjectMetadataList{}
}
func (r *historyREST) Destroy() {
}
func (r *historyREST) ConnectMethods() []string {
return []string{"GET"}
}
func (r *historyREST) ProducesMIMETypes(verb string) []string {
return nil
}
func (r *historyREST) ProducesObject(verb string) interface{} {
return &metav1.PartialObjectMetadataList{}
}
func (r *historyREST) NewConnectOptions() (runtime.Object, bool, string) {
return nil, false, ""
}
func (r *historyREST) Connect(ctx context.Context, uid string, opts runtime.Object, responder rest.Responder) (http.Handler, error) {
info, err := NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, err
}
key := &resource.ResourceKey{
Namespace: info.Value,
Group: r.gr.Group,
Resource: r.gr.Resource,
Name: uid,
}
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
query := req.URL.Query()
rsp, err := r.index.History(ctx, &resource.HistoryRequest{
NextPageToken: query.Get("token"),
Limit: 100, // TODO, from query
Key: key,
})
if err != nil {
responder.Error(err)
return
}
list := &metav1.PartialObjectMetadataList{
ListMeta: metav1.ListMeta{
Continue: rsp.NextPageToken,
},
}
if rsp.ResourceVersion > 0 {
list.ResourceVersion = strconv.FormatInt(rsp.ResourceVersion, 10)
}
for _, v := range rsp.Items {
partial := metav1.PartialObjectMetadata{}
err = json.Unmarshal(v.PartialObjectMeta, &partial)
if err != nil {
responder.Error(err)
return
}
list.Items = append(list.Items, partial)
}
responder.Object(http.StatusOK, list)
}), nil
}
// TODO: This is a temporary copy of the function from pkg/services/apiserver/endpoints/request/namespace.go
func NamespaceInfoFrom(ctx context.Context, requireOrgID bool) (claims.NamespaceInfo, error) {
info, err := claims.ParseNamespace(request.NamespaceValue(ctx))
if err == nil && requireOrgID && info.OrgID < 1 {
return info, fmt.Errorf("expected valid orgId in namespace")
}
return info, err
}
File diff suppressed because it is too large. Load diff
@@ -22,22 +22,6 @@ message ResourceWrapper {
bytes value = 2;
}
// The history and trash commands need access to commit messages
message ResourceMeta {
// The resource version
int64 resource_version = 1;
// Size of the full resource body
int32 size = 3;
// Hash for the resource
string hash = 4;
// The kubernetes metadata section (not the full resource)
// https://github.com/kubernetes/kubernetes/blob/v1.30.2/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/types.go#L1496
bytes partial_object_meta = 6;
}
// Status structure is copied from:
// https://github.com/kubernetes/apimachinery/blob/v0.30.1/pkg/apis/meta/v1/generated.proto#L979
// However, this is only used for error handling, never for succesful results
@@ -443,33 +427,6 @@ message ResourceSearchResponse {
map<string,Facet> facet = 7;
}
message HistoryRequest {
// Starting from the requested page (other query parameters must match!)
string next_page_token = 1;
// Maximum number of items to return
int64 limit = 2;
// Resource identifier
ResourceKey key = 3;
// List the deleted values (eg, show trash)
bool show_deleted = 4;
}
message HistoryResponse {
repeated ResourceMeta items = 1;
// More results exist... pass this in the next request
string next_page_token = 2;
// ResourceVersion of the list response
int64 resource_version = 3;
// Error details
ErrorResult error = 4;
}
// List items within a resource type & repository name
// Access control is managed above this request
message ListRepositoryObjectsRequest {
@@ -791,9 +748,6 @@ service ResourceIndex {
// Get the resource stats
rpc GetStats(ResourceStatsRequest) returns (ResourceStatsResponse);
// Show resource history (and trash)
rpc History(HistoryRequest) returns (HistoryResponse);
}
// Query repository info from the search index.
@@ -388,7 +388,6 @@ var ResourceStore_ServiceDesc = grpc.ServiceDesc{
const (
ResourceIndex_Search_FullMethodName = "/resource.ResourceIndex/Search"
ResourceIndex_GetStats_FullMethodName = "/resource.ResourceIndex/GetStats"
ResourceIndex_History_FullMethodName = "/resource.ResourceIndex/History"
)
// ResourceIndexClient is the client API for ResourceIndex service.
@@ -401,8 +400,6 @@ type ResourceIndexClient interface {
Search(ctx context.Context, in *ResourceSearchRequest, opts ...grpc.CallOption) (*ResourceSearchResponse, error)
// Get the resource stats
GetStats(ctx context.Context, in *ResourceStatsRequest, opts ...grpc.CallOption) (*ResourceStatsResponse, error)
// Show resource history (and trash)
History(ctx context.Context, in *HistoryRequest, opts ...grpc.CallOption) (*HistoryResponse, error)
}
type resourceIndexClient struct {
@@ -433,16 +430,6 @@ func (c *resourceIndexClient) GetStats(ctx context.Context, in *ResourceStatsReq
return out, nil
}
func (c *resourceIndexClient) History(ctx context.Context, in *HistoryRequest, opts ...grpc.CallOption) (*HistoryResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(HistoryResponse)
err := c.cc.Invoke(ctx, ResourceIndex_History_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// ResourceIndexServer is the server API for ResourceIndex service.
// All implementations should embed UnimplementedResourceIndexServer
// for forward compatibility
@@ -453,8 +440,6 @@ type ResourceIndexServer interface {
Search(context.Context, *ResourceSearchRequest) (*ResourceSearchResponse, error)
// Get the resource stats
GetStats(context.Context, *ResourceStatsRequest) (*ResourceStatsResponse, error)
// Show resource history (and trash)
History(context.Context, *HistoryRequest) (*HistoryResponse, error)
}
// UnimplementedResourceIndexServer should be embedded to have forward compatible implementations.
@@ -467,9 +452,6 @@ func (UnimplementedResourceIndexServer) Search(context.Context, *ResourceSearchR
func (UnimplementedResourceIndexServer) GetStats(context.Context, *ResourceStatsRequest) (*ResourceStatsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetStats not implemented")
}
func (UnimplementedResourceIndexServer) History(context.Context, *HistoryRequest) (*HistoryResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method History not implemented")
}
// UnsafeResourceIndexServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to ResourceIndexServer will
@@ -518,24 +500,6 @@ func _ResourceIndex_GetStats_Handler(srv interface{}, ctx context.Context, dec f
return interceptor(ctx, in, info, handler)
}
func _ResourceIndex_History_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(HistoryRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ResourceIndexServer).History(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: ResourceIndex_History_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ResourceIndexServer).History(ctx, req.(*HistoryRequest))
}
return interceptor(ctx, in, info, handler)
}
// ResourceIndex_ServiceDesc is the grpc.ServiceDesc for ResourceIndex service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -551,10 +515,6 @@ var ResourceIndex_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetStats",
Handler: _ResourceIndex_GetStats_Handler,
},
{
MethodName: "History",
Handler: _ResourceIndex_History_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "resource.proto",
-5
View File
@@ -133,11 +133,6 @@ func newSearchSupport(opts SearchOptions, storage StorageBackend, access authz.A
return support, err
}
// History implements ResourceIndexServer.
func (s *searchSupport) History(context.Context, *HistoryRequest) (*HistoryResponse, error) {
return nil, fmt.Errorf("not implemented yet... likely should not be the search server")
}
func (s *searchSupport) ListRepositoryObjects(ctx context.Context, req *ListRepositoryObjectsRequest) (*ListRepositoryObjectsResponse, error) {
if req.NextPageToken != "" {
return &ListRepositoryObjectsResponse{
-5
View File
@@ -1073,11 +1073,6 @@ func (s *server) GetStats(ctx context.Context, req *ResourceStatsRequest) (*Reso
return s.search.GetStats(ctx, req)
}
// History implements ResourceServer.
func (s *server) History(ctx context.Context, req *HistoryRequest) (*HistoryResponse, error) {
return s.search.History(ctx, req)
}
func (s *server) ListRepositoryObjects(ctx context.Context, req *ListRepositoryObjectsRequest) (*ListRepositoryObjectsResponse, error) {
return s.search.ListRepositoryObjects(ctx, req)
}
-10
View File
@@ -53,16 +53,6 @@ func (b *backend) GetStats(ctx context.Context, req *resource.ResourceStatsReque
return rsp, nil
}
// History implements resource.ResourceIndexServer.
func (b *backend) History(context.Context, *resource.HistoryRequest) (*resource.HistoryResponse, error) {
return &resource.HistoryResponse{
Error: &resource.ErrorResult{
Code: http.StatusNotImplemented,
Message: "SQL backend does not implement History",
},
}, nil
}
func (b *backend) RepositoryList(ctx context.Context, req *resource.ListRepositoryObjectsRequest) (*resource.ListRepositoryObjectsResponse, error) {
return nil, fmt.Errorf("SQL backend does not implement RepositoryList")
}
@@ -12,6 +12,7 @@ import (
"net/url"
"path"
"regexp"
"strconv"
"strings"
"time"
@@ -267,8 +268,23 @@ func (e *AzureLogAnalyticsDatasource) buildQuery(ctx context.Context, query back
func (e *AzureLogAnalyticsDatasource) executeQuery(ctx context.Context, query *AzureLogAnalyticsQuery, dsInfo types.DatasourceInfo, client *http.Client, url string) (*backend.DataResponse, error) {
// If azureLogAnalyticsSameAs is defined and set to false, return an error
if sameAs, ok := dsInfo.JSONData["azureLogAnalyticsSameAs"]; ok && !sameAs.(bool) {
return nil, backend.DownstreamError(fmt.Errorf("credentials for Log Analytics are no longer supported. Go to the data source configuration to update Azure Monitor credentials"))
if sameAs, ok := dsInfo.JSONData["azureLogAnalyticsSameAs"]; ok {
sameAsValue, ok := sameAs.(bool)
if !ok {
stringVal, ok := sameAs.(string)
if !ok {
return nil, backend.DownstreamError(fmt.Errorf("unknown value for Log Analytics credentials. Go to the data source configuration to update Azure Monitor credentials"))
}
var err error
sameAsValue, err = strconv.ParseBool(stringVal)
if err != nil {
return nil, backend.DownstreamError(fmt.Errorf("unknown value for Log Analytics credentials. Go to the data source configuration to update Azure Monitor credentials"))
}
}
if !sameAsValue {
return nil, backend.DownstreamError(fmt.Errorf("credentials for Log Analytics are no longer supported. Go to the data source configuration to update Azure Monitor credentials"))
}
}
queryJSONModel := dataquery.AzureMonitorQuery{}
@@ -708,13 +708,26 @@ func Test_executeQueryErrorWithDifferentLogAnalyticsCreds(t *testing.T) {
query := &AzureLogAnalyticsQuery{
TimeRange: backend.TimeRange{},
}
_, err := ds.executeQuery(ctx, query, dsInfo, &http.Client{}, dsInfo.Services["Azure Log Analytics"].URL)
if err == nil {
t.Fatal("expecting an error")
}
if !strings.Contains(err.Error(), "credentials for Log Analytics are no longer supported") {
t.Error("expecting the error to inform of bad credentials")
}
t.Run("errors with azureLogAnalyticsSameAs set to false (boolean)", func(t *testing.T) {
_, err := ds.executeQuery(ctx, query, dsInfo, &http.Client{}, dsInfo.Services["Azure Log Analytics"].URL)
if err == nil {
t.Fatal("expecting an error")
}
if !strings.Contains(err.Error(), "credentials for Log Analytics are no longer supported") {
t.Error("expecting the error to inform of bad credentials")
}
})
t.Run("errors with azureLogAnalyticsSameAs set to false (boolean)", func(t *testing.T) {
dsInfo.JSONData["azureLogAnalyticsSameAs"] = "false"
_, err := ds.executeQuery(ctx, query, dsInfo, &http.Client{}, dsInfo.Services["Azure Log Analytics"].URL)
if err == nil {
t.Fatal("expecting an error")
}
if !strings.Contains(err.Error(), "credentials for Log Analytics are no longer supported") {
t.Error("expecting the error to inform of bad credentials")
}
})
}
func Test_exemplarsFeatureToggle(t *testing.T) {
+208 -5
View File
@@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"path"
@@ -20,6 +21,7 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
"github.com/grafana/grafana-plugin-sdk-go/backend/tracing"
"github.com/grafana/grafana/pkg/services/featuremgmt"
)
// Used in logging to mark a stage
@@ -202,8 +204,6 @@ func (c *baseClientImpl) ExecuteMultisearch(r *MultiSearchRequest) (*MultiSearch
c.logger.Info("Response received from Elasticsearch", "status", "ok", "statusCode", res.StatusCode, "contentLength", res.ContentLength, "duration", time.Since(start), "stage", StageDatabaseRequest)
start = time.Now()
var msr MultiSearchResponse
dec := json.NewDecoder(res.Body)
_, resSpan := tracing.DefaultTracer().Start(c.ctx, "datasource.elasticsearch.queryData.executeMultisearch.decodeResponse")
defer func() {
if err != nil {
@@ -212,19 +212,218 @@ func (c *baseClientImpl) ExecuteMultisearch(r *MultiSearchRequest) (*MultiSearch
}
resSpan.End()
}()
err = dec.Decode(&msr)
var msr MultiSearchResponse
improvedParsingEnabled := isFeatureEnabled(c.ctx, featuremgmt.FlagElasticsearchImprovedParsing)
if improvedParsingEnabled {
err = StreamMultiSearchResponse(res.Body, &msr)
} else {
dec := json.NewDecoder(res.Body)
err = dec.Decode(&msr)
}
if err != nil {
c.logger.Error("Failed to decode response from Elasticsearch", "error", err, "duration", time.Since(start))
c.logger.Error("Failed to decode response from Elasticsearch", "error", err, "duration", time.Since(start), "improvedParsingEnabled", improvedParsingEnabled)
return nil, err
}
c.logger.Debug("Completed decoding of response from Elasticsearch", "duration", time.Since(start))
c.logger.Debug("Completed decoding of response from Elasticsearch", "duration", time.Since(start), "improvedParsingEnabled", improvedParsingEnabled)
msr.Status = res.StatusCode
return &msr, nil
}
// StreamMultiSearchResponse processes the JSON response in a streaming fashion
func StreamMultiSearchResponse(body io.Reader, msr *MultiSearchResponse) error {
dec := json.NewDecoder(body)
_, err := dec.Token() // reads the `{` opening brace
if err != nil {
return err
}
for dec.More() {
tok, err := dec.Token()
if err != nil {
return err
}
if tok == "responses" {
_, err := dec.Token() // reads the `[` opening bracket for responses array
if err != nil {
return err
}
for dec.More() {
var sr SearchResponse
_, err := dec.Token() // reads `{` for each SearchResponse
if err != nil {
return err
}
for dec.More() {
field, err := dec.Token()
if err != nil {
return err
}
switch field {
case "hits":
sr.Hits = &SearchResponseHits{}
err := processHits(dec, &sr)
if err != nil {
return err
}
case "aggregations":
err := dec.Decode(&sr.Aggregations)
if err != nil {
return err
}
case "error":
err := dec.Decode(&sr.Error)
if err != nil {
return err
}
default:
// skip over unknown fields
err := skipUnknownField(dec)
if err != nil {
return err
}
}
}
msr.Responses = append(msr.Responses, &sr)
_, err = dec.Token() // reads `}` closing for each SearchResponse
if err != nil {
return err
}
}
_, err = dec.Token() // reads the `]` closing bracket for responses array
if err != nil {
return err
}
} else {
err := skipUnknownField(dec)
if err != nil {
return err
}
}
}
_, err = dec.Token() // reads the `}` closing brace for the entire JSON
return err
}
// processHits processes the hits in the JSON response incrementally.
func processHits(dec *json.Decoder, sr *SearchResponse) error {
tok, err := dec.Token() // reads the `{` opening brace for the hits object
if err != nil {
return err
}
if tok != json.Delim('{') {
return fmt.Errorf("expected '{' for hits object, got %v", tok)
}
for dec.More() {
tok, err := dec.Token()
if err != nil {
return err
}
if tok == "hits" {
if err := streamHitsArray(dec, sr); err != nil {
return err
}
} else {
// ignore these fields as they are not used in the current implementation
err := skipUnknownField(dec)
if err != nil {
return err
}
}
}
// read the closing `}` for the hits object
_, err = dec.Token()
if err != nil {
return err
}
return nil
}
// streamHitsArray processes the hits array field incrementally.
func streamHitsArray(dec *json.Decoder, sr *SearchResponse) error {
tok, err := dec.Token()
if err != nil {
return err
}
// read the opening `[` for the hits array
if tok != json.Delim('[') {
return fmt.Errorf("expected '[' for hits array, got %v", tok)
}
for dec.More() {
var hit map[string]interface{}
err = dec.Decode(&hit)
if err != nil {
return err
}
sr.Hits.Hits = append(sr.Hits.Hits, hit)
}
// read the closing bracket `]` for the hits array
tok, err = dec.Token()
if err != nil {
return err
}
if tok != json.Delim(']') {
return fmt.Errorf("expected ']' for closing hits array, got %v", tok)
}
return nil
}
// skipUnknownField skips over an unknown JSON field's value in the stream.
func skipUnknownField(dec *json.Decoder) error {
tok, err := dec.Token()
if err != nil {
return err
}
switch tok {
case json.Delim('{'):
// skip everything inside the object until we reach the closing `}`
for dec.More() {
if err := skipUnknownField(dec); err != nil {
return err
}
}
_, err = dec.Token() // read the closing `}`
return err
case json.Delim('['):
// skip everything inside the array until we reach the closing `]`
for dec.More() {
if err := skipUnknownField(dec); err != nil {
return err
}
}
_, err = dec.Token() // read the closing `]`
return err
default:
// no further action needed for primitives
return nil
}
}
func (c *baseClientImpl) createMultiSearchRequests(searchRequests []*SearchRequest) []*multiRequest {
multiRequests := []*multiRequest{}
@@ -264,3 +463,7 @@ func (c *baseClientImpl) getMultiSearchQueryParameters() string {
func (c *baseClientImpl) MultiSearch() *MultiSearchRequestBuilder {
return NewMultiSearchRequestBuilder()
}
func isFeatureEnabled(ctx context.Context, feature string) bool {
return backend.GrafanaConfigFromContext(ctx).FeatureToggles().IsEnabled(feature)
}
@@ -6,6 +6,7 @@ import (
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
@@ -286,6 +287,129 @@ func TestClient_Index(t *testing.T) {
}
}
func TestStreamMultiSearchResponse_Success(t *testing.T) {
jsonBody := `
{
"responses": [
{ "hits": { "hits": [] } },
{ "hits": { "hits": [] } }
]
}`
msr := &MultiSearchResponse{}
err := StreamMultiSearchResponse(strings.NewReader(jsonBody), msr)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if len(msr.Responses) != 2 {
t.Errorf("expected 2 responses, got %d", len(msr.Responses))
}
}
func TestStreamMultiSearchResponse_MalformedJSON(t *testing.T) {
jsonBody := `
{
"responses": [
{ "hits": { "hits": [] } }
` // Missing closing braces
msr := &MultiSearchResponse{}
err := StreamMultiSearchResponse(strings.NewReader(jsonBody), msr)
if err == nil {
t.Fatalf("expected an error, got none")
}
}
func TestStreamMultiSearchResponse_MissingResponses(t *testing.T) {
jsonBody := `
{
"something_else": [
{ "hits": { "hits": [] } }
]
}`
msr := &MultiSearchResponse{}
err := StreamMultiSearchResponse(strings.NewReader(jsonBody), msr)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if len(msr.Responses) != 0 {
t.Errorf("expected 0 responses, got %d", len(msr.Responses))
}
}
func TestStreamMultiSearchResponse_EmptyBody(t *testing.T) {
jsonBody := `{}`
msr := &MultiSearchResponse{}
err := StreamMultiSearchResponse(strings.NewReader(jsonBody), msr)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if len(msr.Responses) != 0 {
t.Errorf("expected 0 responses, got %d", len(msr.Responses))
}
}
func TestStreamMultiSearchResponse_InvalidJSONStart(t *testing.T) {
jsonBody := `invalid_json`
msr := &MultiSearchResponse{}
err := StreamMultiSearchResponse(strings.NewReader(jsonBody), msr)
if err == nil {
t.Fatalf("expected an error due to invalid JSON, got none")
}
}
func TestStreamMultiSearchResponse_InvalidHitsField(t *testing.T) {
jsonBody := `
{
"responses": [
{ "hits": "invalid_string_value" }
]
}`
msr := &MultiSearchResponse{}
err := StreamMultiSearchResponse(strings.NewReader(jsonBody), msr)
if err == nil {
t.Fatalf("expected an error due to invalid 'hits' field, got none")
}
if err.Error() != "expected '{' for hits object, got invalid_string_value" {
t.Errorf("unexpected error message: %v", err)
}
}
func TestStreamMultiSearchResponse_InvalidHitElement(t *testing.T) {
jsonBody := `
{
"responses": [
{ "hits": { "hits": ["invalid_element"] } }
]
}`
msr := &MultiSearchResponse{}
err := StreamMultiSearchResponse(strings.NewReader(jsonBody), msr)
if err == nil {
t.Fatalf("expected an error due to invalid element in 'hits' array, got none")
}
expected := "json: cannot unmarshal string into Go value of type map[string]interface {}"
if err.Error() != expected {
t.Errorf("unexpected error message: expected %v, got %v", expected, err)
}
}
func createMultisearchForTest(t *testing.T, c Client, timeRange backend.TimeRange) (*MultiSearchRequest, error) {
t.Helper()
+35 -28
View File
@@ -968,13 +968,13 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
/>
</div>
)}
{visualisationType === 'logs' && (
<div
className={config.featureToggles.logsInfiniteScrolling ? styles.scrollableLogRows : styles.logRows}
data-testid="logRows"
ref={logsContainerRef}
>
{hasData && (
{visualisationType === 'logs' && hasData && (
<>
<div
className={config.featureToggles.logsInfiniteScrolling ? styles.scrollableLogRows : styles.logRows}
data-testid="logRows"
ref={logsContainerRef}
>
<InfiniteScroll
loading={loading}
loadMoreLogs={infiniteScrollAvailable ? loadMoreLogs : undefined}
@@ -1022,41 +1022,41 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
renderPreview
/>
</InfiniteScroll>
)}
</div>
</div>
<LogsNavigation
logsSortOrder={logsSortOrder}
visibleRange={navigationRange ?? absoluteRange}
absoluteRange={absoluteRange}
timeZone={timeZone}
onChangeTime={onChangeTime}
loading={loading}
queries={logsQueries ?? []}
scrollToTopLogs={scrollToTopLogs}
addResultsToCache={addResultsToCache}
clearCache={clearCache}
/>
</>
)}
{!loading && !hasData && !scanning && (
<div className={styles.logRows}>
<div className={styles.noDataWrapper}>
<div className={styles.noData}>
<Trans i18nKey="explore.logs.no-logs-found">No logs found.</Trans>
<Button size="sm" variant="secondary" onClick={onClickScan}>
<Button size="sm" variant="secondary" className={styles.scanButton} onClick={onClickScan}>
<Trans i18nKey="explore.logs.scan-for-older-logs">Scan for older logs</Trans>
</Button>
</div>
</div>
)}
{scanning && (
<div className={styles.logRows}>
<div className={styles.noDataWrapper}>
<div className={styles.noData}>
<span>{scanText}</span>
<Button size="sm" variant="secondary" onClick={onClickStopScan}>
<Button size="sm" variant="secondary" className={styles.scanButton} onClick={onClickStopScan}>
<Trans i18nKey="explore.logs.stop-scan">Stop scan</Trans>
</Button>
</div>
</div>
)}
<LogsNavigation
logsSortOrder={logsSortOrder}
visibleRange={navigationRange ?? absoluteRange}
absoluteRange={absoluteRange}
timeZone={timeZone}
onChangeTime={onChangeTime}
loading={loading}
queries={logsQueries ?? []}
scrollToTopLogs={scrollToTopLogs}
addResultsToCache={addResultsToCache}
clearCache={clearCache}
/>
</div>
</PanelChrome>
</>
@@ -1067,10 +1067,17 @@ export const Logs = withTheme2(UnthemedLogs);
const getStyles = (theme: GrafanaTheme2, wrapLogMessage: boolean, tableHeight: number) => {
return {
noDataWrapper: css({
display: 'flex',
justifyContent: 'center',
width: '100%',
paddingBottom: theme.spacing(2),
}),
noData: css({
'& > *': {
marginLeft: '0.5em',
},
display: 'inline-block',
}),
scanButton: css({
marginLeft: theme.spacing(1),
}),
logOptions: css({
display: 'flex',
@@ -323,7 +323,7 @@ describe('running queries', () => {
cleanSupplementaryQueryAction({ exploreId, type: SupplementaryQueryType.LogsSample }),
]);
});
it('should cancel running query when a new query is issued', async () => {
it('should cancel running queries when a new query is issued', async () => {
const initialState = {
...makeExplorePaneState(),
};
@@ -333,6 +333,23 @@ describe('running queries', () => {
expect(dispatchedActions).toContainEqual(cancelQueriesAction({ exploreId }));
});
it('should not cancel running queries when scanning', async () => {
const initialState = {
...makeExplorePaneState(),
explore: {
panes: {
[exploreId]: {
scanning: true,
},
},
},
};
const dispatchedActions = await thunkTester(initialState)
.givenThunk(runQueries)
.whenThunkIsDispatched({ exploreId });
expect(dispatchedActions).not.toContainEqual(cancelQueriesAction({ exploreId }));
});
});
describe('changeQueries', () => {
+6 -2
View File
@@ -528,7 +528,10 @@ interface RunQueriesOptions {
export const runQueries = createAsyncThunk<void, RunQueriesOptions>(
'explore/runQueries',
async ({ exploreId, preserveCache }, { dispatch, getState }) => {
dispatch(cancelQueries(exploreId));
// Running cancel queries when Explore is scanning will cancel the scanning state
if (!getState().explore?.panes[exploreId]?.scanning) {
dispatch(cancelQueries(exploreId));
}
const { defaultCorrelationEditorDatasource, scopedVars, showCorrelationEditorLinks } = await getCorrelationsData(
getState(),
@@ -658,11 +661,12 @@ export const runQueries = createAsyncThunk<void, RunQueriesOptions>(
// Keep scanning for results if this was the last scanning transaction
if (exploreState!.scanning) {
console.log(data.series);
if (data.state === LoadingState.Done && data.series.length === 0) {
const range = getShiftedTimeRange(-1, exploreState!.range);
dispatch(updateTime({ exploreId, absoluteRange: range }));
dispatch(runQueries({ exploreId }));
} else {
} else if (data.series[0]?.length > 0 || data.state === LoadingState.Done) {
// We can stop scanning if we have a result
dispatch(scanStopAction({ exploreId }));
}
+84 -9
View File
@@ -39,6 +39,7 @@ import { MetricScene } from './MetricScene';
import { MetricSelectScene } from './MetricSelect/MetricSelectScene';
import { MetricsHeader } from './MetricsHeader';
import { getTrailStore } from './TrailStore/TrailStore';
import { NativeHistogramBanner } from './banners/NativeHistogramBanner';
import { MetricDatasourceHelper } from './helpers/MetricDatasourceHelper';
import { reportChangeInLabelFilters, reportExploreMetrics } from './interactions';
import { migrateOtelDeploymentEnvironment } from './migrations/otelDeploymentEnvironment';
@@ -93,10 +94,16 @@ export interface DataTrailState extends SceneObjectState {
// Synced with url
metric?: string;
metricSearch?: string;
histogramsLoaded: boolean;
nativeHistograms: string[];
nativeHistogramMetric: string;
}
export class DataTrail extends SceneObjectBase<DataTrailState> implements SceneObjectWithUrlSync {
protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['metric', 'metricSearch', 'showPreviews'] });
protected _urlSync = new SceneObjectUrlSyncConfig(this, {
keys: ['metric', 'metricSearch', 'showPreviews', 'nativeHistogramMetric'],
});
public constructor(state: Partial<DataTrailState>) {
super({
@@ -120,6 +127,9 @@ export class DataTrail extends SceneObjectBase<DataTrailState> implements SceneO
// preserve the otel join query
otelJoinQuery: state.otelJoinQuery ?? '',
showPreviews: state.showPreviews ?? true,
nativeHistograms: state.nativeHistograms ?? [],
histogramsLoaded: state.histogramsLoaded ?? false,
nativeHistogramMetric: state.nativeHistogramMetric ?? '',
...state,
});
@@ -209,6 +219,9 @@ export class DataTrail extends SceneObjectBase<DataTrailState> implements SceneO
if (name === VAR_DATASOURCE) {
this.datasourceHelper.reset();
// reset native histograms
this.resetNativeHistograms();
if (this.state.afterFirstOtelCheck) {
// we need a new check for OTel
this.setState({ initialOtelCheckComplete: false });
@@ -262,6 +275,33 @@ export class DataTrail extends SceneObjectBase<DataTrailState> implements SceneO
return this.datasourceHelper.getMetricMetadata(metric);
}
public isNativeHistogram(metric: string) {
return this.datasourceHelper.isNativeHistogram(metric);
}
// use this to initialize histograms in all scenes
public async initializeHistograms() {
if (!this.state.histogramsLoaded) {
await this.datasourceHelper.initializeHistograms();
this.setState({
nativeHistograms: this.listNativeHistograms(),
histogramsLoaded: true,
});
}
}
public listNativeHistograms() {
return this.datasourceHelper.listNativeHistograms() ?? [];
}
private resetNativeHistograms() {
this.setState({
histogramsLoaded: false,
nativeHistograms: [],
});
}
public getCurrentMetricMetadata() {
return this.getMetricMetadata(this.state.metric);
}
@@ -277,6 +317,9 @@ export class DataTrail extends SceneObjectBase<DataTrailState> implements SceneO
history: this.state.history,
metric: !state.metric ? undefined : state.metric,
metricSearch: !state.metricSearch ? undefined : state.metricSearch,
// store type because this requires an expensive api call to determine
// when loading the metric scene
nativeHistogramMetric: !state.nativeHistogramMetric ? undefined : state.nativeHistogramMetric,
})
);
@@ -292,7 +335,13 @@ export class DataTrail extends SceneObjectBase<DataTrailState> implements SceneO
await updateOtelJoinWithGroupLeft(this, metric);
}
this.setState(this.getSceneUpdatesForNewMetricValue(metric));
// from the metric preview panel we have the info loaded to determine that a metric is a native histogram
let nativeHistogramMetric = false;
if (this.isNativeHistogram(metric)) {
nativeHistogramMetric = true;
}
this.setState(this.getSceneUpdatesForNewMetricValue(metric, nativeHistogramMetric));
// Add metric to adhoc filters baseFilter
const filterVar = sceneGraph.lookupVariable(VAR_FILTERS, this);
@@ -303,19 +352,25 @@ export class DataTrail extends SceneObjectBase<DataTrailState> implements SceneO
}
}
private getSceneUpdatesForNewMetricValue(metric: string | undefined) {
private getSceneUpdatesForNewMetricValue(metric: string | undefined, nativeHistogramMetric?: boolean) {
const stateUpdate: Partial<DataTrailState> = {};
stateUpdate.metric = metric;
stateUpdate.topScene = getTopSceneFor(metric);
// refactoring opportunity? Or do we pass metric knowledge all the way down?
// must pass this native histogram prometheus knowledge deep into
// the topscene set on the trail > MetricScene > getAutoQueriesForMetric() > createHistogramMetricQueryDefs();
stateUpdate.nativeHistogramMetric = nativeHistogramMetric ? '1' : '';
stateUpdate.topScene = getTopSceneFor(metric, nativeHistogramMetric);
return stateUpdate;
}
getUrlState(): SceneObjectUrlValues {
const { metric, metricSearch, showPreviews } = this.state;
const { metric, metricSearch, showPreviews, nativeHistogramMetric } = this.state;
return {
metric,
metricSearch,
...{ showPreviews: showPreviews === false ? 'false' : null },
// store the native histogram knowledge in url for the metric scene
nativeHistogramMetric,
};
}
@@ -324,7 +379,14 @@ export class DataTrail extends SceneObjectBase<DataTrailState> implements SceneO
if (typeof values.metric === 'string') {
if (this.state.metric !== values.metric) {
Object.assign(stateUpdate, this.getSceneUpdatesForNewMetricValue(values.metric));
// if we have a metric and we have stored in the url that it is a native histogram
// we can pass that info into the metric scene to generate the appropriate queries
let nativeHistogramMetric = false;
if (values.nativeHistogramMetric === '1') {
nativeHistogramMetric = true;
}
Object.assign(stateUpdate, this.getSceneUpdatesForNewMetricValue(values.metric, nativeHistogramMetric));
}
} else if (values.metric == null) {
stateUpdate.metric = undefined;
@@ -489,11 +551,23 @@ export class DataTrail extends SceneObjectBase<DataTrailState> implements SceneO
}
static Component = ({ model }: SceneComponentProps<DataTrail>) => {
const { controls, topScene, history, settings, useOtelExperience, hasOtelResources, embedded } = model.useState();
const {
controls,
topScene,
history,
settings,
useOtelExperience,
hasOtelResources,
embedded,
histogramsLoaded,
nativeHistograms,
} = model.useState();
const chromeHeaderHeight = useChromeHeaderHeight();
const styles = useStyles2(getStyles, embedded ? 0 : (chromeHeaderHeight ?? 0));
const showHeaderForFirstTimeUsers = getTrailStore().recent.length < 2;
// need to initialize this here and not on activate because it requires the data source helper to be fully initialized first
model.initializeHistograms();
useEffect(() => {
if (model.state.addingLabelFromBreakdown) {
@@ -526,6 +600,7 @@ export class DataTrail extends SceneObjectBase<DataTrailState> implements SceneO
return (
<div className={styles.container}>
{NativeHistogramBanner({ histogramsLoaded, nativeHistograms, trail: model })}
{showHeaderForFirstTimeUsers && <MetricsHeader />}
<history.Component model={history} />
{controls && (
@@ -546,9 +621,9 @@ export class DataTrail extends SceneObjectBase<DataTrailState> implements SceneO
};
}
export function getTopSceneFor(metric?: string) {
export function getTopSceneFor(metric?: string, nativeHistogram?: boolean) {
if (metric) {
return new MetricScene({ metric: metric });
return new MetricScene({ metric: metric, nativeHistogram: nativeHistogram ?? false });
} else {
return new MetricSelectScene({});
}
+2 -1
View File
@@ -44,6 +44,7 @@ const relatedLogsFeatureEnabled = config.featureToggles.exploreMetricsRelatedLog
export interface MetricSceneState extends SceneObjectState {
body: MetricGraphScene;
metric: string;
nativeHistogram?: boolean;
actionView?: string;
autoQuery: AutoQueryInfo;
@@ -54,7 +55,7 @@ export class MetricScene extends SceneObjectBase<MetricSceneState> {
protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['actionView'] });
public constructor(state: MakeOptional<MetricSceneState, 'body' | 'autoQuery'>) {
const autoQuery = state.autoQuery ?? getAutoQueriesForMetric(state.metric);
const autoQuery = state.autoQuery ?? getAutoQueriesForMetric(state.metric, state.nativeHistogram);
super({
$variables: state.$variables ?? getVariableSet(state.metric),
body: state.body ?? new MetricGraphScene({}),
@@ -424,7 +424,9 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> i
children.push(metric.itemRef.resolve());
continue;
}
const panel = getPreviewPanelFor(metric.name, index, currentFilterCount, description);
// refactor this into the query generator in future
const isNative = trail.isNativeHistogram(metric.name);
const panel = getPreviewPanelFor(metric.name, index, currentFilterCount, description, isNative);
metric.itemRef = panel.getRef();
metric.isPanel = true;
@@ -0,0 +1,32 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { SceneObjectBase } from '@grafana/scenes';
import { Badge, useStyles2 } from '@grafana/ui';
import { Trans } from '@grafana/ui/src/utils/i18n';
export class NativeHistogramBadge extends SceneObjectBase {
public static Component = () => {
const styles = useStyles2(getStyles);
return (
<Badge
className={styles.badge}
color="blue"
text={<Trans i18nKey="trails.metric-select.native-histogram">Native Histogram</Trans>}
/>
);
};
}
function getStyles(theme: GrafanaTheme2) {
return {
badge: css({
borderRadius: theme.shape.radius.pill,
border: `1px solid ${theme.colors.info.text}`,
background: theme.colors.info.transparent,
cursor: 'auto',
width: '112px',
padding: '0rem 0.25rem 0 0.35rem',
}),
};
}
@@ -6,20 +6,32 @@ import { getVariablesWithMetricConstant, MDP_METRIC_PREVIEW, trailDS } from '../
import { getColorByIndex } from '../utils';
import { AddToExplorationButton } from './AddToExplorationsButton';
import { NativeHistogramBadge } from './NativeHistogramBadge';
import { SelectMetricAction } from './SelectMetricAction';
import { hideEmptyPreviews } from './hideEmptyPreviews';
export function getPreviewPanelFor(metric: string, index: number, currentFilterCount: number, description?: string) {
const autoQuery = getAutoQueriesForMetric(metric);
export function getPreviewPanelFor(
metric: string,
index: number,
currentFilterCount: number,
description?: string,
nativeHistogram?: boolean
) {
const autoQuery = getAutoQueriesForMetric(metric, nativeHistogram);
let actions: Array<SelectMetricAction | AddToExplorationButton | NativeHistogramBadge> = [
new SelectMetricAction({ metric, title: 'Select' }),
new AddToExplorationButton({ labelName: metric }),
];
if (nativeHistogram) {
actions.unshift(new NativeHistogramBadge({}));
}
const vizPanel = autoQuery.preview
.vizBuilder()
.setColor({ mode: 'fixed', fixedColor: getColorByIndex(index) })
.setDescription(description)
.setHeaderActions([
new SelectMetricAction({ metric, title: 'Select' }),
new AddToExplorationButton({ labelName: metric }),
])
.setHeaderActions(actions)
.build();
const queries = autoQuery.preview.queries.map((query) =>
@@ -66,6 +66,7 @@ describe('TrailStore', () => {
'var-ds': 'cb3a3391-700f-4cc6-81be-a122488e93e6',
'var-filters': [],
refresh: '',
nativeHistogramMetric: '',
},
type: 'start',
description: 'Test',
@@ -80,6 +81,7 @@ describe('TrailStore', () => {
'var-ds': 'cb3a3391-700f-4cc6-81be-a122488e93e6',
'var-filters': [],
refresh: '',
nativeHistogramMetric: '',
},
type: 'metric',
description: 'Test',
@@ -228,6 +230,7 @@ describe('TrailStore', () => {
'var-ds': 'ds',
'var-filters': [],
refresh: '',
nativeHistogramMetric: '',
},
type: 'start',
description: 'Test',
@@ -242,6 +245,7 @@ describe('TrailStore', () => {
'var-ds': 'ds',
'var-filters': [],
refresh: '',
nativeHistogramMetric: '',
},
type: 'metric',
description: 'Test',
@@ -256,6 +260,7 @@ describe('TrailStore', () => {
'var-ds': 'ds',
'var-filters': [],
refresh: '',
nativeHistogramMetric: '',
},
type: 'metric',
description: 'Test',
@@ -506,6 +511,7 @@ describe('TrailStore', () => {
{
urlValues: {
metric: 'bookmarked_metric',
nativeHistogramMetric: '',
from: 'now-1h',
to: 'now',
timezone,
@@ -603,6 +609,7 @@ describe('TrailStore', () => {
'var-ds': 'cb3a3391-700f-4cc6-81be-a122488e93e6',
'var-filters': [],
refresh: '',
nativeHistogramMetric: '',
},
type: 'start',
description: 'Test',
@@ -616,6 +623,7 @@ describe('TrailStore', () => {
'var-ds': 'cb3a3391-700f-4cc6-81be-a122488e93e6',
'var-filters': [],
refresh: '',
nativeHistogramMetric: '',
},
type: 'time',
description: 'Test',
@@ -655,6 +663,7 @@ describe('TrailStore', () => {
'var-ds': 'prom-mock',
'var-filters': [],
refresh: '',
nativeHistogramMetric: '',
},
type: 'start',
},
@@ -666,6 +675,7 @@ describe('TrailStore', () => {
'var-ds': 'prom-mock',
'var-filters': [],
refresh: '',
nativeHistogramMetric: '',
},
type: 'time',
},
@@ -677,6 +687,7 @@ describe('TrailStore', () => {
'var-ds': 'prom-mock',
'var-filters': [],
refresh: '',
nativeHistogramMetric: '',
},
type: 'metric',
},
@@ -713,6 +724,7 @@ describe('TrailStore', () => {
history: [
{
urlValues: {
nativeHistogramMetric: '',
from: 'now-1h',
to: 'now',
timezone,
@@ -728,6 +740,7 @@ describe('TrailStore', () => {
{
urlValues: {
metric: 'bookmarked_metric',
nativeHistogramMetric: '',
from: 'now-1h',
to: 'now',
timezone,
@@ -743,6 +756,7 @@ describe('TrailStore', () => {
{
urlValues: {
metric: 'some_other_metric',
nativeHistogramMetric: '',
from: 'now-1h',
to: 'now',
timezone,
@@ -766,6 +780,7 @@ describe('TrailStore', () => {
{
urlValues: {
metric: 'bookmarked_metric',
nativeHistogramMetric: '',
from: 'now-1h',
to: 'now',
timezone,
@@ -4,7 +4,7 @@ import { createSummaryMetricQueryDefs } from './queryGenerators/summary';
import { AutoQueryContext, AutoQueryInfo } from './types';
import { getUnit } from './units';
export function getAutoQueriesForMetric(metric: string): AutoQueryInfo {
export function getAutoQueriesForMetric(metric: string, nativeHistogram?: boolean): AutoQueryInfo {
const isUtf8Metric = false;
const metricParts = metric.split('_');
const suffix = metricParts.at(-1);
@@ -28,7 +28,7 @@ export function getAutoQueriesForMetric(metric: string): AutoQueryInfo {
return createSummaryMetricQueryDefs(ctx);
}
if (suffix === 'bucket') {
if (suffix === 'bucket' || nativeHistogram) {
return createHistogramMetricQueryDefs(ctx);
}
@@ -0,0 +1,54 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { DataTrail } from '../DataTrail';
import { MetricSelectedEvent } from '../shared';
import { NativeHistogramBanner } from './NativeHistogramBanner';
const mockTrail = {
publishEvent: jest.fn(),
} as unknown as DataTrail;
const mockProps = {
histogramsLoaded: true,
nativeHistograms: ['histogram1', 'histogram2'],
trail: mockTrail,
};
describe('NativeHistogramBanner', () => {
test('renders correctly when histograms are loaded', () => {
render(<NativeHistogramBanner {...mockProps} />);
expect(screen.getByText('Native Histogram Support')).toBeInTheDocument();
expect(
screen.getByText(
'Prometheus native histograms offer high resolution, high precision, simple usage in instrumentation and a way to combine and manipulate histograms in queries and in Grafana.'
)
).toBeInTheDocument();
});
test('Has a learn more button works', () => {
render(<NativeHistogramBanner {...mockProps} />);
const learnMoreButton = screen.getByText('Learn more');
expect(learnMoreButton).toBeInTheDocument();
});
test('See examples button works', () => {
render(<NativeHistogramBanner {...mockProps} />);
const seeExamplesButton = screen.getByText('> See examples');
expect(seeExamplesButton).toBeInTheDocument();
fireEvent.click(seeExamplesButton);
expect(screen.getByText('Native Histogram displayed as heatmap:')).toBeInTheDocument();
expect(screen.getByText('Native Histogram displayed as histogram:')).toBeInTheDocument();
expect(screen.getByText('Classic Histogram displayed as heatmap:')).toBeInTheDocument();
expect(screen.getByText('Classic Histogram displayed as histogram:')).toBeInTheDocument();
});
test('Native histograms buttons work', () => {
render(<NativeHistogramBanner {...mockProps} />);
fireEvent.click(screen.getByText('> See examples'));
const histogramButton = screen.getByText('histogram1');
expect(histogramButton).toBeInTheDocument();
fireEvent.click(histogramButton);
expect(mockTrail.publishEvent).toHaveBeenCalledWith(new MetricSelectedEvent('histogram1'), true);
});
});
@@ -0,0 +1,273 @@
import { css } from '@emotion/css';
import { useState, type Dispatch, type SetStateAction } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2, useTheme2, Alert, Button } from '@grafana/ui';
import { t, Trans } from '@grafana/ui/src/utils/i18n';
import { DataTrail } from '../DataTrail';
import { reportExploreMetrics } from '../interactions';
import { MetricSelectedEvent } from '../shared';
interface NativeHistogramInfoProps {
histogramsLoaded: boolean;
nativeHistograms: string[];
trail: DataTrail;
}
export function NativeHistogramBanner(props: NativeHistogramInfoProps) {
const { histogramsLoaded, nativeHistograms, trail } = props;
const [histogramMessage, setHistogramMessage] = useState(true);
const [showHistogramExamples, setShowHistogramExamples] = useState(false);
const styles = useStyles2(getStyles, 0);
if (!histogramsLoaded || nativeHistograms.length === 0 || !histogramMessage) {
return null;
}
return (
<>
{
<Alert
title={'Native Histogram Support'}
severity={'info'}
onRemove={() => {
setHistogramMessage(false);
}}
>
<div className={styles.histogramRow}>
<div className={styles.histogramSentence}>
<Trans i18nKey="trails.native-histogram-banner.sentence">
Prometheus native histograms offer high resolution, high precision, simple usage in instrumentation and
a way to combine and manipulate histograms in queries and in Grafana.
</Trans>
</div>
<div className={styles.histogramLearnMore}>
<div>
<Button
onClick={() =>
window.open('https://grafana.com/docs/grafana-cloud/whats-new/native-histograms/', '_blank')
}
className={styles.button}
>
<Trans i18nKey="trails.native-histogram-banner.learn-more">Learn more</Trans>
</Button>
</div>
</div>
</div>
<NativeHistogramExamplesButton
showHistogramExamples={showHistogramExamples}
setShowHistogramExamples={setShowHistogramExamples}
/>
{showHistogramExamples && (
<NativeHistogramExamples
trail={trail}
nativeHistograms={nativeHistograms}
setHistogramMessage={setHistogramMessage}
/>
)}
</Alert>
}
</>
);
}
interface NativeHistogramExamplesButtonProps {
showHistogramExamples: boolean;
setShowHistogramExamples: Dispatch<SetStateAction<boolean>>;
}
const NativeHistogramExamplesButton = ({
showHistogramExamples,
setShowHistogramExamples,
}: NativeHistogramExamplesButtonProps) => {
const styles = useStyles2(getStyles, 0);
return (
<div>
<Button
className={`${styles.seeExamplesButton} native-histogram-examples-button`}
type="button"
fill="text"
variant="primary"
onClick={() => {
if (showHistogramExamples) {
// hide the examples
reportExploreMetrics('native_histogram_examples_closed', {});
}
setShowHistogramExamples(!showHistogramExamples);
}}
>
{showHistogramExamples
? t(`trails.native-histogram-banner.hide-examples`, `Hide examples`)
: t(`trails.native-histogram-banner.see-examples`, `> See examples`)}
</Button>
</div>
);
};
type NativeHistogramExamplesProps = Pick<NativeHistogramInfoProps, 'trail' | 'nativeHistograms'> & {
setHistogramMessage: Dispatch<SetStateAction<boolean>>;
};
const NativeHistogramExamples = ({ trail, nativeHistograms, setHistogramMessage }: NativeHistogramExamplesProps) => {
const styles = useStyles2(getStyles, 0);
const isDark = useTheme2().isDark;
const selectNativeHistogram = (metric: string) => {
reportExploreMetrics('native_histogram_example_clicked', {
metric,
});
trail.publishEvent(new MetricSelectedEvent(metric), true);
};
const images = {
nativeHeatmap: isDark
? 'public/img/native-histograms/DarkModeHeatmapNativeHistogram.png'
: 'public/img/native-histograms/LightModeHeatmapNativeHistogram.png',
classicHeatmap: isDark
? 'public/img/native-histograms/DarkModeHeatmapClassicHistogram.png'
: 'public/img/native-histograms/LightModeHeatmapClassicHistogram.png',
nativeHistogram: isDark
? 'public/img/native-histograms/DarkModeHistogramNativehistogram.png'
: 'public/img/native-histograms/LightModeHistogramClassicHistogram.png',
classicHistogram: isDark
? 'public/img/native-histograms/DarkModeHistogramClassicHistogram.png'
: 'public/img/native-histograms/LightModeHistogramClassicHistogram.png',
};
return (
<>
<div className={`${styles.histogramRow} ${styles.seeExamplesRow}`}>
<div className={styles.histogramImageCol}>
<div>
<Trans i18nKey="trails.native-histogram-banner.now">Now:</Trans>
</div>
</div>
<div className={`${styles.histogramImageCol} ${styles.rightCol}`}>
<div>
<Trans i18nKey="trails.native-histogram-banner.previously">Previously:</Trans>
</div>
</div>
</div>
<div className={`${styles.histogramRow} ${styles.seeExamplesRow}`}>
<div className={styles.histogramImageCol}>
<div className={styles.histogramRow}>
<div className={`${styles.histogramImageCol} ${styles.fontSmall}`}>
<div className={styles.imageText}>
<Trans i18nKey="trails.native-histogram-banner.nh-heatmap">
Native Histogram displayed as heatmap:
</Trans>
</div>
<div>
<img width="100%" src={images.nativeHeatmap} alt="Native Histogram displayed as heatmap" />
</div>
</div>
<div className={`${styles.histogramImageCol} ${styles.fontSmall}`}>
<div className={styles.imageText}>
<Trans i18nKey="trails.native-histogram-banner.nh-histogram">
Native Histogram displayed as histogram:
</Trans>
</div>
<div>
<img width="100%" src={images.nativeHistogram} alt="Native Histogram displayed as histogram" />
</div>
</div>
</div>
</div>
<div className={`${styles.histogramImageCol} ${styles.rightImageCol} ${styles.rightCol}`}>
<div className={styles.histogramRow}>
<div className={`${styles.histogramImageCol} ${styles.fontSmall}`}>
<div className={styles.imageText}>
<Trans i18nKey="trails.native-histogram-banner.ch-heatmap">
Classic Histogram displayed as heatmap:
</Trans>
</div>
<div>
<img width="100%" src={images.classicHeatmap} alt="Classic Histogram displayed as heatmap" />
</div>
</div>
<div className={`${styles.histogramImageCol} ${styles.fontSmall}`}>
<div className={styles.imageText}>
<Trans i18nKey="trails.native-histogram-banner.ch-histogram">
Classic Histogram displayed as histogram:
</Trans>
</div>
<div>
<img width="100%" src={images.classicHistogram} alt="Classic Histogram displayed as histogram" />
</div>
</div>
</div>
</div>
</div>
<br />
<div>
<Trans i18nKey="trails.native-histogram-banner.click-histogram">
Click any of the native histograms below to explore them:
</Trans>
</div>
<div>
{nativeHistograms.map((el) => {
return (
<div key={el}>
<Button
onClick={() => {
selectNativeHistogram(el);
setHistogramMessage(false);
}}
key={el}
variant="primary"
size="sm"
fill="text"
className={`native-histogram-example-clicked`}
>
{t('trails.native-histogram-banner.metric-examples', el)}
</Button>
</div>
);
})}
</div>
</>
);
};
function getStyles(theme: GrafanaTheme2, _chromeHeaderHeight: number) {
return {
histogramRow: css({
display: 'flex',
flexDirection: 'row',
gap: theme.spacing(2),
}),
histogramSentence: css({
width: '90%',
}),
histogramLearnMore: css({
width: '10%',
}),
button: css({
float: 'right',
}),
seeExamplesButton: css({
paddingLeft: '0px',
}),
seeExamplesRow: css({
paddingTop: '4px',
}),
histogramImageCol: css({
display: 'flex',
flexDirection: 'column',
flexBasis: '100%',
flex: '1',
}),
fontSmall: css({
fontSize: theme.typography.size.sm,
}),
imageText: css({
paddingBottom: '4px',
}),
rightImageCol: css({
borderLeft: `1px solid ${theme.colors.secondary.borderTransparent}`,
}),
rightCol: css({
paddingLeft: '16px',
}),
};
}
@@ -0,0 +1,45 @@
import { DataTrail } from '../DataTrail';
import { MetricDatasourceHelper } from './MetricDatasourceHelper';
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
config: {
...jest.requireActual('@grafana/runtime').config,
publicDashboardAccessToken: '123',
},
}));
const NATIVE_HISTOGRAM = 'test_metric';
describe('MetricDatasourceHelper', () => {
let metricDatasourceHelper: MetricDatasourceHelper;
beforeEach(() => {
const trail = new DataTrail({});
metricDatasourceHelper = new MetricDatasourceHelper(trail);
metricDatasourceHelper['_classicHistograms'] = {
test_metric_bucket: 1,
};
});
afterEach(() => {
jest.clearAllMocks();
});
describe('isNativeHistogram', () => {
it('should return false if metric is not provided', async () => {
const result = await metricDatasourceHelper.isNativeHistogram('');
expect(result).toBe(false);
});
it('should return true if metric is a native histogram', async () => {
const result = await metricDatasourceHelper.isNativeHistogram(NATIVE_HISTOGRAM);
expect(result).toBe(true);
});
it('should return false if metric is not a native histogram', async () => {
const result = await metricDatasourceHelper.isNativeHistogram('non_histogram_metric');
expect(result).toBe(false);
});
});
});
@@ -19,6 +19,8 @@ export class MetricDatasourceHelper {
public reset() {
this._datasource = undefined;
this._metricsMetadata = undefined;
this._classicHistograms = {};
this._nativeHistograms = [];
}
private _trail: DataTrail;
@@ -61,6 +63,67 @@ export class MetricDatasourceHelper {
return metadata?.[metric];
}
private _classicHistograms: Record<string, number> = {};
private _nativeHistograms: string[] = [];
public listNativeHistograms() {
return this._nativeHistograms;
}
/**
* Identify native histograms by querying classic histograms and all metrics,
* then comparing the results and build the collection of native histograms.
*
* classic histogram = test_metric_bucket
* native histogram = test_metric
*/
public async initializeHistograms() {
const ds = await this.getDatasource();
if (Object.keys(this._classicHistograms).length === 0 && ds instanceof PrometheusDatasource) {
const classicHistogramsCall = ds.metricFindQuery('metrics(.*_bucket)');
const allMetricsCall = ds.metricFindQuery('metrics(.*)');
const [classicHistograms, allMetrics] = await Promise.all([classicHistogramsCall, allMetricsCall]);
classicHistograms.forEach((m) => {
this._classicHistograms[m.text] = 1;
});
allMetrics.forEach((m) => {
if (this.isNativeHistogram(m.text)) {
// Build the collection of native histograms.
this.addNativeHistogram(m.text);
}
});
}
}
/**
*
* If a metric name + _bucket exists in the classic histograms, then it is a native histogram
*
* classic histogram = test_metric_bucket
* native histogram = test_metric
* @param metric
* @returns
*/
public isNativeHistogram(metric: string): boolean {
if (!metric) {
return false;
}
if (this._classicHistograms[`${metric}_bucket`]) {
return true;
}
return false;
}
private addNativeHistogram(metric: string) {
if (!this._nativeHistograms.includes(metric)) {
this._nativeHistograms.push(metric);
}
}
/**
* Used for additional filtering for adhoc vars labels in Explore metrics.
* @param options
+5 -1
View File
@@ -131,7 +131,11 @@ type Interactions = {
otel_experience_used: {},
otel_experience_toggled: {
value: ('on'| 'off')
}
},
native_histogram_examples_closed: {},
native_histogram_example_clicked: {
metric: string;
},
};
const PREFIX = 'grafana_explore_metrics_';
Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

+15
View File
@@ -3345,9 +3345,24 @@
},
"metric-select": {
"filter-by": "Filter by",
"native-histogram": "Native Histogram",
"new-badge": "New",
"otel-switch": "This switch enables filtering by OTel resources for OTel native data sources."
},
"native-histogram-banner": {
"ch-heatmap": "Classic Histogram displayed as heatmap:",
"ch-histogram": "Classic Histogram displayed as histogram:",
"click-histogram": "Click any of the native histograms below to explore them:",
"hide-examples": "Hide examples",
"learn-more": "Learn more",
"metric-examples": "",
"nh-heatmap": "Native Histogram displayed as heatmap:",
"nh-histogram": "Native Histogram displayed as histogram:",
"now": "Now:",
"previously": "Previously:",
"see-examples": "> See examples",
"sentence": "Prometheus native histograms offer high resolution, high precision, simple usage in instrumentation and a way to combine and manipulate histograms in queries and in Grafana."
},
"recent-metrics": {
"or-view-a-recent-exploration": "Or view a recent exploration"
},
+15
View File
@@ -3345,9 +3345,24 @@
},
"metric-select": {
"filter-by": "Fįľŧęř þy",
"native-histogram": "Ńäŧįvę Ħįşŧőģřäm",
"new-badge": "Ńęŵ",
"otel-switch": "Ŧĥįş şŵįŧčĥ ęʼnäþľęş ƒįľŧęřįʼnģ þy ØŦęľ řęşőūřčęş ƒőř ØŦęľ ʼnäŧįvę đäŧä şőūřčęş."
},
"native-histogram-banner": {
"ch-heatmap": "Cľäşşįč Ħįşŧőģřäm đįşpľäyęđ äş ĥęäŧmäp:",
"ch-histogram": "Cľäşşįč Ħįşŧőģřäm đįşpľäyęđ äş ĥįşŧőģřäm:",
"click-histogram": "Cľįčĸ äʼny őƒ ŧĥę ʼnäŧįvę ĥįşŧőģřämş þęľőŵ ŧő ęχpľőřę ŧĥęm:",
"hide-examples": "Ħįđę ęχämpľęş",
"learn-more": "Ŀęäřʼn mőřę",
"metric-examples": "",
"nh-heatmap": "Ńäŧįvę Ħįşŧőģřäm đįşpľäyęđ äş ĥęäŧmäp:",
"nh-histogram": "Ńäŧįvę Ħįşŧőģřäm đįşpľäyęđ äş ĥįşŧőģřäm:",
"now": "Ńőŵ:",
"previously": "Přęvįőūşľy:",
"see-examples": "> Ŝęę ęχämpľęş",
"sentence": "Přőmęŧĥęūş ʼnäŧįvę ĥįşŧőģřämş őƒƒęř ĥįģĥ řęşőľūŧįőʼn, ĥįģĥ přęčįşįőʼn, şįmpľę ūşäģę įʼn įʼnşŧřūmęʼnŧäŧįőʼn äʼnđ ä ŵäy ŧő čőmþįʼnę äʼnđ mäʼnįpūľäŧę ĥįşŧőģřämş įʼn qūęřįęş äʼnđ įʼn Ğřäƒäʼnä."
},
"recent-metrics": {
"or-view-a-recent-exploration": "Øř vįęŵ ä řęčęʼnŧ ęχpľőřäŧįőʼn"
},