diff --git a/go.mod b/go.mod
index 60900a32..a3ef67d5 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/K-Phoen/dark
go 1.17
require (
- github.com/K-Phoen/grabana v0.18.1
+ github.com/K-Phoen/grabana v0.19.0
github.com/K-Phoen/sdk v0.0.0-20211119151408-adef1e5fdd11
github.com/spf13/cobra v1.2.1
github.com/stretchr/testify v1.7.0
diff --git a/go.sum b/go.sum
index 4fda64a3..48dd88d7 100644
--- a/go.sum
+++ b/go.sum
@@ -51,8 +51,8 @@ github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6L
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
-github.com/K-Phoen/grabana v0.18.1 h1:L9kFCCRyaH6EucYZ2sepgLUXicNEtrdPa0l84DbRb+M=
-github.com/K-Phoen/grabana v0.18.1/go.mod h1:e6GwAnk6L0g+tba2yhKdzWvDah8cPPlwPptyrnkK8ng=
+github.com/K-Phoen/grabana v0.19.0 h1:/rjWWZt3qkuze3EBXxlNjGxc9ZkBfPj4nZJCmu8RkOs=
+github.com/K-Phoen/grabana v0.19.0/go.mod h1:e6GwAnk6L0g+tba2yhKdzWvDah8cPPlwPptyrnkK8ng=
github.com/K-Phoen/sdk v0.0.0-20211119151408-adef1e5fdd11 h1:bcJqfzSQge+pyJ1beMzUmf3bh9bRyIzEFu8PXUx8Hgk=
github.com/K-Phoen/sdk v0.0.0-20211119151408-adef1e5fdd11/go.mod h1:fnbOsbRksULSfcXjOI6W1/HISz5o/u1iEhF/fLedqTg=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
diff --git a/internal/pkg/converter/alert.go b/internal/pkg/converter/alert.go
new file mode 100644
index 00000000..e921b4d2
--- /dev/null
+++ b/internal/pkg/converter/alert.go
@@ -0,0 +1,71 @@
+package converter
+
+import (
+ grabana "github.com/K-Phoen/grabana/decoder"
+ "github.com/K-Phoen/sdk"
+)
+
+func (converter *JSON) convertAlert(panel sdk.Panel) *grabana.Alert {
+ if panel.Alert == nil {
+ return nil
+ }
+
+ sdkAlert := panel.Alert
+
+ notifications := make([]string, 0, len(sdkAlert.Notifications))
+ for _, notification := range sdkAlert.Notifications {
+ notifications = append(notifications, notification.UID)
+ }
+
+ alert := &grabana.Alert{
+ Title: sdkAlert.Name,
+ Message: sdkAlert.Message,
+ EvaluateEvery: sdkAlert.Frequency,
+ For: sdkAlert.For,
+ Tags: sdkAlert.AlertRuleTags,
+ OnNoData: sdkAlert.NoDataState,
+ OnExecutionError: sdkAlert.ExecutionErrorState,
+ Notifications: notifications,
+ If: converter.convertAlertConditions(sdkAlert),
+ }
+
+ return alert
+}
+
+func (converter *JSON) convertAlertConditions(sdkAlert *sdk.Alert) []grabana.AlertCondition {
+ conditions := make([]grabana.AlertCondition, 0, len(sdkAlert.Conditions))
+
+ for _, condition := range sdkAlert.Conditions {
+ conditions = append(conditions, grabana.AlertCondition{
+ Operand: condition.Operator.Type,
+ Value: grabana.AlertValue{
+ Func: condition.Reducer.Type,
+ QueryRef: condition.Query.Params[0],
+ From: condition.Query.Params[1],
+ To: condition.Query.Params[2],
+ },
+ Threshold: converter.convertAlertThreshold(condition),
+ })
+ }
+
+ return conditions
+}
+
+func (converter *JSON) convertAlertThreshold(sdkCondition sdk.AlertCondition) grabana.AlertThreshold {
+ threshold := grabana.AlertThreshold{}
+
+ switch sdkCondition.Evaluator.Type {
+ case "no_value":
+ threshold.HasNoValue = true
+ case "lt":
+ threshold.Below = &sdkCondition.Evaluator.Params[0]
+ case "gt":
+ threshold.Above = &sdkCondition.Evaluator.Params[0]
+ case "outside_range":
+ threshold.OutsideRange = [2]float64{sdkCondition.Evaluator.Params[0], sdkCondition.Evaluator.Params[1]}
+ case "within_range":
+ threshold.WithinRange = [2]float64{sdkCondition.Evaluator.Params[0], sdkCondition.Evaluator.Params[1]}
+ }
+
+ return threshold
+}
diff --git a/internal/pkg/converter/annotation.go b/internal/pkg/converter/annotation.go
new file mode 100644
index 00000000..e04e5ebc
--- /dev/null
+++ b/internal/pkg/converter/annotation.go
@@ -0,0 +1,39 @@
+package converter
+
+import (
+ grabanaDashboard "github.com/K-Phoen/grabana/dashboard"
+ grabana "github.com/K-Phoen/grabana/decoder"
+ "github.com/K-Phoen/sdk"
+ "go.uber.org/zap"
+)
+
+func (converter *JSON) convertAnnotations(annotations []sdk.Annotation, dashboard *grabana.DashboardModel) {
+ for _, annotation := range annotations {
+ // grafana-sdk doesn't expose the "builtIn" field, so we work around that by skipping
+ // the annotation we know to be built-in by its name
+ if annotation.Name == "Annotations & Alerts" {
+ continue
+ }
+
+ if annotation.Type != "tags" {
+ converter.logger.Warn("unhandled annotation type: skipped", zap.String("type", annotation.Type), zap.String("name", annotation.Name))
+ continue
+ }
+
+ converter.convertTagAnnotation(annotation, dashboard)
+ }
+}
+
+func (converter *JSON) convertTagAnnotation(annotation sdk.Annotation, dashboard *grabana.DashboardModel) {
+ datasource := ""
+ if annotation.Datasource != nil {
+ datasource = *annotation.Datasource
+ }
+
+ dashboard.TagsAnnotation = append(dashboard.TagsAnnotation, grabanaDashboard.TagAnnotation{
+ Name: annotation.Name,
+ Datasource: datasource,
+ IconColor: annotation.IconColor,
+ Tags: annotation.Tags,
+ })
+}
diff --git a/internal/pkg/converter/annotation_test.go b/internal/pkg/converter/annotation_test.go
new file mode 100644
index 00000000..a12dce94
--- /dev/null
+++ b/internal/pkg/converter/annotation_test.go
@@ -0,0 +1,56 @@
+package converter
+
+import (
+ "testing"
+
+ grabana "github.com/K-Phoen/grabana/decoder"
+ "github.com/K-Phoen/sdk"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/zap"
+)
+
+func TestConvertTagAnnotationIgnoresBuiltIn(t *testing.T) {
+ req := require.New(t)
+
+ annotation := sdk.Annotation{Name: "Annotations & Alerts"}
+ dashboard := &grabana.DashboardModel{}
+
+ NewJSON(zap.NewNop()).convertAnnotations([]sdk.Annotation{annotation}, dashboard)
+
+ req.Len(dashboard.TagsAnnotation, 0)
+}
+
+func TestConvertTagAnnotationIgnoresUnknownTypes(t *testing.T) {
+ req := require.New(t)
+
+ annotation := sdk.Annotation{Name: "Will be ignored", Type: "dashboard"}
+ dashboard := &grabana.DashboardModel{}
+
+ NewJSON(zap.NewNop()).convertAnnotations([]sdk.Annotation{annotation}, dashboard)
+
+ req.Len(dashboard.TagsAnnotation, 0)
+}
+
+func TestConvertTagAnnotation(t *testing.T) {
+ req := require.New(t)
+
+ converter := NewJSON(zap.NewNop())
+
+ datasource := "-- Grafana --"
+ annotation := sdk.Annotation{
+ Type: "tags",
+ Datasource: &datasource,
+ IconColor: "#5794F2",
+ Name: "Deployments",
+ Tags: []string{"deploy"},
+ }
+ dashboard := &grabana.DashboardModel{}
+
+ converter.convertAnnotations([]sdk.Annotation{annotation}, dashboard)
+
+ req.Len(dashboard.TagsAnnotation, 1)
+ req.Equal("Deployments", dashboard.TagsAnnotation[0].Name)
+ req.ElementsMatch([]string{"deploy"}, dashboard.TagsAnnotation[0].Tags)
+ req.Equal("#5794F2", dashboard.TagsAnnotation[0].IconColor)
+ req.Equal(datasource, dashboard.TagsAnnotation[0].Datasource)
+}
diff --git a/internal/pkg/converter/externallink.go b/internal/pkg/converter/externallink.go
new file mode 100644
index 00000000..ec606056
--- /dev/null
+++ b/internal/pkg/converter/externallink.go
@@ -0,0 +1,51 @@
+package converter
+
+import (
+ grabana "github.com/K-Phoen/grabana/decoder"
+ "github.com/K-Phoen/sdk"
+ "go.uber.org/zap"
+)
+
+func (converter *JSON) convertExternalLinks(links []sdk.Link, dashboard *grabana.DashboardModel) {
+ for _, link := range links {
+ extLink := converter.convertExternalLink(link)
+ if extLink == nil {
+ continue
+ }
+
+ dashboard.ExternalLinks = append(dashboard.ExternalLinks, *extLink)
+ }
+}
+
+func (converter *JSON) convertExternalLink(link sdk.Link) *grabana.DashboardExternalLink {
+ if link.Type != "link" {
+ converter.logger.Warn("unhandled link type: skipped", zap.String("type", link.Type), zap.String("title", link.Title))
+ return nil
+ }
+
+ if link.URL == nil || *link.URL == "" {
+ converter.logger.Warn("link URL empty: skipped", zap.String("title", link.Title))
+ return nil
+ }
+
+ extLink := &grabana.DashboardExternalLink{
+ Title: link.Title,
+ URL: *link.URL,
+ IncludeVariableValues: link.IncludeVars,
+ }
+
+ if link.Tooltip != nil && *link.Tooltip != "" {
+ extLink.Description = *link.Tooltip
+ }
+ if link.Icon != nil && *link.Icon != "" {
+ extLink.Icon = *link.Icon
+ }
+ if link.TargetBlank != nil {
+ extLink.OpenInNewTab = *link.TargetBlank
+ }
+ if link.KeepTime != nil {
+ extLink.IncludeTimeRange = *link.KeepTime
+ }
+
+ return extLink
+}
diff --git a/internal/pkg/converter/externallink_test.go b/internal/pkg/converter/externallink_test.go
new file mode 100644
index 00000000..6971e7bd
--- /dev/null
+++ b/internal/pkg/converter/externallink_test.go
@@ -0,0 +1,45 @@
+package converter
+
+import (
+ "testing"
+
+ grabana "github.com/K-Phoen/grabana/decoder"
+ "github.com/K-Phoen/sdk"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/zap"
+)
+
+func TestConvertExternalLink(t *testing.T) {
+ req := require.New(t)
+
+ externalLink := sdk.Link{
+ Title: "joe",
+ Type: "link",
+ Icon: strPtr("cloud"),
+ IncludeVars: true,
+ KeepTime: boolPtr(true),
+ TargetBlank: boolPtr(true),
+ Tooltip: strPtr("description"),
+ URL: strPtr("http://lala"),
+ }
+ dashLink := sdk.Link{
+ Title: "not link",
+ }
+
+ converter := NewJSON(zap.NewNop())
+
+ dashboard := &grabana.DashboardModel{}
+ converter.convertExternalLinks([]sdk.Link{externalLink, dashLink}, dashboard)
+
+ req.Len(dashboard.ExternalLinks, 1)
+
+ link := dashboard.ExternalLinks[0]
+
+ req.Equal("joe", link.Title)
+ req.Equal("description", link.Description)
+ req.Equal("cloud", link.Icon)
+ req.Equal("http://lala", link.URL)
+ req.True(link.OpenInNewTab)
+ req.True(link.IncludeTimeRange)
+ req.True(link.IncludeVariableValues)
+}
diff --git a/internal/pkg/converter/graph.go b/internal/pkg/converter/graph.go
new file mode 100644
index 00000000..3ff0d2f2
--- /dev/null
+++ b/internal/pkg/converter/graph.go
@@ -0,0 +1,144 @@
+package converter
+
+import (
+ grabana "github.com/K-Phoen/grabana/decoder"
+ "github.com/K-Phoen/sdk"
+)
+
+func (converter *JSON) convertGraph(panel sdk.Panel) grabana.DashboardPanel {
+ graph := &grabana.DashboardGraph{
+ Title: panel.Title,
+ Span: panelSpan(panel),
+ Transparent: panel.Transparent,
+ Axes: &grabana.GraphAxes{
+ Bottom: converter.convertGraphAxis(panel.Xaxis),
+ },
+ Legend: converter.convertGraphLegend(panel.GraphPanel.Legend),
+ Visualization: converter.convertGraphVisualization(panel),
+ Alert: converter.convertAlert(panel),
+ }
+
+ if panel.Description != nil {
+ graph.Description = *panel.Description
+ }
+ if panel.Repeat != nil {
+ graph.Repeat = *panel.Repeat
+ }
+ if panel.Height != nil {
+ graph.Height = *(panel.Height).(*string)
+ }
+ if panel.Datasource != nil {
+ graph.Datasource = *panel.Datasource
+ }
+
+ if len(panel.Yaxes) == 2 {
+ graph.Axes.Left = converter.convertGraphAxis(panel.Yaxes[0])
+ graph.Axes.Right = converter.convertGraphAxis(panel.Yaxes[1])
+ }
+
+ for _, target := range panel.GraphPanel.Targets {
+ graphTarget := converter.convertTarget(target)
+ if graphTarget == nil {
+ continue
+ }
+
+ graph.Targets = append(graph.Targets, *graphTarget)
+ }
+
+ return grabana.DashboardPanel{Graph: graph}
+}
+
+func (converter *JSON) convertGraphVisualization(panel sdk.Panel) *grabana.GraphVisualization {
+ graphViz := &grabana.GraphVisualization{
+ NullValue: panel.GraphPanel.NullPointMode,
+ Staircase: panel.GraphPanel.SteppedLine,
+ Overrides: converter.convertGraphOverrides(panel),
+ }
+
+ return graphViz
+}
+
+func (converter *JSON) convertGraphOverrides(panel sdk.Panel) []grabana.GraphSeriesOverride {
+ if len(panel.GraphPanel.SeriesOverrides) == 0 {
+ return nil
+ }
+
+ overrides := make([]grabana.GraphSeriesOverride, 0, len(panel.GraphPanel.SeriesOverrides))
+
+ for _, sdkOverride := range panel.GraphPanel.SeriesOverrides {
+ color := ""
+ if sdkOverride.Color != nil {
+ color = *sdkOverride.Color
+ }
+
+ overrides = append(overrides, grabana.GraphSeriesOverride{
+ Alias: sdkOverride.Alias,
+ Color: color,
+ Dashes: sdkOverride.Dashes,
+ Lines: sdkOverride.Lines,
+ Fill: sdkOverride.Fill,
+ LineWidth: sdkOverride.LineWidth,
+ })
+ }
+
+ return overrides
+}
+
+func (converter *JSON) convertGraphLegend(sdkLegend sdk.Legend) []string {
+ var legend []string
+
+ if !sdkLegend.Show {
+ legend = append(legend, "hide")
+ }
+ if sdkLegend.AlignAsTable {
+ legend = append(legend, "as_table")
+ }
+ if sdkLegend.RightSide {
+ legend = append(legend, "to_the_right")
+ }
+ if sdkLegend.Min {
+ legend = append(legend, "min")
+ }
+ if sdkLegend.Max {
+ legend = append(legend, "max")
+ }
+ if sdkLegend.Avg {
+ legend = append(legend, "avg")
+ }
+ if sdkLegend.Current {
+ legend = append(legend, "current")
+ }
+ if sdkLegend.Total {
+ legend = append(legend, "total")
+ }
+ if sdkLegend.HideEmpty {
+ legend = append(legend, "no_null_series")
+ }
+ if sdkLegend.HideZero {
+ legend = append(legend, "no_zero_series")
+ }
+
+ return legend
+}
+
+func (converter *JSON) convertGraphAxis(sdkAxis sdk.Axis) *grabana.GraphAxis {
+ hidden := !sdkAxis.Show
+ var min *float64
+ var max *float64
+
+ if sdkAxis.Min != nil {
+ min = &sdkAxis.Min.Value
+ }
+ if sdkAxis.Max != nil {
+ max = &sdkAxis.Max.Value
+ }
+
+ return &grabana.GraphAxis{
+ Hidden: &hidden,
+ Label: sdkAxis.Label,
+ Unit: &sdkAxis.Format,
+ Min: min,
+ Max: max,
+ LogBase: sdkAxis.LogBase,
+ }
+}
diff --git a/internal/pkg/converter/graph_test.go b/internal/pkg/converter/graph_test.go
new file mode 100644
index 00000000..19f649c6
--- /dev/null
+++ b/internal/pkg/converter/graph_test.go
@@ -0,0 +1,182 @@
+package converter
+
+import (
+ "testing"
+
+ "github.com/K-Phoen/sdk"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/zap"
+)
+
+func TestConvertGraphPanel(t *testing.T) {
+ req := require.New(t)
+
+ converter := NewJSON(zap.NewNop())
+ height := "400px"
+ datasource := "prometheus"
+
+ graphPanel := sdk.Panel{
+ CommonPanel: sdk.CommonPanel{
+ Title: "graph panel",
+ Type: "graph",
+ Description: strPtr("graph description"),
+ Transparent: true,
+ Height: &height,
+ Datasource: &datasource,
+ },
+ GraphPanel: &sdk.GraphPanel{},
+ }
+
+ converted, ok := converter.convertDataPanel(graphPanel)
+
+ req.True(ok)
+ req.NotNil(converted.Graph)
+
+ graph := converted.Graph
+ req.True(graph.Transparent)
+ req.Equal("graph panel", graph.Title)
+ req.Equal("graph description", graph.Description)
+ req.Equal(height, graph.Height)
+ req.Equal(datasource, graph.Datasource)
+}
+
+func TestConvertGraphLegend(t *testing.T) {
+ req := require.New(t)
+
+ converter := NewJSON(zap.NewNop())
+
+ rawLegend := sdk.Legend{
+ AlignAsTable: true,
+ Avg: true,
+ Current: true,
+ HideEmpty: true,
+ HideZero: true,
+ Max: true,
+ Min: true,
+ RightSide: true,
+ Show: true,
+ Total: true,
+ }
+
+ legend := converter.convertGraphLegend(rawLegend)
+
+ req.ElementsMatch(
+ []string{"as_table", "to_the_right", "min", "max", "avg", "current", "total", "no_null_series", "no_zero_series"},
+ legend,
+ )
+}
+
+func TestConvertGraphCanHideLegend(t *testing.T) {
+ req := require.New(t)
+ converter := NewJSON(zap.NewNop())
+
+ legend := converter.convertGraphLegend(sdk.Legend{Show: false})
+ req.ElementsMatch([]string{"hide"}, legend)
+}
+
+func TestConvertGraphAxis(t *testing.T) {
+ req := require.New(t)
+
+ converter := NewJSON(zap.NewNop())
+
+ rawAxis := sdk.Axis{
+ Format: "bytes",
+ LogBase: 2,
+ Min: &sdk.FloatString{Value: 0},
+ Max: &sdk.FloatString{Value: 42},
+ Show: true,
+ Label: "Axis",
+ }
+
+ axis := converter.convertGraphAxis(rawAxis)
+
+ req.Equal("bytes", *axis.Unit)
+ req.Equal("Axis", axis.Label)
+ req.EqualValues(0, *axis.Min)
+ req.EqualValues(42, *axis.Max)
+ req.False(*axis.Hidden)
+}
+
+func TestConvertGraphVisualization(t *testing.T) {
+ req := require.New(t)
+ converter := NewJSON(zap.NewNop())
+ enabled := true
+
+ graphPanel := sdk.Panel{
+ CommonPanel: sdk.CommonPanel{
+ Title: "graph panel",
+ Type: "graph",
+ },
+ GraphPanel: &sdk.GraphPanel{
+ NullPointMode: "connected",
+ SteppedLine: true,
+ SeriesOverrides: []sdk.SeriesOverride{
+ {
+ Alias: "alias",
+ Dashes: &enabled,
+ },
+ },
+ },
+ }
+
+ visualization := converter.convertGraphVisualization(graphPanel)
+
+ req.True(visualization.Staircase)
+ req.Equal("connected", visualization.NullValue)
+ req.Len(visualization.Overrides, 1)
+}
+
+func TestConvertGraphOverridesWithNoOverride(t *testing.T) {
+ req := require.New(t)
+ converter := NewJSON(zap.NewNop())
+
+ graphPanel := sdk.Panel{
+ CommonPanel: sdk.CommonPanel{
+ Title: "graph panel",
+ Type: "graph",
+ },
+ GraphPanel: &sdk.GraphPanel{},
+ }
+
+ overrides := converter.convertGraphOverrides(graphPanel)
+
+ req.Len(overrides, 0)
+}
+
+func TestConvertGraphOverridesWithOneOverride(t *testing.T) {
+ req := require.New(t)
+ converter := NewJSON(zap.NewNop())
+ color := "red"
+ enabled := true
+ number := 2
+
+ graphPanel := sdk.Panel{
+ CommonPanel: sdk.CommonPanel{
+ Title: "heatmap panel",
+ Type: "graph",
+ },
+ GraphPanel: &sdk.GraphPanel{
+ SeriesOverrides: []sdk.SeriesOverride{
+ {
+ Alias: "alias",
+ Color: &color,
+ Dashes: &enabled,
+ Fill: &number,
+ Lines: &enabled,
+ },
+ },
+ },
+ }
+
+ overrides := converter.convertGraphOverrides(graphPanel)
+
+ req.Len(overrides, 1)
+
+ override := overrides[0]
+
+ req.Equal("alias", override.Alias)
+ req.Equal(color, override.Color)
+ req.True(*override.Dashes)
+ req.True(*override.Lines)
+ req.Equal(number, *override.Fill)
+}
diff --git a/internal/pkg/converter/heatmap.go b/internal/pkg/converter/heatmap.go
new file mode 100644
index 00000000..075c26e7
--- /dev/null
+++ b/internal/pkg/converter/heatmap.go
@@ -0,0 +1,89 @@
+package converter
+
+import (
+ "strconv"
+
+ grabana "github.com/K-Phoen/grabana/decoder"
+ "github.com/K-Phoen/sdk"
+ "go.uber.org/zap"
+)
+
+func (converter *JSON) convertHeatmap(panel sdk.Panel) grabana.DashboardPanel {
+ heatmap := &grabana.DashboardHeatmap{
+ Title: panel.Title,
+ Span: panelSpan(panel),
+ Transparent: panel.Transparent,
+ HideZeroBuckets: panel.HeatmapPanel.HideZeroBuckets,
+ HighlightCards: panel.HeatmapPanel.HighlightCards,
+ ReverseYBuckets: panel.HeatmapPanel.ReverseYBuckets,
+ Tooltip: &grabana.HeatmapTooltip{
+ Show: panel.HeatmapPanel.Tooltip.Show,
+ ShowHistogram: panel.HeatmapPanel.Tooltip.ShowHistogram,
+ Decimals: &panel.HeatmapPanel.TooltipDecimals,
+ },
+ YAxis: converter.convertHeatmapYAxis(panel),
+ }
+
+ if panel.Description != nil {
+ heatmap.Description = *panel.Description
+ }
+ if panel.Repeat != nil {
+ heatmap.Repeat = *panel.Repeat
+ }
+ if panel.Height != nil {
+ heatmap.Height = *(panel.Height).(*string)
+ }
+ if panel.Datasource != nil {
+ heatmap.Datasource = *panel.Datasource
+ }
+ if panel.HeatmapPanel.DataFormat != "" {
+ switch panel.HeatmapPanel.DataFormat {
+ case "tsbuckets":
+ heatmap.DataFormat = "time_series_buckets"
+ case "time_series":
+ heatmap.DataFormat = "time_series"
+ default:
+ converter.logger.Warn("unknown data format: skipping heatmap", zap.String("data_format", panel.HeatmapPanel.DataFormat), zap.String("heatmap_title", panel.Title))
+ }
+ }
+
+ for _, target := range panel.HeatmapPanel.Targets {
+ heatmapTarget := converter.convertTarget(target)
+ if heatmapTarget == nil {
+ continue
+ }
+
+ heatmap.Targets = append(heatmap.Targets, *heatmapTarget)
+ }
+
+ return grabana.DashboardPanel{Heatmap: heatmap}
+}
+
+func (converter *JSON) convertHeatmapYAxis(panel sdk.Panel) *grabana.HeatmapYAxis {
+ panelAxis := panel.HeatmapPanel.YAxis
+
+ axis := &grabana.HeatmapYAxis{
+ Decimals: panelAxis.Decimals,
+ Unit: panelAxis.Format,
+ }
+
+ if panelAxis.Max != nil {
+ max, err := strconv.ParseFloat(*panelAxis.Max, 64)
+ if err != nil {
+ converter.logger.Warn("could not parse max value on heatmap Y axis %s: %s", zap.String("value", *panelAxis.Max), zap.Error(err))
+ } else {
+ axis.Max = &max
+ }
+ }
+
+ if panelAxis.Min != nil {
+ min, err := strconv.ParseFloat(*panelAxis.Min, 64)
+ if err != nil {
+ converter.logger.Warn("could not parse min value on heatmap Y axis %s: %s", zap.String("value", *panelAxis.Min), zap.Error(err))
+ } else {
+ axis.Min = &min
+ }
+ }
+
+ return axis
+}
diff --git a/internal/pkg/converter/heatmap_test.go b/internal/pkg/converter/heatmap_test.go
new file mode 100644
index 00000000..420ed9b4
--- /dev/null
+++ b/internal/pkg/converter/heatmap_test.go
@@ -0,0 +1,47 @@
+package converter
+
+import (
+ "testing"
+
+ "github.com/K-Phoen/sdk"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/zap"
+)
+
+func TestConvertHeatmapPanel(t *testing.T) {
+ req := require.New(t)
+
+ converter := NewJSON(zap.NewNop())
+ height := "400px"
+ datasource := "prometheus"
+
+ heatmapPanel := sdk.Panel{
+ CommonPanel: sdk.CommonPanel{
+ Title: "heatmap panel",
+ Type: "heatmap",
+ Description: strPtr("heatmap description"),
+ Transparent: true,
+ Height: &height,
+ Datasource: &datasource,
+ },
+ HeatmapPanel: &sdk.HeatmapPanel{
+ HideZeroBuckets: true,
+ HighlightCards: true,
+ ReverseYBuckets: true,
+ DataFormat: "tsbuckets",
+ },
+ }
+
+ converted, ok := converter.convertDataPanel(heatmapPanel)
+
+ req.True(ok)
+ req.True(converted.Heatmap.Transparent)
+ req.Equal("heatmap panel", converted.Heatmap.Title)
+ req.Equal("heatmap description", converted.Heatmap.Description)
+ req.Equal(height, converted.Heatmap.Height)
+ req.Equal(datasource, converted.Heatmap.Datasource)
+ req.True(converted.Heatmap.ReverseYBuckets)
+ req.True(converted.Heatmap.HideZeroBuckets)
+ req.True(converted.Heatmap.HighlightCards)
+ req.Equal("time_series_buckets", converted.Heatmap.DataFormat)
+}
diff --git a/internal/pkg/converter/json.go b/internal/pkg/converter/json.go
index 96768b0b..04d8c459 100644
--- a/internal/pkg/converter/json.go
+++ b/internal/pkg/converter/json.go
@@ -5,15 +5,9 @@ import (
"fmt"
"io"
"io/ioutil"
- "strconv"
- "strings"
v1 "github.com/K-Phoen/dark/internal/pkg/apis/controller/v1"
- grabanaDashboard "github.com/K-Phoen/grabana/dashboard"
grabana "github.com/K-Phoen/grabana/decoder"
- "github.com/K-Phoen/grabana/singlestat"
- grabanaTable "github.com/K-Phoen/grabana/table"
- "github.com/K-Phoen/grabana/target/stackdriver"
"github.com/K-Phoen/sdk"
"go.uber.org/zap"
"gopkg.in/yaml.v3"
@@ -144,199 +138,6 @@ func (converter *JSON) convertGeneralSettings(board *sdk.Board, dashboard *graba
}
}
-func (converter *JSON) convertExternalLinks(links []sdk.Link, dashboard *grabana.DashboardModel) {
- for _, link := range links {
- if link.Type != "link" {
- converter.logger.Warn("unhandled link type: skipped", zap.String("type", link.Type), zap.String("title", link.Title))
- continue
- }
-
- if link.URL == nil || *link.URL == "" {
- converter.logger.Warn("link URL empty: skipped", zap.String("title", link.Title))
- continue
- }
-
- extLink := grabana.DashboardExternalLink{
- Title: link.Title,
- URL: *link.URL,
- IncludeVariableValues: link.IncludeVars,
- }
-
- if link.Tooltip != nil && *link.Tooltip != "" {
- extLink.Description = *link.Tooltip
- }
- if link.Icon != nil && *link.Icon != "" {
- extLink.Icon = *link.Icon
- }
- if link.TargetBlank != nil {
- extLink.OpenInNewTab = *link.TargetBlank
- }
- if link.KeepTime != nil {
- extLink.IncludeTimeRange = *link.KeepTime
- }
-
- dashboard.ExternalLinks = append(dashboard.ExternalLinks, extLink)
- }
-}
-
-func (converter *JSON) convertAnnotations(annotations []sdk.Annotation, dashboard *grabana.DashboardModel) {
- for _, annotation := range annotations {
- // grafana-sdk doesn't expose the "builtIn" field, so we work around that by skipping
- // the annotation we know to be built-in by its name
- if annotation.Name == "Annotations & Alerts" {
- continue
- }
-
- if annotation.Type != "tags" {
- converter.logger.Warn("unhandled annotation type: skipped", zap.String("type", annotation.Type), zap.String("name", annotation.Name))
- continue
- }
-
- converter.convertTagAnnotation(annotation, dashboard)
- }
-}
-
-func (converter *JSON) convertTagAnnotation(annotation sdk.Annotation, dashboard *grabana.DashboardModel) {
- datasource := ""
- if annotation.Datasource != nil {
- datasource = *annotation.Datasource
- }
-
- dashboard.TagsAnnotation = append(dashboard.TagsAnnotation, grabanaDashboard.TagAnnotation{
- Name: annotation.Name,
- Datasource: datasource,
- IconColor: annotation.IconColor,
- Tags: annotation.Tags,
- })
-}
-
-func (converter *JSON) convertVariables(variables []sdk.TemplateVar, dashboard *grabana.DashboardModel) {
- for _, variable := range variables {
- converter.convertVariable(variable, dashboard)
- }
-}
-
-func (converter *JSON) convertVariable(variable sdk.TemplateVar, dashboard *grabana.DashboardModel) {
- switch variable.Type {
- case "interval":
- converter.convertIntervalVar(variable, dashboard)
- case "custom":
- converter.convertCustomVar(variable, dashboard)
- case "query":
- converter.convertQueryVar(variable, dashboard)
- case "const":
- converter.convertConstVar(variable, dashboard)
- case "datasource":
- converter.convertDatasourceVar(variable, dashboard)
- default:
- converter.logger.Warn("unhandled variable type found: skipped", zap.String("type", variable.Type), zap.String("name", variable.Name))
- }
-}
-
-func (converter *JSON) convertIntervalVar(variable sdk.TemplateVar, dashboard *grabana.DashboardModel) {
- interval := &grabana.VariableInterval{
- Name: variable.Name,
- Label: variable.Label,
- Default: defaultOption(variable.Current),
- Values: make([]string, 0, len(variable.Options)),
- Hide: converter.convertVarHide(variable),
- }
-
- for _, opt := range variable.Options {
- interval.Values = append(interval.Values, opt.Value)
- }
-
- dashboard.Variables = append(dashboard.Variables, grabana.DashboardVariable{Interval: interval})
-}
-
-func (converter *JSON) convertCustomVar(variable sdk.TemplateVar, dashboard *grabana.DashboardModel) {
- custom := &grabana.VariableCustom{
- Name: variable.Name,
- Label: variable.Label,
- Default: defaultOption(variable.Current),
- ValuesMap: make(map[string]string, len(variable.Options)),
- AllValue: variable.AllValue,
- IncludeAll: variable.IncludeAll,
- Hide: converter.convertVarHide(variable),
- }
-
- for _, opt := range variable.Options {
- custom.ValuesMap[opt.Text] = opt.Value
- }
-
- dashboard.Variables = append(dashboard.Variables, grabana.DashboardVariable{Custom: custom})
-}
-
-func (converter *JSON) convertQueryVar(variable sdk.TemplateVar, dashboard *grabana.DashboardModel) {
- datasource := ""
- if variable.Datasource != nil {
- datasource = *variable.Datasource
- }
-
- query := &grabana.VariableQuery{
- Name: variable.Name,
- Label: variable.Label,
- Datasource: datasource,
- Regex: variable.Regex,
- IncludeAll: variable.IncludeAll,
- DefaultAll: variable.Current.Value == "$__all",
- AllValue: variable.AllValue,
- Hide: converter.convertVarHide(variable),
- }
-
- if variable.Query != nil {
- query.Request = variable.Query.(string)
- }
-
- dashboard.Variables = append(dashboard.Variables, grabana.DashboardVariable{Query: query})
-}
-
-func (converter *JSON) convertDatasourceVar(variable sdk.TemplateVar, dashboard *grabana.DashboardModel) {
- datasource := &grabana.VariableDatasource{
- Name: variable.Name,
- Label: variable.Label,
- Regex: variable.Regex,
- IncludeAll: variable.IncludeAll,
- Hide: converter.convertVarHide(variable),
- }
-
- if variable.Query != nil {
- datasource.Type = variable.Query.(string)
- }
-
- dashboard.Variables = append(dashboard.Variables, grabana.DashboardVariable{Datasource: datasource})
-}
-
-func (converter *JSON) convertConstVar(variable sdk.TemplateVar, dashboard *grabana.DashboardModel) {
- constant := &grabana.VariableConst{
- Name: variable.Name,
- Label: variable.Label,
- Default: strings.Join(variable.Current.Text.Value, ","),
- ValuesMap: make(map[string]string, len(variable.Options)),
- Hide: converter.convertVarHide(variable),
- }
-
- for _, opt := range variable.Options {
- constant.ValuesMap[opt.Text] = opt.Value
- }
-
- dashboard.Variables = append(dashboard.Variables, grabana.DashboardVariable{Const: constant})
-}
-
-func (converter *JSON) convertVarHide(variable sdk.TemplateVar) string {
- switch variable.Hide {
- case 0:
- return ""
- case 1:
- return "label"
- case 2:
- return "variable"
- default:
- converter.logger.Warn("unknown hide value for variable %s", zap.String("variable", variable.Name))
- return ""
- }
-}
-
func (converter *JSON) convertPanels(panels []*sdk.Panel, dashboard *grabana.DashboardModel) {
var currentRow *grabana.DashboardRow
@@ -384,6 +185,8 @@ func (converter *JSON) convertDataPanel(panel sdk.Panel) (grabana.DashboardPanel
return converter.convertTable(panel), true
case "text":
return converter.convertText(panel), true
+ case "timeseries":
+ return converter.convertTimeSeries(panel), true
default:
converter.logger.Warn("unhandled panel type: skipped", zap.String("type", panel.Type), zap.String("title", panel.Title))
}
@@ -391,684 +194,6 @@ func (converter *JSON) convertDataPanel(panel sdk.Panel) (grabana.DashboardPanel
return grabana.DashboardPanel{}, false
}
-func (converter *JSON) convertRow(panel sdk.Panel) *grabana.DashboardRow {
- repeat := ""
- if panel.Repeat != nil {
- repeat = *panel.Repeat
- }
- collapse := false
- if panel.RowPanel != nil && panel.RowPanel.Collapsed {
- collapse = true
- }
-
- return &grabana.DashboardRow{
- Name: panel.Title,
- Repeat: repeat,
- Collapse: collapse,
- Panels: nil,
- }
-}
-
-func (converter *JSON) convertGraph(panel sdk.Panel) grabana.DashboardPanel {
- graph := &grabana.DashboardGraph{
- Title: panel.Title,
- Span: panelSpan(panel),
- Transparent: panel.Transparent,
- Axes: &grabana.GraphAxes{
- Bottom: converter.convertAxis(panel.Xaxis),
- },
- Legend: converter.convertLegend(panel.GraphPanel.Legend),
- Visualization: converter.convertVisualization(panel),
- Alert: converter.convertAlert(panel),
- }
-
- if panel.Description != nil {
- graph.Description = *panel.Description
- }
- if panel.Repeat != nil {
- graph.Repeat = *panel.Repeat
- }
- if panel.Height != nil {
- graph.Height = *(panel.Height).(*string)
- }
- if panel.Datasource != nil {
- graph.Datasource = *panel.Datasource
- }
-
- if len(panel.Yaxes) == 2 {
- graph.Axes.Left = converter.convertAxis(panel.Yaxes[0])
- graph.Axes.Right = converter.convertAxis(panel.Yaxes[1])
- }
-
- for _, target := range panel.GraphPanel.Targets {
- graphTarget := converter.convertTarget(target)
- if graphTarget == nil {
- continue
- }
-
- graph.Targets = append(graph.Targets, *graphTarget)
- }
-
- return grabana.DashboardPanel{Graph: graph}
-}
-
-func (converter *JSON) convertAlert(panel sdk.Panel) *grabana.GraphAlert {
- if panel.Alert == nil {
- return nil
- }
-
- sdkAlert := panel.Alert
-
- notifications := make([]string, 0, len(sdkAlert.Notifications))
- for _, notification := range sdkAlert.Notifications {
- notifications = append(notifications, notification.UID)
- }
-
- alert := &grabana.GraphAlert{
- Title: sdkAlert.Name,
- Message: sdkAlert.Message,
- EvaluateEvery: sdkAlert.Frequency,
- For: sdkAlert.For,
- Tags: sdkAlert.AlertRuleTags,
- OnNoData: sdkAlert.NoDataState,
- OnExecutionError: sdkAlert.ExecutionErrorState,
- Notifications: notifications,
- If: converter.convertAlertConditions(sdkAlert),
- }
-
- return alert
-}
-
-func (converter *JSON) convertAlertConditions(sdkAlert *sdk.Alert) []grabana.AlertCondition {
- conditions := make([]grabana.AlertCondition, 0, len(sdkAlert.Conditions))
-
- for _, condition := range sdkAlert.Conditions {
- conditions = append(conditions, grabana.AlertCondition{
- Operand: condition.Operator.Type,
- Value: grabana.AlertValue{
- Func: condition.Reducer.Type,
- QueryRef: condition.Query.Params[0],
- From: condition.Query.Params[1],
- To: condition.Query.Params[2],
- },
- Threshold: converter.convertAlertThreshold(condition),
- })
- }
-
- return conditions
-}
-
-func (converter *JSON) convertAlertThreshold(sdkCondition sdk.AlertCondition) grabana.AlertThreshold {
- threshold := grabana.AlertThreshold{}
-
- switch sdkCondition.Evaluator.Type {
- case "no_value":
- threshold.HasNoValue = true
- case "lt":
- threshold.Below = &sdkCondition.Evaluator.Params[0]
- case "gt":
- threshold.Above = &sdkCondition.Evaluator.Params[0]
- case "outside_range":
- threshold.OutsideRange = [2]float64{sdkCondition.Evaluator.Params[0], sdkCondition.Evaluator.Params[1]}
- case "within_range":
- threshold.WithinRange = [2]float64{sdkCondition.Evaluator.Params[0], sdkCondition.Evaluator.Params[1]}
- }
-
- return threshold
-}
-
-func (converter *JSON) convertVisualization(panel sdk.Panel) *grabana.GraphVisualization {
- graphViz := &grabana.GraphVisualization{
- NullValue: panel.GraphPanel.NullPointMode,
- Staircase: panel.GraphPanel.SteppedLine,
- Overrides: converter.convertGraphOverrides(panel),
- }
-
- return graphViz
-}
-
-func (converter *JSON) convertGraphOverrides(panel sdk.Panel) []grabana.GraphSeriesOverride {
- if len(panel.GraphPanel.SeriesOverrides) == 0 {
- return nil
- }
-
- overrides := make([]grabana.GraphSeriesOverride, 0, len(panel.GraphPanel.SeriesOverrides))
-
- for _, sdkOverride := range panel.GraphPanel.SeriesOverrides {
- color := ""
- if sdkOverride.Color != nil {
- color = *sdkOverride.Color
- }
-
- overrides = append(overrides, grabana.GraphSeriesOverride{
- Alias: sdkOverride.Alias,
- Color: color,
- Dashes: sdkOverride.Dashes,
- Lines: sdkOverride.Lines,
- Fill: sdkOverride.Fill,
- LineWidth: sdkOverride.LineWidth,
- })
- }
-
- return overrides
-}
-
-func (converter *JSON) convertLegend(sdkLegend sdk.Legend) []string {
- var legend []string
-
- if !sdkLegend.Show {
- legend = append(legend, "hide")
- }
- if sdkLegend.AlignAsTable {
- legend = append(legend, "as_table")
- }
- if sdkLegend.RightSide {
- legend = append(legend, "to_the_right")
- }
- if sdkLegend.Min {
- legend = append(legend, "min")
- }
- if sdkLegend.Max {
- legend = append(legend, "max")
- }
- if sdkLegend.Avg {
- legend = append(legend, "avg")
- }
- if sdkLegend.Current {
- legend = append(legend, "current")
- }
- if sdkLegend.Total {
- legend = append(legend, "total")
- }
- if sdkLegend.HideEmpty {
- legend = append(legend, "no_null_series")
- }
- if sdkLegend.HideZero {
- legend = append(legend, "no_zero_series")
- }
-
- return legend
-}
-
-func (converter *JSON) convertAxis(sdkAxis sdk.Axis) *grabana.GraphAxis {
- hidden := !sdkAxis.Show
- var min *float64
- var max *float64
-
- if sdkAxis.Min != nil {
- min = &sdkAxis.Min.Value
- }
- if sdkAxis.Max != nil {
- max = &sdkAxis.Max.Value
- }
-
- return &grabana.GraphAxis{
- Hidden: &hidden,
- Label: sdkAxis.Label,
- Unit: &sdkAxis.Format,
- Min: min,
- Max: max,
- LogBase: sdkAxis.LogBase,
- }
-}
-
-func (converter *JSON) convertHeatmap(panel sdk.Panel) grabana.DashboardPanel {
- heatmap := &grabana.DashboardHeatmap{
- Title: panel.Title,
- Span: panelSpan(panel),
- Transparent: panel.Transparent,
- HideZeroBuckets: panel.HeatmapPanel.HideZeroBuckets,
- HighlightCards: panel.HeatmapPanel.HighlightCards,
- ReverseYBuckets: panel.HeatmapPanel.ReverseYBuckets,
- Tooltip: &grabana.HeatmapTooltip{
- Show: panel.HeatmapPanel.Tooltip.Show,
- ShowHistogram: panel.HeatmapPanel.Tooltip.ShowHistogram,
- Decimals: &panel.HeatmapPanel.TooltipDecimals,
- },
- YAxis: converter.convertHeatmapYAxis(panel),
- }
-
- if panel.Description != nil {
- heatmap.Description = *panel.Description
- }
- if panel.Repeat != nil {
- heatmap.Repeat = *panel.Repeat
- }
- if panel.Height != nil {
- heatmap.Height = *(panel.Height).(*string)
- }
- if panel.Datasource != nil {
- heatmap.Datasource = *panel.Datasource
- }
- if panel.HeatmapPanel.DataFormat != "" {
- switch panel.HeatmapPanel.DataFormat {
- case "tsbuckets":
- heatmap.DataFormat = "time_series_buckets"
- case "time_series":
- heatmap.DataFormat = "time_series"
- default:
- converter.logger.Warn("unknown data format: skipping heatmap", zap.String("data_format", panel.HeatmapPanel.DataFormat), zap.String("heatmap_title", panel.Title))
- }
- }
-
- for _, target := range panel.HeatmapPanel.Targets {
- heatmapTarget := converter.convertTarget(target)
- if heatmapTarget == nil {
- continue
- }
-
- heatmap.Targets = append(heatmap.Targets, *heatmapTarget)
- }
-
- return grabana.DashboardPanel{Heatmap: heatmap}
-}
-
-func (converter *JSON) convertHeatmapYAxis(panel sdk.Panel) *grabana.HeatmapYAxis {
- panelAxis := panel.HeatmapPanel.YAxis
-
- axis := &grabana.HeatmapYAxis{
- Decimals: panelAxis.Decimals,
- Unit: panelAxis.Format,
- }
-
- if panelAxis.Max != nil {
- max, err := strconv.ParseFloat(*panelAxis.Max, 64)
- if err != nil {
- converter.logger.Warn("could not parse max value on heatmap Y axis %s: %s", zap.String("value", *panelAxis.Max), zap.Error(err))
- } else {
- axis.Max = &max
- }
- }
-
- if panelAxis.Min != nil {
- min, err := strconv.ParseFloat(*panelAxis.Min, 64)
- if err != nil {
- converter.logger.Warn("could not parse min value on heatmap Y axis %s: %s", zap.String("value", *panelAxis.Min), zap.Error(err))
- } else {
- axis.Min = &min
- }
- }
-
- return axis
-}
-
-func (converter *JSON) convertSingleStat(panel sdk.Panel) grabana.DashboardPanel {
- singleStat := &grabana.DashboardSingleStat{
- Title: panel.Title,
- Span: panelSpan(panel),
- Unit: panel.SinglestatPanel.Format,
- Decimals: &panel.SinglestatPanel.Decimals,
- ValueType: panel.SinglestatPanel.ValueName,
- Transparent: panel.Transparent,
- ValueFontSize: panel.SinglestatPanel.ValueFontSize,
- }
-
- if panel.Description != nil {
- singleStat.Description = *panel.Description
- }
- if panel.Repeat != nil {
- singleStat.Repeat = *panel.Repeat
- }
- if panel.Height != nil {
- singleStat.Height = *(panel.Height).(*string)
- }
- if panel.Datasource != nil {
- singleStat.Datasource = *panel.Datasource
- }
-
- thresholds := strings.Split(panel.SinglestatPanel.Thresholds, ",")
- if len(thresholds) == 2 {
- singleStat.Thresholds = [2]string{thresholds[0], thresholds[1]}
- }
-
- if len(panel.SinglestatPanel.Colors) == 3 {
- singleStat.Colors = [3]string{
- panel.SinglestatPanel.Colors[0],
- panel.SinglestatPanel.Colors[1],
- panel.SinglestatPanel.Colors[2],
- }
- }
-
- var colorOpts []string
- if panel.SinglestatPanel.ColorBackground {
- colorOpts = append(colorOpts, "background")
- }
- if panel.SinglestatPanel.ColorValue {
- colorOpts = append(colorOpts, "value")
- }
- if len(colorOpts) != 0 {
- singleStat.Color = colorOpts
- }
-
- if panel.SinglestatPanel.SparkLine.Show && panel.SinglestatPanel.SparkLine.Full {
- singleStat.SparkLine = "full"
- }
- if panel.SinglestatPanel.SparkLine.Show && !panel.SinglestatPanel.SparkLine.Full {
- singleStat.SparkLine = "bottom"
- }
-
- // Font sizes
- if panel.SinglestatPanel.PrefixFontSize != nil && *panel.SinglestatPanel.PrefixFontSize != "" {
- singleStat.PrefixFontSize = *panel.SinglestatPanel.PrefixFontSize
- }
- if panel.SinglestatPanel.PostfixFontSize != nil && *panel.SinglestatPanel.PostfixFontSize != "" {
- singleStat.PostfixFontSize = *panel.SinglestatPanel.PostfixFontSize
- }
-
- // ranges to text mapping
- singleStat.RangesToText = converter.convertSingleStatRangesToText(panel)
-
- for _, target := range panel.SinglestatPanel.Targets {
- graphTarget := converter.convertTarget(target)
- if graphTarget == nil {
- continue
- }
-
- singleStat.Targets = append(singleStat.Targets, *graphTarget)
- }
-
- return grabana.DashboardPanel{SingleStat: singleStat}
-}
-
-func (converter *JSON) convertSingleStatRangesToText(panel sdk.Panel) []singlestat.RangeMap {
- if panel.SinglestatPanel.MappingType == nil || *panel.SinglestatPanel.MappingType != 2 {
- return nil
- }
-
- mappings := make([]singlestat.RangeMap, 0, len(panel.SinglestatPanel.RangeMaps))
- for _, mapping := range panel.SinglestatPanel.RangeMaps {
- converted := singlestat.RangeMap{
- From: "",
- To: "",
- Text: "",
- }
-
- if mapping.From != nil {
- converted.From = *mapping.From
- }
- if mapping.To != nil {
- converted.To = *mapping.To
- }
- if mapping.Text != nil {
- converted.Text = *mapping.Text
- }
-
- mappings = append(mappings, converted)
- }
-
- return mappings
-}
-
-func (converter *JSON) convertTable(panel sdk.Panel) grabana.DashboardPanel {
- table := &grabana.DashboardTable{
- Title: panel.Title,
- Span: panelSpan(panel),
- Transparent: panel.Transparent,
- }
-
- if panel.Description != nil {
- table.Description = *panel.Description
- }
- if panel.Height != nil {
- table.Height = *(panel.Height).(*string)
- }
- if panel.Datasource != nil {
- table.Datasource = *panel.Datasource
- }
-
- for _, target := range panel.TablePanel.Targets {
- graphTarget := converter.convertTarget(target)
- if graphTarget == nil {
- continue
- }
-
- table.Targets = append(table.Targets, *graphTarget)
- }
-
- // hidden columns
- for _, columnStyle := range panel.TablePanel.Styles {
- if columnStyle.Type != "hidden" {
- continue
- }
-
- table.HiddenColumns = append(table.HiddenColumns, columnStyle.Pattern)
- }
-
- // time series aggregations
- if panel.TablePanel.Transform == "timeseries_aggregations" {
- for _, column := range panel.TablePanel.Columns {
- table.TimeSeriesAggregations = append(table.TimeSeriesAggregations, grabanaTable.Aggregation{
- Label: column.TextType,
- Type: grabanaTable.AggregationType(column.Value),
- })
- }
- } else {
- converter.logger.Warn("unhandled transform type: skipped", zap.String("transform", panel.TablePanel.Transform), zap.String("panel", panel.Title))
- }
-
- return grabana.DashboardPanel{Table: table}
-}
-
-func (converter *JSON) convertText(panel sdk.Panel) grabana.DashboardPanel {
- text := &grabana.DashboardText{
- Title: panel.Title,
- Span: panelSpan(panel),
- Transparent: panel.Transparent,
- }
-
- if panel.Description != nil {
- text.Description = *panel.Description
- }
- if panel.Height != nil {
- text.Height = *(panel.Height).(*string)
- }
-
- if panel.TextPanel.Options.Mode == "markdown" {
- text.Markdown = panel.TextPanel.Options.Content
- } else {
- fmt.Printf("%#v\n", panel.TextPanel)
- text.HTML = panel.TextPanel.Options.Content
- }
-
- return grabana.DashboardPanel{Text: text}
-}
-
-func (converter *JSON) convertTarget(target sdk.Target) *grabana.Target {
- // looks like a prometheus target
- if target.Expr != "" {
- return converter.convertPrometheusTarget(target)
- }
-
- // looks like graphite
- if target.Target != "" {
- return converter.convertGraphiteTarget(target)
- }
-
- // looks like influxdb
- if target.Measurement != "" {
- return converter.convertInfluxDBTarget(target)
- }
-
- // looks like stackdriver
- if target.MetricType != "" {
- return converter.convertStackdriverTarget(target)
- }
-
- converter.logger.Warn("unhandled target type: skipped", zap.Any("target", target))
-
- return nil
-}
-
-func (converter *JSON) convertPrometheusTarget(target sdk.Target) *grabana.Target {
- return &grabana.Target{
- Prometheus: &grabana.PrometheusTarget{
- Query: target.Expr,
- Legend: target.LegendFormat,
- Ref: target.RefID,
- Hidden: target.Hide,
- Format: target.Format,
- Instant: target.Instant,
- IntervalFactor: &target.IntervalFactor,
- },
- }
-}
-
-func (converter *JSON) convertGraphiteTarget(target sdk.Target) *grabana.Target {
- return &grabana.Target{
- Graphite: &grabana.GraphiteTarget{
- Query: target.Target,
- Ref: target.RefID,
- Hidden: target.Hide,
- },
- }
-}
-
-func (converter *JSON) convertInfluxDBTarget(target sdk.Target) *grabana.Target {
- return &grabana.Target{
- InfluxDB: &grabana.InfluxDBTarget{
- Query: target.Measurement,
- Ref: target.RefID,
- Hidden: target.Hide,
- },
- }
-}
-
-func (converter *JSON) convertStackdriverTarget(target sdk.Target) *grabana.Target {
- switch strings.ToLower(target.MetricKind) {
- case "cumulative":
- case "gauge":
- case "delta":
- default:
- converter.logger.Warn("unhandled stackdriver metric kind: target skipped", zap.Any("metricKind", target.MetricKind))
- return nil
- }
-
- var aggregation string
- if target.CrossSeriesReducer != "" {
- aggregationMap := map[string]string{
- string(stackdriver.ReduceNone): "none",
- string(stackdriver.ReduceMean): "mean",
- string(stackdriver.ReduceMin): "min",
- string(stackdriver.ReduceMax): "max",
- string(stackdriver.ReduceSum): "sum",
- string(stackdriver.ReduceStdDev): "stddev",
- string(stackdriver.ReduceCount): "count",
- string(stackdriver.ReduceCountTrue): "count_true",
- string(stackdriver.ReduceCountFalse): "count_false",
- string(stackdriver.ReduceCountFractionTrue): "fraction_true",
- string(stackdriver.ReducePercentile99): "percentile_99",
- string(stackdriver.ReducePercentile95): "percentile_95",
- string(stackdriver.ReducePercentile50): "percentile_50",
- string(stackdriver.ReducePercentile05): "percentile_05",
- }
-
- if agg, ok := aggregationMap[target.CrossSeriesReducer]; ok {
- aggregation = agg
- } else {
- converter.logger.Warn("unhandled stackdriver crossSeriesReducer: target skipped", zap.Any("crossSeriesReducer", target.CrossSeriesReducer))
- }
- }
-
- var alignment *grabana.StackdriverAlignment
- if target.PerSeriesAligner != "" {
- alignmentMethodMap := map[string]string{
- string(stackdriver.AlignNone): "none",
- string(stackdriver.AlignDelta): "delta",
- string(stackdriver.AlignRate): "rate",
- string(stackdriver.AlignInterpolate): "interpolate",
- string(stackdriver.AlignNextOlder): "next_older",
- string(stackdriver.AlignMin): "min",
- string(stackdriver.AlignMax): "max",
- string(stackdriver.AlignMean): "mean",
- string(stackdriver.AlignCount): "count",
- string(stackdriver.AlignSum): "sum",
- string(stackdriver.AlignStdDev): "stddev",
- string(stackdriver.AlignCountTrue): "count_true",
- string(stackdriver.AlignCountFalse): "count_false",
- string(stackdriver.AlignFractionTrue): "fraction_true",
- string(stackdriver.AlignPercentile99): "percentile_99",
- string(stackdriver.AlignPercentile95): "percentile_95",
- string(stackdriver.AlignPercentile50): "percentile_50",
- string(stackdriver.AlignPercentile05): "percentile_05",
- string(stackdriver.AlignPercentChange): "percent_change",
- }
-
- if method, ok := alignmentMethodMap[target.PerSeriesAligner]; ok {
- alignment = &grabana.StackdriverAlignment{
- Period: target.AlignmentPeriod,
- Method: method,
- }
- } else {
- converter.logger.Warn("unhandled stackdriver perSeriesAligner: target skipped", zap.Any("perSeriesAligner", target.PerSeriesAligner))
- }
- }
-
- return &grabana.Target{
- Stackdriver: &grabana.StackdriverTarget{
- Project: target.ProjectName,
- Type: strings.ToLower(target.MetricKind),
- Metric: target.MetricType,
- Filters: converter.convertStackdriverFilters(target),
- Aggregation: aggregation,
- Alignment: alignment,
- GroupBy: target.GroupBys,
- Legend: target.AliasBy,
- Ref: target.RefID,
- Hidden: target.Hide,
- },
- }
-}
-
-func (converter *JSON) convertStackdriverFilters(target sdk.Target) grabana.StackdriverFilters {
- filters := grabana.StackdriverFilters{
- Eq: map[string]string{},
- Neq: map[string]string{},
- Matches: map[string]string{},
- NotMatches: map[string]string{},
- }
-
- var leftOperand, rightOperand, operator *string
- for i := range target.Filters {
- if target.Filters[i] == "AND" {
- continue
- }
-
- if leftOperand == nil {
- leftOperand = &target.Filters[i]
- continue
- }
- if operator == nil {
- operator = &target.Filters[i]
- continue
- }
- if rightOperand == nil {
- rightOperand = &target.Filters[i]
- }
-
- if leftOperand != nil && operator != nil && rightOperand != nil {
- switch *operator {
- case "=":
- filters.Eq[*leftOperand] = *rightOperand
- case "!=":
- filters.Neq[*leftOperand] = *rightOperand
- case "=~":
- filters.Matches[*leftOperand] = *rightOperand
- case "!=~":
- filters.NotMatches[*leftOperand] = *rightOperand
- default:
- converter.logger.Warn("unhandled stackdriver filter operator: filter skipped", zap.Any("operator", *operator))
- }
-
- leftOperand = nil
- rightOperand = nil
- operator = nil
- }
- }
-
- return filters
-
-}
-
func panelSpan(panel sdk.Panel) float32 {
span := panel.Span
if span == 0 && panel.GridPos.H != nil {
diff --git a/internal/pkg/converter/json_test.go b/internal/pkg/converter/json_test.go
index be1fb71c..c7e4f73f 100644
--- a/internal/pkg/converter/json_test.go
+++ b/internal/pkg/converter/json_test.go
@@ -2,7 +2,6 @@ package converter
import (
"bytes"
- "reflect"
"testing"
grabana "github.com/K-Phoen/grabana/decoder"
@@ -11,14 +10,6 @@ import (
"go.uber.org/zap"
)
-func defaultVar(varType string) sdk.TemplateVar {
- return sdk.TemplateVar{
- Type: varType,
- Name: "var",
- Label: "Label",
- }
-}
-
func TestConvertInvalidJSONToYAML(t *testing.T) {
req := require.New(t)
@@ -98,806 +89,6 @@ func TestConvertGeneralSettings(t *testing.T) {
req.True(dashboard.SharedCrosshair)
}
-func TestConvertUnknownVar(t *testing.T) {
- req := require.New(t)
-
- variable := defaultVar("unknown")
-
- converter := NewJSON(zap.NewNop())
-
- dashboard := &grabana.DashboardModel{}
- converter.convertVariables([]sdk.TemplateVar{variable}, dashboard)
-
- req.Len(dashboard.Variables, 0)
-}
-
-func TestConvertIntervalVar(t *testing.T) {
- req := require.New(t)
-
- variable := defaultVar("interval")
- variable.Name = "var_interval"
- variable.Label = "Label interval"
- variable.Hide = 2
- variable.Current = sdk.Current{Text: &sdk.StringSliceString{Value: []string{"30sec"}, Valid: true}, Value: "30s"}
- variable.Options = []sdk.Option{
- {Text: "10sec", Value: "10s"},
- {Text: "30sec", Value: "30s"},
- {Text: "1min", Value: "1m"},
- }
-
- converter := NewJSON(zap.NewNop())
-
- dashboard := &grabana.DashboardModel{}
- converter.convertVariables([]sdk.TemplateVar{variable}, dashboard)
-
- req.Len(dashboard.Variables, 1)
- req.NotNil(dashboard.Variables[0].Interval)
-
- interval := dashboard.Variables[0].Interval
-
- req.Equal("var_interval", interval.Name)
- req.Equal("Label interval", interval.Label)
- req.Equal("30s", interval.Default)
- req.Equal("variable", interval.Hide)
- req.ElementsMatch([]string{"10s", "30s", "1m"}, interval.Values)
-}
-
-func TestConvertCustomVar(t *testing.T) {
- req := require.New(t)
-
- variable := defaultVar("custom")
- variable.Name = "var_custom"
- variable.Label = "Label custom"
- variable.Hide = 3 // unknown Hide value
- variable.Current = sdk.Current{Text: &sdk.StringSliceString{Value: []string{"85th"}, Valid: true}, Value: "85"}
- variable.Options = []sdk.Option{
- {Text: "50th", Value: "50"},
- {Text: "85th", Value: "85"},
- {Text: "99th", Value: "99"},
- }
-
- converter := NewJSON(zap.NewNop())
-
- dashboard := &grabana.DashboardModel{}
- converter.convertVariables([]sdk.TemplateVar{variable}, dashboard)
-
- req.Len(dashboard.Variables, 1)
- req.NotNil(dashboard.Variables[0].Custom)
-
- custom := dashboard.Variables[0].Custom
-
- req.Equal("var_custom", custom.Name)
- req.Equal("Label custom", custom.Label)
- req.Equal("85", custom.Default)
- req.Empty(custom.Hide)
- req.True(reflect.DeepEqual(custom.ValuesMap, map[string]string{
- "50th": "50",
- "85th": "85",
- "99th": "99",
- }))
-}
-
-func TestConvertDatasourceVar(t *testing.T) {
- req := require.New(t)
-
- variable := defaultVar("datasource")
- variable.Name = "var_datasource"
- variable.Label = "Label datasource"
-
- converter := NewJSON(zap.NewNop())
-
- dashboard := &grabana.DashboardModel{}
- converter.convertVariables([]sdk.TemplateVar{variable}, dashboard)
-
- req.Len(dashboard.Variables, 1)
- req.NotNil(dashboard.Variables[0].Datasource)
-
- dsVar := dashboard.Variables[0].Datasource
-
- req.Equal("var_datasource", dsVar.Name)
- req.Equal("Label datasource", dsVar.Label)
- req.Empty(dsVar.Hide)
- req.False(dsVar.IncludeAll)
-}
-
-func TestConvertConstVar(t *testing.T) {
- req := require.New(t)
-
- variable := defaultVar("const")
- variable.Name = "var_const"
- variable.Label = "Label const"
- variable.Hide = 0
- variable.Current = sdk.Current{Text: &sdk.StringSliceString{Value: []string{"85th"}, Valid: true}, Value: "85"}
- variable.Options = []sdk.Option{
- {Text: "85th", Value: "85"},
- {Text: "99th", Value: "99"},
- }
-
- converter := NewJSON(zap.NewNop())
-
- dashboard := &grabana.DashboardModel{}
- converter.convertVariables([]sdk.TemplateVar{variable}, dashboard)
-
- req.Len(dashboard.Variables, 1)
- req.NotNil(dashboard.Variables[0].Const)
-
- constant := dashboard.Variables[0].Const
-
- req.Equal("var_const", constant.Name)
- req.Equal("Label const", constant.Label)
- req.Equal("85th", constant.Default)
- req.Empty(constant.Hide)
- req.True(reflect.DeepEqual(constant.ValuesMap, map[string]string{
- "85th": "85",
- "99th": "99",
- }))
-}
-
-func TestConvertQueryVar(t *testing.T) {
- req := require.New(t)
- datasource := "prometheus-default"
-
- variable := defaultVar("query")
- variable.Name = "var_query"
- variable.Label = "Query"
- variable.IncludeAll = true
- variable.Hide = 1
- variable.Current = sdk.Current{Value: "$__all"}
- variable.Datasource = &datasource
- variable.Query = "prom_query"
-
- converter := NewJSON(zap.NewNop())
-
- dashboard := &grabana.DashboardModel{}
- converter.convertVariables([]sdk.TemplateVar{variable}, dashboard)
-
- req.Len(dashboard.Variables, 1)
- req.NotNil(dashboard.Variables[0].Query)
-
- query := dashboard.Variables[0].Query
-
- req.Equal("var_query", query.Name)
- req.Equal("Query", query.Label)
- req.Equal(datasource, query.Datasource)
- req.Equal("prom_query", query.Request)
- req.Equal("label", query.Hide)
- req.True(query.IncludeAll)
- req.True(query.DefaultAll)
-}
-
-func TestConvertExternalLink(t *testing.T) {
- req := require.New(t)
-
- externalLink := sdk.Link{
- Title: "joe",
- Type: "link",
- Icon: strPtr("cloud"),
- IncludeVars: true,
- KeepTime: boolPtr(true),
- TargetBlank: boolPtr(true),
- Tooltip: strPtr("description"),
- URL: strPtr("http://lala"),
- }
- dashLink := sdk.Link{
- Title: "not link",
- }
-
- converter := NewJSON(zap.NewNop())
-
- dashboard := &grabana.DashboardModel{}
- converter.convertExternalLinks([]sdk.Link{externalLink, dashLink}, dashboard)
-
- req.Len(dashboard.ExternalLinks, 1)
-
- link := dashboard.ExternalLinks[0]
-
- req.Equal("joe", link.Title)
- req.Equal("description", link.Description)
- req.Equal("cloud", link.Icon)
- req.Equal("http://lala", link.URL)
- req.True(link.OpenInNewTab)
- req.True(link.IncludeTimeRange)
- req.True(link.IncludeVariableValues)
-}
-
-func TestConvertRow(t *testing.T) {
- req := require.New(t)
-
- converter := NewJSON(zap.NewNop())
-
- row := converter.convertRow(sdk.Panel{CommonPanel: sdk.CommonPanel{Title: "Row title"}})
-
- req.Equal("Row title", row.Name)
-}
-
-func TestConvertCollapsedRow(t *testing.T) {
- req := require.New(t)
-
- converter := NewJSON(zap.NewNop())
-
- row := converter.convertRow(sdk.Panel{
- CommonPanel: sdk.CommonPanel{
- Title: "Row title",
- },
- RowPanel: &sdk.RowPanel{
- Collapsed: true,
- },
- })
-
- req.Equal("Row title", row.Name)
- req.True(row.Collapse)
-}
-
-func TestConvertTargetFailsIfNoValidTargetIsGiven(t *testing.T) {
- req := require.New(t)
- converter := NewJSON(zap.NewNop())
-
- convertedTarget := converter.convertTarget(sdk.Target{})
- req.Nil(convertedTarget)
-}
-
-func TestConvertTargetWithPrometheusTarget(t *testing.T) {
- req := require.New(t)
-
- converter := NewJSON(zap.NewNop())
-
- target := sdk.Target{
- Expr: "prometheus_query",
- LegendFormat: "{{ field }}",
- RefID: "A",
- }
-
- convertedTarget := converter.convertTarget(target)
-
- req.NotNil(convertedTarget)
- req.Nil(convertedTarget.Stackdriver)
- req.Equal("prometheus_query", convertedTarget.Prometheus.Query)
- req.Equal("{{ field }}", convertedTarget.Prometheus.Legend)
- req.Equal("A", convertedTarget.Prometheus.Ref)
-}
-
-func TestConvertTargetWithGraphiteTarget(t *testing.T) {
- req := require.New(t)
-
- converter := NewJSON(zap.NewNop())
-
- target := sdk.Target{
- Target: "graphite_query",
- RefID: "A",
- Hide: true,
- }
-
- convertedTarget := converter.convertTarget(target)
-
- req.NotNil(convertedTarget)
- req.NotNil(convertedTarget.Graphite)
- req.Equal("graphite_query", convertedTarget.Graphite.Query)
- req.Equal("A", convertedTarget.Graphite.Ref)
- req.True(convertedTarget.Graphite.Hidden)
-}
-
-func TestConvertTargetWithInfluxDBTarget(t *testing.T) {
- req := require.New(t)
-
- converter := NewJSON(zap.NewNop())
-
- target := sdk.Target{
- Measurement: "influxdb_query",
- RefID: "A",
- Hide: true,
- }
-
- convertedTarget := converter.convertTarget(target)
-
- req.NotNil(convertedTarget)
- req.NotNil(convertedTarget.InfluxDB)
- req.Equal("influxdb_query", convertedTarget.InfluxDB.Query)
- req.Equal("A", convertedTarget.InfluxDB.Ref)
- req.True(convertedTarget.InfluxDB.Hidden)
-}
-
-func TestConvertTargetWithStackdriverTargetFailsIfNoMetricKind(t *testing.T) {
- req := require.New(t)
- converter := NewJSON(zap.NewNop())
-
- target := sdk.Target{
- MetricType: "pubsub.googleapis.com/subscription/ack_message_count",
- }
-
- convertedTarget := converter.convertTarget(target)
-
- req.Nil(convertedTarget)
-}
-
-func TestConvertTargetWithStackdriverTargetIgnoresUnknownCrossSeriesReducer(t *testing.T) {
- req := require.New(t)
- converter := NewJSON(zap.NewNop())
-
- target := sdk.Target{
- MetricKind: "DELTA",
- MetricType: "pubsub.googleapis.com/subscription/ack_message_count",
- CrossSeriesReducer: "unknown",
- }
-
- convertedTarget := converter.convertTarget(target)
-
- req.NotNil(convertedTarget)
- req.NotNil(convertedTarget.Stackdriver)
- req.Empty(convertedTarget.Stackdriver.Aggregation)
-}
-
-func TestConvertTargetWithStackdriverTargetIgnoresUnknownAligner(t *testing.T) {
- req := require.New(t)
- converter := NewJSON(zap.NewNop())
-
- target := sdk.Target{
- MetricKind: "DELTA",
- MetricType: "pubsub.googleapis.com/subscription/ack_message_count",
- PerSeriesAligner: "unknown",
- }
-
- convertedTarget := converter.convertTarget(target)
-
- req.NotNil(convertedTarget)
- req.NotNil(convertedTarget.Stackdriver)
- req.Empty(convertedTarget.Stackdriver.Alignment)
-}
-
-func TestConvertTargetWithStackdriverTarget(t *testing.T) {
- req := require.New(t)
-
- converter := NewJSON(zap.NewNop())
-
- target := sdk.Target{
- MetricKind: "DELTA",
- MetricType: "pubsub.googleapis.com/subscription/ack_message_count",
- CrossSeriesReducer: "REDUCE_MEAN",
- PerSeriesAligner: "ALIGN_DELTA",
- AlignmentPeriod: "stackdriver-auto",
- GroupBys: []string{"field"},
- AliasBy: "legend",
- RefID: "A",
- Filters: []string{
- "resource.label.subscription_id",
- "=",
- "subscription_name",
- "AND",
- "other-property",
- "!=",
- "other-value",
- "AND",
- "regex-property",
- "=~",
- "regex-value",
- "AND",
- "regex-not-property",
- "!=~",
- "regex-not-value",
- },
- }
-
- convertedTarget := converter.convertTarget(target)
-
- req.NotNil(convertedTarget)
- req.Nil(convertedTarget.Prometheus)
- req.NotNil(convertedTarget.Stackdriver)
- req.Equal("delta", convertedTarget.Stackdriver.Type)
- req.Equal("pubsub.googleapis.com/subscription/ack_message_count", convertedTarget.Stackdriver.Metric)
- req.Equal("mean", convertedTarget.Stackdriver.Aggregation)
- req.Equal("stackdriver-auto", convertedTarget.Stackdriver.Alignment.Period)
- req.Equal("delta", convertedTarget.Stackdriver.Alignment.Method)
- req.Equal("legend", convertedTarget.Stackdriver.Legend)
- req.Equal("A", convertedTarget.Stackdriver.Ref)
- req.ElementsMatch([]string{"field"}, convertedTarget.Stackdriver.GroupBy)
- req.EqualValues(map[string]string{"resource.label.subscription_id": "subscription_name"}, convertedTarget.Stackdriver.Filters.Eq)
- req.EqualValues(map[string]string{"other-property": "other-value"}, convertedTarget.Stackdriver.Filters.Neq)
- req.EqualValues(map[string]string{"regex-property": "regex-value"}, convertedTarget.Stackdriver.Filters.Matches)
- req.EqualValues(map[string]string{"regex-not-property": "regex-not-value"}, convertedTarget.Stackdriver.Filters.NotMatches)
-}
-
-func TestConvertTargetWithStackdriverGauge(t *testing.T) {
- req := require.New(t)
-
- converter := NewJSON(zap.NewNop())
-
- target := sdk.Target{
- MetricKind: "GAUGE",
- MetricType: "pubsub.googleapis.com/subscription/ack_message_count",
- }
-
- convertedTarget := converter.convertTarget(target)
-
- req.NotNil(convertedTarget)
- req.Nil(convertedTarget.Prometheus)
- req.NotNil(convertedTarget.Stackdriver)
- req.Equal("gauge", convertedTarget.Stackdriver.Type)
- req.Equal("pubsub.googleapis.com/subscription/ack_message_count", convertedTarget.Stackdriver.Metric)
-}
-
-func TestConvertTargetWithStackdriverCumulative(t *testing.T) {
- req := require.New(t)
-
- converter := NewJSON(zap.NewNop())
-
- target := sdk.Target{
- MetricKind: "CUMULATIVE",
- MetricType: "pubsub.googleapis.com/subscription/ack_message_count",
- }
-
- convertedTarget := converter.convertTarget(target)
-
- req.NotNil(convertedTarget)
- req.Nil(convertedTarget.Prometheus)
- req.NotNil(convertedTarget.Stackdriver)
- req.Equal("cumulative", convertedTarget.Stackdriver.Type)
- req.Equal("pubsub.googleapis.com/subscription/ack_message_count", convertedTarget.Stackdriver.Metric)
-}
-
-func TestConvertTagAnnotationIgnoresBuiltIn(t *testing.T) {
- req := require.New(t)
-
- annotation := sdk.Annotation{Name: "Annotations & Alerts"}
- dashboard := &grabana.DashboardModel{}
-
- NewJSON(zap.NewNop()).convertAnnotations([]sdk.Annotation{annotation}, dashboard)
-
- req.Len(dashboard.TagsAnnotation, 0)
-}
-
-func TestConvertTagAnnotationIgnoresUnknownTypes(t *testing.T) {
- req := require.New(t)
-
- annotation := sdk.Annotation{Name: "Will be ignored", Type: "dashboard"}
- dashboard := &grabana.DashboardModel{}
-
- NewJSON(zap.NewNop()).convertAnnotations([]sdk.Annotation{annotation}, dashboard)
-
- req.Len(dashboard.TagsAnnotation, 0)
-}
-
-func TestConvertTagAnnotation(t *testing.T) {
- req := require.New(t)
-
- converter := NewJSON(zap.NewNop())
-
- datasource := "-- Grafana --"
- annotation := sdk.Annotation{
- Type: "tags",
- Datasource: &datasource,
- IconColor: "#5794F2",
- Name: "Deployments",
- Tags: []string{"deploy"},
- }
- dashboard := &grabana.DashboardModel{}
-
- converter.convertAnnotations([]sdk.Annotation{annotation}, dashboard)
-
- req.Len(dashboard.TagsAnnotation, 1)
- req.Equal("Deployments", dashboard.TagsAnnotation[0].Name)
- req.ElementsMatch([]string{"deploy"}, dashboard.TagsAnnotation[0].Tags)
- req.Equal("#5794F2", dashboard.TagsAnnotation[0].IconColor)
- req.Equal(datasource, dashboard.TagsAnnotation[0].Datasource)
-}
-
-func TestConvertLegend(t *testing.T) {
- req := require.New(t)
-
- converter := NewJSON(zap.NewNop())
-
- rawLegend := sdk.Legend{
- AlignAsTable: true,
- Avg: true,
- Current: true,
- HideEmpty: true,
- HideZero: true,
- Max: true,
- Min: true,
- RightSide: true,
- Show: true,
- Total: true,
- }
-
- legend := converter.convertLegend(rawLegend)
-
- req.ElementsMatch(
- []string{"as_table", "to_the_right", "min", "max", "avg", "current", "total", "no_null_series", "no_zero_series"},
- legend,
- )
-}
-
-func TestConvertCanHideLegend(t *testing.T) {
- req := require.New(t)
- converter := NewJSON(zap.NewNop())
-
- legend := converter.convertLegend(sdk.Legend{Show: false})
- req.ElementsMatch([]string{"hide"}, legend)
-}
-
-func TestConvertAxis(t *testing.T) {
- req := require.New(t)
-
- converter := NewJSON(zap.NewNop())
-
- rawAxis := sdk.Axis{
- Format: "bytes",
- LogBase: 2,
- Min: &sdk.FloatString{Value: 0},
- Max: &sdk.FloatString{Value: 42},
- Show: true,
- Label: "Axis",
- }
-
- axis := converter.convertAxis(rawAxis)
-
- req.Equal("bytes", *axis.Unit)
- req.Equal("Axis", axis.Label)
- req.EqualValues(0, *axis.Min)
- req.EqualValues(42, *axis.Max)
- req.False(*axis.Hidden)
-}
-
-func TestConvertTextPanelWithMarkdown(t *testing.T) {
- req := require.New(t)
-
- converter := NewJSON(zap.NewNop())
- height := "200px"
-
- textPanel := sdk.Panel{
- CommonPanel: sdk.CommonPanel{
- Title: "Text panel",
- Transparent: true,
- Height: &height,
- Type: "text",
- },
- TextPanel: &sdk.TextPanel{
- Options: struct {
- Content string `json:"content"`
- Mode string `json:"mode"`
- }{Content: "# hello world", Mode: "markdown"},
- },
- }
-
- converted, ok := converter.convertDataPanel(textPanel)
-
- req.True(ok)
- req.True(converted.Text.Transparent)
- req.Equal("Text panel", converted.Text.Title)
- req.Equal("# hello world", converted.Text.Markdown)
- req.Equal(height, converted.Text.Height)
-}
-
-func TestConvertTextPanelWithHTML(t *testing.T) {
- req := require.New(t)
-
- converter := NewJSON(zap.NewNop())
-
- textPanel := sdk.Panel{
- CommonPanel: sdk.CommonPanel{
- Title: "Text panel html",
- Type: "text",
- Description: strPtr("panel description"),
- },
- TextPanel: &sdk.TextPanel{
- Mode: "html",
- Options: struct {
- Content string `json:"content"`
- Mode string `json:"mode"`
- }{Content: "
hello world
", Mode: "html"},
- },
- }
-
- converted, ok := converter.convertDataPanel(textPanel)
-
- req.True(ok)
- req.False(converted.Text.Transparent)
- req.Equal("Text panel html", converted.Text.Title)
- req.Equal("panel description", converted.Text.Description)
- req.Equal("hello world
", converted.Text.HTML)
-}
-
-func TestConvertSingleStatPanel(t *testing.T) {
- req := require.New(t)
-
- converter := NewJSON(zap.NewNop())
- height := "200px"
- datasource := "prometheus"
-
- singlestatPanel := sdk.Panel{
- CommonPanel: sdk.CommonPanel{
- Title: "Singlestat panel",
- Description: strPtr("panel desc"),
- Type: "singlestat",
- Transparent: true,
- Height: &height,
- Datasource: &datasource,
- },
- SinglestatPanel: &sdk.SinglestatPanel{
- Format: "none",
- ValueName: "current",
- ValueFontSize: "120%",
- PrefixFontSize: strPtr("80%"),
- PostfixFontSize: strPtr("80%"),
- Colors: []string{"blue", "red", "green"},
- ColorBackground: true,
- ColorValue: true,
- },
- }
-
- converted, ok := converter.convertDataPanel(singlestatPanel)
-
- req.True(ok)
- req.True(converted.SingleStat.Transparent)
- req.Equal("Singlestat panel", converted.SingleStat.Title)
- req.Equal("panel desc", converted.SingleStat.Description)
- req.Equal("none", converted.SingleStat.Unit)
- req.Equal("current", converted.SingleStat.ValueType)
- req.Equal("120%", converted.SingleStat.ValueFontSize)
- req.Equal("80%", converted.SingleStat.PrefixFontSize)
- req.Equal("80%", converted.SingleStat.PostfixFontSize)
- req.Equal(height, converted.SingleStat.Height)
- req.Equal(datasource, converted.SingleStat.Datasource)
- req.True(reflect.DeepEqual(converted.SingleStat.Colors, [3]string{
- "blue", "red", "green",
- }))
- req.True(reflect.DeepEqual(converted.SingleStat.Color, []string{
- "background", "value",
- }))
-}
-
-func TestConvertHeatmapPanel(t *testing.T) {
- req := require.New(t)
-
- converter := NewJSON(zap.NewNop())
- height := "400px"
- datasource := "prometheus"
-
- heatmapPanel := sdk.Panel{
- CommonPanel: sdk.CommonPanel{
- Title: "heatmap panel",
- Type: "heatmap",
- Description: strPtr("heatmap description"),
- Transparent: true,
- Height: &height,
- Datasource: &datasource,
- },
- HeatmapPanel: &sdk.HeatmapPanel{
- HideZeroBuckets: true,
- HighlightCards: true,
- ReverseYBuckets: true,
- DataFormat: "tsbuckets",
- },
- }
-
- converted, ok := converter.convertDataPanel(heatmapPanel)
-
- req.True(ok)
- req.True(converted.Heatmap.Transparent)
- req.Equal("heatmap panel", converted.Heatmap.Title)
- req.Equal("heatmap description", converted.Heatmap.Description)
- req.Equal(height, converted.Heatmap.Height)
- req.Equal(datasource, converted.Heatmap.Datasource)
- req.True(converted.Heatmap.ReverseYBuckets)
- req.True(converted.Heatmap.HideZeroBuckets)
- req.True(converted.Heatmap.HighlightCards)
- req.Equal("time_series_buckets", converted.Heatmap.DataFormat)
-}
-
-func TestConvertGraphPanel(t *testing.T) {
- req := require.New(t)
-
- converter := NewJSON(zap.NewNop())
- height := "400px"
- datasource := "prometheus"
-
- graphPanel := sdk.Panel{
- CommonPanel: sdk.CommonPanel{
- Title: "graph panel",
- Type: "graph",
- Description: strPtr("graph description"),
- Transparent: true,
- Height: &height,
- Datasource: &datasource,
- },
- GraphPanel: &sdk.GraphPanel{},
- }
-
- converted, ok := converter.convertDataPanel(graphPanel)
-
- req.True(ok)
- req.NotNil(converted.Graph)
-
- graph := converted.Graph
- req.True(graph.Transparent)
- req.Equal("graph panel", graph.Title)
- req.Equal("graph description", graph.Description)
- req.Equal(height, graph.Height)
- req.Equal(datasource, graph.Datasource)
-}
-
-func TestConvertVisualization(t *testing.T) {
- req := require.New(t)
- converter := NewJSON(zap.NewNop())
- enabled := true
-
- graphPanel := sdk.Panel{
- CommonPanel: sdk.CommonPanel{
- Title: "graph panel",
- Type: "graph",
- },
- GraphPanel: &sdk.GraphPanel{
- NullPointMode: "connected",
- SteppedLine: true,
- SeriesOverrides: []sdk.SeriesOverride{
- {
- Alias: "alias",
- Dashes: &enabled,
- },
- },
- },
- }
-
- visualization := converter.convertVisualization(graphPanel)
-
- req.True(visualization.Staircase)
- req.Equal("connected", visualization.NullValue)
- req.Len(visualization.Overrides, 1)
-}
-
-func TestConvertGraphOverridesWithNoOverride(t *testing.T) {
- req := require.New(t)
- converter := NewJSON(zap.NewNop())
-
- graphPanel := sdk.Panel{
- CommonPanel: sdk.CommonPanel{
- Title: "graph panel",
- Type: "graph",
- },
- GraphPanel: &sdk.GraphPanel{},
- }
-
- overrides := converter.convertGraphOverrides(graphPanel)
-
- req.Len(overrides, 0)
-}
-
-func TestConvertGraphOverridesWithOneOverride(t *testing.T) {
- req := require.New(t)
- converter := NewJSON(zap.NewNop())
- color := "red"
- enabled := true
- number := 2
-
- graphPanel := sdk.Panel{
- CommonPanel: sdk.CommonPanel{
- Title: "heatmap panel",
- Type: "graph",
- },
- GraphPanel: &sdk.GraphPanel{
- SeriesOverrides: []sdk.SeriesOverride{
- {
- Alias: "alias",
- Color: &color,
- Dashes: &enabled,
- Fill: &number,
- Lines: &enabled,
- },
- },
- },
- }
-
- overrides := converter.convertGraphOverrides(graphPanel)
-
- req.Len(overrides, 1)
-
- override := overrides[0]
-
- req.Equal("alias", override.Alias)
- req.Equal(color, override.Color)
- req.True(*override.Dashes)
- req.True(*override.Lines)
- req.Equal(number, *override.Fill)
-}
-
func strPtr(input string) *string {
return &input
}
diff --git a/internal/pkg/converter/row.go b/internal/pkg/converter/row.go
new file mode 100644
index 00000000..46324354
--- /dev/null
+++ b/internal/pkg/converter/row.go
@@ -0,0 +1,24 @@
+package converter
+
+import (
+ grabana "github.com/K-Phoen/grabana/decoder"
+ "github.com/K-Phoen/sdk"
+)
+
+func (converter *JSON) convertRow(panel sdk.Panel) *grabana.DashboardRow {
+ repeat := ""
+ if panel.Repeat != nil {
+ repeat = *panel.Repeat
+ }
+ collapse := false
+ if panel.RowPanel != nil && panel.RowPanel.Collapsed {
+ collapse = true
+ }
+
+ return &grabana.DashboardRow{
+ Name: panel.Title,
+ Repeat: repeat,
+ Collapse: collapse,
+ Panels: nil,
+ }
+}
diff --git a/internal/pkg/converter/row_test.go b/internal/pkg/converter/row_test.go
new file mode 100644
index 00000000..a74759b4
--- /dev/null
+++ b/internal/pkg/converter/row_test.go
@@ -0,0 +1,37 @@
+package converter
+
+import (
+ "testing"
+
+ "github.com/K-Phoen/sdk"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/zap"
+)
+
+func TestConvertRow(t *testing.T) {
+ req := require.New(t)
+
+ converter := NewJSON(zap.NewNop())
+
+ row := converter.convertRow(sdk.Panel{CommonPanel: sdk.CommonPanel{Title: "Row title"}})
+
+ req.Equal("Row title", row.Name)
+}
+
+func TestConvertCollapsedRow(t *testing.T) {
+ req := require.New(t)
+
+ converter := NewJSON(zap.NewNop())
+
+ row := converter.convertRow(sdk.Panel{
+ CommonPanel: sdk.CommonPanel{
+ Title: "Row title",
+ },
+ RowPanel: &sdk.RowPanel{
+ Collapsed: true,
+ },
+ })
+
+ req.Equal("Row title", row.Name)
+ req.True(row.Collapse)
+}
diff --git a/internal/pkg/converter/single_stat.go b/internal/pkg/converter/single_stat.go
new file mode 100644
index 00000000..4c284001
--- /dev/null
+++ b/internal/pkg/converter/single_stat.go
@@ -0,0 +1,116 @@
+package converter
+
+import (
+ "strings"
+
+ grabana "github.com/K-Phoen/grabana/decoder"
+ "github.com/K-Phoen/grabana/singlestat"
+ "github.com/K-Phoen/sdk"
+)
+
+func (converter *JSON) convertSingleStat(panel sdk.Panel) grabana.DashboardPanel {
+ singleStat := &grabana.DashboardSingleStat{
+ Title: panel.Title,
+ Span: panelSpan(panel),
+ Unit: panel.SinglestatPanel.Format,
+ Decimals: &panel.SinglestatPanel.Decimals,
+ ValueType: panel.SinglestatPanel.ValueName,
+ Transparent: panel.Transparent,
+ ValueFontSize: panel.SinglestatPanel.ValueFontSize,
+ }
+
+ if panel.Description != nil {
+ singleStat.Description = *panel.Description
+ }
+ if panel.Repeat != nil {
+ singleStat.Repeat = *panel.Repeat
+ }
+ if panel.Height != nil {
+ singleStat.Height = *(panel.Height).(*string)
+ }
+ if panel.Datasource != nil {
+ singleStat.Datasource = *panel.Datasource
+ }
+
+ thresholds := strings.Split(panel.SinglestatPanel.Thresholds, ",")
+ if len(thresholds) == 2 {
+ singleStat.Thresholds = [2]string{thresholds[0], thresholds[1]}
+ }
+
+ if len(panel.SinglestatPanel.Colors) == 3 {
+ singleStat.Colors = [3]string{
+ panel.SinglestatPanel.Colors[0],
+ panel.SinglestatPanel.Colors[1],
+ panel.SinglestatPanel.Colors[2],
+ }
+ }
+
+ var colorOpts []string
+ if panel.SinglestatPanel.ColorBackground {
+ colorOpts = append(colorOpts, "background")
+ }
+ if panel.SinglestatPanel.ColorValue {
+ colorOpts = append(colorOpts, "value")
+ }
+ if len(colorOpts) != 0 {
+ singleStat.Color = colorOpts
+ }
+
+ if panel.SinglestatPanel.SparkLine.Show && panel.SinglestatPanel.SparkLine.Full {
+ singleStat.SparkLine = "full"
+ }
+ if panel.SinglestatPanel.SparkLine.Show && !panel.SinglestatPanel.SparkLine.Full {
+ singleStat.SparkLine = "bottom"
+ }
+
+ // Font sizes
+ if panel.SinglestatPanel.PrefixFontSize != nil && *panel.SinglestatPanel.PrefixFontSize != "" {
+ singleStat.PrefixFontSize = *panel.SinglestatPanel.PrefixFontSize
+ }
+ if panel.SinglestatPanel.PostfixFontSize != nil && *panel.SinglestatPanel.PostfixFontSize != "" {
+ singleStat.PostfixFontSize = *panel.SinglestatPanel.PostfixFontSize
+ }
+
+ // ranges to text mapping
+ singleStat.RangesToText = converter.convertSingleStatRangesToText(panel)
+
+ for _, target := range panel.SinglestatPanel.Targets {
+ graphTarget := converter.convertTarget(target)
+ if graphTarget == nil {
+ continue
+ }
+
+ singleStat.Targets = append(singleStat.Targets, *graphTarget)
+ }
+
+ return grabana.DashboardPanel{SingleStat: singleStat}
+}
+
+func (converter *JSON) convertSingleStatRangesToText(panel sdk.Panel) []singlestat.RangeMap {
+ if panel.SinglestatPanel.MappingType == nil || *panel.SinglestatPanel.MappingType != 2 {
+ return nil
+ }
+
+ mappings := make([]singlestat.RangeMap, 0, len(panel.SinglestatPanel.RangeMaps))
+ for _, mapping := range panel.SinglestatPanel.RangeMaps {
+ converted := singlestat.RangeMap{
+ From: "",
+ To: "",
+ Text: "",
+ }
+
+ if mapping.From != nil {
+ converted.From = *mapping.From
+ }
+ if mapping.To != nil {
+ converted.To = *mapping.To
+ }
+ if mapping.Text != nil {
+ converted.Text = *mapping.Text
+ }
+
+ mappings = append(mappings, converted)
+ }
+
+ return mappings
+}
diff --git a/internal/pkg/converter/single_stat_test.go b/internal/pkg/converter/single_stat_test.go
new file mode 100644
index 00000000..d2cc3799
--- /dev/null
+++ b/internal/pkg/converter/single_stat_test.go
@@ -0,0 +1,59 @@
+package converter
+
+import (
+ "reflect"
+ "testing"
+
+ "github.com/K-Phoen/sdk"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/zap"
+)
+
+func TestConvertSingleStatPanel(t *testing.T) {
+ req := require.New(t)
+
+ converter := NewJSON(zap.NewNop())
+ height := "200px"
+ datasource := "prometheus"
+
+ singlestatPanel := sdk.Panel{
+ CommonPanel: sdk.CommonPanel{
+ Title: "Singlestat panel",
+ Description: strPtr("panel desc"),
+ Type: "singlestat",
+ Transparent: true,
+ Height: &height,
+ Datasource: &datasource,
+ },
+ SinglestatPanel: &sdk.SinglestatPanel{
+ Format: "none",
+ ValueName: "current",
+ ValueFontSize: "120%",
+ PrefixFontSize: strPtr("80%"),
+ PostfixFontSize: strPtr("80%"),
+ Colors: []string{"blue", "red", "green"},
+ ColorBackground: true,
+ ColorValue: true,
+ },
+ }
+
+ converted, ok := converter.convertDataPanel(singlestatPanel)
+
+ req.True(ok)
+ req.True(converted.SingleStat.Transparent)
+ req.Equal("Singlestat panel", converted.SingleStat.Title)
+ req.Equal("panel desc", converted.SingleStat.Description)
+ req.Equal("none", converted.SingleStat.Unit)
+ req.Equal("current", converted.SingleStat.ValueType)
+ req.Equal("120%", converted.SingleStat.ValueFontSize)
+ req.Equal("80%", converted.SingleStat.PrefixFontSize)
+ req.Equal("80%", converted.SingleStat.PostfixFontSize)
+ req.Equal(height, converted.SingleStat.Height)
+ req.Equal(datasource, converted.SingleStat.Datasource)
+ req.True(reflect.DeepEqual(converted.SingleStat.Colors, [3]string{
+ "blue", "red", "green",
+ }))
+ req.True(reflect.DeepEqual(converted.SingleStat.Color, []string{
+ "background", "value",
+ }))
+}
diff --git a/internal/pkg/converter/table.go b/internal/pkg/converter/table.go
new file mode 100644
index 00000000..22d5c382
--- /dev/null
+++ b/internal/pkg/converter/table.go
@@ -0,0 +1,58 @@
+package converter
+
+import (
+ grabana "github.com/K-Phoen/grabana/decoder"
+ grabanaTable "github.com/K-Phoen/grabana/table"
+ "github.com/K-Phoen/sdk"
+ "go.uber.org/zap"
+)
+
+func (converter *JSON) convertTable(panel sdk.Panel) grabana.DashboardPanel {
+ table := &grabana.DashboardTable{
+ Title: panel.Title,
+ Span: panelSpan(panel),
+ Transparent: panel.Transparent,
+ }
+
+ if panel.Description != nil {
+ table.Description = *panel.Description
+ }
+ if panel.Height != nil {
+ table.Height = *(panel.Height).(*string)
+ }
+ if panel.Datasource != nil {
+ table.Datasource = *panel.Datasource
+ }
+
+ for _, target := range panel.TablePanel.Targets {
+ graphTarget := converter.convertTarget(target)
+ if graphTarget == nil {
+ continue
+ }
+
+ table.Targets = append(table.Targets, *graphTarget)
+ }
+
+ // hidden columns
+ for _, columnStyle := range panel.TablePanel.Styles {
+ if columnStyle.Type != "hidden" {
+ continue
+ }
+
+ table.HiddenColumns = append(table.HiddenColumns, columnStyle.Pattern)
+ }
+
+ // time series aggregations
+ if panel.TablePanel.Transform == "timeseries_aggregations" {
+ for _, column := range panel.TablePanel.Columns {
+ table.TimeSeriesAggregations = append(table.TimeSeriesAggregations, grabanaTable.Aggregation{
+ Label: column.TextType,
+ Type: grabanaTable.AggregationType(column.Value),
+ })
+ }
+ } else {
+ converter.logger.Warn("unhandled transform type: skipped", zap.String("transform", panel.TablePanel.Transform), zap.String("panel", panel.Title))
+ }
+
+ return grabana.DashboardPanel{Table: table}
+}
diff --git a/internal/pkg/converter/target.go b/internal/pkg/converter/target.go
new file mode 100644
index 00000000..7edb590f
--- /dev/null
+++ b/internal/pkg/converter/target.go
@@ -0,0 +1,205 @@
+package converter
+
+import (
+ "strings"
+
+ grabana "github.com/K-Phoen/grabana/decoder"
+ "github.com/K-Phoen/grabana/target/stackdriver"
+ "github.com/K-Phoen/sdk"
+ "go.uber.org/zap"
+)
+
+func (converter *JSON) convertTarget(target sdk.Target) *grabana.Target {
+ // looks like a prometheus target
+ if target.Expr != "" {
+ return converter.convertPrometheusTarget(target)
+ }
+
+ // looks like graphite
+ if target.Target != "" {
+ return converter.convertGraphiteTarget(target)
+ }
+
+ // looks like influxdb
+ if target.Measurement != "" {
+ return converter.convertInfluxDBTarget(target)
+ }
+
+ // looks like stackdriver
+ if target.MetricType != "" {
+ return converter.convertStackdriverTarget(target)
+ }
+
+ converter.logger.Warn("unhandled target type: skipped", zap.Any("target", target))
+
+ return nil
+}
+
+func (converter *JSON) convertPrometheusTarget(target sdk.Target) *grabana.Target {
+ return &grabana.Target{
+ Prometheus: &grabana.PrometheusTarget{
+ Query: target.Expr,
+ Legend: target.LegendFormat,
+ Ref: target.RefID,
+ Hidden: target.Hide,
+ Format: target.Format,
+ Instant: target.Instant,
+ IntervalFactor: &target.IntervalFactor,
+ },
+ }
+}
+
+func (converter *JSON) convertGraphiteTarget(target sdk.Target) *grabana.Target {
+ return &grabana.Target{
+ Graphite: &grabana.GraphiteTarget{
+ Query: target.Target,
+ Ref: target.RefID,
+ Hidden: target.Hide,
+ },
+ }
+}
+
+func (converter *JSON) convertInfluxDBTarget(target sdk.Target) *grabana.Target {
+ return &grabana.Target{
+ InfluxDB: &grabana.InfluxDBTarget{
+ Query: target.Measurement,
+ Ref: target.RefID,
+ Hidden: target.Hide,
+ },
+ }
+}
+
+func (converter *JSON) convertStackdriverTarget(target sdk.Target) *grabana.Target {
+ switch strings.ToLower(target.MetricKind) {
+ case "cumulative":
+ case "gauge":
+ case "delta":
+ default:
+ converter.logger.Warn("unhandled stackdriver metric kind: target skipped", zap.Any("metricKind", target.MetricKind))
+ return nil
+ }
+
+ var aggregation string
+ if target.CrossSeriesReducer != "" {
+ aggregationMap := map[string]string{
+ string(stackdriver.ReduceNone): "none",
+ string(stackdriver.ReduceMean): "mean",
+ string(stackdriver.ReduceMin): "min",
+ string(stackdriver.ReduceMax): "max",
+ string(stackdriver.ReduceSum): "sum",
+ string(stackdriver.ReduceStdDev): "stddev",
+ string(stackdriver.ReduceCount): "count",
+ string(stackdriver.ReduceCountTrue): "count_true",
+ string(stackdriver.ReduceCountFalse): "count_false",
+ string(stackdriver.ReduceCountFractionTrue): "fraction_true",
+ string(stackdriver.ReducePercentile99): "percentile_99",
+ string(stackdriver.ReducePercentile95): "percentile_95",
+ string(stackdriver.ReducePercentile50): "percentile_50",
+ string(stackdriver.ReducePercentile05): "percentile_05",
+ }
+
+ if agg, ok := aggregationMap[target.CrossSeriesReducer]; ok {
+ aggregation = agg
+ } else {
+ converter.logger.Warn("unhandled stackdriver crossSeriesReducer: target skipped", zap.Any("crossSeriesReducer", target.CrossSeriesReducer))
+ }
+ }
+
+ var alignment *grabana.StackdriverAlignment
+ if target.PerSeriesAligner != "" {
+ alignmentMethodMap := map[string]string{
+ string(stackdriver.AlignNone): "none",
+ string(stackdriver.AlignDelta): "delta",
+ string(stackdriver.AlignRate): "rate",
+ string(stackdriver.AlignInterpolate): "interpolate",
+ string(stackdriver.AlignNextOlder): "next_older",
+ string(stackdriver.AlignMin): "min",
+ string(stackdriver.AlignMax): "max",
+ string(stackdriver.AlignMean): "mean",
+ string(stackdriver.AlignCount): "count",
+ string(stackdriver.AlignSum): "sum",
+ string(stackdriver.AlignStdDev): "stddev",
+ string(stackdriver.AlignCountTrue): "count_true",
+ string(stackdriver.AlignCountFalse): "count_false",
+ string(stackdriver.AlignFractionTrue): "fraction_true",
+ string(stackdriver.AlignPercentile99): "percentile_99",
+ string(stackdriver.AlignPercentile95): "percentile_95",
+ string(stackdriver.AlignPercentile50): "percentile_50",
+ string(stackdriver.AlignPercentile05): "percentile_05",
+ string(stackdriver.AlignPercentChange): "percent_change",
+ }
+
+ if method, ok := alignmentMethodMap[target.PerSeriesAligner]; ok {
+ alignment = &grabana.StackdriverAlignment{
+ Period: target.AlignmentPeriod,
+ Method: method,
+ }
+ } else {
+ converter.logger.Warn("unhandled stackdriver perSeriesAligner: target skipped", zap.Any("perSeriesAligner", target.PerSeriesAligner))
+ }
+ }
+
+ return &grabana.Target{
+ Stackdriver: &grabana.StackdriverTarget{
+ Project: target.ProjectName,
+ Type: strings.ToLower(target.MetricKind),
+ Metric: target.MetricType,
+ Filters: converter.convertStackdriverFilters(target),
+ Aggregation: aggregation,
+ Alignment: alignment,
+ GroupBy: target.GroupBys,
+ Legend: target.AliasBy,
+ Ref: target.RefID,
+ Hidden: target.Hide,
+ },
+ }
+}
+
+func (converter *JSON) convertStackdriverFilters(target sdk.Target) grabana.StackdriverFilters {
+ filters := grabana.StackdriverFilters{
+ Eq: map[string]string{},
+ Neq: map[string]string{},
+ Matches: map[string]string{},
+ NotMatches: map[string]string{},
+ }
+
+ var leftOperand, rightOperand, operator *string
+ for i := range target.Filters {
+ if target.Filters[i] == "AND" {
+ continue
+ }
+
+ if leftOperand == nil {
+ leftOperand = &target.Filters[i]
+ continue
+ }
+ if operator == nil {
+ operator = &target.Filters[i]
+ continue
+ }
+ if rightOperand == nil {
+ rightOperand = &target.Filters[i]
+ }
+
+ if leftOperand != nil && operator != nil && rightOperand != nil {
+ switch *operator {
+ case "=":
+ filters.Eq[*leftOperand] = *rightOperand
+ case "!=":
+ filters.Neq[*leftOperand] = *rightOperand
+ case "=~":
+ filters.Matches[*leftOperand] = *rightOperand
+ case "!=~":
+ filters.NotMatches[*leftOperand] = *rightOperand
+ default:
+ converter.logger.Warn("unhandled stackdriver filter operator: filter skipped", zap.Any("operator", *operator))
+ }
+
+ leftOperand = nil
+ rightOperand = nil
+ operator = nil
+ }
+ }
+
+ return filters
+}
diff --git a/internal/pkg/converter/target_test.go b/internal/pkg/converter/target_test.go
new file mode 100644
index 00000000..c53e7e5f
--- /dev/null
+++ b/internal/pkg/converter/target_test.go
@@ -0,0 +1,214 @@
+package converter
+
+import (
+ "testing"
+
+ "github.com/K-Phoen/sdk"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/zap"
+)
+
+func TestConvertTargetFailsIfNoValidTargetIsGiven(t *testing.T) {
+ req := require.New(t)
+ converter := NewJSON(zap.NewNop())
+
+ convertedTarget := converter.convertTarget(sdk.Target{})
+ req.Nil(convertedTarget)
+}
+
+func TestConvertTargetWithPrometheusTarget(t *testing.T) {
+ req := require.New(t)
+
+ converter := NewJSON(zap.NewNop())
+
+ target := sdk.Target{
+ Expr: "prometheus_query",
+ LegendFormat: "{{ field }}",
+ RefID: "A",
+ }
+
+ convertedTarget := converter.convertTarget(target)
+
+ req.NotNil(convertedTarget)
+ req.Nil(convertedTarget.Stackdriver)
+ req.Equal("prometheus_query", convertedTarget.Prometheus.Query)
+ req.Equal("{{ field }}", convertedTarget.Prometheus.Legend)
+ req.Equal("A", convertedTarget.Prometheus.Ref)
+}
+
+func TestConvertTargetWithGraphiteTarget(t *testing.T) {
+ req := require.New(t)
+
+ converter := NewJSON(zap.NewNop())
+
+ target := sdk.Target{
+ Target: "graphite_query",
+ RefID: "A",
+ Hide: true,
+ }
+
+ convertedTarget := converter.convertTarget(target)
+
+ req.NotNil(convertedTarget)
+ req.NotNil(convertedTarget.Graphite)
+ req.Equal("graphite_query", convertedTarget.Graphite.Query)
+ req.Equal("A", convertedTarget.Graphite.Ref)
+ req.True(convertedTarget.Graphite.Hidden)
+}
+
+func TestConvertTargetWithInfluxDBTarget(t *testing.T) {
+ req := require.New(t)
+
+ converter := NewJSON(zap.NewNop())
+
+ target := sdk.Target{
+ Measurement: "influxdb_query",
+ RefID: "A",
+ Hide: true,
+ }
+
+ convertedTarget := converter.convertTarget(target)
+
+ req.NotNil(convertedTarget)
+ req.NotNil(convertedTarget.InfluxDB)
+ req.Equal("influxdb_query", convertedTarget.InfluxDB.Query)
+ req.Equal("A", convertedTarget.InfluxDB.Ref)
+ req.True(convertedTarget.InfluxDB.Hidden)
+}
+
+func TestConvertTargetWithStackdriverTargetFailsIfNoMetricKind(t *testing.T) {
+ req := require.New(t)
+ converter := NewJSON(zap.NewNop())
+
+ target := sdk.Target{
+ MetricType: "pubsub.googleapis.com/subscription/ack_message_count",
+ }
+
+ convertedTarget := converter.convertTarget(target)
+
+ req.Nil(convertedTarget)
+}
+
+func TestConvertTargetWithStackdriverTargetIgnoresUnknownCrossSeriesReducer(t *testing.T) {
+ req := require.New(t)
+ converter := NewJSON(zap.NewNop())
+
+ target := sdk.Target{
+ MetricKind: "DELTA",
+ MetricType: "pubsub.googleapis.com/subscription/ack_message_count",
+ CrossSeriesReducer: "unknown",
+ }
+
+ convertedTarget := converter.convertTarget(target)
+
+ req.NotNil(convertedTarget)
+ req.NotNil(convertedTarget.Stackdriver)
+ req.Empty(convertedTarget.Stackdriver.Aggregation)
+}
+
+func TestConvertTargetWithStackdriverTargetIgnoresUnknownAligner(t *testing.T) {
+ req := require.New(t)
+ converter := NewJSON(zap.NewNop())
+
+ target := sdk.Target{
+ MetricKind: "DELTA",
+ MetricType: "pubsub.googleapis.com/subscription/ack_message_count",
+ PerSeriesAligner: "unknown",
+ }
+
+ convertedTarget := converter.convertTarget(target)
+
+ req.NotNil(convertedTarget)
+ req.NotNil(convertedTarget.Stackdriver)
+ req.Empty(convertedTarget.Stackdriver.Alignment)
+}
+
+func TestConvertTargetWithStackdriverTarget(t *testing.T) {
+ req := require.New(t)
+
+ converter := NewJSON(zap.NewNop())
+
+ target := sdk.Target{
+ MetricKind: "DELTA",
+ MetricType: "pubsub.googleapis.com/subscription/ack_message_count",
+ CrossSeriesReducer: "REDUCE_MEAN",
+ PerSeriesAligner: "ALIGN_DELTA",
+ AlignmentPeriod: "stackdriver-auto",
+ GroupBys: []string{"field"},
+ AliasBy: "legend",
+ RefID: "A",
+ Filters: []string{
+ "resource.label.subscription_id",
+ "=",
+ "subscription_name",
+ "AND",
+ "other-property",
+ "!=",
+ "other-value",
+ "AND",
+ "regex-property",
+ "=~",
+ "regex-value",
+ "AND",
+ "regex-not-property",
+ "!=~",
+ "regex-not-value",
+ },
+ }
+
+ convertedTarget := converter.convertTarget(target)
+
+ req.NotNil(convertedTarget)
+ req.Nil(convertedTarget.Prometheus)
+ req.NotNil(convertedTarget.Stackdriver)
+ req.Equal("delta", convertedTarget.Stackdriver.Type)
+ req.Equal("pubsub.googleapis.com/subscription/ack_message_count", convertedTarget.Stackdriver.Metric)
+ req.Equal("mean", convertedTarget.Stackdriver.Aggregation)
+ req.Equal("stackdriver-auto", convertedTarget.Stackdriver.Alignment.Period)
+ req.Equal("delta", convertedTarget.Stackdriver.Alignment.Method)
+ req.Equal("legend", convertedTarget.Stackdriver.Legend)
+ req.Equal("A", convertedTarget.Stackdriver.Ref)
+ req.ElementsMatch([]string{"field"}, convertedTarget.Stackdriver.GroupBy)
+ req.EqualValues(map[string]string{"resource.label.subscription_id": "subscription_name"}, convertedTarget.Stackdriver.Filters.Eq)
+ req.EqualValues(map[string]string{"other-property": "other-value"}, convertedTarget.Stackdriver.Filters.Neq)
+ req.EqualValues(map[string]string{"regex-property": "regex-value"}, convertedTarget.Stackdriver.Filters.Matches)
+ req.EqualValues(map[string]string{"regex-not-property": "regex-not-value"}, convertedTarget.Stackdriver.Filters.NotMatches)
+}
+
+func TestConvertTargetWithStackdriverGauge(t *testing.T) {
+ req := require.New(t)
+
+ converter := NewJSON(zap.NewNop())
+
+ target := sdk.Target{
+ MetricKind: "GAUGE",
+ MetricType: "pubsub.googleapis.com/subscription/ack_message_count",
+ }
+
+ convertedTarget := converter.convertTarget(target)
+
+ req.NotNil(convertedTarget)
+ req.Nil(convertedTarget.Prometheus)
+ req.NotNil(convertedTarget.Stackdriver)
+ req.Equal("gauge", convertedTarget.Stackdriver.Type)
+ req.Equal("pubsub.googleapis.com/subscription/ack_message_count", convertedTarget.Stackdriver.Metric)
+}
+
+func TestConvertTargetWithStackdriverCumulative(t *testing.T) {
+ req := require.New(t)
+
+ converter := NewJSON(zap.NewNop())
+
+ target := sdk.Target{
+ MetricKind: "CUMULATIVE",
+ MetricType: "pubsub.googleapis.com/subscription/ack_message_count",
+ }
+
+ convertedTarget := converter.convertTarget(target)
+
+ req.NotNil(convertedTarget)
+ req.Nil(convertedTarget.Prometheus)
+ req.NotNil(convertedTarget.Stackdriver)
+ req.Equal("cumulative", convertedTarget.Stackdriver.Type)
+ req.Equal("pubsub.googleapis.com/subscription/ack_message_count", convertedTarget.Stackdriver.Metric)
+}
diff --git a/internal/pkg/converter/text.go b/internal/pkg/converter/text.go
new file mode 100644
index 00000000..742e5c02
--- /dev/null
+++ b/internal/pkg/converter/text.go
@@ -0,0 +1,29 @@
+package converter
+
+import (
+ grabana "github.com/K-Phoen/grabana/decoder"
+ "github.com/K-Phoen/sdk"
+)
+
+func (converter *JSON) convertText(panel sdk.Panel) grabana.DashboardPanel {
+ text := &grabana.DashboardText{
+ Title: panel.Title,
+ Span: panelSpan(panel),
+ Transparent: panel.Transparent,
+ }
+
+ if panel.Description != nil {
+ text.Description = *panel.Description
+ }
+ if panel.Height != nil {
+ text.Height = *(panel.Height).(*string)
+ }
+
+ if panel.TextPanel.Options.Mode == "markdown" {
+ text.Markdown = panel.TextPanel.Options.Content
+ } else {
+ text.HTML = panel.TextPanel.Options.Content
+ }
+
+ return grabana.DashboardPanel{Text: text}
+}
diff --git a/internal/pkg/converter/text_test.go b/internal/pkg/converter/text_test.go
new file mode 100644
index 00000000..a97242fb
--- /dev/null
+++ b/internal/pkg/converter/text_test.go
@@ -0,0 +1,68 @@
+package converter
+
+import (
+ "testing"
+
+ "github.com/K-Phoen/sdk"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/zap"
+)
+
+func TestConvertTextPanelWithMarkdown(t *testing.T) {
+ req := require.New(t)
+
+ converter := NewJSON(zap.NewNop())
+ height := "200px"
+
+ textPanel := sdk.Panel{
+ CommonPanel: sdk.CommonPanel{
+ Title: "Text panel",
+ Transparent: true,
+ Height: &height,
+ Type: "text",
+ },
+ TextPanel: &sdk.TextPanel{
+ Options: struct {
+ Content string `json:"content"`
+ Mode string `json:"mode"`
+ }{Content: "# hello world", Mode: "markdown"},
+ },
+ }
+
+ converted, ok := converter.convertDataPanel(textPanel)
+
+ req.True(ok)
+ req.True(converted.Text.Transparent)
+ req.Equal("Text panel", converted.Text.Title)
+ req.Equal("# hello world", converted.Text.Markdown)
+ req.Equal(height, converted.Text.Height)
+}
+
+func TestConvertTextPanelWithHTML(t *testing.T) {
+ req := require.New(t)
+
+ converter := NewJSON(zap.NewNop())
+
+ textPanel := sdk.Panel{
+ CommonPanel: sdk.CommonPanel{
+ Title: "Text panel html",
+ Type: "text",
+ Description: strPtr("panel description"),
+ },
+ TextPanel: &sdk.TextPanel{
+ Mode: "html",
+ Options: struct {
+ Content string `json:"content"`
+ Mode string `json:"mode"`
+ }{Content: "hello world
", Mode: "html"},
+ },
+ }
+
+ converted, ok := converter.convertDataPanel(textPanel)
+
+ req.True(ok)
+ req.False(converted.Text.Transparent)
+ req.Equal("Text panel html", converted.Text.Title)
+ req.Equal("panel description", converted.Text.Description)
+ req.Equal("hello world
", converted.Text.HTML)
+}
diff --git a/internal/pkg/converter/timeseries.go b/internal/pkg/converter/timeseries.go
new file mode 100644
index 00000000..d749cacd
--- /dev/null
+++ b/internal/pkg/converter/timeseries.go
@@ -0,0 +1,171 @@
+package converter
+
+import (
+ grabana "github.com/K-Phoen/grabana/decoder"
+ "github.com/K-Phoen/sdk"
+)
+
+func (converter *JSON) convertTimeSeries(panel sdk.Panel) grabana.DashboardPanel {
+ tsPanel := &grabana.DashboardTimeSeries{
+ Title: panel.Title,
+ Span: panelSpan(panel),
+ Transparent: panel.Transparent,
+ Alert: converter.convertAlert(panel),
+ Legend: converter.convertTimeSeriesLegend(panel.TimeseriesPanel.Options.Legend),
+ Visualization: converter.convertTimeSeriesVisualization(panel),
+ Axis: converter.convertTimeSeriesAxis(panel),
+ }
+
+ if panel.Description != nil {
+ tsPanel.Description = *panel.Description
+ }
+ if panel.Repeat != nil {
+ tsPanel.Repeat = *panel.Repeat
+ }
+ if panel.Height != nil {
+ tsPanel.Height = panel.Height.(string)
+ }
+ if panel.Datasource != nil {
+ tsPanel.Datasource = *panel.Datasource
+ }
+
+ for _, target := range panel.TimeseriesPanel.Targets {
+ tsTarget := converter.convertTarget(target)
+ if tsTarget == nil {
+ continue
+ }
+
+ tsPanel.Targets = append(tsPanel.Targets, *tsTarget)
+ }
+
+ return grabana.DashboardPanel{TimeSeries: tsPanel}
+}
+
+func (converter *JSON) convertTimeSeriesAxis(panel sdk.Panel) *grabana.TimeSeriesAxis {
+ fieldConfig := panel.TimeseriesPanel.FieldConfig
+
+ tsAxis := &grabana.TimeSeriesAxis{
+ Unit: fieldConfig.Defaults.Unit,
+ Label: fieldConfig.Defaults.Custom.AxisLabel,
+ }
+
+ // decimals
+ if fieldConfig.Defaults.Decimals != nil {
+ tsAxis.Decimals = fieldConfig.Defaults.Decimals
+ }
+
+ // boundaries
+ if fieldConfig.Defaults.Min != nil {
+ tsAxis.Min = fieldConfig.Defaults.Min
+ }
+ if fieldConfig.Defaults.Max != nil {
+ tsAxis.Max = fieldConfig.Defaults.Max
+ }
+ if fieldConfig.Defaults.Custom.AxisSoftMin != nil {
+ tsAxis.SoftMin = fieldConfig.Defaults.Custom.AxisSoftMin
+ }
+ if fieldConfig.Defaults.Custom.AxisSoftMax != nil {
+ tsAxis.SoftMax = fieldConfig.Defaults.Custom.AxisSoftMax
+ }
+
+ // placement
+ switch fieldConfig.Defaults.Custom.AxisPlacement {
+ case "hidden":
+ tsAxis.Display = "hidden"
+ case "left":
+ tsAxis.Display = "left"
+ case "right":
+ tsAxis.Display = "right"
+ case "auto":
+ tsAxis.Display = "auto"
+ }
+
+ // scale
+ switch fieldConfig.Defaults.Custom.ScaleDistribution.Type {
+ case "linear":
+ tsAxis.Scale = "linear"
+ case "log":
+ if fieldConfig.Defaults.Custom.ScaleDistribution.Log == 2 {
+ tsAxis.Scale = "log2"
+ } else {
+ tsAxis.Scale = "log10"
+ }
+ }
+
+ return tsAxis
+}
+
+func (converter *JSON) convertTimeSeriesVisualization(panel sdk.Panel) *grabana.TimeSeriesVisualization {
+ tsViz := &grabana.TimeSeriesVisualization{
+ FillOpacity: &panel.TimeseriesPanel.FieldConfig.Defaults.Custom.FillOpacity,
+ PointSize: &panel.TimeseriesPanel.FieldConfig.Defaults.Custom.PointSize,
+ }
+
+ // Tooltip mode
+ if panel.TimeseriesPanel.Options.Tooltip.Mode == "none" {
+ tsViz.Tooltip = "none"
+ } else if panel.TimeseriesPanel.Options.Tooltip.Mode == "multi" {
+ tsViz.Tooltip = "all_series"
+ } else {
+ tsViz.Tooltip = "single_series"
+ }
+
+ // Gradient mode
+ if panel.TimeseriesPanel.FieldConfig.Defaults.Custom.GradientMode == "none" {
+ tsViz.GradientMode = "none"
+ } else if panel.TimeseriesPanel.FieldConfig.Defaults.Custom.GradientMode == "hue" {
+ tsViz.GradientMode = "hue"
+ } else if panel.TimeseriesPanel.FieldConfig.Defaults.Custom.GradientMode == "scheme" {
+ tsViz.GradientMode = "scheme"
+ } else {
+ tsViz.GradientMode = "opacity"
+ }
+
+ return tsViz
+}
+
+func (converter *JSON) convertTimeSeriesLegend(legend sdk.TimeseriesLegendOptions) []string {
+ options := []string{}
+
+ // Display mode
+ if legend.DisplayMode == "list" {
+ options = append(options, "as_list")
+ } else if legend.DisplayMode == "hidden" {
+ options = append(options, "hide")
+ } else {
+ options = append(options, "as_table")
+ }
+
+ // Placement
+ if legend.Placement == "right" {
+ options = append(options, "to_the_right")
+ } else {
+ options = append(options, "to_bottom")
+ }
+
+ // Automatic calculations
+ calcs := map[string]string{
+ "first": "first",
+ "firstNotNull": "first_non_null",
+ "last": "last",
+ "lastNotNull": "last_non_null",
+
+ "min": "min",
+ "max": "max",
+ "mean": "avg",
+
+ "count": "count",
+ "sum": "total",
+ "range": "range",
+ }
+
+ for sdkCalc, grabanaCalc := range calcs {
+ if !stringInSlice(sdkCalc, legend.Calcs) {
+ continue
+ }
+
+ options = append(options, grabanaCalc)
+ }
+
+ return options
+}
diff --git a/internal/pkg/converter/timeseries_test.go b/internal/pkg/converter/timeseries_test.go
new file mode 100644
index 00000000..4a32165e
--- /dev/null
+++ b/internal/pkg/converter/timeseries_test.go
@@ -0,0 +1,393 @@
+package converter
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/K-Phoen/sdk"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/zap"
+)
+
+func TestConvertTimeSeriesPanel(t *testing.T) {
+ req := require.New(t)
+
+ converter := NewJSON(zap.NewNop())
+ height := "400px"
+ datasource := "prometheus"
+
+ panel := sdk.Panel{
+ CommonPanel: sdk.CommonPanel{
+ OfType: sdk.TimeseriesType,
+ Title: "test timeseries",
+ Type: "timeseries",
+ Description: strPtr("timeseries description"),
+ Transparent: true,
+ Height: height,
+ Datasource: &datasource,
+ },
+ TimeseriesPanel: &sdk.TimeseriesPanel{
+ Targets: []sdk.Target{
+ {
+ Expr: "prometheus_query",
+ LegendFormat: "{{ field }}",
+ RefID: "A",
+ },
+ },
+ },
+ }
+
+ converted, ok := converter.convertDataPanel(panel)
+
+ req.True(ok)
+ req.NotNil(converted.TimeSeries)
+
+ convertedTs := converted.TimeSeries
+ req.True(convertedTs.Transparent)
+ req.Equal("test timeseries", convertedTs.Title)
+ req.Equal("timeseries description", convertedTs.Description)
+ req.Equal(height, convertedTs.Height)
+ req.Equal(datasource, convertedTs.Datasource)
+ req.Len(convertedTs.Targets, 1)
+}
+
+func TestConvertTimeSeriesLegendDisplay(t *testing.T) {
+ testCases := []struct {
+ display string
+ expected string
+ }{
+ {
+ display: "list",
+ expected: "as_list",
+ },
+ {
+ display: "table",
+ expected: "as_table",
+ },
+ {
+ display: "hidden",
+ expected: "hide",
+ },
+ }
+
+ for _, testCase := range testCases {
+ tc := testCase
+
+ t.Run(tc.display, func(t *testing.T) {
+ req := require.New(t)
+
+ rawLegend := sdk.TimeseriesLegendOptions{
+ DisplayMode: tc.display,
+ }
+
+ converter := NewJSON(zap.NewNop())
+ legend := converter.convertTimeSeriesLegend(rawLegend)
+
+ req.Contains(legend, tc.expected)
+ })
+ }
+}
+
+func TestConvertTimeSeriesLegendPlacement(t *testing.T) {
+ testCases := []struct {
+ placement string
+ expected string
+ }{
+ {
+ placement: "right",
+ expected: "to_the_right",
+ },
+ {
+ placement: "bottom",
+ expected: "to_bottom",
+ },
+ }
+
+ for _, testCase := range testCases {
+ tc := testCase
+
+ t.Run(tc.placement, func(t *testing.T) {
+ req := require.New(t)
+
+ rawLegend := sdk.TimeseriesLegendOptions{
+ Placement: tc.placement,
+ }
+
+ converter := NewJSON(zap.NewNop())
+ legend := converter.convertTimeSeriesLegend(rawLegend)
+
+ req.Contains(legend, tc.expected)
+ })
+ }
+}
+
+func TestConvertTimeSeriesLegendCalculations(t *testing.T) {
+ req := require.New(t)
+
+ rawLegend := sdk.TimeseriesLegendOptions{
+ Calcs: []string{
+ "first",
+ "firstNotNull",
+ "last",
+ "lastNotNull",
+ "min",
+ "max",
+ "mean",
+ "count",
+ "sum",
+ "range",
+ },
+ }
+
+ converter := NewJSON(zap.NewNop())
+ legend := converter.convertTimeSeriesLegend(rawLegend)
+
+ expected := []string{
+ "first",
+ "first_non_null",
+ "last",
+ "last_non_null",
+ "min",
+ "max",
+ "avg",
+ "count",
+ "total",
+ "range",
+ }
+ for _, expectedItem := range expected {
+ req.Contains(legend, expectedItem)
+ }
+}
+
+func TestConvertTimeSeriesVisualizationGradient(t *testing.T) {
+ testCases := []struct {
+ mode string
+ expected string
+ }{
+ {
+ mode: "none",
+ expected: "none",
+ },
+ {
+ mode: "hue",
+ expected: "hue",
+ },
+ {
+ mode: "opacity",
+ expected: "opacity",
+ },
+ {
+ mode: "scheme",
+ expected: "scheme",
+ },
+ }
+
+ for _, testCase := range testCases {
+ tc := testCase
+
+ t.Run(tc.mode, func(t *testing.T) {
+ req := require.New(t)
+
+ panel := sdk.Panel{
+ CommonPanel: sdk.CommonPanel{},
+ TimeseriesPanel: &sdk.TimeseriesPanel{
+ Options: sdk.TimeseriesOptions{},
+ FieldConfig: sdk.FieldConfig{
+ Defaults: sdk.FieldConfigDefaults{
+ Custom: sdk.FieldConfigCustom{
+ GradientMode: tc.mode,
+ },
+ },
+ },
+ },
+ }
+
+ converter := NewJSON(zap.NewNop())
+ tsViz := converter.convertTimeSeriesVisualization(panel)
+
+ req.Equal(tc.expected, tsViz.GradientMode)
+ })
+ }
+}
+
+func TestConvertTimeSeriesVisualizationTooltipMode(t *testing.T) {
+ testCases := []struct {
+ mode string
+ expected string
+ }{
+ {
+ mode: "none",
+ expected: "none",
+ },
+ {
+ mode: "single",
+ expected: "single_series",
+ },
+ {
+ mode: "multi",
+ expected: "all_series",
+ },
+ }
+
+ for _, testCase := range testCases {
+ tc := testCase
+
+ t.Run(tc.mode, func(t *testing.T) {
+ req := require.New(t)
+
+ panel := sdk.Panel{
+ CommonPanel: sdk.CommonPanel{},
+ TimeseriesPanel: &sdk.TimeseriesPanel{
+ Options: sdk.TimeseriesOptions{
+ Tooltip: sdk.TimeseriesTooltipOptions{
+ Mode: tc.mode,
+ },
+ },
+ FieldConfig: sdk.FieldConfig{},
+ },
+ }
+
+ converter := NewJSON(zap.NewNop())
+ tsViz := converter.convertTimeSeriesVisualization(panel)
+
+ req.Equal(tc.expected, tsViz.Tooltip)
+ })
+ }
+}
+
+func TestConvertTimeSeriesAxisPlacement(t *testing.T) {
+ testCases := []struct {
+ placement string
+ expected string
+ }{
+ {placement: "hidden", expected: "hidden"},
+ {placement: "left", expected: "left"},
+ {placement: "right", expected: "right"},
+ {placement: "auto", expected: "auto"},
+ }
+
+ for _, testCase := range testCases {
+ tc := testCase
+
+ t.Run(tc.placement, func(t *testing.T) {
+ req := require.New(t)
+
+ panel := sdk.Panel{
+ CommonPanel: sdk.CommonPanel{},
+ TimeseriesPanel: &sdk.TimeseriesPanel{
+ FieldConfig: sdk.FieldConfig{
+ Defaults: sdk.FieldConfigDefaults{
+ Custom: sdk.FieldConfigCustom{
+ AxisPlacement: tc.placement,
+ },
+ },
+ },
+ },
+ }
+
+ converter := NewJSON(zap.NewNop())
+ tsAxis := converter.convertTimeSeriesAxis(panel)
+
+ req.Equal(tc.expected, tsAxis.Display)
+ })
+ }
+}
+func TestConvertTimeSeriesAxisOptions(t *testing.T) {
+ req := require.New(t)
+
+ panel := sdk.Panel{
+ CommonPanel: sdk.CommonPanel{},
+ TimeseriesPanel: &sdk.TimeseriesPanel{
+ FieldConfig: sdk.FieldConfig{
+ Defaults: sdk.FieldConfigDefaults{
+ Unit: "short",
+ Decimals: intPtr(2),
+ Min: intPtr(1),
+ Max: intPtr(11),
+ Custom: sdk.FieldConfigCustom{
+ AxisLabel: "label",
+ AxisSoftMin: intPtr(0),
+ AxisSoftMax: intPtr(10),
+ },
+ },
+ },
+ },
+ }
+
+ converter := NewJSON(zap.NewNop())
+ tsAxis := converter.convertTimeSeriesAxis(panel)
+
+ req.Equal("label", tsAxis.Label)
+ req.Equal("short", tsAxis.Unit)
+ req.Equal(2, *tsAxis.Decimals)
+ req.Equal(1, *tsAxis.Min)
+ req.Equal(11, *tsAxis.Max)
+ req.Equal(0, *tsAxis.SoftMin)
+ req.Equal(10, *tsAxis.SoftMax)
+}
+
+func TestConvertTimeSeriesAxisScale(t *testing.T) {
+ testCases := []struct {
+ scaleMode struct {
+ Type string `json:"type"`
+ Log int `json:"log,omitempty"`
+ }
+ expected string
+ }{
+ {
+ scaleMode: struct {
+ Type string `json:"type"`
+ Log int `json:"log,omitempty"`
+ }{
+ Type: "linear",
+ },
+ expected: "linear",
+ },
+ {
+ scaleMode: struct {
+ Type string `json:"type"`
+ Log int `json:"log,omitempty"`
+ }{
+ Type: "log",
+ Log: 2,
+ },
+ expected: "log2",
+ },
+ {
+ scaleMode: struct {
+ Type string `json:"type"`
+ Log int `json:"log,omitempty"`
+ }{
+ Type: "log",
+ Log: 10,
+ },
+ expected: "log10",
+ },
+ }
+
+ for _, testCase := range testCases {
+ tc := testCase
+
+ t.Run(fmt.Sprintf("%s %d", tc.scaleMode.Type, tc.scaleMode.Log), func(t *testing.T) {
+ req := require.New(t)
+
+ panel := sdk.Panel{
+ CommonPanel: sdk.CommonPanel{},
+ TimeseriesPanel: &sdk.TimeseriesPanel{
+ FieldConfig: sdk.FieldConfig{
+ Defaults: sdk.FieldConfigDefaults{
+ Custom: sdk.FieldConfigCustom{
+ ScaleDistribution: tc.scaleMode,
+ },
+ },
+ },
+ },
+ }
+
+ converter := NewJSON(zap.NewNop())
+ tsAxis := converter.convertTimeSeriesAxis(panel)
+
+ req.Equal(tc.expected, tsAxis.Scale)
+ })
+ }
+}
diff --git a/internal/pkg/converter/utils.go b/internal/pkg/converter/utils.go
new file mode 100644
index 00000000..feacbbd1
--- /dev/null
+++ b/internal/pkg/converter/utils.go
@@ -0,0 +1,15 @@
+package converter
+
+func stringInSlice(search string, haystack []string) bool {
+ for _, item := range haystack {
+ if item == search {
+ return true
+ }
+ }
+
+ return false
+}
+
+func intPtr(input int) *int {
+ return &input
+}
diff --git a/internal/pkg/converter/variable.go b/internal/pkg/converter/variable.go
new file mode 100644
index 00000000..7f2459a9
--- /dev/null
+++ b/internal/pkg/converter/variable.go
@@ -0,0 +1,136 @@
+package converter
+
+import (
+ "strings"
+
+ grabana "github.com/K-Phoen/grabana/decoder"
+ "github.com/K-Phoen/sdk"
+ "go.uber.org/zap"
+)
+
+func (converter *JSON) convertVariables(variables []sdk.TemplateVar, dashboard *grabana.DashboardModel) {
+ for _, variable := range variables {
+ converter.convertVariable(variable, dashboard)
+ }
+}
+
+func (converter *JSON) convertVariable(variable sdk.TemplateVar, dashboard *grabana.DashboardModel) {
+ switch variable.Type {
+ case "interval":
+ converter.convertIntervalVar(variable, dashboard)
+ case "custom":
+ converter.convertCustomVar(variable, dashboard)
+ case "query":
+ converter.convertQueryVar(variable, dashboard)
+ case "const":
+ converter.convertConstVar(variable, dashboard)
+ case "datasource":
+ converter.convertDatasourceVar(variable, dashboard)
+ default:
+ converter.logger.Warn("unhandled variable type found: skipped", zap.String("type", variable.Type), zap.String("name", variable.Name))
+ }
+}
+
+func (converter *JSON) convertIntervalVar(variable sdk.TemplateVar, dashboard *grabana.DashboardModel) {
+ interval := &grabana.VariableInterval{
+ Name: variable.Name,
+ Label: variable.Label,
+ Default: defaultOption(variable.Current),
+ Values: make([]string, 0, len(variable.Options)),
+ Hide: converter.convertVarHide(variable),
+ }
+
+ for _, opt := range variable.Options {
+ interval.Values = append(interval.Values, opt.Value)
+ }
+
+ dashboard.Variables = append(dashboard.Variables, grabana.DashboardVariable{Interval: interval})
+}
+
+func (converter *JSON) convertCustomVar(variable sdk.TemplateVar, dashboard *grabana.DashboardModel) {
+ custom := &grabana.VariableCustom{
+ Name: variable.Name,
+ Label: variable.Label,
+ Default: defaultOption(variable.Current),
+ ValuesMap: make(map[string]string, len(variable.Options)),
+ AllValue: variable.AllValue,
+ IncludeAll: variable.IncludeAll,
+ Hide: converter.convertVarHide(variable),
+ }
+
+ for _, opt := range variable.Options {
+ custom.ValuesMap[opt.Text] = opt.Value
+ }
+
+ dashboard.Variables = append(dashboard.Variables, grabana.DashboardVariable{Custom: custom})
+}
+
+func (converter *JSON) convertQueryVar(variable sdk.TemplateVar, dashboard *grabana.DashboardModel) {
+ datasource := ""
+ if variable.Datasource != nil {
+ datasource = *variable.Datasource
+ }
+
+ query := &grabana.VariableQuery{
+ Name: variable.Name,
+ Label: variable.Label,
+ Datasource: datasource,
+ Regex: variable.Regex,
+ IncludeAll: variable.IncludeAll,
+ DefaultAll: variable.Current.Value == "$__all",
+ AllValue: variable.AllValue,
+ Hide: converter.convertVarHide(variable),
+ }
+
+ if variable.Query != nil {
+ query.Request = variable.Query.(string)
+ }
+
+ dashboard.Variables = append(dashboard.Variables, grabana.DashboardVariable{Query: query})
+}
+
+func (converter *JSON) convertDatasourceVar(variable sdk.TemplateVar, dashboard *grabana.DashboardModel) {
+ datasource := &grabana.VariableDatasource{
+ Name: variable.Name,
+ Label: variable.Label,
+ Regex: variable.Regex,
+ IncludeAll: variable.IncludeAll,
+ Hide: converter.convertVarHide(variable),
+ }
+
+ if variable.Query != nil {
+ datasource.Type = variable.Query.(string)
+ }
+
+ dashboard.Variables = append(dashboard.Variables, grabana.DashboardVariable{Datasource: datasource})
+}
+
+func (converter *JSON) convertConstVar(variable sdk.TemplateVar, dashboard *grabana.DashboardModel) {
+ constant := &grabana.VariableConst{
+ Name: variable.Name,
+ Label: variable.Label,
+ Default: strings.Join(variable.Current.Text.Value, ","),
+ ValuesMap: make(map[string]string, len(variable.Options)),
+ Hide: converter.convertVarHide(variable),
+ }
+
+ for _, opt := range variable.Options {
+ constant.ValuesMap[opt.Text] = opt.Value
+ }
+
+ dashboard.Variables = append(dashboard.Variables, grabana.DashboardVariable{Const: constant})
+}
+
+func (converter *JSON) convertVarHide(variable sdk.TemplateVar) string {
+ switch variable.Hide {
+ case 0:
+ return ""
+ case 1:
+ return "label"
+ case 2:
+ return "variable"
+ default:
+ converter.logger.Warn("unknown hide value for variable %s", zap.String("variable", variable.Name))
+ return ""
+ }
+}
diff --git a/internal/pkg/converter/variable_test.go b/internal/pkg/converter/variable_test.go
new file mode 100644
index 00000000..73b81be5
--- /dev/null
+++ b/internal/pkg/converter/variable_test.go
@@ -0,0 +1,186 @@
+package converter
+
+import (
+ "reflect"
+ "testing"
+
+ grabana "github.com/K-Phoen/grabana/decoder"
+ "github.com/K-Phoen/sdk"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/zap"
+)
+
+func defaultVar(varType string) sdk.TemplateVar {
+ return sdk.TemplateVar{
+ Type: varType,
+ Name: "var",
+ Label: "Label",
+ }
+}
+
+func TestConvertUnknownVar(t *testing.T) {
+ req := require.New(t)
+
+ variable := defaultVar("unknown")
+
+ converter := NewJSON(zap.NewNop())
+
+ dashboard := &grabana.DashboardModel{}
+ converter.convertVariables([]sdk.TemplateVar{variable}, dashboard)
+
+ req.Len(dashboard.Variables, 0)
+}
+
+func TestConvertIntervalVar(t *testing.T) {
+ req := require.New(t)
+
+ variable := defaultVar("interval")
+ variable.Name = "var_interval"
+ variable.Label = "Label interval"
+ variable.Hide = 2
+ variable.Current = sdk.Current{Text: &sdk.StringSliceString{Value: []string{"30sec"}, Valid: true}, Value: "30s"}
+ variable.Options = []sdk.Option{
+ {Text: "10sec", Value: "10s"},
+ {Text: "30sec", Value: "30s"},
+ {Text: "1min", Value: "1m"},
+ }
+
+ converter := NewJSON(zap.NewNop())
+
+ dashboard := &grabana.DashboardModel{}
+ converter.convertVariables([]sdk.TemplateVar{variable}, dashboard)
+
+ req.Len(dashboard.Variables, 1)
+ req.NotNil(dashboard.Variables[0].Interval)
+
+ interval := dashboard.Variables[0].Interval
+
+ req.Equal("var_interval", interval.Name)
+ req.Equal("Label interval", interval.Label)
+ req.Equal("30s", interval.Default)
+ req.Equal("variable", interval.Hide)
+ req.ElementsMatch([]string{"10s", "30s", "1m"}, interval.Values)
+}
+
+func TestConvertCustomVar(t *testing.T) {
+ req := require.New(t)
+
+ variable := defaultVar("custom")
+ variable.Name = "var_custom"
+ variable.Label = "Label custom"
+ variable.Hide = 3 // unknown Hide value
+ variable.Current = sdk.Current{Text: &sdk.StringSliceString{Value: []string{"85th"}, Valid: true}, Value: "85"}
+ variable.Options = []sdk.Option{
+ {Text: "50th", Value: "50"},
+ {Text: "85th", Value: "85"},
+ {Text: "99th", Value: "99"},
+ }
+
+ converter := NewJSON(zap.NewNop())
+
+ dashboard := &grabana.DashboardModel{}
+ converter.convertVariables([]sdk.TemplateVar{variable}, dashboard)
+
+ req.Len(dashboard.Variables, 1)
+ req.NotNil(dashboard.Variables[0].Custom)
+
+ custom := dashboard.Variables[0].Custom
+
+ req.Equal("var_custom", custom.Name)
+ req.Equal("Label custom", custom.Label)
+ req.Equal("85", custom.Default)
+ req.Empty(custom.Hide)
+ req.True(reflect.DeepEqual(custom.ValuesMap, map[string]string{
+ "50th": "50",
+ "85th": "85",
+ "99th": "99",
+ }))
+}
+
+func TestConvertDatasourceVar(t *testing.T) {
+ req := require.New(t)
+
+ variable := defaultVar("datasource")
+ variable.Name = "var_datasource"
+ variable.Label = "Label datasource"
+
+ converter := NewJSON(zap.NewNop())
+
+ dashboard := &grabana.DashboardModel{}
+ converter.convertVariables([]sdk.TemplateVar{variable}, dashboard)
+
+ req.Len(dashboard.Variables, 1)
+ req.NotNil(dashboard.Variables[0].Datasource)
+
+ dsVar := dashboard.Variables[0].Datasource
+
+ req.Equal("var_datasource", dsVar.Name)
+ req.Equal("Label datasource", dsVar.Label)
+ req.Empty(dsVar.Hide)
+ req.False(dsVar.IncludeAll)
+}
+
+func TestConvertConstVar(t *testing.T) {
+ req := require.New(t)
+
+ variable := defaultVar("const")
+ variable.Name = "var_const"
+ variable.Label = "Label const"
+ variable.Hide = 0
+ variable.Current = sdk.Current{Text: &sdk.StringSliceString{Value: []string{"85th"}, Valid: true}, Value: "85"}
+ variable.Options = []sdk.Option{
+ {Text: "85th", Value: "85"},
+ {Text: "99th", Value: "99"},
+ }
+
+ converter := NewJSON(zap.NewNop())
+
+ dashboard := &grabana.DashboardModel{}
+ converter.convertVariables([]sdk.TemplateVar{variable}, dashboard)
+
+ req.Len(dashboard.Variables, 1)
+ req.NotNil(dashboard.Variables[0].Const)
+
+ constant := dashboard.Variables[0].Const
+
+ req.Equal("var_const", constant.Name)
+ req.Equal("Label const", constant.Label)
+ req.Equal("85th", constant.Default)
+ req.Empty(constant.Hide)
+ req.True(reflect.DeepEqual(constant.ValuesMap, map[string]string{
+ "85th": "85",
+ "99th": "99",
+ }))
+}
+
+func TestConvertQueryVar(t *testing.T) {
+ req := require.New(t)
+ datasource := "prometheus-default"
+
+ variable := defaultVar("query")
+ variable.Name = "var_query"
+ variable.Label = "Query"
+ variable.IncludeAll = true
+ variable.Hide = 1
+ variable.Current = sdk.Current{Value: "$__all"}
+ variable.Datasource = &datasource
+ variable.Query = "prom_query"
+
+ converter := NewJSON(zap.NewNop())
+
+ dashboard := &grabana.DashboardModel{}
+ converter.convertVariables([]sdk.TemplateVar{variable}, dashboard)
+
+ req.Len(dashboard.Variables, 1)
+ req.NotNil(dashboard.Variables[0].Query)
+
+ query := dashboard.Variables[0].Query
+
+ req.Equal("var_query", query.Name)
+ req.Equal("Query", query.Label)
+ req.Equal(datasource, query.Datasource)
+ req.Equal("prom_query", query.Request)
+ req.Equal("label", query.Hide)
+ req.True(query.IncludeAll)
+ req.True(query.DefaultAll)
+}
diff --git a/vendor/github.com/K-Phoen/grabana/.golangci.yaml b/vendor/github.com/K-Phoen/grabana/.golangci.yaml
index 5987273d..6f71ba05 100644
--- a/vendor/github.com/K-Phoen/grabana/.golangci.yaml
+++ b/vendor/github.com/K-Phoen/grabana/.golangci.yaml
@@ -17,16 +17,15 @@ linters:
- gocyclo
- gofmt
- goimports
- - interfacer
- misspell
- nakedret
- prealloc
- - scopelint
+ - exportloopref
- stylecheck
- unconvert
- unparam
- gosec
- - golint
+ - revive
- gochecknoglobals
disable:
- lll
diff --git a/vendor/github.com/K-Phoen/grabana/Makefile b/vendor/github.com/K-Phoen/grabana/Makefile
index e9f2156c..f4a1f2bb 100644
--- a/vendor/github.com/K-Phoen/grabana/Makefile
+++ b/vendor/github.com/K-Phoen/grabana/Makefile
@@ -6,14 +6,14 @@ VERSION?=$(TAG_NAME)-$(SHORT_SHA)
LDFLAGS=-ldflags "-X=main.version=$(VERSION)"
ifeq ($(WITH_COVERAGE),true)
-GOCMD_TEST?=go test -mod=vendor -coverpkg=./... -coverprofile=coverage.txt -covermode=atomic ./...
+GOCMD_TEST?=go test -coverpkg=./... -coverprofile=coverage.txt -covermode=atomic ./...
else
-GOCMD_TEST?=go test -mod=vendor
+GOCMD_TEST?=go test
endif
.PHONY: lint
lint:
- docker run --rm -v $(shell pwd):/app -w /app golangci/golangci-lint:v1.23.1 golangci-lint run -c .golangci.yaml
+ docker run --rm -v $(shell pwd):/app -w /app golangci/golangci-lint:v1.43.0 golangci-lint run -c .golangci.yaml
.PHONY: tests
tests:
@@ -60,4 +60,4 @@ down:
docker rm -f grabana_prometheus
build_cli:
- go build -mod vendor $(LDFLAGS) -o grabana github.com/K-Phoen/grabana/cmd/cli
\ No newline at end of file
+ go build $(LDFLAGS) -o grabana github.com/K-Phoen/grabana/cmd/cli
\ No newline at end of file
diff --git a/vendor/github.com/K-Phoen/grabana/decoder/alert.go b/vendor/github.com/K-Phoen/grabana/decoder/alert.go
new file mode 100644
index 00000000..bb07134c
--- /dev/null
+++ b/vendor/github.com/K-Phoen/grabana/decoder/alert.go
@@ -0,0 +1,176 @@
+package decoder
+
+import (
+ "fmt"
+
+ "github.com/K-Phoen/grabana/alert"
+)
+
+var ErrNoAlertThresholdDefined = fmt.Errorf("no threshold defined")
+var ErrInvalidAlertValueFunc = fmt.Errorf("invalid alert value function")
+
+type Alert struct {
+ Title string
+ EvaluateEvery string `yaml:"evaluate_every"`
+ For string
+ If []AlertCondition
+ Notify string `yaml:",omitempty"`
+ Notifications []string `yaml:",omitempty,flow"`
+ Message string `yaml:",omitempty"`
+ OnNoData string `yaml:"on_no_data"`
+ OnExecutionError string `yaml:"on_execution_error"`
+ Tags map[string]string `yaml:",omitempty"`
+}
+
+func (a Alert) toOptions() ([]alert.Option, error) {
+ opts := []alert.Option{
+ alert.EvaluateEvery(a.EvaluateEvery),
+ alert.For(a.For),
+ }
+
+ if a.OnNoData != "" {
+ var mode alert.NoDataMode
+
+ switch a.OnNoData {
+ case "no_data":
+ mode = alert.NoData
+ case "alerting":
+ mode = alert.Error
+ case "keep_state":
+ mode = alert.KeepLastState
+ case "ok":
+ mode = alert.OK
+ default:
+ return nil, fmt.Errorf("unknown on_no_data mode '%s'", a.OnNoData)
+ }
+
+ opts = append(opts, alert.OnNoData(mode))
+ }
+ if a.OnExecutionError != "" {
+ var mode alert.ErrorMode
+
+ switch a.OnExecutionError {
+ case "alerting":
+ mode = alert.Alerting
+ case "keep_state":
+ mode = alert.LastState
+ default:
+ return nil, fmt.Errorf("unknown on_execution_error mode '%s'", a.OnExecutionError)
+ }
+
+ opts = append(opts, alert.OnExecutionError(mode))
+ }
+ if a.Notify != "" {
+ opts = append(opts, alert.NotifyChannel(a.Notify))
+ }
+ if a.Message != "" {
+ opts = append(opts, alert.Message(a.Message))
+ }
+ if len(a.Tags) != 0 {
+ opts = append(opts, alert.Tags(a.Tags))
+ }
+
+ for _, channel := range a.Notifications {
+ opts = append(opts, alert.NotifyChannel(channel))
+ }
+
+ for _, condition := range a.If {
+ conditionOpt, err := condition.toOption()
+ if err != nil {
+ return nil, err
+ }
+
+ opts = append(opts, conditionOpt)
+ }
+
+ return opts, nil
+}
+
+type AlertThreshold struct {
+ HasNoValue bool `yaml:"has_no_value,omitempty"`
+ Above *float64 `yaml:",omitempty"`
+ Below *float64 `yaml:",omitempty"`
+ OutsideRange [2]float64 `yaml:"outside_range,omitempty,flow"`
+ WithinRange [2]float64 `yaml:"within_range,omitempty,flow"`
+}
+
+func (threshold AlertThreshold) toOption() (alert.ConditionOption, error) {
+ if threshold.HasNoValue {
+ return alert.HasNoValue(), nil
+ }
+ if threshold.Above != nil {
+ return alert.IsAbove(*threshold.Above), nil
+ }
+ if threshold.Below != nil {
+ return alert.IsBelow(*threshold.Below), nil
+ }
+ if threshold.OutsideRange[0] != 0 && threshold.OutsideRange[1] != 0 {
+ return alert.IsOutsideRange(threshold.OutsideRange[0], threshold.OutsideRange[1]), nil
+ }
+ if threshold.WithinRange[0] != 0 && threshold.WithinRange[1] != 0 {
+ return alert.IsWithinRange(threshold.WithinRange[0], threshold.WithinRange[1]), nil
+ }
+
+ return nil, ErrNoAlertThresholdDefined
+}
+
+type AlertValue struct {
+ Func string
+ QueryRef string `yaml:"ref"`
+ From string
+ To string
+}
+
+func (v AlertValue) toOption() (alert.ConditionOption, error) {
+ var alertFunc func(refID string, from string, to string) alert.ConditionOption
+
+ switch v.Func {
+ case "avg":
+ alertFunc = alert.Avg
+ case "sum":
+ alertFunc = alert.Sum
+ case "count":
+ alertFunc = alert.Count
+ case "last":
+ alertFunc = alert.Last
+ case "min":
+ alertFunc = alert.Min
+ case "max":
+ alertFunc = alert.Max
+ case "median":
+ alertFunc = alert.Median
+ case "diff":
+ alertFunc = alert.Diff
+ case "percent_diff":
+ alertFunc = alert.PercentDiff
+ default:
+ return nil, ErrInvalidAlertValueFunc
+ }
+
+ return alertFunc(v.QueryRef, v.From, v.To), nil
+}
+
+type AlertCondition struct {
+ Operand string
+ Value AlertValue `yaml:",flow"`
+ Threshold AlertThreshold
+}
+
+func (c AlertCondition) toOption() (alert.Option, error) {
+ operand := alert.And
+ if c.Operand == "or" {
+ operand = alert.Or
+ }
+
+ threshold, err := c.Threshold.toOption()
+ if err != nil {
+ return nil, err
+ }
+
+ value, err := c.Value.toOption()
+ if err != nil {
+ return nil, err
+ }
+
+ return alert.If(operand, value, threshold), nil
+}
diff --git a/vendor/github.com/K-Phoen/grabana/decoder/dashboard.go b/vendor/github.com/K-Phoen/grabana/decoder/dashboard.go
index b8ab2ea8..9b590939 100644
--- a/vendor/github.com/K-Phoen/grabana/decoder/dashboard.go
+++ b/vendor/github.com/K-Phoen/grabana/decoder/dashboard.go
@@ -18,11 +18,11 @@ type DashboardModel struct {
AutoRefresh string `yaml:"auto_refresh"`
Time [2]string
- Timezone string
+ Timezone string `yaml:",omitempty"`
- TagsAnnotation []dashboard.TagAnnotation `yaml:"tags_annotations"`
- Variables []DashboardVariable
- ExternalLinks []DashboardExternalLink `yaml:"external_links,omitempty"`
+ TagsAnnotation []dashboard.TagAnnotation `yaml:"tags_annotations,omitempty"`
+ Variables []DashboardVariable `yaml:",omitempty"`
+ ExternalLinks []DashboardExternalLink `yaml:"external_links,omitempty"`
Rows []DashboardRow
}
@@ -109,12 +109,16 @@ type DashboardPanel struct {
SingleStat *DashboardSingleStat `yaml:"single_stat,omitempty"`
Text *DashboardText `yaml:",omitempty"`
Heatmap *DashboardHeatmap `yaml:",omitempty"`
+ TimeSeries *DashboardTimeSeries `yaml:"timeseries,omitempty"`
}
func (panel DashboardPanel) toOption() (row.Option, error) {
if panel.Graph != nil {
return panel.Graph.toOption()
}
+ if panel.TimeSeries != nil {
+ return panel.TimeSeries.toOption()
+ }
if panel.Table != nil {
return panel.Table.toOption()
}
diff --git a/vendor/github.com/K-Phoen/grabana/decoder/graph.go b/vendor/github.com/K-Phoen/grabana/decoder/graph.go
index 556f0aa4..2f1fa688 100644
--- a/vendor/github.com/K-Phoen/grabana/decoder/graph.go
+++ b/vendor/github.com/K-Phoen/grabana/decoder/graph.go
@@ -3,15 +3,12 @@ package decoder
import (
"fmt"
- "github.com/K-Phoen/grabana/alert"
"github.com/K-Phoen/grabana/axis"
"github.com/K-Phoen/grabana/graph"
"github.com/K-Phoen/grabana/graph/series"
"github.com/K-Phoen/grabana/row"
)
-var ErrNoAlertThresholdDefined = fmt.Errorf("no threshold defined")
-var ErrInvalidAlertValueFunc = fmt.Errorf("invalid alert value function")
var ErrInvalidLegendAttribute = fmt.Errorf("invalid legend attribute")
type DashboardGraph struct {
@@ -25,7 +22,7 @@ type DashboardGraph struct {
Targets []Target
Axes *GraphAxes `yaml:",omitempty"`
Legend []string `yaml:",omitempty,flow"`
- Alert *GraphAlert `yaml:",omitempty"`
+ Alert *Alert `yaml:",omitempty"`
Visualization *GraphVisualization `yaml:",omitempty"`
}
@@ -135,7 +132,7 @@ func (graphViz *GraphVisualization) toOptions() []graph.Option {
return nil
}
- var opts []graph.Option
+ opts := []graph.Option{}
if graphViz.NullValue != "" {
mode := graph.AsZero
switch graphViz.NullValue {
@@ -259,169 +256,3 @@ type GraphAxes struct {
Right *GraphAxis `yaml:",omitempty"`
Bottom *GraphAxis `yaml:",omitempty"`
}
-
-type GraphAlert struct {
- Title string
- EvaluateEvery string `yaml:"evaluate_every"`
- For string
- If []AlertCondition
- Notify string `yaml:",omitempty"`
- Notifications []string `yaml:",omitempty,flow"`
- Message string `yaml:",omitempty"`
- OnNoData string `yaml:"on_no_data"`
- OnExecutionError string `yaml:"on_execution_error"`
- Tags map[string]string `yaml:",omitempty"`
-}
-
-func (a GraphAlert) toOptions() ([]alert.Option, error) {
- opts := []alert.Option{
- alert.EvaluateEvery(a.EvaluateEvery),
- alert.For(a.For),
- }
-
- if a.OnNoData != "" {
- var mode alert.NoDataMode
-
- switch a.OnNoData {
- case "no_data":
- mode = alert.NoData
- case "alerting":
- mode = alert.Error
- case "keep_state":
- mode = alert.KeepLastState
- case "ok":
- mode = alert.OK
- default:
- return nil, fmt.Errorf("unknown on_no_data mode '%s'", a.OnNoData)
- }
-
- opts = append(opts, alert.OnNoData(mode))
- }
- if a.OnExecutionError != "" {
- var mode alert.ErrorMode
-
- switch a.OnExecutionError {
- case "alerting":
- mode = alert.Alerting
- case "keep_state":
- mode = alert.LastState
- default:
- return nil, fmt.Errorf("unknown on_execution_error mode '%s'", a.OnExecutionError)
- }
-
- opts = append(opts, alert.OnExecutionError(mode))
- }
- if a.Notify != "" {
- opts = append(opts, alert.NotifyChannel(a.Notify))
- }
- if a.Message != "" {
- opts = append(opts, alert.Message(a.Message))
- }
- if len(a.Tags) != 0 {
- opts = append(opts, alert.Tags(a.Tags))
- }
-
- for _, channel := range a.Notifications {
- opts = append(opts, alert.NotifyChannel(channel))
- }
-
- for _, condition := range a.If {
- conditionOpt, err := condition.toOption()
- if err != nil {
- return nil, err
- }
-
- opts = append(opts, conditionOpt)
- }
-
- return opts, nil
-}
-
-type AlertThreshold struct {
- HasNoValue bool `yaml:"has_no_value,omitempty"`
- Above *float64 `yaml:",omitempty"`
- Below *float64 `yaml:",omitempty"`
- OutsideRange [2]float64 `yaml:"outside_range,omitempty,flow"`
- WithinRange [2]float64 `yaml:"within_range,omitempty,flow"`
-}
-
-func (threshold AlertThreshold) toOption() (alert.ConditionOption, error) {
- if threshold.HasNoValue {
- return alert.HasNoValue(), nil
- }
- if threshold.Above != nil {
- return alert.IsAbove(*threshold.Above), nil
- }
- if threshold.Below != nil {
- return alert.IsBelow(*threshold.Below), nil
- }
- if threshold.OutsideRange[0] != 0 && threshold.OutsideRange[1] != 0 {
- return alert.IsOutsideRange(threshold.OutsideRange[0], threshold.OutsideRange[1]), nil
- }
- if threshold.WithinRange[0] != 0 && threshold.WithinRange[1] != 0 {
- return alert.IsWithinRange(threshold.WithinRange[0], threshold.WithinRange[1]), nil
- }
-
- return nil, ErrNoAlertThresholdDefined
-}
-
-type AlertValue struct {
- Func string
- QueryRef string `yaml:"ref"`
- From string
- To string
-}
-
-func (v AlertValue) toOption() (alert.ConditionOption, error) {
- var alertFunc func(refID string, from string, to string) alert.ConditionOption
-
- switch v.Func {
- case "avg":
- alertFunc = alert.Avg
- case "sum":
- alertFunc = alert.Sum
- case "count":
- alertFunc = alert.Count
- case "last":
- alertFunc = alert.Last
- case "min":
- alertFunc = alert.Min
- case "max":
- alertFunc = alert.Max
- case "median":
- alertFunc = alert.Median
- case "diff":
- alertFunc = alert.Diff
- case "percent_diff":
- alertFunc = alert.PercentDiff
- default:
- return nil, ErrInvalidAlertValueFunc
- }
-
- return alertFunc(v.QueryRef, v.From, v.To), nil
-}
-
-type AlertCondition struct {
- Operand string
- Value AlertValue `yaml:",flow"`
- Threshold AlertThreshold
-}
-
-func (c AlertCondition) toOption() (alert.Option, error) {
- operand := alert.And
- if c.Operand == "or" {
- operand = alert.Or
- }
-
- threshold, err := c.Threshold.toOption()
- if err != nil {
- return nil, err
- }
-
- value, err := c.Value.toOption()
- if err != nil {
- return nil, err
- }
-
- return alert.If(operand, value, threshold), nil
-}
diff --git a/vendor/github.com/K-Phoen/grabana/decoder/timeseries.go b/vendor/github.com/K-Phoen/grabana/decoder/timeseries.go
new file mode 100644
index 00000000..353b80cb
--- /dev/null
+++ b/vendor/github.com/K-Phoen/grabana/decoder/timeseries.go
@@ -0,0 +1,338 @@
+package decoder
+
+import (
+ "fmt"
+
+ "github.com/K-Phoen/grabana/row"
+ "github.com/K-Phoen/grabana/timeseries"
+ "github.com/K-Phoen/grabana/timeseries/axis"
+)
+
+var ErrInvalidGradientMode = fmt.Errorf("invalid gradient mode")
+var ErrInvalidTooltipMode = fmt.Errorf("invalid tooltip mode")
+var ErrInvalidAxisDisplay = fmt.Errorf("invalid axis display")
+var ErrInvalidAxisScale = fmt.Errorf("invalid axis scale")
+
+type DashboardTimeSeries struct {
+ Title string
+ Description string `yaml:",omitempty"`
+ Span float32 `yaml:",omitempty"`
+ Height string `yaml:",omitempty"`
+ Transparent bool `yaml:",omitempty"`
+ Datasource string `yaml:",omitempty"`
+ Repeat string `yaml:",omitempty"`
+ Targets []Target
+ Legend []string `yaml:",omitempty,flow"`
+ Alert *Alert `yaml:",omitempty"`
+ Visualization *TimeSeriesVisualization `yaml:",omitempty"`
+ Axis *TimeSeriesAxis `yaml:",omitempty"`
+}
+
+func (timeseriesPanel DashboardTimeSeries) toOption() (row.Option, error) {
+ opts := []timeseries.Option{}
+
+ if timeseriesPanel.Description != "" {
+ opts = append(opts, timeseries.Description(timeseriesPanel.Description))
+ }
+ if timeseriesPanel.Span != 0 {
+ opts = append(opts, timeseries.Span(timeseriesPanel.Span))
+ }
+ if timeseriesPanel.Height != "" {
+ opts = append(opts, timeseries.Height(timeseriesPanel.Height))
+ }
+ if timeseriesPanel.Transparent {
+ opts = append(opts, timeseries.Transparent())
+ }
+ if timeseriesPanel.Datasource != "" {
+ opts = append(opts, timeseries.DataSource(timeseriesPanel.Datasource))
+ }
+ if timeseriesPanel.Repeat != "" {
+ opts = append(opts, timeseries.Repeat(timeseriesPanel.Repeat))
+ }
+ if len(timeseriesPanel.Legend) != 0 {
+ legendOpts, err := timeseriesPanel.legend()
+ if err != nil {
+ return nil, err
+ }
+
+ opts = append(opts, timeseries.Legend(legendOpts...))
+ }
+ if timeseriesPanel.Alert != nil {
+ alertOpts, err := timeseriesPanel.Alert.toOptions()
+ if err != nil {
+ return nil, err
+ }
+
+ opts = append(opts, timeseries.Alert(timeseriesPanel.Alert.Title, alertOpts...))
+ }
+ if timeseriesPanel.Visualization != nil {
+ vizOpts, err := timeseriesPanel.Visualization.toOptions()
+ if err != nil {
+ return nil, err
+ }
+
+ opts = append(opts, vizOpts...)
+ }
+ if timeseriesPanel.Axis != nil {
+ axisOpts, err := timeseriesPanel.Axis.toOptions()
+ if err != nil {
+ return nil, err
+ }
+
+ opts = append(opts, timeseries.Axis(axisOpts...))
+ }
+
+ for _, t := range timeseriesPanel.Targets {
+ opt, err := timeseriesPanel.target(t)
+ if err != nil {
+ return nil, err
+ }
+
+ opts = append(opts, opt)
+ }
+
+ return row.WithTimeSeries(timeseriesPanel.Title, opts...), nil
+}
+
+func (timeseriesPanel DashboardTimeSeries) legend() ([]timeseries.LegendOption, error) {
+ opts := make([]timeseries.LegendOption, 0, len(timeseriesPanel.Legend))
+
+ for _, attribute := range timeseriesPanel.Legend {
+ var opt timeseries.LegendOption
+
+ switch attribute {
+ case "hide":
+ opt = timeseries.Hide
+ case "as_table":
+ opt = timeseries.AsTable
+ case "as_list":
+ opt = timeseries.AsList
+ case "to_bottom":
+ opt = timeseries.Bottom
+ case "to_the_right":
+ opt = timeseries.ToTheRight
+
+ case "min":
+ opt = timeseries.Min
+ case "max":
+ opt = timeseries.Max
+ case "avg":
+ opt = timeseries.Avg
+
+ case "first":
+ opt = timeseries.First
+ case "first_non_null":
+ opt = timeseries.FirstNonNull
+ case "last":
+ opt = timeseries.Last
+ case "last_non_null":
+ opt = timeseries.LastNonNull
+
+ case "count":
+ opt = timeseries.Count
+ case "total":
+ opt = timeseries.Total
+ case "range":
+ opt = timeseries.Range
+ default:
+ return nil, ErrInvalidLegendAttribute
+ }
+
+ opts = append(opts, opt)
+ }
+
+ return opts, nil
+}
+
+func (timeseriesPanel DashboardTimeSeries) target(t Target) (timeseries.Option, error) {
+ if t.Prometheus != nil {
+ return timeseries.WithPrometheusTarget(t.Prometheus.Query, t.Prometheus.toOptions()...), nil
+ }
+ if t.Graphite != nil {
+ return timeseries.WithGraphiteTarget(t.Graphite.Query, t.Graphite.toOptions()...), nil
+ }
+ if t.InfluxDB != nil {
+ return timeseries.WithInfluxDBTarget(t.InfluxDB.Query, t.InfluxDB.toOptions()...), nil
+ }
+ if t.Stackdriver != nil {
+ stackdriverTarget, err := t.Stackdriver.toTarget()
+ if err != nil {
+ return nil, err
+ }
+
+ return timeseries.WithStackdriverTarget(stackdriverTarget), nil
+ }
+
+ return nil, ErrTargetNotConfigured
+}
+
+type TimeSeriesVisualization struct {
+ GradientMode string `yaml:"gradient_mode,omitempty"`
+ Tooltip string `yaml:"tooltip,omitempty"`
+ FillOpacity *int `yaml:"fill_opacity,omitempty"`
+ PointSize *int `yaml:"point_size,omitempty"`
+ // TODO: draw: {bars: {}, lines: {}}
+}
+
+func (timeseriesViz *TimeSeriesVisualization) toOptions() ([]timeseries.Option, error) {
+ if timeseriesViz == nil {
+ return nil, nil
+ }
+
+ opts := []timeseries.Option{}
+
+ if timeseriesViz.FillOpacity != nil {
+ opts = append(opts, timeseries.FillOpacity(*timeseriesViz.FillOpacity))
+ }
+ if timeseriesViz.PointSize != nil {
+ opts = append(opts, timeseries.PointSize(*timeseriesViz.PointSize))
+ }
+ if timeseriesViz.GradientMode != "" {
+ gradient, err := timeseriesViz.gradientModeOption()
+ if err != nil {
+ return nil, err
+ }
+
+ opts = append(opts, gradient)
+ }
+ if timeseriesViz.Tooltip != "" {
+ gradient, err := timeseriesViz.tooltipOption()
+ if err != nil {
+ return nil, err
+ }
+
+ opts = append(opts, gradient)
+ }
+
+ return opts, nil
+}
+
+func (timeseriesViz *TimeSeriesVisualization) gradientModeOption() (timeseries.Option, error) {
+ var mode timeseries.GradientType
+ switch timeseriesViz.GradientMode {
+ case "none":
+ mode = timeseries.NoGradient
+ case "opacity":
+ mode = timeseries.Opacity
+ case "hue":
+ mode = timeseries.Hue
+ case "scheme":
+ mode = timeseries.Scheme
+ default:
+ return timeseries.GradientMode(mode), ErrInvalidGradientMode
+ }
+
+ return timeseries.GradientMode(mode), nil
+}
+
+func (timeseriesViz *TimeSeriesVisualization) tooltipOption() (timeseries.Option, error) {
+ var mode timeseries.TooltipMode
+ switch timeseriesViz.Tooltip {
+ case "single_series":
+ mode = timeseries.SingleSeries
+ case "all_series":
+ mode = timeseries.AllSeries
+ case "none":
+ mode = timeseries.NoSeries
+ default:
+ return timeseries.Tooltip(mode), ErrInvalidTooltipMode
+ }
+
+ return timeseries.Tooltip(mode), nil
+}
+
+type TimeSeriesAxis struct {
+ SoftMin *int `yaml:"soft_min,omitempty"`
+ SoftMax *int `yaml:"soft_max,omitempty"`
+ Min *int `yaml:",omitempty"`
+ Max *int `yaml:",omitempty"`
+
+ Decimals *int `yaml:",omitempty"`
+
+ Display string `yaml:",omitempty"`
+ Scale string `yaml:",omitempty"`
+
+ Unit string `yaml:",omitempty"`
+ Label string `yaml:",omitempty"`
+}
+
+func (tsAxis *TimeSeriesAxis) toOptions() ([]axis.Option, error) {
+ opts := []axis.Option{}
+
+ if tsAxis.SoftMin != nil {
+ opts = append(opts, axis.SoftMin(*tsAxis.SoftMin))
+ }
+ if tsAxis.SoftMax != nil {
+ opts = append(opts, axis.SoftMax(*tsAxis.SoftMax))
+ }
+ if tsAxis.Min != nil {
+ opts = append(opts, axis.Min(*tsAxis.Min))
+ }
+ if tsAxis.Max != nil {
+ opts = append(opts, axis.Max(*tsAxis.Max))
+ }
+
+ if tsAxis.Decimals != nil {
+ opts = append(opts, axis.Decimals(*tsAxis.Decimals))
+ }
+
+ if tsAxis.Display != "" {
+ opt, err := tsAxis.placementOption()
+ if err != nil {
+ return nil, err
+ }
+ opts = append(opts, opt)
+ }
+ if tsAxis.Scale != "" {
+ opt, err := tsAxis.scaleOption()
+ if err != nil {
+ return nil, err
+ }
+ opts = append(opts, opt)
+ }
+
+ if tsAxis.Unit != "" {
+ opts = append(opts, axis.Unit(tsAxis.Unit))
+ }
+ if tsAxis.Label != "" {
+ opts = append(opts, axis.Label(tsAxis.Label))
+ }
+
+ return opts, nil
+}
+
+func (tsAxis *TimeSeriesAxis) placementOption() (axis.Option, error) {
+ var placementMode axis.PlacementMode
+
+ switch tsAxis.Display {
+ case "none":
+ placementMode = axis.Hidden
+ case "auto":
+ placementMode = axis.Auto
+ case "left":
+ placementMode = axis.Left
+ case "right":
+ placementMode = axis.Right
+ default:
+ return nil, ErrInvalidAxisDisplay
+ }
+
+ return axis.Placement(placementMode), nil
+}
+
+func (tsAxis *TimeSeriesAxis) scaleOption() (axis.Option, error) {
+ var scaleMode axis.ScaleMode
+
+ switch tsAxis.Scale {
+ case "linear":
+ scaleMode = axis.Linear
+ case "log2":
+ scaleMode = axis.Log2
+ case "log10":
+ scaleMode = axis.Log10
+ default:
+ return nil, ErrInvalidAxisScale
+ }
+
+ return axis.Scale(scaleMode), nil
+}
diff --git a/vendor/github.com/K-Phoen/grabana/decoder/utils.go b/vendor/github.com/K-Phoen/grabana/decoder/utils.go
new file mode 100644
index 00000000..1cae4d4a
--- /dev/null
+++ b/vendor/github.com/K-Phoen/grabana/decoder/utils.go
@@ -0,0 +1,5 @@
+package decoder
+
+func intPtr(input int) *int {
+ return &input
+}
diff --git a/vendor/github.com/K-Phoen/grabana/row/row.go b/vendor/github.com/K-Phoen/grabana/row/row.go
index 30e163d4..096cfa0b 100644
--- a/vendor/github.com/K-Phoen/grabana/row/row.go
+++ b/vendor/github.com/K-Phoen/grabana/row/row.go
@@ -6,6 +6,7 @@ import (
"github.com/K-Phoen/grabana/singlestat"
"github.com/K-Phoen/grabana/table"
"github.com/K-Phoen/grabana/text"
+ "github.com/K-Phoen/grabana/timeseries"
"github.com/K-Phoen/sdk"
)
@@ -43,6 +44,15 @@ func WithGraph(title string, options ...graph.Option) Option {
}
}
+// WithTimeSeries adds a "timeseries" panel in the row.
+func WithTimeSeries(title string, options ...timeseries.Option) Option {
+ return func(row *Row) {
+ panel := timeseries.New(title, options...)
+
+ row.builder.Add(panel.Builder)
+ }
+}
+
// WithSingleStat adds a "single stat" panel in the row.
func WithSingleStat(title string, options ...singlestat.Option) Option {
return func(row *Row) {
diff --git a/vendor/github.com/K-Phoen/grabana/timeseries/axis/axis.go b/vendor/github.com/K-Phoen/grabana/timeseries/axis/axis.go
new file mode 100644
index 00000000..0f502c37
--- /dev/null
+++ b/vendor/github.com/K-Phoen/grabana/timeseries/axis/axis.go
@@ -0,0 +1,124 @@
+package axis
+
+import (
+ "github.com/K-Phoen/sdk"
+)
+
+// PlacementMode represents the axis display placement mode.
+type PlacementMode string
+
+const (
+ Hidden PlacementMode = "hidden"
+ Auto PlacementMode = "auto"
+ Left PlacementMode = "left"
+ Right PlacementMode = "right"
+)
+
+// ScaleMode represents the axis scale distribution.
+type ScaleMode uint8
+
+const (
+ Linear ScaleMode = iota
+ Log2
+ Log10
+)
+
+// Option represents an option that can be used to configure an axis.
+type Option func(axis *Axis)
+
+// Axis represents a visualization axis.
+type Axis struct {
+ fieldConfig *sdk.FieldConfig
+}
+
+// New creates a new Axis configuration.
+func New(fieldConfig *sdk.FieldConfig, options ...Option) *Axis {
+ axis := &Axis{fieldConfig: fieldConfig}
+
+ for _, opt := range options {
+ opt(axis)
+ }
+
+ return axis
+}
+
+// Placement defines how the axis should be placed in the panel.
+func Placement(placement PlacementMode) Option {
+ return func(axis *Axis) {
+ axis.fieldConfig.Defaults.Custom.AxisPlacement = string(placement)
+ }
+}
+
+// SoftMin defines a soft minimum value for the axis.
+func SoftMin(value int) Option {
+ return func(axis *Axis) {
+ axis.fieldConfig.Defaults.Custom.AxisSoftMin = &value
+ }
+}
+
+// SoftMax defines a soft maximum value for the axis.
+func SoftMax(value int) Option {
+ return func(axis *Axis) {
+ axis.fieldConfig.Defaults.Custom.AxisSoftMax = &value
+ }
+}
+
+// Min defines a hard minimum value for the axis.
+func Min(value int) Option {
+ return func(axis *Axis) {
+ axis.fieldConfig.Defaults.Min = &value
+ }
+}
+
+// SoftMax defines a hard maximum value for the axis.
+func Max(value int) Option {
+ return func(axis *Axis) {
+ axis.fieldConfig.Defaults.Max = &value
+ }
+}
+
+// Unit sets the unit of the data displayed in this series.
+func Unit(unit string) Option {
+ return func(axis *Axis) {
+ axis.fieldConfig.Defaults.Unit = unit
+ }
+}
+
+// Scale sets the scale to use for the Y-axis values..
+func Scale(mode ScaleMode) Option {
+ return func(axis *Axis) {
+ scaleConfig := struct {
+ Type string `json:"type"`
+ Log int `json:"log,omitempty"`
+ }{
+ Type: "linear",
+ }
+
+ switch mode {
+ case Linear:
+ scaleConfig.Type = "linear"
+ case Log2:
+ scaleConfig.Type = "log"
+ scaleConfig.Log = 2
+ case Log10:
+ scaleConfig.Type = "log"
+ scaleConfig.Log = 10
+ }
+
+ axis.fieldConfig.Defaults.Custom.ScaleDistribution = scaleConfig
+ }
+}
+
+// Label sets a Y-axis text label.
+func Label(label string) Option {
+ return func(axis *Axis) {
+ axis.fieldConfig.Defaults.Custom.AxisLabel = label
+ }
+}
+
+// Decimals sets how many decimal points should be displayed.
+func Decimals(decimals int) Option {
+ return func(axis *Axis) {
+ axis.fieldConfig.Defaults.Decimals = &decimals
+ }
+}
diff --git a/vendor/github.com/K-Phoen/grabana/timeseries/targets.go b/vendor/github.com/K-Phoen/grabana/timeseries/targets.go
new file mode 100644
index 00000000..1a054de0
--- /dev/null
+++ b/vendor/github.com/K-Phoen/grabana/timeseries/targets.go
@@ -0,0 +1,53 @@
+package timeseries
+
+import (
+ "github.com/K-Phoen/grabana/target/graphite"
+ "github.com/K-Phoen/grabana/target/influxdb"
+ "github.com/K-Phoen/grabana/target/prometheus"
+ "github.com/K-Phoen/grabana/target/stackdriver"
+ "github.com/K-Phoen/sdk"
+)
+
+// WithPrometheusTarget adds a prometheus query to the graph.
+func WithPrometheusTarget(query string, options ...prometheus.Option) Option {
+ target := prometheus.New(query, options...)
+
+ return func(graph *TimeSeries) {
+ graph.Builder.AddTarget(&sdk.Target{
+ RefID: target.Ref,
+ Hide: target.Hidden,
+ Expr: target.Expr,
+ IntervalFactor: target.IntervalFactor,
+ Interval: target.Interval,
+ Step: target.Step,
+ LegendFormat: target.LegendFormat,
+ Instant: target.Instant,
+ Format: target.Format,
+ })
+ }
+}
+
+// WithGraphiteTarget adds a Graphite target to the table.
+func WithGraphiteTarget(query string, options ...graphite.Option) Option {
+ target := graphite.New(query, options...)
+
+ return func(graph *TimeSeries) {
+ graph.Builder.AddTarget(target.Builder)
+ }
+}
+
+// WithInfluxDBTarget adds an InfluxDB target to the graph.
+func WithInfluxDBTarget(query string, options ...influxdb.Option) Option {
+ target := influxdb.New(query, options...)
+
+ return func(graph *TimeSeries) {
+ graph.Builder.AddTarget(target.Builder)
+ }
+}
+
+// WithStackdriverTarget adds a stackdriver query to the graph.
+func WithStackdriverTarget(target *stackdriver.Stackdriver) Option {
+ return func(graph *TimeSeries) {
+ graph.Builder.AddTarget(target.Builder)
+ }
+}
diff --git a/vendor/github.com/K-Phoen/grabana/timeseries/timeseries.go b/vendor/github.com/K-Phoen/grabana/timeseries/timeseries.go
new file mode 100644
index 00000000..f0ace463
--- /dev/null
+++ b/vendor/github.com/K-Phoen/grabana/timeseries/timeseries.go
@@ -0,0 +1,307 @@
+package timeseries
+
+import (
+ "github.com/K-Phoen/grabana/alert"
+ "github.com/K-Phoen/grabana/timeseries/axis"
+ "github.com/K-Phoen/sdk"
+)
+
+// Option represents an option that can be used to configure a graph panel.
+type Option func(timeseries *TimeSeries)
+
+// TooltipMode configures which series will be displayed in the tooltip.
+type TooltipMode string
+
+const (
+ // SingleSeries will only display the hovered series.
+ SingleSeries TooltipMode = "single"
+ // AllSeries will display all series.
+ AllSeries TooltipMode = "multi"
+ // NoSeries will hide the tooltip completely.
+ NoSeries TooltipMode = "none"
+)
+
+// LineInterpolationMode defines how Grafana interpolates series lines when drawn as lines.
+type LineInterpolationMode string
+
+const (
+ // Points are joined by straight lines.
+ Linear LineInterpolationMode = "linear"
+ // Points are joined by curved lines resulting in smooth transitions between points.
+ Smooth LineInterpolationMode = "smooth"
+ // The line is displayed as steps between points. Points are rendered at the end of the step.
+ StepBefore LineInterpolationMode = "stepBefore"
+ // Line is displayed as steps between points. Points are rendered at the beginning of the step.
+ StepAfter LineInterpolationMode = "stepAfter"
+)
+
+// BarAlignment defines how Grafana aligns bars.
+type BarAlignment int
+
+const (
+ // The bar is drawn around the point. The point is placed in the center of the bar.
+ AlignCenter BarAlignment = 0
+ // The bar is drawn before the point. The point is placed on the trailing corner of the bar.
+ AlignBefore BarAlignment = -1
+ // The bar is drawn after the point. The point is placed on the leading corner of the bar.
+ AlignAfter BarAlignment = 1
+)
+
+// GradientType defines the mode of the gradient fill.
+type GradientType string
+
+const (
+ // No gradient fill.
+ NoGradient GradientType = "none"
+ // Transparency of the gradient is calculated based on the values on the y-axis.
+ // Opacity of the fill is increasing with the values on the Y-axis.
+ Opacity GradientType = "opacity"
+ // Gradient color is generated based on the hue of the line color.
+ Hue GradientType = "hue"
+ // In this mode the whole bar will use a color gradient defined by the color scheme.
+ Scheme GradientType = "scheme"
+)
+
+// LegendOption allows to configure a legend.
+type LegendOption uint16
+
+const (
+ // Hide keeps the legend from being displayed.
+ Hide LegendOption = iota
+ // AsTable displays the legend as a table.
+ AsTable
+ // AsList displays the legend as a list.
+ AsList
+ // Bottom displays the legend below the graph.
+ Bottom
+ // ToTheRight displays the legend on the right side of the graph.
+ ToTheRight
+
+ // Min displays the smallest value of the series.
+ Min
+ // Max displays the largest value of the series.
+ Max
+ // Avg displays the average of the series.
+ Avg
+
+ // First displays the first value of the series.
+ First
+ // FirstNonNull displays the first non-null value of the series.
+ FirstNonNull
+ // Last displays the last value of the series.
+ Last
+ // LastNonNull displays the last non-null value of the series.
+ LastNonNull
+
+ // Total displays the sum of values in the series.
+ Total
+ // Count displays the number of value in the series.
+ Count
+ // Range displays the difference between the minimum and maximum values.
+ Range
+)
+
+// TimeSeries represents a time series panel.
+type TimeSeries struct {
+ Builder *sdk.Panel
+}
+
+// New creates a new time series panel.
+func New(title string, options ...Option) *TimeSeries {
+ panel := &TimeSeries{Builder: sdk.NewTimeseries(title)}
+ panel.Builder.IsNew = false
+
+ for _, opt := range append(defaults(), options...) {
+ opt(panel)
+ }
+
+ return panel
+}
+
+func defaults() []Option {
+ return []Option{
+ Span(6),
+ LineWidth(1),
+ FillOpacity(25),
+ PointSize(5),
+ Tooltip(SingleSeries),
+ Legend(Bottom, AsList),
+ Lines(Linear),
+ GradientMode(Opacity),
+ Axis(
+ axis.Placement(axis.Auto),
+ axis.Scale(axis.Linear),
+ ),
+ }
+}
+
+// DataSource sets the data source to be used by the graph.
+func DataSource(source string) Option {
+ return func(timeseries *TimeSeries) {
+ timeseries.Builder.Datasource = &source
+ }
+}
+
+// Tooltip configures the tooltip content.
+func Tooltip(mode TooltipMode) Option {
+ return func(timeseries *TimeSeries) {
+ timeseries.Builder.TimeseriesPanel.Options.Tooltip.Mode = string(mode)
+ }
+}
+
+// LineWidth defines the width of the line for a series (default 1, max 10, 0 is none).
+func LineWidth(value int) Option {
+ return func(timeseries *TimeSeries) {
+ timeseries.Builder.TimeseriesPanel.FieldConfig.Defaults.Custom.LineWidth = value
+ }
+}
+
+// FillOpacity defines the opacity level of the series. The lower the value, the more transparent.
+func FillOpacity(value int) Option {
+ return func(timeseries *TimeSeries) {
+ timeseries.Builder.TimeseriesPanel.FieldConfig.Defaults.Custom.FillOpacity = value
+ }
+}
+
+// PointSize adjusts the size of points.
+func PointSize(value int) Option {
+ return func(timeseries *TimeSeries) {
+ timeseries.Builder.TimeseriesPanel.FieldConfig.Defaults.Custom.PointSize = value
+ }
+}
+
+// Lines displays the series as lines, with a given interpolation strategy.
+func Lines(mode LineInterpolationMode) Option {
+ return func(timeseries *TimeSeries) {
+ timeseries.Builder.TimeseriesPanel.FieldConfig.Defaults.Custom.LineInterpolation = string(mode)
+ timeseries.Builder.TimeseriesPanel.FieldConfig.Defaults.Custom.DrawStyle = "line"
+ timeseries.Builder.TimeseriesPanel.FieldConfig.Defaults.Custom.LineStyle = struct {
+ Fill string `json:"fill"`
+ }{
+ Fill: "solid",
+ }
+ }
+}
+
+// Bars displays the series as bars, with a given alignment strategy.
+func Bars(alignment BarAlignment) Option {
+ return func(timeseries *TimeSeries) {
+ timeseries.Builder.TimeseriesPanel.FieldConfig.Defaults.Custom.BarAlignment = int(alignment)
+ timeseries.Builder.TimeseriesPanel.FieldConfig.Defaults.Custom.DrawStyle = "bars"
+ }
+}
+
+// Points displays the series as points.
+func Points() Option {
+ return func(timeseries *TimeSeries) {
+ timeseries.Builder.TimeseriesPanel.FieldConfig.Defaults.Custom.DrawStyle = "points"
+ }
+}
+
+// GradientMode sets the mode of the gradient fill.
+func GradientMode(mode GradientType) Option {
+ return func(timeseries *TimeSeries) {
+ timeseries.Builder.TimeseriesPanel.FieldConfig.Defaults.Custom.GradientMode = string(mode)
+ }
+}
+
+// Axis configures the axis for this time series.
+func Axis(options ...axis.Option) Option {
+ return func(timeseries *TimeSeries) {
+ axis.New(×eries.Builder.TimeseriesPanel.FieldConfig, options...)
+ }
+}
+
+// Legend defines what should be shown in the legend.
+func Legend(opts ...LegendOption) Option {
+ return func(timeseries *TimeSeries) {
+ legend := sdk.TimeseriesLegendOptions{
+ DisplayMode: "list",
+ Placement: "bottom",
+ Calcs: make([]string, 0),
+ }
+
+ for _, opt := range opts {
+ switch opt {
+ case Hide:
+ legend.DisplayMode = "hidden"
+ case AsList:
+ legend.DisplayMode = "list"
+ case AsTable:
+ legend.DisplayMode = "table"
+ case ToTheRight:
+ legend.Placement = "right"
+ case Bottom:
+ legend.Placement = "bottom"
+
+ case First:
+ legend.Calcs = append(legend.Calcs, "first")
+ case FirstNonNull:
+ legend.Calcs = append(legend.Calcs, "firstNotNull")
+ case Last:
+ legend.Calcs = append(legend.Calcs, "last")
+ case LastNonNull:
+ legend.Calcs = append(legend.Calcs, "lastNotNull")
+
+ case Min:
+ legend.Calcs = append(legend.Calcs, "min")
+ case Max:
+ legend.Calcs = append(legend.Calcs, "max")
+ case Avg:
+ legend.Calcs = append(legend.Calcs, "mean")
+
+ case Count:
+ legend.Calcs = append(legend.Calcs, "count")
+ case Total:
+ legend.Calcs = append(legend.Calcs, "sum")
+ case Range:
+ legend.Calcs = append(legend.Calcs, "range")
+ }
+ }
+
+ timeseries.Builder.TimeseriesPanel.Options.Legend = legend
+ }
+}
+
+// Span sets the width of the panel, in grid units. Should be a positive
+// number between 1 and 12. Example: 6.
+func Span(span float32) Option {
+ return func(timeseries *TimeSeries) {
+ timeseries.Builder.Span = span
+ }
+}
+
+// Height sets the height of the panel, in pixels. Example: "400px".
+func Height(height string) Option {
+ return func(timeseries *TimeSeries) {
+ timeseries.Builder.Height = &height
+ }
+}
+
+// Description annotates the current visualization with a human-readable description.
+func Description(content string) Option {
+ return func(timeseries *TimeSeries) {
+ timeseries.Builder.Description = &content
+ }
+}
+
+// Transparent makes the background transparent.
+func Transparent() Option {
+ return func(timeseries *TimeSeries) {
+ timeseries.Builder.Transparent = true
+ }
+}
+
+// Alert creates an alert for this graph.
+func Alert(name string, opts ...alert.Option) Option {
+ return func(timeseries *TimeSeries) {
+ timeseries.Builder.Alert = alert.New(name, opts...).Builder
+ }
+}
+
+// Repeat configures repeating a panel for a variable
+func Repeat(repeat string) Option {
+ return func(timeseries *TimeSeries) {
+ timeseries.Builder.Repeat = &repeat
+ }
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 60e7e95d..b89c9e67 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -1,7 +1,7 @@
# cloud.google.com/go v0.81.0
## explicit; go 1.11
cloud.google.com/go/compute/metadata
-# github.com/K-Phoen/grabana v0.18.1
+# github.com/K-Phoen/grabana v0.19.0
## explicit; go 1.16
github.com/K-Phoen/grabana
github.com/K-Phoen/grabana/alert
@@ -20,6 +20,8 @@ github.com/K-Phoen/grabana/target/influxdb
github.com/K-Phoen/grabana/target/prometheus
github.com/K-Phoen/grabana/target/stackdriver
github.com/K-Phoen/grabana/text
+github.com/K-Phoen/grabana/timeseries
+github.com/K-Phoen/grabana/timeseries/axis
github.com/K-Phoen/grabana/variable/constant
github.com/K-Phoen/grabana/variable/custom
github.com/K-Phoen/grabana/variable/datasource