diff --git a/Makefile b/Makefile index 181df5b5..8b6a82c7 100644 --- a/Makefile +++ b/Makefile @@ -26,6 +26,15 @@ up: -p 9090:9090 \ -v $(shell pwd)/testdata/prometheus.yml:/etc/prometheus/prometheus.yml \ prom/prometheus + docker run -d\ + --name graphite\ + --restart=always\ + -p 8081:80\ + -p 2003-2004:2003-2004\ + -p 2023-2024:2023-2024\ + -p 8125:8125/udp\ + -p 8126:8126\ + graphiteapp/graphite-statsd docker run -d \ -p 3000:3000 \ --name=grabana_grafana \ diff --git a/decoder/dashboard_test.go b/decoder/dashboard_test.go index 38e71bfb..d28b1e06 100644 --- a/decoder/dashboard_test.go +++ b/decoder/dashboard_test.go @@ -30,6 +30,7 @@ func TestUnmarshalYAML(t *testing.T) { tablePanel(), graphPanelWithStackdriverTarget(), heatmapPanel(), + graphPanelWithGraphiteTarget(), } for _, testCase := range testCases { @@ -1165,6 +1166,129 @@ rows: } } +func graphPanelWithGraphiteTarget() testCase { + yaml := `title: Awesome dashboard + +rows: + - name: Test row + panels: + - graph: + title: Packets received + datasource: graphite-test + targets: + - graphite: + ref: A + query: stats_counts.statsd.packets_received +` + json := `{ + "slug": "", + "title": "Awesome dashboard", + "originalTitle": "", + "tags": null, + "style": "dark", + "timezone": "", + "editable": false, + "hideControls": false, + "sharedCrosshair": false, + "templating": {"list": null}, + "annotations": {"list": null}, + "links": null, + "panels": null, + "rows": [ + { + "title": "Test row", + "collapse": false, + "editable": true, + "height": "250px", + "repeat": null, + "showTitle": true, + "panels": [ + { + "type": "graph", + "datasource": "graphite-test", + "editable": false, + "error": false, + "gridPos": {}, + "id": 8, + "isNew": false, + "renderer": "flot", + "span": 6, + "fill": 1, + "title": "Packets received", + "aliasColors": {}, + "bars": false, + "points": false, + "stack": false, + "steppedLine": false, + "lines": true, + "linewidth": 1, + "pointradius": 5, + "percentage": false, + "nullPointMode": "null as zero", + "legend": { + "alignAsTable": false, + "avg": false, + "current": false, + "hideEmpty": true, + "hideZero": true, + "max": false, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": false + }, + "targets": [ + { + "refId": "A", + "target": "stats_counts.statsd.packets_received" + } + ], + "tooltip": { + "shared": true, + "value_type": "", + "sort": 2 + }, + "x-axis": true, + "y-axis": true, + "xaxis": { + "format": "time", + "logBase": 1, + "show": true + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": false + } + ], + "transparent": false + } + ] + } + ], + "time": {"from": "now-3h", "to": "now"}, + "timepicker": { + "refresh_intervals": ["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"], + "time_options": ["5m","15m","1h","6h","12h","24h","2d","7d","30d"] + }, + "schemaVersion": 0, + "version": 0 +}` + + return testCase{ + name: "single row with single graph panel and graphite target", + yaml: yaml, + expectedGrafanaJSON: json, + } +} + func singleStatPanel() testCase { yaml := `title: Awesome dashboard diff --git a/decoder/graph.go b/decoder/graph.go index f4cbf3d1..c0740825 100644 --- a/decoder/graph.go +++ b/decoder/graph.go @@ -159,6 +159,9 @@ func (graphPanel *DashboardGraph) target(t Target) (graph.Option, error) { if t.Prometheus != nil { return graph.WithPrometheusTarget(t.Prometheus.Query, t.Prometheus.toOptions()...), nil } + if t.Graphite != nil { + return graph.WithGraphiteTarget(t.Graphite.Query, t.Graphite.toOptions()...), nil + } if t.Stackdriver != nil { stackdriverTarget, err := t.Stackdriver.toTarget() if err != nil { diff --git a/decoder/heatmap.go b/decoder/heatmap.go index 0ac899d8..eaa83aa0 100644 --- a/decoder/heatmap.go +++ b/decoder/heatmap.go @@ -110,6 +110,9 @@ func (heatmapPanel DashboardHeatmap) target(t Target) (heatmap.Option, error) { if t.Prometheus != nil { return heatmap.WithPrometheusTarget(t.Prometheus.Query, t.Prometheus.toOptions()...), nil } + if t.Graphite != nil { + return heatmap.WithGraphiteTarget(t.Graphite.Query, t.Graphite.toOptions()...), nil + } if t.Stackdriver != nil { stackdriverTarget, err := t.Stackdriver.toTarget() if err != nil { diff --git a/decoder/singlestat.go b/decoder/singlestat.go index 03a01fb7..b632c5af 100644 --- a/decoder/singlestat.go +++ b/decoder/singlestat.go @@ -132,6 +132,9 @@ func (singleStatPanel DashboardSingleStat) target(t Target) (singlestat.Option, if t.Prometheus != nil { return singlestat.WithPrometheusTarget(t.Prometheus.Query, t.Prometheus.toOptions()...), nil } + if t.Graphite != nil { + return singlestat.WithGraphiteTarget(t.Graphite.Query, t.Graphite.toOptions()...), nil + } if t.Stackdriver != nil { stackdriverTarget, err := t.Stackdriver.toTarget() if err != nil { diff --git a/decoder/table.go b/decoder/table.go index 840b3a5d..53aa116c 100644 --- a/decoder/table.go +++ b/decoder/table.go @@ -60,6 +60,9 @@ func (tablePanel *DashboardTable) target(t Target) (table.Option, error) { if t.Prometheus != nil { return table.WithPrometheusTarget(t.Prometheus.Query, t.Prometheus.toOptions()...), nil } + if t.Graphite != nil { + return table.WithGraphiteTarget(t.Graphite.Query, t.Graphite.toOptions()...), nil + } return nil, ErrTargetNotConfigured } diff --git a/decoder/target.go b/decoder/target.go index 584f29bb..86258948 100644 --- a/decoder/target.go +++ b/decoder/target.go @@ -3,6 +3,7 @@ package decoder import ( "fmt" + "github.com/K-Phoen/grabana/target/graphite" "github.com/K-Phoen/grabana/target/prometheus" "github.com/K-Phoen/grabana/target/stackdriver" ) @@ -14,6 +15,7 @@ var ErrInvalidStackdriverAlignment = fmt.Errorf("invalid stackdriver alignment m type Target struct { Prometheus *PrometheusTarget `yaml:",omitempty"` + Graphite *GraphiteTarget `yaml:",omitempty"` Stackdriver *StackdriverTarget `yaml:",omitempty"` } @@ -56,6 +58,24 @@ func (t PrometheusTarget) toOptions() []prometheus.Option { return opts } +type GraphiteTarget struct { + Query string + Ref string `yaml:",omitempty"` + Hidden bool `yaml:",omitempty"` +} + +func (t GraphiteTarget) toOptions() []graphite.Option { + opts := []graphite.Option{ + graphite.Ref(t.Ref), + } + + if t.Hidden { + opts = append(opts, graphite.Hide()) + } + + return opts +} + type StackdriverTarget struct { Project string Type string diff --git a/decoder/target_test.go b/decoder/target_test.go index c4e88e6a..fb168e55 100644 --- a/decoder/target_test.go +++ b/decoder/target_test.go @@ -3,10 +3,9 @@ package decoder import ( "testing" + "github.com/K-Phoen/grabana/target/graphite" "github.com/K-Phoen/grabana/target/prometheus" - "github.com/K-Phoen/grabana/target/stackdriver" - "github.com/stretchr/testify/require" ) @@ -273,3 +272,25 @@ func TestPrometheusComplexTarget(t *testing.T) { req.Equal(1/10, target.IntervalFactor) } } + +func TestGraphiteTarget(t *testing.T) { + req := require.New(t) + + opts := GraphiteTarget{ + Query: "stats_counts.statsd.packets_received", + Ref: "A", + }.toOptions() + target := graphite.New("query", opts...) + + req.False(target.Builder.Hide) + req.Equal("A", target.Builder.RefID) +} + +func TestGraphiteHiddenTarget(t *testing.T) { + req := require.New(t) + + opts := GraphiteTarget{Hidden: true}.toOptions() + target := graphite.New("query", opts...) + + req.True(target.Builder.Hide) +} diff --git a/graph/graph.go b/graph/graph.go index f19bca49..fbe07727 100644 --- a/graph/graph.go +++ b/graph/graph.go @@ -3,6 +3,7 @@ package graph import ( "github.com/K-Phoen/grabana/alert" "github.com/K-Phoen/grabana/axis" + "github.com/K-Phoen/grabana/target/graphite" "github.com/K-Phoen/grabana/target/prometheus" "github.com/K-Phoen/grabana/target/stackdriver" "github.com/grafana-tools/sdk" @@ -128,6 +129,15 @@ func WithPrometheusTarget(query string, options ...prometheus.Option) Option { } } +// WithGraphiteTarget adds a Graphite target to the table. +func WithGraphiteTarget(query string, options ...graphite.Option) Option { + target := graphite.New(query, options...) + + return func(graph *Graph) { + graph.Builder.AddTarget(target.Builder) + } +} + // WithStackdriverTarget adds a stackdriver query to the graph. func WithStackdriverTarget(target *stackdriver.Stackdriver) Option { return func(graph *Graph) { diff --git a/graph/graph_test.go b/graph/graph_test.go index 1e82e9e2..f6bbdf28 100644 --- a/graph/graph_test.go +++ b/graph/graph_test.go @@ -28,6 +28,14 @@ func TestGraphPanelCanHavePrometheusTargets(t *testing.T) { req.Len(panel.Builder.GraphPanel.Targets, 1) } +func TestGraphPanelPanelCanHaveGraphiteTargets(t *testing.T) { + req := require.New(t) + + panel := New("", WithGraphiteTarget("stats_counts.statsd.packets_received")) + + req.Len(panel.Builder.GraphPanel.Targets, 1) +} + func TestGraphPanelCanHaveStackdriverTargets(t *testing.T) { req := require.New(t) diff --git a/heatmap/heatmap.go b/heatmap/heatmap.go index 1333b2e2..9ba52dad 100644 --- a/heatmap/heatmap.go +++ b/heatmap/heatmap.go @@ -1,6 +1,7 @@ package heatmap import ( + "github.com/K-Phoen/grabana/target/graphite" "github.com/K-Phoen/grabana/target/prometheus" "github.com/K-Phoen/grabana/target/stackdriver" "github.com/grafana-tools/sdk" @@ -136,6 +137,15 @@ func WithPrometheusTarget(query string, options ...prometheus.Option) Option { } } +// WithGraphiteTarget adds a Graphite target to the table. +func WithGraphiteTarget(query string, options ...graphite.Option) Option { + target := graphite.New(query, options...) + + return func(heatmap *Heatmap) { + heatmap.Builder.AddTarget(target.Builder) + } +} + // WithStackdriverTarget adds a stackdriver query to the graph. func WithStackdriverTarget(target *stackdriver.Stackdriver) Option { return func(heatmap *Heatmap) { diff --git a/heatmap/heatmap_test.go b/heatmap/heatmap_test.go index cf98733f..48fe8fdf 100644 --- a/heatmap/heatmap_test.go +++ b/heatmap/heatmap_test.go @@ -27,6 +27,14 @@ func TestHeatmapPanelCanHavePrometheusTargets(t *testing.T) { req.Len(panel.Builder.HeatmapPanel.Targets, 1) } +func TestHeatmapPanelPanelCanHaveGraphiteTargets(t *testing.T) { + req := require.New(t) + + panel := New("", WithGraphiteTarget("stats_counts.statsd.packets_received")) + + req.Len(panel.Builder.HeatmapPanel.Targets, 1) +} + func TestHeatmapPanelCanHaveStackdriverTargets(t *testing.T) { req := require.New(t) diff --git a/singlestat/singlestat.go b/singlestat/singlestat.go index 7eef6fa8..32972bee 100644 --- a/singlestat/singlestat.go +++ b/singlestat/singlestat.go @@ -3,6 +3,7 @@ package singlestat import ( "strings" + "github.com/K-Phoen/grabana/target/graphite" "github.com/K-Phoen/grabana/target/prometheus" "github.com/K-Phoen/grabana/target/stackdriver" "github.com/grafana-tools/sdk" @@ -147,6 +148,15 @@ func WithPrometheusTarget(query string, options ...prometheus.Option) Option { } } +// WithGraphiteTarget adds a Graphite target to the table. +func WithGraphiteTarget(query string, options ...graphite.Option) Option { + target := graphite.New(query, options...) + + return func(singleStat *SingleStat) { + singleStat.Builder.AddTarget(target.Builder) + } +} + // WithStackdriverTarget adds a stackdriver query to the graph. func WithStackdriverTarget(target *stackdriver.Stackdriver) Option { return func(singleStat *SingleStat) { diff --git a/singlestat/singlestat_test.go b/singlestat/singlestat_test.go index b199dabe..d8ee1307 100644 --- a/singlestat/singlestat_test.go +++ b/singlestat/singlestat_test.go @@ -27,6 +27,14 @@ func TestSingleStatPanelCanHavePrometheusTargets(t *testing.T) { req.Len(panel.Builder.SinglestatPanel.Targets, 1) } +func TestSingleStatPanelPanelCanHaveGraphiteTargets(t *testing.T) { + req := require.New(t) + + panel := New("", WithGraphiteTarget("stats_counts.statsd.packets_received")) + + req.Len(panel.Builder.SinglestatPanel.Targets, 1) +} + func TestSingleStatPanelCanHaveStackdriverTargets(t *testing.T) { req := require.New(t) diff --git a/table/table.go b/table/table.go index dff6f523..a3e3afdd 100644 --- a/table/table.go +++ b/table/table.go @@ -1,6 +1,7 @@ package table import ( + "github.com/K-Phoen/grabana/target/graphite" "github.com/K-Phoen/grabana/target/prometheus" "github.com/grafana-tools/sdk" ) @@ -85,6 +86,15 @@ func WithPrometheusTarget(query string, options ...prometheus.Option) Option { } } +// WithGraphiteTarget adds a Graphite target to the table. +func WithGraphiteTarget(query string, options ...graphite.Option) Option { + target := graphite.New(query, options...) + + return func(table *Table) { + table.Builder.AddTarget(target.Builder) + } +} + // HideColumn hides the column having a label matching the given pattern. func HideColumn(columnLabelPattern string) Option { return func(table *Table) { diff --git a/table/table_test.go b/table/table_test.go index 5734cd2c..cecd77f2 100644 --- a/table/table_test.go +++ b/table/table_test.go @@ -48,6 +48,14 @@ func TestTablePanelCanHavePrometheusTargets(t *testing.T) { req.Len(panel.Builder.TablePanel.Targets, 1) } +func TestTablePanelCanHaveGraphiteTargets(t *testing.T) { + req := require.New(t) + + panel := New("", WithGraphiteTarget("stats_counts.statsd.packets_received")) + + req.Len(panel.Builder.TablePanel.Targets, 1) +} + func TestColumnsCanBeHidden(t *testing.T) { req := require.New(t) diff --git a/target/graphite/graphite.go b/target/graphite/graphite.go new file mode 100644 index 00000000..8b62127e --- /dev/null +++ b/target/graphite/graphite.go @@ -0,0 +1,40 @@ +package graphite + +import "github.com/grafana-tools/sdk" + +// Option represents an option that can be used to configure a graphite query. +type Option func(target *Graphite) + +// Graphite represents a graphite query. +type Graphite struct { + Builder *sdk.Target +} + +func New(target string, options ...Option) *Graphite { + graphite := &Graphite{ + Builder: &sdk.Target{ + Target: target, + }, + } + + for _, opt := range options { + opt(graphite) + } + + return graphite +} + +// Ref sets the reference ID for this query. +func Ref(ref string) Option { + return func(graphite *Graphite) { + graphite.Builder.RefID = ref + } +} + +// Hide the query. Grafana does not send hidden queries to the data source, +// but they can still be referenced in alerts. +func Hide() Option { + return func(graphite *Graphite) { + graphite.Builder.Hide = true + } +} diff --git a/target/graphite/graphite_test.go b/target/graphite/graphite_test.go new file mode 100644 index 00000000..82c625b3 --- /dev/null +++ b/target/graphite/graphite_test.go @@ -0,0 +1,32 @@ +package graphite_test + +import ( + "testing" + + "github.com/K-Phoen/grabana/target/graphite" + "github.com/stretchr/testify/require" +) + +func TestQueriesCanBeCreated(t *testing.T) { + req := require.New(t) + + target := graphite.New("stats_counts.statsd.packets_received") + + req.Equal("stats_counts.statsd.packets_received", target.Builder.Target) +} + +func TestRefCanBeConfigured(t *testing.T) { + req := require.New(t) + + target := graphite.New("", graphite.Ref("A")) + + req.Equal("A", target.Builder.RefID) +} + +func TestTargetCanBeHidden(t *testing.T) { + req := require.New(t) + + target := graphite.New("", graphite.Hide()) + + req.True(target.Builder.Hide) +}