Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| abe93b6474 | |||
| c81b7a0400 | |||
| dea7b621f8 | |||
| 8fbf19c8b0 |
@@ -79,6 +79,24 @@ The following table highlights the key differences between mute timings and sile
|
||||
| **Setup** | Created and then added to notification policies | Matches alerts using labels to determine whether to silence them |
|
||||
| **Period** | Uses time interval definitions that can repeat periodically | Has a fixed start and end time |
|
||||
|
||||
**When to use mute timings**
|
||||
|
||||
Use mute timings for predictable, recurring time periods when you don't want to receive notifications:
|
||||
|
||||
- Regular maintenance windows (for example, every Sunday from 2:00 AM to 4:00 AM)
|
||||
- Non-business hours (for example, nights and weekends)
|
||||
- Scheduled deployments or known change windows
|
||||
- Regular testing periods
|
||||
|
||||
**When to use silences**
|
||||
|
||||
Use silences for one-time or as-needed suppression of notifications:
|
||||
|
||||
- Active incident response (suppress notifications while investigating)
|
||||
- Immediate suppression of a specific alert or group of alerts
|
||||
- One-time maintenance or deployment events
|
||||
- Temporarily suppress alerts for specific services or components
|
||||
|
||||
[//]: <> ({{< docs/shared lookup="alerts/mute-timings-vs-silences.md" source="grafana" version="<GRAFANA_VERSION>" >}})
|
||||
|
||||
## Add silences
|
||||
@@ -168,7 +186,22 @@ To remove a silence, complete the following steps.
|
||||
1. Click **Silences** to view the list of existing silences.
|
||||
1. Select the silence you want to end, then click **Unsilence**.
|
||||
|
||||
> **Note:** You cannot remove a silence manually. Silences that have ended are retained and listed for five days.
|
||||
{{< admonition type="note" >}}
|
||||
Clicking **Unsilence** ends the silence immediately, which is the only way to end a silence before its configured end time. Silences cannot be permanently deleted manually.
|
||||
{{< /admonition >}}
|
||||
|
||||
## Silence duration and retention
|
||||
|
||||
**Duration limits**
|
||||
|
||||
There is no maximum duration for a silence. You can create a silence for any length of time. However, administrators can configure the following limits:
|
||||
|
||||
- `alertmanager_max_silences_count`: Maximum number of active and pending silences per tenant (default: 0, no limit)
|
||||
- `alertmanager_max_silence_size_bytes`: Maximum size of a silence in bytes (default: 0, no limit)
|
||||
|
||||
**Expired silence retention**
|
||||
|
||||
Expired silences are automatically deleted after 5 days. This retention period is not configurable. The cleanup process runs automatically every 15 minutes to remove expired silences that are older than the retention period.
|
||||
|
||||
## Rule-specific silences
|
||||
|
||||
|
||||
@@ -63,6 +63,24 @@ The following table highlights the key differences between mute timings and sile
|
||||
| **Setup** | Created and then added to notification policies | Matches alerts using labels to determine whether to silence them |
|
||||
| **Period** | Uses time interval definitions that can repeat periodically | Has a fixed start and end time |
|
||||
|
||||
**When to use mute timings**
|
||||
|
||||
Use mute timings for predictable, recurring time periods when you don't want to receive notifications:
|
||||
|
||||
- Regular maintenance windows (for example, every Sunday from 2:00 AM to 4:00 AM)
|
||||
- Non-business hours (for example, nights and weekends)
|
||||
- Scheduled deployments or known change windows
|
||||
- Regular testing periods
|
||||
|
||||
**When to use silences**
|
||||
|
||||
Use silences for one-time or as-needed suppression of notifications:
|
||||
|
||||
- Active incident response (suppress notifications while investigating)
|
||||
- Immediate suppression of a specific alert or group of alerts
|
||||
- One-time maintenance or deployment events
|
||||
- Temporarily suppress alerts for specific services or components
|
||||
|
||||
[//]: <> ({{< docs/shared lookup="alerts/mute-timings-vs-silences.md" source="grafana" version="<GRAFANA_VERSION>" >}})
|
||||
|
||||
## Add time intervals
|
||||
|
||||
@@ -14,4 +14,4 @@ The following table highlights the key differences between mute timings and sile
|
||||
| | Mute timing | Silence |
|
||||
| ---------- | ----------------------------------------------------------- | ---------------------------------------------------------------- |
|
||||
| **Setup** | Created and then added to notification policies | Matches alerts using labels to determine whether to silence them |
|
||||
| **Period** | Uses time interval definitions that can repeat periodically | Has a fixed start and end time |
|
||||
| **Period** | Uses time interval definitions that can repeat periodically | Has a fixed start and end time |
|
||||
@@ -552,7 +552,6 @@ func (s *SearchHandler) getDashboardsUIDsSharedWithUser(ctx context.Context, use
|
||||
// gets dashboards that the user was granted read access to
|
||||
permissions := user.GetPermissions()
|
||||
dashboardPermissions := permissions[dashboards.ActionDashboardsRead]
|
||||
folderPermissions := permissions[dashboards.ActionFoldersRead]
|
||||
dashboardUids := make([]string, 0)
|
||||
sharedDashboards := make([]string, 0)
|
||||
|
||||
@@ -563,13 +562,6 @@ func (s *SearchHandler) getDashboardsUIDsSharedWithUser(ctx context.Context, use
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, folderPermission := range folderPermissions {
|
||||
if folderUid, found := strings.CutPrefix(folderPermission, dashboards.ScopeFoldersPrefix); found {
|
||||
if !slices.Contains(dashboardUids, folderUid) && folderUid != foldermodel.SharedWithMeFolderUID && folderUid != foldermodel.GeneralFolderUID {
|
||||
dashboardUids = append(dashboardUids, folderUid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(dashboardUids) == 0 {
|
||||
return sharedDashboards, nil
|
||||
@@ -580,15 +572,9 @@ func (s *SearchHandler) getDashboardsUIDsSharedWithUser(ctx context.Context, use
|
||||
return sharedDashboards, err
|
||||
}
|
||||
|
||||
folderKey, err := asResourceKey(user.GetNamespace(), folders.RESOURCE)
|
||||
if err != nil {
|
||||
return sharedDashboards, err
|
||||
}
|
||||
|
||||
dashboardSearchRequest := &resourcepb.ResourceSearchRequest{
|
||||
Federated: []*resourcepb.ResourceKey{folderKey},
|
||||
Fields: []string{"folder"},
|
||||
Limit: int64(len(dashboardUids)),
|
||||
Fields: []string{"folder"},
|
||||
Limit: int64(len(dashboardUids)),
|
||||
Options: &resourcepb.ListOptions{
|
||||
Key: key,
|
||||
Fields: []*resourcepb.Requirement{{
|
||||
@@ -624,6 +610,12 @@ func (s *SearchHandler) getDashboardsUIDsSharedWithUser(ctx context.Context, use
|
||||
}
|
||||
}
|
||||
|
||||
// only folders the user has access to will be returned here
|
||||
folderKey, err := asResourceKey(user.GetNamespace(), folders.RESOURCE)
|
||||
if err != nil {
|
||||
return sharedDashboards, err
|
||||
}
|
||||
|
||||
folderSearchRequest := &resourcepb.ResourceSearchRequest{
|
||||
Fields: []string{"folder"},
|
||||
Limit: int64(len(allFolders)),
|
||||
@@ -636,7 +628,6 @@ func (s *SearchHandler) getDashboardsUIDsSharedWithUser(ctx context.Context, use
|
||||
}},
|
||||
},
|
||||
}
|
||||
// only folders the user has access to will be returned here
|
||||
foldersResult, err := s.client.Search(ctx, folderSearchRequest)
|
||||
if err != nil {
|
||||
return sharedDashboards, err
|
||||
|
||||
@@ -507,15 +507,6 @@ func TestSearchHandlerSharedDashboards(t *testing.T) {
|
||||
[]byte("publicfolder"), // folder uid
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: &resourcepb.ResourceKey{
|
||||
Name: "sharedfolder",
|
||||
Resource: "folder",
|
||||
},
|
||||
Cells: [][]byte{
|
||||
[]byte("privatefolder"), // folder uid
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -559,15 +550,6 @@ func TestSearchHandlerSharedDashboards(t *testing.T) {
|
||||
[]byte("privatefolder"), // folder uid
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: &resourcepb.ResourceKey{
|
||||
Name: "sharedfolder",
|
||||
Resource: "folder",
|
||||
},
|
||||
Cells: [][]byte{
|
||||
[]byte("privatefolder"), // folder uid
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -589,7 +571,6 @@ func TestSearchHandlerSharedDashboards(t *testing.T) {
|
||||
allPermissions := make(map[int64]map[string][]string)
|
||||
permissions := make(map[string][]string)
|
||||
permissions[dashboards.ActionDashboardsRead] = []string{"dashboards:uid:dashboardinroot", "dashboards:uid:dashboardinprivatefolder", "dashboards:uid:dashboardinpublicfolder"}
|
||||
permissions[dashboards.ActionFoldersRead] = []string{"folders:uid:sharedfolder"}
|
||||
allPermissions[1] = permissions
|
||||
// "Permissions" is where we store the uid of dashboards shared with the user
|
||||
req = req.WithContext(identity.WithRequester(req.Context(), &user.SignedInUser{Namespace: "test", OrgID: 1, Permissions: allPermissions}))
|
||||
@@ -600,19 +581,14 @@ func TestSearchHandlerSharedDashboards(t *testing.T) {
|
||||
|
||||
// first call gets all dashboards user has permission for
|
||||
firstCall := mockClient.MockCalls[0]
|
||||
assert.Equal(t, firstCall.Options.Fields[0].Values, []string{"dashboardinroot", "dashboardinprivatefolder", "dashboardinpublicfolder", "sharedfolder"})
|
||||
// verify federated field is set to include folders
|
||||
assert.NotNil(t, firstCall.Federated)
|
||||
assert.Equal(t, 1, len(firstCall.Federated))
|
||||
assert.Equal(t, "folder.grafana.app", firstCall.Federated[0].Group)
|
||||
assert.Equal(t, "folders", firstCall.Federated[0].Resource)
|
||||
assert.Equal(t, firstCall.Options.Fields[0].Values, []string{"dashboardinroot", "dashboardinprivatefolder", "dashboardinpublicfolder"})
|
||||
// second call gets folders associated with the previous dashboards
|
||||
secondCall := mockClient.MockCalls[1]
|
||||
assert.Equal(t, secondCall.Options.Fields[0].Values, []string{"privatefolder", "publicfolder"})
|
||||
// lastly, search ONLY for dashboards and folders user has permission to read that are within folders the user does NOT have
|
||||
// lastly, search ONLY for dashboards user has permission to read that are within folders the user does NOT have
|
||||
// permission to read
|
||||
thirdCall := mockClient.MockCalls[2]
|
||||
assert.Equal(t, thirdCall.Options.Fields[0].Values, []string{"dashboardinprivatefolder", "sharedfolder"})
|
||||
assert.Equal(t, thirdCall.Options.Fields[0].Values, []string{"dashboardinprivatefolder"})
|
||||
|
||||
resp := rr.Result()
|
||||
defer func() {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"io"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb/tempo/traceql"
|
||||
stream_utils "github.com/grafana/grafana/pkg/tsdb/tempo/utils"
|
||||
"google.golang.org/grpc/metadata"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/tracing"
|
||||
@@ -64,7 +64,10 @@ func (s *Service) runMetricsStream(ctx context.Context, req *backend.RunStreamRe
|
||||
qrr.Start = uint64(backendQuery.TimeRange.From.UnixNano())
|
||||
qrr.End = uint64(backendQuery.TimeRange.To.UnixNano())
|
||||
|
||||
ctx = stream_utils.AppendHeadersToOutgoingContext(ctx, req)
|
||||
// Setting the user agent for the gRPC call. When DS is decoupled we don't recreate instance when grafana config
|
||||
// changes or updates, so we have to get it from context.
|
||||
// Ideally this would be pushed higher, so it's set once for all rpc calls, but we have only one now.
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, "User-Agent", backend.UserAgentFromContext(ctx).String())
|
||||
|
||||
if isInstantQuery(tempoQuery.MetricsQueryType) {
|
||||
instantQuery := &tempopb.QueryInstantRequest{
|
||||
|
||||
@@ -7,11 +7,12 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"google.golang.org/grpc/metadata"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/tracing"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/grafana/grafana/pkg/tsdb/tempo/kinds/dataquery"
|
||||
stream_utils "github.com/grafana/grafana/pkg/tsdb/tempo/utils"
|
||||
"github.com/grafana/tempo/pkg/tempopb"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
@@ -33,6 +34,7 @@ type StreamSender interface {
|
||||
func (s *Service) runSearchStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender, datasource *DatasourceInfo) error {
|
||||
ctx, span := tracing.DefaultTracer().Start(ctx, "datasource.tempo.runSearchStream")
|
||||
defer span.End()
|
||||
|
||||
response := &backend.DataResponse{}
|
||||
|
||||
var backendQuery *backend.DataQuery
|
||||
@@ -60,7 +62,10 @@ func (s *Service) runSearchStream(ctx context.Context, req *backend.RunStreamReq
|
||||
sr.Start = uint32(backendQuery.TimeRange.From.Unix())
|
||||
sr.End = uint32(backendQuery.TimeRange.To.Unix())
|
||||
|
||||
ctx = stream_utils.AppendHeadersToOutgoingContext(ctx, req)
|
||||
// Setting the user agent for the gRPC call. When DS is decoupled we don't recreate instance when grafana config
|
||||
// changes or updates, so we have to get it from context.
|
||||
// Ideally this would be pushed higher, so it's set once for all rpc calls, but we have only one now.
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, "User-Agent", backend.UserAgentFromContext(ctx).String())
|
||||
|
||||
stream, err := datasource.StreamingClient.Search(ctx, sr)
|
||||
if err != nil {
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
stream_utils "github.com/grafana/grafana/pkg/tsdb/tempo/utils"
|
||||
)
|
||||
|
||||
func (s *Service) SubscribeStream(_ context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) {
|
||||
@@ -40,18 +39,11 @@ func (s *Service) PublishStream(_ context.Context, _ *backend.PublishStreamReque
|
||||
|
||||
func (s *Service) RunStream(ctx context.Context, request *backend.RunStreamRequest, sender *backend.StreamSender) error {
|
||||
s.logger.Debug("New stream call", "path", request.Path)
|
||||
tempoDatasource, dsInfoErr := s.getDSInfo(ctx, request.PluginContext)
|
||||
|
||||
// get incoming and team http headers and append to stream request.
|
||||
headers, err := stream_utils.SetHeadersFromIncomingContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request.Headers = headers
|
||||
tempoDatasource, err := s.getDSInfo(ctx, request.PluginContext)
|
||||
|
||||
if strings.HasPrefix(request.Path, SearchPathPrefix) {
|
||||
if dsInfoErr != nil {
|
||||
return backend.DownstreamErrorf("failed to get datasource information: %w", dsInfoErr)
|
||||
if err != nil {
|
||||
return backend.DownstreamErrorf("failed to get datasource information: %w", err)
|
||||
}
|
||||
if err = s.runSearchStream(ctx, request, sender, tempoDatasource); err != nil {
|
||||
return sendError(err, sender)
|
||||
@@ -60,8 +52,8 @@ func (s *Service) RunStream(ctx context.Context, request *backend.RunStreamReque
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(request.Path, MetricsPathPrefix) {
|
||||
if dsInfoErr != nil {
|
||||
return backend.DownstreamErrorf("failed to get datasource information: %w", dsInfoErr)
|
||||
if err != nil {
|
||||
return backend.DownstreamErrorf("failed to get datasource information: %w", err)
|
||||
}
|
||||
if err = s.runMetricsStream(ctx, request, sender, tempoDatasource); err != nil {
|
||||
return sendError(err, sender)
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
package stream_utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
// Appends incoming request headers to the outgoing context to make sure none are lost when we make the request to tempo.
|
||||
func AppendHeadersToOutgoingContext(ctx context.Context, req *backend.RunStreamRequest) context.Context {
|
||||
// append all incoming headers
|
||||
for key, value := range req.Headers {
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, key, value)
|
||||
}
|
||||
// Setting the user agent for the gRPC call. When DS is decoupled we don't recreate instance when grafana config
|
||||
// changes or updates, so we have to get it from context.
|
||||
// Ideally this would be pushed higher, so it's set once for all rpc calls, but we have only one now.
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, "User-Agent", backend.UserAgentFromContext(ctx).String())
|
||||
return ctx
|
||||
}
|
||||
|
||||
// When we receive a new query request we should make sure that all incoming HTTP headers are being forwarding to the grpc stream request
|
||||
// this is to make sure that no headers are lost when we make the actual call to Tempo later on.
|
||||
func SetHeadersFromIncomingContext(ctx context.Context) (map[string]string, error) {
|
||||
// get the plugin from context
|
||||
plugin := backend.PluginConfigFromContext(ctx)
|
||||
|
||||
// get the HTTP headers
|
||||
teamHeaders, error := getTeamHTTPHeaders(plugin)
|
||||
if error != nil {
|
||||
return nil, error
|
||||
}
|
||||
|
||||
// get the rest of the incoming headers
|
||||
headers, err := getClientOptionsHeaders(ctx, plugin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for key, value := range teamHeaders {
|
||||
headers[key] = value
|
||||
}
|
||||
return headers, nil
|
||||
}
|
||||
|
||||
func getTeamHTTPHeaders(plugin backend.PluginContext) (map[string]string, error) {
|
||||
headers := map[string]string{}
|
||||
// Grab the JSON data from the datasource instance settings
|
||||
jsonData := plugin.DataSourceInstanceSettings.JSONData
|
||||
var data map[string]interface{}
|
||||
err := json.Unmarshal(jsonData, &data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// fetch team http headers
|
||||
if teamHttpHeaders, ok := data["teamHttpHeaders"]; ok {
|
||||
// team headers have the following structure
|
||||
// headers: [<team_id>: [{header: <header_name>, value: <header_value>}]]
|
||||
// header_value is whatever the user has set under LBAC permissions for their given rule.
|
||||
if lbacHeaders, ok := teamHttpHeaders.(map[string]interface{})["headers"]; ok {
|
||||
headerMap := lbacHeaders.(map[string]interface{})
|
||||
labelPolicyKey, labelPolicyValue := getLabelPolicyKeyValue(headerMap)
|
||||
|
||||
if labelPolicyKey != "" && labelPolicyValue != "" {
|
||||
headers[labelPolicyKey] = labelPolicyValue
|
||||
}
|
||||
}
|
||||
}
|
||||
return headers, nil
|
||||
}
|
||||
|
||||
func getLabelPolicyKeyValue(headerWithRules map[string]interface{}) (string, string) {
|
||||
labelPolicyKey := ""
|
||||
labelPolicyValue := ""
|
||||
// we go through each teams' rule and ignoring the team, go through their set rules and prepare them to be all appended for the X-Prom-Label-Policy header value
|
||||
// the result will be a comma separated list of the rules:
|
||||
// "<rule_num>:<rule_value>, <rule_num>:<rule_value>"
|
||||
for _, accessRuleValue := range headerWithRules {
|
||||
rules := accessRuleValue.([]interface{})
|
||||
for _, accessRule := range rules {
|
||||
header := accessRule.(map[string]interface{})
|
||||
for key, value := range header {
|
||||
// for now, team headers only contain a single header key value, but in case in the future more are introduced, we make sure we only set the one we care about.
|
||||
if key == "header" && value == "X-Prom-Label-Policy" {
|
||||
labelPolicyKey = value.(string)
|
||||
continue
|
||||
}
|
||||
if key == "value" {
|
||||
if valueStr, ok := value.(string); ok {
|
||||
if labelPolicyValue == "" {
|
||||
labelPolicyValue = valueStr
|
||||
} else {
|
||||
labelPolicyValue += "," + valueStr
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return labelPolicyKey, labelPolicyValue
|
||||
}
|
||||
|
||||
func getClientOptionsHeaders(ctx context.Context, plugin backend.PluginContext) (map[string]string, error) {
|
||||
headers := map[string]string{}
|
||||
opts, err := plugin.DataSourceInstanceSettings.HTTPClientOptions(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get HTTP client options: %w", err)
|
||||
}
|
||||
|
||||
for name, values := range opts.Header {
|
||||
for _, value := range values {
|
||||
headers[name] = value
|
||||
}
|
||||
}
|
||||
return headers, nil
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
package stream_utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/useragent"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
func TestAppendHeadersToOutgoingContext_AppendsHeadersAndUserAgent(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
ua, err := useragent.New("10.0.0", "linux", "amd64")
|
||||
require.NoError(t, err)
|
||||
ctx = backend.WithUserAgent(ctx, ua)
|
||||
ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs("Existing", "one"))
|
||||
|
||||
req := &backend.RunStreamRequest{
|
||||
Headers: map[string]string{
|
||||
"X-Test": "value",
|
||||
},
|
||||
}
|
||||
|
||||
out := AppendHeadersToOutgoingContext(ctx, req)
|
||||
outgoingMD, ok := metadata.FromOutgoingContext(out)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, []string{"value"}, outgoingMD.Get("x-test"))
|
||||
assert.Equal(t, []string{ua.String()}, outgoingMD.Get("user-agent"))
|
||||
assert.Equal(t, []string{"one"}, outgoingMD.Get("existing"))
|
||||
}
|
||||
|
||||
func TestSetHeadersFromIncomingContext_MergesTeamAndClientHeaders(t *testing.T) {
|
||||
jsonData := []byte(`{
|
||||
"teamHttpHeaders": {
|
||||
"headers": {
|
||||
"101": [
|
||||
{"header": "X-Prom-Label-Policy", "value": "1:team-value"},
|
||||
{"header": "X-Prom-Label-Policy", "value": "2:team-wins"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"httpHeaderName1": "X-Client",
|
||||
"httpHeaderName2": "X-Shared"
|
||||
}`)
|
||||
|
||||
pluginCtx := backend.PluginContext{
|
||||
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{
|
||||
JSONData: jsonData,
|
||||
DecryptedSecureJSONData: map[string]string{
|
||||
"httpHeaderValue1": "client-value",
|
||||
"httpHeaderValue2": "client-overridden",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx := backend.WithPluginContext(context.Background(), pluginCtx)
|
||||
headers, err := SetHeadersFromIncomingContext(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := map[string]string{
|
||||
"X-Client": "client-value",
|
||||
"X-Prom-Label-Policy": "1:team-value,2:team-wins",
|
||||
"X-Shared": "client-overridden",
|
||||
}
|
||||
assert.Equal(t, expected, headers)
|
||||
}
|
||||
|
||||
func TestGetTeamHTTPHeaders_NoTeamHeaders(t *testing.T) {
|
||||
pluginCtx := backend.PluginContext{
|
||||
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{
|
||||
JSONData: []byte(`{"httpHeaderName1": "X-Client"}`),
|
||||
},
|
||||
}
|
||||
|
||||
headers, err := getTeamHTTPHeaders(pluginCtx)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, headers)
|
||||
}
|
||||
|
||||
func TestGetTeamHTTPHeaders_LabelPolicyValue(t *testing.T) {
|
||||
pluginCtx := backend.PluginContext{
|
||||
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{
|
||||
JSONData: []byte(`{
|
||||
"teamHttpHeaders": {
|
||||
"headers": {
|
||||
"101": [
|
||||
{"header": "X-Prom-Label-Policy", "value": "1:team-value"},
|
||||
{"header": "X-Prom-Label-Policy", "value": "2:team-wins"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}`),
|
||||
},
|
||||
}
|
||||
|
||||
headers, err := getTeamHTTPHeaders(pluginCtx)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, map[string]string{
|
||||
"X-Prom-Label-Policy": "1:team-value,2:team-wins",
|
||||
}, headers)
|
||||
}
|
||||
|
||||
func TestGetLabelPolicyKeyValue_AppendsValues(t *testing.T) {
|
||||
headerWithRules := map[string]interface{}{
|
||||
"101": []interface{}{
|
||||
map[string]interface{}{
|
||||
"header": "X-Prom-Label-Policy",
|
||||
"value": "1:alpha",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"header": "X-Prom-Label-Policy",
|
||||
"value": "2:beta",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
key, value := getLabelPolicyKeyValue(headerWithRules)
|
||||
assert.Equal(t, "X-Prom-Label-Policy", key)
|
||||
assert.Equal(t, "1:alpha,2:beta", value)
|
||||
}
|
||||
|
||||
func TestGetClientOptionsHeaders_ParsesHeaders(t *testing.T) {
|
||||
pluginCtx := backend.PluginContext{
|
||||
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{
|
||||
JSONData: []byte(`{"httpHeaderName1": "X-Client"}`),
|
||||
DecryptedSecureJSONData: map[string]string{
|
||||
"httpHeaderValue1": "client-value",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
headers, err := getClientOptionsHeaders(context.Background(), pluginCtx)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, map[string]string{"X-Client": "client-value"}, headers)
|
||||
}
|
||||
|
||||
func TestGetClientOptionsHeaders_InvalidJSON(t *testing.T) {
|
||||
pluginCtx := backend.PluginContext{
|
||||
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{
|
||||
JSONData: []byte("{"),
|
||||
},
|
||||
}
|
||||
|
||||
_, err := getClientOptionsHeaders(context.Background(), pluginCtx)
|
||||
require.Error(t, err)
|
||||
}
|
||||
Reference in New Issue
Block a user