Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Grafana #257

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/synyx/tuwat/pkg/connectors/example"
"github.com/synyx/tuwat/pkg/connectors/github"
"github.com/synyx/tuwat/pkg/connectors/gitlabmr"
"github.com/synyx/tuwat/pkg/connectors/grafana"
"github.com/synyx/tuwat/pkg/connectors/graylog"
"github.com/synyx/tuwat/pkg/connectors/icinga2"
"github.com/synyx/tuwat/pkg/connectors/nagiosapi"
Expand Down Expand Up @@ -91,6 +92,7 @@ type rootConfig struct {
Orderview []orderview.Config `toml:"orderview"`
Example []example.Config `toml:"example"`
Graylogs []graylog.Config `toml:"graylog"`
Grafanas []grafana.Config `toml:"grafana"`
}

func NewConfiguration() (config *Config, err error) {
Expand Down Expand Up @@ -244,6 +246,9 @@ func (cfg *Config) configureMain(rootConfig *rootConfig) (err error) {
for _, connectorConfig := range rootConfig.Graylogs {
cfg.Connectors = append(cfg.Connectors, graylog.NewConnector(&connectorConfig))
}
for _, connectorConfig := range rootConfig.Grafanas {
cfg.Connectors = append(cfg.Connectors, grafana.NewConnector(&connectorConfig))
}

// Add template for
cfg.WhereTemplate, err = template.New("where").
Expand Down
54 changes: 54 additions & 0 deletions pkg/connectors/grafana/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package grafana

// https://raw.githubusercontent.com/grafana/grafana/main/pkg/services/ngalert/api/tooling/post.json
// https://prometheus.io/docs/prometheus/latest/querying/api/#rules

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 alertingRuleState `json:"state"`
Name string `json:"name"`
ActiveAt string `json:"activeAt"`
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 alertingState `json:"state"`
ActiveAt string `json:"activeAt"`
Value string `json:"value"`
}

type alertingRuleState = string

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

type alertingState = string

const (
alertingStateAlerting alertingState = "alerting"
alertingStateNoData alertingState = "nodata"
alertingStateNormal alertingState = "normal"
alertingStateError alertingState = "error"
)
164 changes: 164 additions & 0 deletions pkg/connectors/grafana/connector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
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/common"
)

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

type Config struct {
Tag string
Cluster string
common.HTTPConfig
}

func NewConnector(cfg *Config) *Connector {
c := &Connector{config: *cfg, client: cfg.HTTPConfig.Client()}

return c
}

func (c *Connector) Tag() string {
return c.config.Tag
}

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

var alerts []connectors.Alert

for _, sourceAlertGroup := range sourceAlertGroups {
rule := sourceAlertGroup.Rules[0]
switch rule.State {
case alertingStateInactive:
continue
case alertingStatePending:
fallthrough
case alertingStateFiring:
// ok
}

if rule.Type != "alerting" {
continue
}

sourceAlert := rule.Alerts[0]

state := grafanaStateToState(sourceAlert.State)
if state == connectors.OK {
continue
}

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

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)
}

return alerts, nil
}

func grafanaStateToState(state string) connectors.State {
switch strings.ToLower(state) {
case alertingStateAlerting:
return connectors.Critical
case alertingStateNoData:
return connectors.Unknown
case alertingStateError:
return connectors.Warning
case alertingStateNormal:
return connectors.OK
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
}
120 changes: 120 additions & 0 deletions pkg/connectors/grafana/connector_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package grafana

import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"

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

func TestConnector(t *testing.T) {
connector, mockServer := testConnector(map[string]string{
"/api/prometheus/grafana/api/v1/rules": mockResponse,
})
defer func() { mockServer.Close() }()

alerts, err := connector.Collect(context.Background())
if err != nil {
t.Fatal(err)
}

if len(alerts) == 0 {
t.Error("There should be alerts")
}
}

func testConnector(endpoints map[string]string) (*Connector, *httptest.Server) {
mockServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
res.WriteHeader(http.StatusOK)

for endpoint, body := range endpoints {
if strings.HasPrefix(req.URL.Path, endpoint) {
if _, err := res.Write([]byte(body)); err != nil {
panic(err)
}
}
}
}))

cfg := Config{
Tag: "test",
HTTPConfig: common.HTTPConfig{
URL: mockServer.URL,
},
}

return NewConnector(&cfg), mockServer
}

const mockResponse = `
{
"status": "success",
"data": {
"groups": [
{
"name": "failed authentications alert",
"file": "Folder",
"rules": [
{
"state": "alerting",
"name": "Consumed no things alert",
"query": "",
"annotations": {
"__alertId__": "81",
"__dashboardUid__": "UlpdFLWMz",
"__panelId__": "7",
"message": "Long Message"
},
"alerts": [
{
"labels": {
"__contacts__": "\"Team\",\"jbuch mail\"",
"alertname": "Consumed no things alert",
"grafana_folder": "Folder",
"rule_uid": "kbMKlW04z"
},
"annotations": {
"__alertId__": "81",
"__dashboardUid__": "UlpdFLWMz",
"__panelId__": "7",
"message": "Long Message"
},
"state": "Alerting",
"activeAt": "2024-08-13T12:41:40+02:00",
"value": ""
}
],
"totals": {
"normal": 1
},
"totalsFiltered": {
"normal": 1
},
"labels": {
"__contacts__": "\"Team\",\"jbuch mail\"",
"rule_uid": "kbMKlW04z"
},
"health": "nodata",
"type": "alerting",
"lastEvaluation": "2024-08-30T15:18:40+02:00",
"evaluationTime": 6.723146319
}
],
"totals": {
"inactive": 1
},
"interval": 60,
"lastEvaluation": "2024-08-30T15:18:40+02:00",
"evaluationTime": 6.723146319
}
],
"totals": {
"inactive": 10,
"nodata": 5
}
}
}
`
Loading