From 04f7f2451dbc11ffe2093c8f74d418b7279fa8cf Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Mon, 19 May 2025 19:42:22 +0200 Subject: [PATCH] Alerting: Add custom headers support for recording rules custom datasource writer (#105618) Alerting: Add custom headers support for recording rules datasource writer --- pkg/services/ngalert/ngalert.go | 1 + .../ngalert/writer/datasourcewriter.go | 11 +++++++++ .../ngalert/writer/datasourcewriter_test.go | 24 +++++++++++++++++++ pkg/services/ngalert/writer/testing.go | 4 ++++ 4 files changed, 40 insertions(+) diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index aaad47e2d0e..e57c7c82a43 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -656,6 +656,7 @@ func createRecordingWriter(featureToggles featuremgmt.FeatureToggles, settings s if featureToggles.IsEnabledGlobally(featuremgmt.FlagGrafanaManagedRecordingRulesDatasources) { cfg := writer.DatasourceWriterConfig{ Timeout: settings.Timeout, + CustomHeaders: settings.CustomHeaders, DefaultDatasourceUID: settings.DefaultDatasourceUID, } diff --git a/pkg/services/ngalert/writer/datasourcewriter.go b/pkg/services/ngalert/writer/datasourcewriter.go index 9be600ae157..6e7fb92092e 100644 --- a/pkg/services/ngalert/writer/datasourcewriter.go +++ b/pkg/services/ngalert/writer/datasourcewriter.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "net/http" "net/url" "path" "strings" @@ -37,6 +38,10 @@ type DatasourceWriterConfig struct { // This exists to cater for upgrading from old versions of Grafana, where rule // definitions may not have a target data source specified. DefaultDatasourceUID string + + // CustomHeaders is a map of optional custom HTTP headers + // to include in recording rule write requests. + CustomHeaders map[string]string } type DatasourceWriter struct { @@ -172,12 +177,18 @@ func (w *DatasourceWriter) makeWriter(ctx context.Context, orgID int64, dsUID st return nil, err } + headers := make(http.Header) + for k, v := range w.cfg.CustomHeaders { + headers.Add(k, v) + } + cfg := PrometheusWriterConfig{ URL: u.String(), HTTPOptions: httpclient.Options{ Timeouts: ho.Timeouts, TLS: ho.TLS, BasicAuth: ho.BasicAuth, + Header: headers, }, Timeout: w.cfg.Timeout, } diff --git a/pkg/services/ngalert/writer/datasourcewriter_test.go b/pkg/services/ngalert/writer/datasourcewriter_test.go index 165f735d10d..0766b5aff43 100644 --- a/pkg/services/ngalert/writer/datasourcewriter_test.go +++ b/pkg/services/ngalert/writer/datasourcewriter_test.go @@ -120,6 +120,30 @@ func TestDatasourceWriter(t *testing.T) { err := writer.WriteDatasource(context.Background(), "", "metric", time.Now(), frames, 1, map[string]string{}) require.NoError(t, err) }) + + t.Run("when custom headers are configured, they are passed to the request", func(t *testing.T) { + datasources.Reset() + + header1 := "X-Custom-Header" + header2 := "X-Another-Header" + headers := map[string]string{ + header1: "test-value", + header2: "another-value", + } + + cfg = DatasourceWriterConfig{ + Timeout: time.Second * 5, + DefaultDatasourceUID: "prom-2", + CustomHeaders: headers, + } + writer = NewDatasourceWriter(cfg, datasources, httpclient.NewProvider(), clock.New(), log.New("test"), met) + + err := writer.WriteDatasource(context.Background(), "prom-1", "metric", time.Now(), frames, 1, map[string]string{}) + require.NoError(t, err) + + assert.Equal(t, headers[header1], datasources.prom1.LastHeaders.Get(header1)) + assert.Equal(t, headers[header2], datasources.prom1.LastHeaders.Get(header2)) + }) } func TestDatasourceWriterGetRemoteWriteURL(t *testing.T) { diff --git a/pkg/services/ngalert/writer/testing.go b/pkg/services/ngalert/writer/testing.go index 2100c9cfc7a..84e71d04b3c 100644 --- a/pkg/services/ngalert/writer/testing.go +++ b/pkg/services/ngalert/writer/testing.go @@ -20,6 +20,7 @@ type TestRemoteWriteTarget struct { mtx sync.Mutex RequestsCount int LastRequestBody string + LastHeaders http.Header ExpectedPath string } @@ -30,6 +31,7 @@ func NewTestRemoteWriteTarget(t *testing.T) *TestRemoteWriteTarget { target := &TestRemoteWriteTarget{ RequestsCount: 0, LastRequestBody: "", + LastHeaders: http.Header{}, ExpectedPath: RemoteWriteEndpoint, } @@ -41,6 +43,7 @@ func NewTestRemoteWriteTarget(t *testing.T) *TestRemoteWriteTarget { target.mtx.Lock() defer target.mtx.Unlock() target.RequestsCount += 1 + target.LastHeaders = r.Header.Clone() bd, err := io.ReadAll(r.Body) defer func() { _ = r.Body.Close() @@ -81,4 +84,5 @@ func (s *TestRemoteWriteTarget) Reset() { defer s.mtx.Unlock() s.RequestsCount = 0 s.LastRequestBody = "" + s.LastHeaders = http.Header{} }