diff --git a/pkg/services/ngalert/state/historian/loki.go b/pkg/services/ngalert/state/historian/loki.go index 047fca0cbad..7b04b08fba9 100644 --- a/pkg/services/ngalert/state/historian/loki.go +++ b/pkg/services/ngalert/state/historian/loki.go @@ -242,7 +242,7 @@ func merge(res queryRes, ruleUID string) (*data.Frame, error) { } func statesToStreams(rule history_model.RuleMeta, states []state.StateTransition, externalLabels map[string]string, logger log.Logger) []stream { - buckets := make(map[string][]sample) // label repr -> entries + buckets := make(map[string][]sample) // label repr (JSON) -> entries for _, state := range states { if !shouldRecord(state) { continue @@ -254,7 +254,12 @@ func statesToStreams(rule history_model.RuleMeta, states []state.StateTransition labels[RuleUIDLabel] = fmt.Sprint(rule.UID) labels[GroupLabel] = fmt.Sprint(rule.Group) labels[FolderUIDLabel] = fmt.Sprint(rule.NamespaceUID) - repr := labels.String() + lblJsn, err := json.Marshal(labels) + if err != nil { + logger.Error("Failed to marshal labels to JSON", "error", err) + continue + } + repr := string(lblJsn) entry := lokiEntry{ SchemaVersion: 1, diff --git a/pkg/services/ngalert/state/historian/loki_test.go b/pkg/services/ngalert/state/historian/loki_test.go index 11966324933..6b848cd56de 100644 --- a/pkg/services/ngalert/state/historian/loki_test.go +++ b/pkg/services/ngalert/state/historian/loki_test.go @@ -323,6 +323,29 @@ grafana_alerting_state_history_writes_total{org="1"} 2 require.NoError(t, err) require.Nil(t, req.lastRequest) }) + + t.Run("succeeds with special chars in labels", func(t *testing.T) { + req := NewFakeRequester() + loki := createTestLokiBackend(req, metrics.NewHistorianMetrics(prometheus.NewRegistry())) + rule := createTestRule() + states := singleFromNormal(&state.State{ + State: eval.Alerting, + Labels: data.Labels{ + "dots": "contains.dot", + "equals": "contains=equals", + "emoji": "contains🤔emoji", + }, + }) + + err := <-loki.Record(context.Background(), rule, states) + + require.NoError(t, err) + require.Contains(t, "/loki/api/v1/push", req.lastRequest.URL.Path) + sent := string(readBody(t, req.lastRequest)) + require.Contains(t, sent, "contains.dot") + require.Contains(t, sent, "contains=equals") + require.Contains(t, sent, "contains🤔emoji") + }) } func createTestLokiBackend(req client.Requester, met *metrics.Historian) *RemoteLokiBackend { @@ -378,3 +401,11 @@ func badResponse() *http.Response { Header: make(http.Header, 0), } } + +func readBody(t *testing.T, req *http.Request) []byte { + t.Helper() + + val, err := io.ReadAll(req.Body) + require.NoError(t, err) + return val +}