diff --git a/pkg/services/ngalert/state/historian/loki.go b/pkg/services/ngalert/state/historian/loki.go index 0423a7dc7b3..3e8409f83b4 100644 --- a/pkg/services/ngalert/state/historian/loki.go +++ b/pkg/services/ngalert/state/historian/loki.go @@ -238,7 +238,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][]row) // label repr -> entries + buckets := make(map[string][]row) // label repr (JSON) -> entries for _, state := range states { if !shouldRecord(state) { continue @@ -250,7 +250,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 3fd4f540b6d..01e53402cc3 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 +}