diff --git a/go.mod b/go.mod index daa35abd..86e370cf 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/K-Phoen/dark go 1.19 require ( - github.com/K-Phoen/grabana v0.21.14 + github.com/K-Phoen/grabana v0.21.15 github.com/K-Phoen/sdk v0.12.0 github.com/go-logr/logr v1.2.3 github.com/onsi/ginkgo v1.16.5 diff --git a/go.sum b/go.sum index 3c64734c..06269a46 100644 --- a/go.sum +++ b/go.sum @@ -38,8 +38,8 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 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.21.14 h1:sosOZQ5APT4s08F6amqLEbv7b+//7YFyzSdWazIQPsQ= -github.com/K-Phoen/grabana v0.21.14/go.mod h1:HDl99djdna5Auu7RcOgxE64HnvQ9UBIpqc+A2A5XnL8= +github.com/K-Phoen/grabana v0.21.15 h1:rUyJm9pV2XiPWQNVDjR+ubEBP+A8zv0hpRQmZjFcBnM= +github.com/K-Phoen/grabana v0.21.15/go.mod h1:HDl99djdna5Auu7RcOgxE64HnvQ9UBIpqc+A2A5XnL8= github.com/K-Phoen/sdk v0.12.0 h1:+0QqHoDZbO6zetFMggM3zKF48GKRu744Ycc9w4oyY0E= github.com/K-Phoen/sdk v0.12.0/go.mod h1:wp7qXARaIhCYktmoOjRZ+TDMlek5nbayC+waN7vigxI= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= diff --git a/internal/pkg/converter/json_test.go b/internal/pkg/converter/json_test.go index 9a631bdb..d7c15fbc 100644 --- a/internal/pkg/converter/json_test.go +++ b/internal/pkg/converter/json_test.go @@ -120,11 +120,3 @@ func TestConvertLinks(t *testing.T) { req.Equal(1, len(dashboard.DashboardLinks)) req.Equal(1, len(dashboard.ExternalLinks)) } - -func strPtr(input string) *string { - return &input -} - -func boolPtr(input bool) *bool { - return &input -} diff --git a/internal/pkg/converter/timeseries.go b/internal/pkg/converter/timeseries.go index 487b68c5..51206f9b 100644 --- a/internal/pkg/converter/timeseries.go +++ b/internal/pkg/converter/timeseries.go @@ -1,6 +1,8 @@ package converter import ( + "fmt" + grabana "github.com/K-Phoen/grabana/decoder" "github.com/K-Phoen/sdk" "go.uber.org/zap" @@ -14,6 +16,7 @@ func (converter *JSON) convertTimeSeries(panel sdk.Panel) grabana.DashboardPanel Legend: converter.convertTimeSeriesLegend(panel.TimeseriesPanel.Options.Legend), Visualization: converter.convertTimeSeriesVisualization(panel), Axis: converter.convertTimeSeriesAxis(panel), + Overrides: converter.convertTimeSeriesOverrides(panel), } if panel.Description != nil { @@ -210,3 +213,104 @@ func (converter *JSON) convertTimeSeriesLegend(legend sdk.TimeseriesLegendOption return options } + +func (converter *JSON) convertTimeSeriesOverrides(panel sdk.Panel) []grabana.TimeSeriesOverride { + overrides := make([]grabana.TimeSeriesOverride, 0, len(panel.TimeseriesPanel.FieldConfig.Overrides)) + + for _, sdkOverride := range panel.TimeseriesPanel.FieldConfig.Overrides { + override, err := converter.convertTimeSeriesOverride(sdkOverride) + if err != nil { + converter.logger.Warn("could not convert field override: skipping", zap.Error(err)) + continue + } + + overrides = append(overrides, override) + } + + return overrides +} + +func (converter *JSON) convertTimeSeriesOverride(sdkOverride sdk.FieldConfigOverride) (grabana.TimeSeriesOverride, error) { + override := grabana.TimeSeriesOverride{} + + matcher, err := converter.convertTimeSeriesOverrideMatcher(sdkOverride.Matcher) + if err != nil { + return override, err + } + override.Matcher = matcher + + properties, err := converter.convertTimeSeriesOverrideProperties(sdkOverride.Properties) + if err != nil { + return override, err + } + override.Properties = properties + + return override, nil +} + +func (converter *JSON) convertTimeSeriesOverrideMatcher(matcher struct { + ID string `json:"id"` + Options string `json:"options"` +}) (grabana.TimeSeriesOverrideMatcher, error) { + switch matcher.ID { + case "byName": + return grabana.TimeSeriesOverrideMatcher{FieldName: &matcher.Options}, nil + case "byFrameRefID": + return grabana.TimeSeriesOverrideMatcher{QueryRef: &matcher.Options}, nil + case "byRegexp": + return grabana.TimeSeriesOverrideMatcher{Regex: &matcher.Options}, nil + case "byType": + return grabana.TimeSeriesOverrideMatcher{Type: &matcher.Options}, nil + default: + return grabana.TimeSeriesOverrideMatcher{}, fmt.Errorf("unknown field override matcher '%s'", matcher.ID) + } +} + +func (converter *JSON) convertTimeSeriesOverrideProperties(sdkProperties []sdk.FieldConfigOverrideProperty) (grabana.TimeSeriesOverrideProperties, error) { + properties := grabana.TimeSeriesOverrideProperties{} + + for _, sdkProperty := range sdkProperties { + converter.convertTimeSeriesOverrideProperty(sdkProperty, &properties) + } + + return properties, nil +} + +func (converter *JSON) convertTimeSeriesOverrideProperty(sdkProperty sdk.FieldConfigOverrideProperty, properties *grabana.TimeSeriesOverrideProperties) { + switch sdkProperty.ID { + case "unit": + properties.Unit = strPtr(sdkProperty.Value.(string)) + case "custom.axisPlacement": + properties.AxisDisplay = strPtr(sdkProperty.Value.(string)) + case "custom.fillOpacity": + properties.FillOpacity = intPtr(int(sdkProperty.Value.(float64))) + case "custom.stacking": + options, ok := sdkProperty.Value.(map[string]interface{}) + if !ok { + converter.logger.Warn("could not convert custom.stacking field override: invalid options") + break + } + properties.Stack = strPtr(options["mode"].(string)) + case "custom.transform": + transformType := sdkProperty.Value.(string) + if transformType != "negative-Y" { + converter.logger.Warn("could not convert transform field override: invalid option") + break + } + properties.NegativeY = boolPtr(true) + case "color": + options, ok := sdkProperty.Value.(map[string]interface{}) + if !ok { + converter.logger.Warn("could not convert color field override: invalid options") + break + } + if options["mode"] != "fixed" { + converter.logger.Warn("could not convert color field override: unsupported mode") + break + } + + properties.Color = strPtr(options["fixedColor"].(string)) + default: + converter.logger.Warn(fmt.Sprintf("unhandled override type '%s'", sdkProperty.ID)) + } +} diff --git a/internal/pkg/converter/utils.go b/internal/pkg/converter/utils.go index 75b867b4..8c29e73f 100644 --- a/internal/pkg/converter/utils.go +++ b/internal/pkg/converter/utils.go @@ -10,6 +10,10 @@ func stringInSlice(search string, haystack []string) bool { return false } +func boolPtr(input bool) *bool { + return &input +} + func intPtr(input int) *int { return &input } @@ -17,3 +21,7 @@ func intPtr(input int) *int { func float64Ptr(input float64) *float64 { return &input } + +func strPtr(input string) *string { + return &input +} diff --git a/vendor/github.com/K-Phoen/grabana/decoder/timeseries.go b/vendor/github.com/K-Phoen/grabana/decoder/timeseries.go index 33f3675b..33d915b1 100644 --- a/vendor/github.com/K-Phoen/grabana/decoder/timeseries.go +++ b/vendor/github.com/K-Phoen/grabana/decoder/timeseries.go @@ -6,6 +6,7 @@ import ( "github.com/K-Phoen/grabana/row" "github.com/K-Phoen/grabana/timeseries" "github.com/K-Phoen/grabana/timeseries/axis" + "github.com/K-Phoen/grabana/timeseries/fields" ) var ErrInvalidGradientMode = fmt.Errorf("invalid gradient mode") @@ -14,6 +15,7 @@ var ErrInvalidTooltipMode = fmt.Errorf("invalid tooltip mode") var ErrInvalidStackMode = fmt.Errorf("invalid stack mode") var ErrInvalidAxisDisplay = fmt.Errorf("invalid axis display") var ErrInvalidAxisScale = fmt.Errorf("invalid axis scale") +var ErrInvalidOverrideMatcher = fmt.Errorf("invalid override matcher") type DashboardTimeSeries struct { Title string @@ -29,6 +31,7 @@ type DashboardTimeSeries struct { Alert *Alert `yaml:",omitempty"` Visualization *TimeSeriesVisualization `yaml:",omitempty"` Axis *TimeSeriesAxis `yaml:",omitempty"` + Overrides []TimeSeriesOverride `yaml:",omitempty"` } func (timeseriesPanel DashboardTimeSeries) toOption() (row.Option, error) { @@ -88,6 +91,15 @@ func (timeseriesPanel DashboardTimeSeries) toOption() (row.Option, error) { opts = append(opts, timeseries.Axis(axisOpts...)) } + for _, override := range timeseriesPanel.Overrides { + opt, err := override.toOption() + if err != nil { + return nil, err + } + + opts = append(opts, opt) + } + for _, t := range timeseriesPanel.Targets { opt, err := timeseriesPanel.target(t) if err != nil { @@ -367,22 +379,29 @@ func (tsAxis *TimeSeriesAxis) toOptions() ([]axis.Option, error) { } func (tsAxis *TimeSeriesAxis) placementOption() (axis.Option, error) { - var placementMode axis.PlacementMode + placementMode, err := axisPlacementFromString(tsAxis.Display) + if err != nil { + return nil, err + } + + return axis.Placement(placementMode), nil +} - switch tsAxis.Display { +func axisPlacementFromString(input string) (axis.PlacementMode, error) { + switch input { case "none": - placementMode = axis.Hidden + return axis.Hidden, nil + case "hidden": + return axis.Hidden, nil case "auto": - placementMode = axis.Auto + return axis.Auto, nil case "left": - placementMode = axis.Left + return axis.Left, nil case "right": - placementMode = axis.Right + return axis.Right, nil default: - return nil, ErrInvalidAxisDisplay + return axis.Auto, ErrInvalidAxisDisplay } - - return axis.Placement(placementMode), nil } func (tsAxis *TimeSeriesAxis) scaleOption() (axis.Option, error) { @@ -401,3 +420,85 @@ func (tsAxis *TimeSeriesAxis) scaleOption() (axis.Option, error) { return axis.Scale(scaleMode), nil } + +type TimeSeriesOverride struct { + Matcher TimeSeriesOverrideMatcher `yaml:"match,flow"` + Properties TimeSeriesOverrideProperties +} + +func (override TimeSeriesOverride) toOption() (timeseries.Option, error) { + matcher, err := override.Matcher.toOption() + if err != nil { + return nil, err + } + + overrideOpts, err := override.Properties.toOptions() + if err != nil { + return nil, err + } + + return timeseries.FieldOverride(matcher, overrideOpts...), nil +} + +type TimeSeriesOverrideMatcher struct { + FieldName *string `yaml:"field_name,omitempty"` + QueryRef *string `yaml:"query_ref,omitempty"` + Regex *string `yaml:"regex,omitempty"` + Type *string `yaml:"field_type,omitempty"` +} + +func (matcher TimeSeriesOverrideMatcher) toOption() (fields.Matcher, error) { + if matcher.FieldName != nil { + return fields.ByName(*matcher.FieldName), nil + } + if matcher.QueryRef != nil { + return fields.ByQuery(*matcher.QueryRef), nil + } + if matcher.Regex != nil { + return fields.ByRegex(*matcher.Regex), nil + } + if matcher.Type != nil { + return fields.ByType(fields.FieldType(*matcher.Type)), nil + } + + return nil, ErrInvalidOverrideMatcher +} + +type TimeSeriesOverrideProperties struct { + Unit *string `yaml:",omitempty"` + Color *string `yaml:"color,omitempty"` + FillOpacity *int `yaml:"fill_opacity,omitempty"` + NegativeY *bool `yaml:"negative_Y,omitempty"` + AxisDisplay *string `yaml:"axis_display,omitempty"` + Stack *string `yaml:",omitempty"` +} + +func (properties TimeSeriesOverrideProperties) toOptions() ([]fields.OverrideOption, error) { + var opts []fields.OverrideOption + + if properties.Unit != nil { + opts = append(opts, fields.Unit(*properties.Unit)) + } + if properties.Color != nil { + opts = append(opts, fields.FixedColorScheme(*properties.Color)) + } + if properties.FillOpacity != nil { + opts = append(opts, fields.FillOpacity(*properties.FillOpacity)) + } + if properties.NegativeY != nil && *properties.NegativeY { + opts = append(opts, fields.NegativeY()) + } + if properties.AxisDisplay != nil { + axisPlacement, err := axisPlacementFromString(*properties.AxisDisplay) + if err != nil { + return nil, err + } + + opts = append(opts, fields.AxisPlacement(axisPlacement)) + } + if properties.Stack != nil { + opts = append(opts, fields.Stack(fields.StackMode(*properties.Stack))) + } + + return opts, nil +} diff --git a/vendor/github.com/K-Phoen/grabana/gauge/gauge.go b/vendor/github.com/K-Phoen/grabana/gauge/gauge.go index 0eb0f2a1..ecab455e 100644 --- a/vendor/github.com/K-Phoen/grabana/gauge/gauge.go +++ b/vendor/github.com/K-Phoen/grabana/gauge/gauge.go @@ -131,6 +131,7 @@ func WithPrometheusTarget(query string, options ...prometheus.Option) Option { return func(gauge *Gauge) error { gauge.Builder.AddTarget(&sdk.Target{ RefID: target.Ref, + Hide: target.Hidden, Expr: target.Expr, IntervalFactor: target.IntervalFactor, Interval: target.Interval, diff --git a/vendor/github.com/K-Phoen/grabana/heatmap/heatmap.go b/vendor/github.com/K-Phoen/grabana/heatmap/heatmap.go index 5931c4e4..0d4746a2 100644 --- a/vendor/github.com/K-Phoen/grabana/heatmap/heatmap.go +++ b/vendor/github.com/K-Phoen/grabana/heatmap/heatmap.go @@ -147,6 +147,7 @@ func WithPrometheusTarget(query string, options ...prometheus.Option) Option { return func(heatmap *Heatmap) error { heatmap.Builder.AddTarget(&sdk.Target{ RefID: target.Ref, + Hide: target.Hidden, Expr: target.Expr, IntervalFactor: target.IntervalFactor, Interval: target.Interval, diff --git a/vendor/github.com/K-Phoen/grabana/singlestat/singlestat.go b/vendor/github.com/K-Phoen/grabana/singlestat/singlestat.go index 3e5731fa..c653cb66 100644 --- a/vendor/github.com/K-Phoen/grabana/singlestat/singlestat.go +++ b/vendor/github.com/K-Phoen/grabana/singlestat/singlestat.go @@ -158,6 +158,7 @@ func WithPrometheusTarget(query string, options ...prometheus.Option) Option { return func(singleStat *SingleStat) error { singleStat.Builder.AddTarget(&sdk.Target{ RefID: target.Ref, + Hide: target.Hidden, Expr: target.Expr, IntervalFactor: target.IntervalFactor, Interval: target.Interval, diff --git a/vendor/github.com/K-Phoen/grabana/stat/stat.go b/vendor/github.com/K-Phoen/grabana/stat/stat.go index 20f33dec..3b278cf1 100644 --- a/vendor/github.com/K-Phoen/grabana/stat/stat.go +++ b/vendor/github.com/K-Phoen/grabana/stat/stat.go @@ -144,6 +144,7 @@ func WithPrometheusTarget(query string, options ...prometheus.Option) Option { return func(stat *Stat) error { stat.Builder.AddTarget(&sdk.Target{ RefID: target.Ref, + Hide: target.Hidden, Expr: target.Expr, IntervalFactor: target.IntervalFactor, Interval: target.Interval, diff --git a/vendor/github.com/K-Phoen/grabana/timeseries/axis/axis.go b/vendor/github.com/K-Phoen/grabana/timeseries/axis/axis.go index 3fcf2027..58077b9b 100644 --- a/vendor/github.com/K-Phoen/grabana/timeseries/axis/axis.go +++ b/vendor/github.com/K-Phoen/grabana/timeseries/axis/axis.go @@ -83,7 +83,7 @@ func Min(value float64) Option { } } -// SoftMax defines a hard maximum value for the axis. +// Max defines a hard maximum value for the axis. func Max(value float64) Option { return func(axis *Axis) error { axis.fieldConfig.Defaults.Max = &value diff --git a/vendor/github.com/K-Phoen/grabana/timeseries/fields/matcher.go b/vendor/github.com/K-Phoen/grabana/timeseries/fields/matcher.go index 77e98639..906b81ab 100644 --- a/vendor/github.com/K-Phoen/grabana/timeseries/fields/matcher.go +++ b/vendor/github.com/K-Phoen/grabana/timeseries/fields/matcher.go @@ -6,6 +6,12 @@ import ( type Matcher func(field *sdk.FieldConfigOverride) +type FieldType string + +const ( + FieldTypeTime FieldType = "time" +) + // ByName matches a specific field name. func ByName(name string) Matcher { return func(field *sdk.FieldConfigOverride) { @@ -21,3 +27,19 @@ func ByQuery(ref string) Matcher { field.Matcher.Options = ref } } + +// ByRegex matches fields names using a regex. +func ByRegex(regex string) Matcher { + return func(field *sdk.FieldConfigOverride) { + field.Matcher.ID = "byRegexp" + field.Matcher.Options = regex + } +} + +// ByType matches fields with a specific type. +func ByType(fieldType FieldType) Matcher { + return func(field *sdk.FieldConfigOverride) { + field.Matcher.ID = "byType" + field.Matcher.Options = string(fieldType) + } +} diff --git a/vendor/github.com/K-Phoen/grabana/timeseries/fields/override.go b/vendor/github.com/K-Phoen/grabana/timeseries/fields/override.go index a594141b..e0bdb228 100644 --- a/vendor/github.com/K-Phoen/grabana/timeseries/fields/override.go +++ b/vendor/github.com/K-Phoen/grabana/timeseries/fields/override.go @@ -1,6 +1,22 @@ package fields -import "github.com/K-Phoen/sdk" +import ( + "github.com/K-Phoen/grabana/timeseries/axis" + "github.com/K-Phoen/sdk" +) + +// StackMode configures mode of series stacking. +// FIXME: copied here to avoid circular imports with parent package +type StackMode string + +const ( + // Unstacked will not stack series + Unstacked StackMode = "none" + // NormalStack will stack series as absolute numbers + NormalStack StackMode = "normal" + // PercentStack will stack series as percents + PercentStack StackMode = "percent" +) type OverrideOption func(field *sdk.FieldConfigOverride) @@ -39,3 +55,39 @@ func FixedColorScheme(color string) OverrideOption { }) } } + +// NegativeY flips the results to negative values on the Y axis. +func NegativeY() OverrideOption { + return func(field *sdk.FieldConfigOverride) { + field.Properties = append(field.Properties, + sdk.FieldConfigOverrideProperty{ + ID: "custom.transform", + Value: "negative-Y", + }) + } +} + +// AxisPlacement overrides how the axis should be placed in the panel. +func AxisPlacement(placement axis.PlacementMode) OverrideOption { + return func(field *sdk.FieldConfigOverride) { + field.Properties = append(field.Properties, + sdk.FieldConfigOverrideProperty{ + ID: "custom.axisPlacement", + Value: string(placement), + }) + } +} + +// Stack overrides if the series should be stacked and using which mode (default not stacked). +func Stack(mode StackMode) OverrideOption { + return func(field *sdk.FieldConfigOverride) { + field.Properties = append(field.Properties, + sdk.FieldConfigOverrideProperty{ + ID: "custom.stacking", + Value: map[string]interface{}{ + "group": false, + "mode": string(mode), + }, + }) + } +} diff --git a/vendor/github.com/K-Phoen/grabana/timeseries/timeseries.go b/vendor/github.com/K-Phoen/grabana/timeseries/timeseries.go index b73b9503..02d14b11 100644 --- a/vendor/github.com/K-Phoen/grabana/timeseries/timeseries.go +++ b/vendor/github.com/K-Phoen/grabana/timeseries/timeseries.go @@ -202,7 +202,7 @@ func LineWidth(value int) Option { } } -// Stack defines if the series should be stacked and using which mode (default not stacked). the opacity level of the series. The lower the value, the more transparent. +// Stack defines if the series should be stacked and using which mode (default not stacked). func Stack(value StackMode) Option { return func(timeseries *TimeSeries) error { timeseries.Builder.TimeseriesPanel.FieldConfig.Defaults.Custom.Stacking.Mode = string(value) diff --git a/vendor/modules.txt b/vendor/modules.txt index b3b063fd..607154c2 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,4 +1,4 @@ -# github.com/K-Phoen/grabana v0.21.14 +# github.com/K-Phoen/grabana v0.21.15 ## explicit; go 1.19 github.com/K-Phoen/grabana github.com/K-Phoen/grabana/alert