Alerting: Support JSON responses in the Prometheus conversion API (#109070)
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -155,7 +156,7 @@ func NewConvertPrometheusSrv(
|
|||||||
|
|
||||||
// RouteConvertPrometheusGetRules returns all Grafana-managed alert rules in all namespaces (folders)
|
// RouteConvertPrometheusGetRules returns all Grafana-managed alert rules in all namespaces (folders)
|
||||||
// that were imported from a Prometheus-compatible source.
|
// that were imported from a Prometheus-compatible source.
|
||||||
// It responds with a YAML containing a mapping of folders to arrays of Prometheus rule groups.
|
// It responds with JSON or YAML containing a mapping of folders to arrays of Prometheus rule groups.
|
||||||
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetRules(c *contextmodel.ReqContext) response.Response {
|
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetRules(c *contextmodel.ReqContext) response.Response {
|
||||||
logger := srv.logger.FromContext(c.Req.Context())
|
logger := srv.logger.FromContext(c.Req.Context())
|
||||||
|
|
||||||
@@ -166,7 +167,7 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetRules(c *contextmodel.
|
|||||||
if len(folders) == 0 || errors.Is(err, dashboards.ErrFolderNotFound) {
|
if len(folders) == 0 || errors.Is(err, dashboards.ErrFolderNotFound) {
|
||||||
// If there is no such folder or no children, return empty response
|
// If there is no such folder or no children, return empty response
|
||||||
// because mimirtool expects 200 OK response in this case.
|
// because mimirtool expects 200 OK response in this case.
|
||||||
return response.YAML(http.StatusOK, map[string][]apimodels.PrometheusRuleGroup{})
|
return convertPrometheusResponse(c, http.StatusOK, map[string][]apimodels.PrometheusRuleGroup{})
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Failed to get folders", "error", err)
|
logger.Error("Failed to get folders", "error", err)
|
||||||
@@ -193,7 +194,7 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetRules(c *contextmodel.
|
|||||||
return errorToResponse(err)
|
return errorToResponse(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.YAML(http.StatusOK, namespaces)
|
return convertPrometheusResponse(c, http.StatusOK, namespaces)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RouteConvertPrometheusDeleteNamespace deletes all rule groups that were imported from a Prometheus-compatible source
|
// RouteConvertPrometheusDeleteNamespace deletes all rule groups that were imported from a Prometheus-compatible source
|
||||||
@@ -256,7 +257,7 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteRuleGroup(c *contex
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RouteConvertPrometheusGetNamespace returns the Grafana-managed alert rules for a specified namespace (folder).
|
// RouteConvertPrometheusGetNamespace returns the Grafana-managed alert rules for a specified namespace (folder).
|
||||||
// It responds with a YAML containing a mapping of a single folder to an array of Prometheus rule groups.
|
// It responds with JSON or YAML containing a mapping of a single folder to an array of Prometheus rule groups.
|
||||||
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetNamespace(c *contextmodel.ReqContext, namespaceTitle string) response.Response {
|
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetNamespace(c *contextmodel.ReqContext, namespaceTitle string) response.Response {
|
||||||
logger := srv.logger.FromContext(c.Req.Context())
|
logger := srv.logger.FromContext(c.Req.Context())
|
||||||
|
|
||||||
@@ -286,11 +287,11 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetNamespace(c *contextmo
|
|||||||
return errorToResponse(err)
|
return errorToResponse(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.YAML(http.StatusOK, ns)
|
return convertPrometheusResponse(c, http.StatusOK, ns)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RouteConvertPrometheusGetRuleGroup retrieves a single rule group for a given namespace (folder)
|
// RouteConvertPrometheusGetRuleGroup retrieves a single rule group for a given namespace (folder)
|
||||||
// in Prometheus-compatible YAML format if it was imported from a Prometheus-compatible source.
|
// in Prometheus-compatible JSON or YAML format if it was imported from a Prometheus-compatible source.
|
||||||
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetRuleGroup(c *contextmodel.ReqContext, namespaceTitle string, group string) response.Response {
|
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetRuleGroup(c *contextmodel.ReqContext, namespaceTitle string, group string) response.Response {
|
||||||
logger := srv.logger.FromContext(c.Req.Context())
|
logger := srv.logger.FromContext(c.Req.Context())
|
||||||
|
|
||||||
@@ -332,7 +333,7 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetRuleGroup(c *contextmo
|
|||||||
return errorToResponse(err)
|
return errorToResponse(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.YAML(http.StatusOK, promGroup)
|
return convertPrometheusResponse(c, http.StatusOK, promGroup)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RouteConvertPrometheusPostRuleGroup converts a Prometheus rule group into a Grafana rule group
|
// RouteConvertPrometheusPostRuleGroup converts a Prometheus rule group into a Grafana rule group
|
||||||
@@ -608,7 +609,7 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetAlertmanagerConfig(c *
|
|||||||
TemplateFiles: extraCfg.TemplateFiles,
|
TemplateFiles: extraCfg.TemplateFiles,
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := response.YAML(http.StatusOK, respBody)
|
resp := convertPrometheusResponse(c, http.StatusOK, respBody)
|
||||||
resp.SetHeader(configIdentifierHeader, extraCfg.Identifier)
|
resp.SetHeader(configIdentifierHeader, extraCfg.Identifier)
|
||||||
resp.SetHeader(mergeMatchersHeader, formatMergeMatchers(extraCfg.MergeMatchers))
|
resp.SetHeader(mergeMatchersHeader, formatMergeMatchers(extraCfg.MergeMatchers))
|
||||||
|
|
||||||
@@ -808,3 +809,22 @@ func parseConfigIdentifierHeader(c *contextmodel.ReqContext) string {
|
|||||||
}
|
}
|
||||||
return identifier
|
return identifier
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// convertPrometheusResponse returns a JSON or YAML response based on the Accept header.
|
||||||
|
// Default is YAML for backward compatibility with mimirtool.
|
||||||
|
func convertPrometheusResponse(c *contextmodel.ReqContext, status int, body interface{}) *response.NormalResponse {
|
||||||
|
acceptHeader := c.Req.Header.Get("Accept")
|
||||||
|
|
||||||
|
for _, accept := range strings.Split(acceptHeader, ",") {
|
||||||
|
mediaType, _, err := mime.ParseMediaType(accept)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if mediaType == "application/json" {
|
||||||
|
return response.JSON(status, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.YAML(status, body)
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -597,17 +598,34 @@ func TestRouteConvertPrometheusGetRuleGroup(t *testing.T) {
|
|||||||
GenerateRef()
|
GenerateRef()
|
||||||
ruleStore.PutRule(context.Background(), ruleInOtherFolder)
|
ruleStore.PutRule(context.Background(), ruleInOtherFolder)
|
||||||
|
|
||||||
getResp := srv.RouteConvertPrometheusGetRuleGroup(rc, fldr.Title, groupKey.RuleGroup)
|
t.Run("YAML response", func(t *testing.T) {
|
||||||
require.Equal(t, http.StatusOK, getResp.Status())
|
getResp := srv.RouteConvertPrometheusGetRuleGroup(rc, fldr.Title, groupKey.RuleGroup)
|
||||||
|
require.Equal(t, http.StatusOK, getResp.Status())
|
||||||
|
|
||||||
var respGroup apimodels.PrometheusRuleGroup
|
var respGroup apimodels.PrometheusRuleGroup
|
||||||
err := yaml.Unmarshal(getResp.Body(), &respGroup)
|
err := yaml.Unmarshal(getResp.Body(), &respGroup)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Equal(t, groupKey.RuleGroup, respGroup.Name)
|
require.Equal(t, groupKey.RuleGroup, respGroup.Name)
|
||||||
require.Equal(t, prommodel.Duration(time.Duration(rule.IntervalSeconds)*time.Second), respGroup.Interval)
|
require.Equal(t, prommodel.Duration(time.Duration(rule.IntervalSeconds)*time.Second), respGroup.Interval)
|
||||||
require.Len(t, respGroup.Rules, 1)
|
require.Len(t, respGroup.Rules, 1)
|
||||||
require.Equal(t, promRule.Alert, respGroup.Rules[0].Alert)
|
require.Equal(t, promRule.Alert, respGroup.Rules[0].Alert)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("JSON response", func(t *testing.T) {
|
||||||
|
rc.Req.Header.Set("Accept", "application/json")
|
||||||
|
getResp := srv.RouteConvertPrometheusGetRuleGroup(rc, fldr.Title, groupKey.RuleGroup)
|
||||||
|
require.Equal(t, http.StatusOK, getResp.Status())
|
||||||
|
|
||||||
|
var jsonGroup apimodels.PrometheusRuleGroup
|
||||||
|
err := json.Unmarshal(getResp.Body(), &jsonGroup)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, groupKey.RuleGroup, jsonGroup.Name)
|
||||||
|
require.Equal(t, prommodel.Duration(time.Duration(rule.IntervalSeconds)*time.Second), jsonGroup.Interval)
|
||||||
|
require.Len(t, jsonGroup.Rules, 1)
|
||||||
|
require.Equal(t, promRule.Alert, jsonGroup.Rules[0].Alert)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -690,16 +708,32 @@ func TestRouteConvertPrometheusGetNamespace(t *testing.T) {
|
|||||||
ruleStore.PutRule(context.Background(), rule)
|
ruleStore.PutRule(context.Background(), rule)
|
||||||
}
|
}
|
||||||
|
|
||||||
response := srv.RouteConvertPrometheusGetNamespace(rc, fldr.Title)
|
t.Run("YAML response", func(t *testing.T) {
|
||||||
require.Equal(t, http.StatusOK, response.Status())
|
response := srv.RouteConvertPrometheusGetNamespace(rc, fldr.Title)
|
||||||
|
require.Equal(t, http.StatusOK, response.Status())
|
||||||
|
|
||||||
var respNamespaces map[string][]apimodels.PrometheusRuleGroup
|
var respNamespaces map[string][]apimodels.PrometheusRuleGroup
|
||||||
err := yaml.Unmarshal(response.Body(), &respNamespaces)
|
err := yaml.Unmarshal(response.Body(), &respNamespaces)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Len(t, respNamespaces, 1)
|
require.Len(t, respNamespaces, 1)
|
||||||
require.Contains(t, respNamespaces, fldr.Title)
|
require.Contains(t, respNamespaces, fldr.Title)
|
||||||
require.ElementsMatch(t, respNamespaces[fldr.Title], []apimodels.PrometheusRuleGroup{promGroup1, promGroup2})
|
require.ElementsMatch(t, respNamespaces[fldr.Title], []apimodels.PrometheusRuleGroup{promGroup1, promGroup2})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("JSON response", func(t *testing.T) {
|
||||||
|
rc.Req.Header.Set("Accept", "application/json")
|
||||||
|
response := srv.RouteConvertPrometheusGetNamespace(rc, fldr.Title)
|
||||||
|
require.Equal(t, http.StatusOK, response.Status())
|
||||||
|
|
||||||
|
var jsonNamespaces map[string][]apimodels.PrometheusRuleGroup
|
||||||
|
err := json.Unmarshal(response.Body(), &jsonNamespaces)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Len(t, jsonNamespaces, 1)
|
||||||
|
require.Contains(t, jsonNamespaces, fldr.Title)
|
||||||
|
require.ElementsMatch(t, jsonNamespaces[fldr.Title], []apimodels.PrometheusRuleGroup{promGroup1, promGroup2})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -821,19 +855,120 @@ func TestRouteConvertPrometheusGetRules(t *testing.T) {
|
|||||||
ruleStore.PutRule(context.Background(), rule)
|
ruleStore.PutRule(context.Background(), rule)
|
||||||
}
|
}
|
||||||
|
|
||||||
response := srv.RouteConvertPrometheusGetRules(rc)
|
t.Run("YAML response", func(t *testing.T) {
|
||||||
require.Equal(t, http.StatusOK, response.Status())
|
response := srv.RouteConvertPrometheusGetRules(rc)
|
||||||
|
require.Equal(t, http.StatusOK, response.Status())
|
||||||
|
|
||||||
var respNamespaces map[string][]apimodels.PrometheusRuleGroup
|
var respNamespaces map[string][]apimodels.PrometheusRuleGroup
|
||||||
err := yaml.Unmarshal(response.Body(), &respNamespaces)
|
err := yaml.Unmarshal(response.Body(), &respNamespaces)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Len(t, respNamespaces, 1)
|
require.Len(t, respNamespaces, 1)
|
||||||
require.Contains(t, respNamespaces, fldr.Title)
|
require.Contains(t, respNamespaces, fldr.Title)
|
||||||
require.ElementsMatch(t, respNamespaces[fldr.Title], []apimodels.PrometheusRuleGroup{promGroup1, promGroup2})
|
require.ElementsMatch(t, respNamespaces[fldr.Title], []apimodels.PrometheusRuleGroup{promGroup1, promGroup2})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("JSON response", func(t *testing.T) {
|
||||||
|
rc.Req.Header.Set("Accept", "application/json")
|
||||||
|
response := srv.RouteConvertPrometheusGetRules(rc)
|
||||||
|
require.Equal(t, http.StatusOK, response.Status())
|
||||||
|
|
||||||
|
var jsonNamespaces map[string][]apimodels.PrometheusRuleGroup
|
||||||
|
err := json.Unmarshal(response.Body(), &jsonNamespaces)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Len(t, jsonNamespaces, 1)
|
||||||
|
require.Contains(t, jsonNamespaces, fldr.Title)
|
||||||
|
require.ElementsMatch(t, jsonNamespaces[fldr.Title], []apimodels.PrometheusRuleGroup{promGroup1, promGroup2})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConvertPrometheusResponse(t *testing.T) {
|
||||||
|
testData := map[string][]apimodels.PrometheusRuleGroup{
|
||||||
|
"test": {
|
||||||
|
{
|
||||||
|
Name: "test-group",
|
||||||
|
Rules: []apimodels.PrometheusRule{
|
||||||
|
{
|
||||||
|
Alert: "TestAlert",
|
||||||
|
Expr: "up == 0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
acceptHeader string
|
||||||
|
expectedType string
|
||||||
|
checkResponse func(t *testing.T, body []byte)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "by default returns YAML",
|
||||||
|
expectedType: "text/yaml",
|
||||||
|
checkResponse: func(t *testing.T, body []byte) {
|
||||||
|
require.True(t, strings.Contains(string(body), "test-group"))
|
||||||
|
require.True(t, strings.Contains(string(body), "TestAlert"))
|
||||||
|
var result map[string][]apimodels.PrometheusRuleGroup
|
||||||
|
err := yaml.Unmarshal(body, &result)
|
||||||
|
require.NoError(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with application/json Accept header returns JSON",
|
||||||
|
acceptHeader: "application/json",
|
||||||
|
expectedType: "application/json",
|
||||||
|
checkResponse: func(t *testing.T, body []byte) {
|
||||||
|
require.True(t, strings.Contains(string(body), "test-group"))
|
||||||
|
require.True(t, strings.Contains(string(body), "TestAlert"))
|
||||||
|
var result map[string][]apimodels.PrometheusRuleGroup
|
||||||
|
err := json.Unmarshal(body, &result)
|
||||||
|
require.NoError(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with application/yaml accept header returns YAML",
|
||||||
|
acceptHeader: "application/yaml",
|
||||||
|
expectedType: "text/yaml",
|
||||||
|
checkResponse: func(t *testing.T, body []byte) {
|
||||||
|
require.True(t, strings.Contains(string(body), "test-group"))
|
||||||
|
require.True(t, strings.Contains(string(body), "TestAlert"))
|
||||||
|
var result map[string][]apimodels.PrometheusRuleGroup
|
||||||
|
err := yaml.Unmarshal(body, &result)
|
||||||
|
require.NoError(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with a header with both json and yaml returns JSON",
|
||||||
|
acceptHeader: "application/yaml, application/json",
|
||||||
|
expectedType: "application/json",
|
||||||
|
checkResponse: func(t *testing.T, body []byte) {
|
||||||
|
require.True(t, strings.Contains(string(body), "test-group"))
|
||||||
|
require.True(t, strings.Contains(string(body), "TestAlert"))
|
||||||
|
var result map[string][]apimodels.PrometheusRuleGroup
|
||||||
|
err := json.Unmarshal(body, &result)
|
||||||
|
require.NoError(t, err)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
rc := createRequestCtx()
|
||||||
|
if tc.acceptHeader != "" {
|
||||||
|
rc.Req.Header.Set("Accept", tc.acceptHeader)
|
||||||
|
}
|
||||||
|
|
||||||
|
response := convertPrometheusResponse(rc, http.StatusOK, testData)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, response.Status())
|
||||||
|
tc.checkResponse(t, response.Body())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRouteConvertPrometheusDeleteNamespace(t *testing.T) {
|
func TestRouteConvertPrometheusDeleteNamespace(t *testing.T) {
|
||||||
t.Run("for non-existent folder should return 404", func(t *testing.T) {
|
t.Run("for non-existent folder should return 404", func(t *testing.T) {
|
||||||
srv, _, _ := createConvertPrometheusSrv(t)
|
srv, _, _ := createConvertPrometheusSrv(t)
|
||||||
|
|||||||
Reference in New Issue
Block a user