Skip to content

Commit

Permalink
Merge pull request #194 from K-Phoen/gauge-panel
Browse files Browse the repository at this point in the history
Support "gauge" panel type in the builder
  • Loading branch information
K-Phoen authored Aug 14, 2022
2 parents 99b335c + 1b1af1b commit 43a6c86
Show file tree
Hide file tree
Showing 16 changed files with 1,303 additions and 19 deletions.
70 changes: 70 additions & 0 deletions cmd/gauge-example/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package main

import (
"context"
"fmt"
"net/http"
"os"

"github.com/K-Phoen/grabana"
"github.com/K-Phoen/grabana/dashboard"
"github.com/K-Phoen/grabana/gauge"
"github.com/K-Phoen/grabana/row"
)

func main() {
if len(os.Args) != 3 {
fmt.Fprint(os.Stderr, "Usage: go run main.go http://grafana-host:3000 api-key-string-here\n")
os.Exit(1)
}

ctx := context.Background()
client := grabana.NewClient(&http.Client{}, os.Args[1], grabana.WithAPIToken(os.Args[2]))

// create the folder holding the dashboard for the service
folder, err := client.FindOrCreateFolder(ctx, "Test Folder")
if err != nil {
fmt.Printf("Could not find or create folder: %s\n", err)
os.Exit(1)
}

builder, err := dashboard.New(
"Grabana - Gauge example",
dashboard.AutoRefresh("30s"),
dashboard.Time("now-30m", "now"),
dashboard.Row(
"Kubernetes",
row.WithGauge(
"Cluster Pod Usage",
gauge.Span(6),
gauge.Height("400px"),
gauge.Unit("percentunit"),
gauge.Decimals(2),
gauge.AbsoluteThresholds([]gauge.ThresholdStep{
{Color: "#299c46"},
{Color: "rgba(237, 129, 40, 0.89)", Value: float64Ptr(0.8)},
{Color: "#d44a3a", Value: float64Ptr(0.9)},
}),
gauge.WithPrometheusTarget(
"sum(kube_pod_info{}) / sum(kube_node_status_allocatable{resource=\"pods\"})",
),
),
),
)
if err != nil {
fmt.Printf("Could not build dashboard: %s\n", err)
os.Exit(1)
}

dash, err := client.UpsertDashboard(ctx, folder, builder)
if err != nil {
fmt.Printf("Could not create dashboard: %s\n", err)
os.Exit(1)
}

fmt.Printf("The deed is done:\n%s\n", os.Args[1]+dash.URL)
}

func float64Ptr(input float64) *float64 {
return &input
}
4 changes: 4 additions & 0 deletions decoder/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ type DashboardPanel struct {
Heatmap *DashboardHeatmap `yaml:",omitempty"`
TimeSeries *DashboardTimeSeries `yaml:"timeseries,omitempty"`
Logs *DashboardLogs `yaml:"logs,omitempty"`
Gauge *DashboardGauge `yaml:"gauge,omitempty"`
}

func (panel DashboardPanel) toOption() (row.Option, error) {
Expand Down Expand Up @@ -139,6 +140,9 @@ func (panel DashboardPanel) toOption() (row.Option, error) {
if panel.Logs != nil {
return panel.Logs.toOption()
}
if panel.Gauge != nil {
return panel.Gauge.toOption()
}

return nil, ErrPanelNotConfigured
}
35 changes: 35 additions & 0 deletions decoder/dashboard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func TestUnmarshalYAML(t *testing.T) {
collapseRow(),
logsPanel(),
statPanel(),
gaugePanel(),
}

for _, testCase := range testCases {
Expand Down Expand Up @@ -601,6 +602,40 @@ rows:
}
}

func gaugePanel() testCase {
yaml := `title: Awesome dashboard
rows:
- name: Kubernetes
panels:
- gauge:
title: Cluster Pod Usage
description: Some description
height: 400px
span: 4
transparent: true
datasource: prometheus-default
targets:
- prometheus:
query: "sum(kube_pod_info{}) / sum(kube_node_status_allocatable{resource=\"pods\"})"
orientation: horizontal
unit: short
decimals: 2
title_font_size: 100
value_font_size: 150
thresholds:
- {color: green}
- {value: 1, color: orange}
- {value: 4, color: red}
`

return testCase{
name: "single row with one gauge panel",
yaml: yaml,
expectedGrafanaJSON: "gauge_panel.json",
}
}

func tablePanel() testCase {
yaml := `title: Awesome dashboard
Expand Down
199 changes: 199 additions & 0 deletions decoder/gauge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
package decoder

import (
"fmt"

"github.com/K-Phoen/grabana/gauge"
"github.com/K-Phoen/grabana/row"
)

var ErrInvalidGaugeThresholdMode = fmt.Errorf("invalid gauge threshold mode")
var ErrInvalidGaugeValueType = fmt.Errorf("invalid gauge value type")
var ErrInvalidGaugeOrientation = fmt.Errorf("invalid gauge orientation")

type GaugeThresholdStep struct {
Color string
Value *float64 `yaml:",omitempty"`
}

type DashboardGauge 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"`
Links DashboardPanelLinks `yaml:",omitempty"`
Targets []Target

Unit string `yaml:",omitempty"`
Decimals *int `yaml:",omitempty"`

Orientation string `yaml:",omitempty"`
ValueType string `yaml:"value_type,omitempty"`
TitleFontSize int `yaml:"title_font_size,omitempty"`
ValueFontSize int `yaml:"value_font_size,omitempty"`

ThresholdMode string `yaml:"threshold_mode,omitempty"`
Thresholds []GaugeThresholdStep `yaml:",omitempty"`
}

func (gaugePanel DashboardGauge) toOption() (row.Option, error) {
opts := []gauge.Option{}

if gaugePanel.Description != "" {
opts = append(opts, gauge.Description(gaugePanel.Description))
}
if gaugePanel.Span != 0 {
opts = append(opts, gauge.Span(gaugePanel.Span))
}
if gaugePanel.Height != "" {
opts = append(opts, gauge.Height(gaugePanel.Height))
}
if gaugePanel.Transparent {
opts = append(opts, gauge.Transparent())
}
if gaugePanel.Datasource != "" {
opts = append(opts, gauge.DataSource(gaugePanel.Datasource))
}
if gaugePanel.Repeat != "" {
opts = append(opts, gauge.Repeat(gaugePanel.Repeat))
}
if len(gaugePanel.Links) != 0 {
opts = append(opts, gauge.Links(gaugePanel.Links.toModel()...))
}
if gaugePanel.Unit != "" {
opts = append(opts, gauge.Unit(gaugePanel.Unit))
}
if gaugePanel.Decimals != nil {
opts = append(opts, gauge.Decimals(*gaugePanel.Decimals))
}
if gaugePanel.TitleFontSize != 0 {
opts = append(opts, gauge.TitleFontSize(gaugePanel.TitleFontSize))
}
if gaugePanel.ValueFontSize != 0 {
opts = append(opts, gauge.ValueFontSize(gaugePanel.ValueFontSize))
}

if gaugePanel.Orientation != "" {
opt, err := gaugePanel.orientationOpt()
if err != nil {
return nil, err
}
opts = append(opts, opt)
}
if gaugePanel.ValueType != "" {
opt, err := gaugePanel.valueType()
if err != nil {
return nil, err
}

opts = append(opts, opt)
}

if len(gaugePanel.Thresholds) != 0 {
opt, err := gaugePanel.thresholds()
if err != nil {
return nil, err
}

opts = append(opts, opt)
}

for _, t := range gaugePanel.Targets {
opt, err := gaugePanel.target(t)
if err != nil {
return nil, err
}

opts = append(opts, opt)
}

return row.WithGauge(gaugePanel.Title, opts...), nil
}

func (gaugePanel DashboardGauge) thresholds() (gauge.Option, error) {
thresholds := make([]gauge.ThresholdStep, 0, len(gaugePanel.Thresholds))
for _, threshold := range gaugePanel.Thresholds {
thresholds = append(thresholds, gauge.ThresholdStep{
Color: threshold.Color,
Value: threshold.Value,
})
}

switch gaugePanel.ThresholdMode {
case "absolute":
return gauge.AbsoluteThresholds(thresholds), nil
case "":
return gauge.AbsoluteThresholds(thresholds), nil
case "relative":
return gauge.RelativeThresholds(thresholds), nil
}

return nil, fmt.Errorf("got mode '%s': %w", gaugePanel.ThresholdMode, ErrInvalidGaugeThresholdMode)
}

func (gaugePanel DashboardGauge) valueType() (gauge.Option, error) {
switch gaugePanel.ValueType {
case "min":
return gauge.ValueType(gauge.Min), nil
case "max":
return gauge.ValueType(gauge.Max), nil
case "avg":
return gauge.ValueType(gauge.Avg), nil

case "count":
return gauge.ValueType(gauge.Count), nil
case "total":
return gauge.ValueType(gauge.Total), nil
case "range":
return gauge.ValueType(gauge.Range), nil

case "first":
return gauge.ValueType(gauge.First), nil
case "first_non_null":
return gauge.ValueType(gauge.FirstNonNull), nil
case "last":
return gauge.ValueType(gauge.Last), nil
case "last_non_null":
return gauge.ValueType(gauge.LastNonNull), nil
default:
return nil, ErrInvalidGaugeValueType
}
}

func (gaugePanel DashboardGauge) orientationOpt() (gauge.Option, error) {
switch gaugePanel.Orientation {
case "horizontal":
return gauge.Orientation(gauge.OrientationHorizontal), nil
case "vertical":
return gauge.Orientation(gauge.OrientationVertical), nil
case "auto":
return gauge.Orientation(gauge.OrientationAuto), nil
default:
return nil, ErrInvalidGaugeOrientation
}
}

func (gaugePanel DashboardGauge) target(t Target) (gauge.Option, error) {
if t.Prometheus != nil {
return gauge.WithPrometheusTarget(t.Prometheus.Query, t.Prometheus.toOptions()...), nil
}
if t.Graphite != nil {
return gauge.WithGraphiteTarget(t.Graphite.Query, t.Graphite.toOptions()...), nil
}
if t.InfluxDB != nil {
return gauge.WithInfluxDBTarget(t.InfluxDB.Query, t.InfluxDB.toOptions()...), nil
}
if t.Stackdriver != nil {
stackdriverTarget, err := t.Stackdriver.toTarget()
if err != nil {
return nil, err
}

return gauge.WithStackdriverTarget(stackdriverTarget), nil
}

return nil, ErrTargetNotConfigured
}
47 changes: 47 additions & 0 deletions decoder/gauge_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package decoder

import (
"testing"

"github.com/K-Phoen/grabana/gauge"
"github.com/stretchr/testify/require"
)

func TestGaugeValidValueTypes(t *testing.T) {
testCases := []struct {
input string
expected string
}{
{input: "min", expected: "min"},
{input: "max", expected: "max"},
{input: "avg", expected: "mean"},
{input: "count", expected: "count"},
{input: "total", expected: "sum"},
{input: "range", expected: "range"},
{input: "first", expected: "first"},
{input: "first_non_null", expected: "firstNotNull"},
{input: "last", expected: "last"},
{input: "last_non_null", expected: "lastNotNull"},
}

for _, testCase := range testCases {
tc := testCase

t.Run(tc.input, func(t *testing.T) {
req := require.New(t)

panel := DashboardGauge{ValueType: tc.input}

opt, err := panel.valueType()

req.NoError(err)

gaugePanel, err := gauge.New("")
req.NoError(err)

req.NoError(opt(gaugePanel))

req.Equal(tc.expected, gaugePanel.Builder.GaugePanel.Options.ReduceOptions.Calcs[0])
})
}
}
Loading

0 comments on commit 43a6c86

Please sign in to comment.