Skip to content

Commit

Permalink
Revert "grafana, use alertmanager v2 alerts api"
Browse files Browse the repository at this point in the history
* the prometheus v1 API rules at least provides the current state, as
  opposed to:
  * /api/ruler/grafana/api/v1/rules/
  * /api/alertmanager/grafana/api/v2/alerts
  * /api/prometheus/grafana/api/v1/alerts
  * /api/v1/provisioning/alert-rules
  * /api/alerts
* This reverts commit a2214be.
  • Loading branch information
BuJo committed Sep 1, 2024
1 parent a2214be commit 746c0f2
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 22 deletions.
76 changes: 76 additions & 0 deletions pkg/connectors/grafana/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package grafana

import "time"

// https://raw.githubusercontent.com/grafana/grafana/main/pkg/services/ngalert/api/tooling/post.json

type ruleResponse struct {
Status string `json:"status"`
Data ruleDiscovery `json:"data,omitempty"`
}

type ruleDiscovery struct {
Groups []ruleGroup `json:"groups"`
}

type ruleGroup struct {
Name string `json:"name"`
File string `json:"file"`
Rules []alertingRule `json:"rules"`
}

type alertingRule struct {
State alertingState `json:"state"`
Name string `json:"name"`
ActiveAt string `json:"activeAt"`
Health string `json:"health"`
Annotations map[string]string `json:"annotations"`
Labels map[string]string `json:"labels,omitempty"`
Alerts []alert `json:"alerts,omitempty"`
Type string `json:"type"`
}

type alert struct {
Labels map[string]string `json:"labels"`
Annotations map[string]string `json:"annotations"`
State string `json:"state"`
ActiveAt string `json:"activeAt"`
Value string `json:"value"`
}

type alertingState = string

const (
alertingStatePending = "pending"
alertingStateFiring = "firing"
alertingStateInactive = "inactive"
)

const (
alertingStateAlerting = "alerting"
alertingStateNoData = "nodata"
alertingStateNormal = "normal"
alertingStateError = "error"
)

// https://grafana.com/docs/grafana/latest/developers/http_api/alerting_provisioning/#provisioned-alert-rules

type provisionedAlertRule struct {
Annotations map[string]string `json:"annotations"`
Condition string `json:"condition"`
ExecErrState string `json:"execErrState"`
Uid int64 `json:"id"`
IsPaused bool `json:"isPaused"`
Labels map[string]string `json:"labels"`
NoDataState string `json:"noDataState"`
For time.Duration `json:"for"`
Title string `json:"title"`
RuleGroup string `json:"ruleGroup"`
}

const (
noDataStateNoData = "NoData"
noDataStateOk = "OK"
execErrStateAlerting = "Alerting"
execErrStateError = "Error"
)
118 changes: 96 additions & 22 deletions pkg/connectors/grafana/connector.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
package grafana

import (
"bytes"
"context"
"encoding/json"
"fmt"
html "html/template"
"io"
"log/slog"
"net/http"
"strings"
"time"

"github.com/synyx/tuwat/pkg/connectors"
"github.com/synyx/tuwat/pkg/connectors/alertmanager"
"github.com/synyx/tuwat/pkg/connectors/common"
)

type Connector struct {
config Config
ac *alertmanager.Connector
client *http.Client
}

type Config struct {
Expand All @@ -22,14 +28,7 @@ type Config struct {
}

func NewConnector(cfg *Config) *Connector {
alertmanagerConfig := &alertmanager.Config{
Tag: cfg.Tag,
Cluster: cfg.Cluster,
HTTPConfig: cfg.HTTPConfig,
}
alertmanagerConfig.URL += "/api/alertmanager/grafana"

c := &Connector{config: *cfg, ac: alertmanager.NewConnector(alertmanagerConfig)}
c := &Connector{config: *cfg, client: cfg.HTTPConfig.Client()}

return c
}
Expand All @@ -39,27 +38,39 @@ func (c *Connector) Tag() string {
}

func (c *Connector) Collect(ctx context.Context) ([]connectors.Alert, error) {
sourceAlerts, err := c.ac.Collect(ctx)
sourceAlertGroups, err := c.collectAlerts(ctx)
if err != nil {
return nil, err
}

var alerts []connectors.Alert

for _, alert := range sourceAlerts {
alert.Description = alert.Labels["rulename"]
alert.Details = alert.Labels["message"]
labels := map[string]string{
"Hostname": alert.Labels["grafana_folder"],
"Contacts": alert.Labels["__contacts__"],
for _, sourceAlertGroup := range sourceAlertGroups {
rule := sourceAlertGroup.Rules[0]
sourceAlert := rule.Alerts[0]

state := grafanaStateToState(sourceAlert.State)
if state == connectors.OK {
continue
}
for k, v := range labels {
alert.Labels[k] = v

labels := map[string]string{
"Hostname": sourceAlert.Labels["grafana_folder"],
"Folder": sourceAlert.Labels["grafana_folder"],
"Alertname": sourceAlert.Labels["alertname"],
"Contacts": sourceAlert.Labels["__contacts__"],
}

alert.Links = []html.HTML{
html.HTML("<a href=\"" + c.config.URL + "/alerting/grafana/" + alert.Labels["rule_uid"] + "/view?tab=instances" + "\" target=\"_blank\" alt=\"Alert\">🏠</a>"),
html.HTML("<a href=\"" + c.config.URL + "/d/" + alert.Labels["__dashboardUid__"] + "\" target=\"_blank\" alt=\"Dashboard\">🏠</a>"),
alert := connectors.Alert{
Labels: labels,
Start: parseTime(sourceAlert.ActiveAt),
State: state,
Description: rule.Name,
Details: rule.Annotations["message"],
Links: []html.HTML{
html.HTML("<a href=\"" + c.config.URL + "/alerting/grafana/" + rule.Labels["rule_uid"] + "/view?tab=instances" + "\" target=\"_blank\" alt=\"Alert\">🏠</a>"),
html.HTML("<a href=\"" + c.config.URL + "/d/" + rule.Annotations["__dashboardUid__"] + "\" target=\"_blank\" alt=\"Dashboard\">🏠</a>"),
},
}

alerts = append(alerts, alert)
Expand All @@ -68,6 +79,69 @@ func (c *Connector) Collect(ctx context.Context) ([]connectors.Alert, error) {
return alerts, nil
}

func grafanaStateToState(state string) connectors.State {
switch strings.ToLower(state) {
case alertingStateAlerting:
return connectors.Critical
case alertingStateNoData:
return connectors.Warning
default:
return connectors.OK
}
}

func (c *Connector) String() string {
return fmt.Sprintf("Grafana (%s)", c.config.URL)
}

func (c *Connector) collectAlerts(ctx context.Context) ([]ruleGroup, error) {
res, err := c.get(ctx, "/api/prometheus/grafana/api/v1/rules")
if err != nil {
return nil, err
}
defer res.Body.Close()

b, _ := io.ReadAll(res.Body)
buf := bytes.NewBuffer(b)

decoder := json.NewDecoder(buf)

var response ruleResponse
err = decoder.Decode(&response)
if err != nil {
slog.ErrorContext(ctx, "Cannot parse",
slog.String("url", c.config.URL),
slog.String("data", buf.String()),
slog.Any("status", res.StatusCode),
slog.Any("error", err))
return nil, err
}

return response.Data.Groups, nil
}

func (c *Connector) get(ctx context.Context, endpoint string) (*http.Response, error) {

slog.DebugContext(ctx, "getting alerts", slog.String("url", c.config.URL+endpoint))

req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.config.URL+endpoint, nil)
if err != nil {
return nil, err
}

req.Header.Set("Accept", "application/json")

res, err := c.client.Do(req)
if err != nil {
return nil, err
}

return res, nil
}
func parseTime(timeField string) time.Time {
t, err := time.Parse("2006-01-02T15:04:05.999-07:00", timeField)
if err != nil {
return time.Time{}
}
return t
}

0 comments on commit 746c0f2

Please sign in to comment.