From c0140f9fd05af3b863002add952984ca0984c515 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Thu, 19 Mar 2020 23:17:38 +0100 Subject: [PATCH 01/24] Bootstran work on dashboard unmarshaller from Yaml --- cmd/yaml/example.yaml | 5 ++++ cmd/yaml/main.go | 32 +++++++++++++++++++++++++ go.mod | 1 + yaml.go | 54 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 92 insertions(+) create mode 100644 cmd/yaml/example.yaml create mode 100644 cmd/yaml/main.go create mode 100644 yaml.go diff --git a/cmd/yaml/example.yaml b/cmd/yaml/example.yaml new file mode 100644 index 00000000..df165628 --- /dev/null +++ b/cmd/yaml/example.yaml @@ -0,0 +1,5 @@ +title: Awesome dashboard +editable: true +shared_crosshair: true +tags: [generated, yaml] +autorefresh: 10s \ No newline at end of file diff --git a/cmd/yaml/main.go b/cmd/yaml/main.go new file mode 100644 index 00000000..32c9f5a2 --- /dev/null +++ b/cmd/yaml/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + + "github.com/K-Phoen/grabana" +) + +func main() { + if len(os.Args) != 2 { + fmt.Fprint(os.Stderr, "Usage: go run -mod=vendor main.go file\n") + os.Exit(1) + } + + content, err := ioutil.ReadFile(os.Args[1]) + if err != nil { + fmt.Fprintf(os.Stderr, "Could not read file: %s\n", err) + os.Exit(1) + } + + builder, err := grabana.UnmarshalYAML(bytes.NewBuffer(content)) + if err != nil { + fmt.Fprintf(os.Stderr, "Could not parse file: %s\n", err) + os.Exit(1) + } + + json, _ := builder.MarshalJSON() + fmt.Printf("Dashboard JSON: %s\n", json) +} diff --git a/go.mod b/go.mod index c6b6a056..7d2b7491 100644 --- a/go.mod +++ b/go.mod @@ -5,4 +5,5 @@ go 1.13 require ( github.com/grafana-tools/sdk v0.0.0-20200127194913-bdcab199ffde github.com/stretchr/testify v1.4.0 + gopkg.in/yaml.v2 v2.2.2 ) diff --git a/yaml.go b/yaml.go new file mode 100644 index 00000000..834170e8 --- /dev/null +++ b/yaml.go @@ -0,0 +1,54 @@ +package grabana + +import ( + "fmt" + "io" + + "gopkg.in/yaml.v2" +) + +func UnmarshalYAML(input io.Reader) (DashboardBuilder, error) { + decoder := yaml.NewDecoder(input) + decoder.SetStrict(true) + + parsed := &dashboardYaml{} + if err := decoder.Decode(parsed); err != nil { + return DashboardBuilder{}, err + } + + fmt.Printf("parsed: %#v\n", parsed) + + return parsed.toDashboardBuilder() +} + +type dashboardYaml struct { + Title string `yaml:"title"` + Editable bool `yaml:"editable"` + SharedCrosshair bool `yaml:"shared_crosshair"` + Tags []string `yaml:"tags"` + AutoRefresh string `yaml:"autorefresh"` +} + +func (dashboard *dashboardYaml) toDashboardBuilder() (DashboardBuilder, error) { + opts := []DashboardBuilderOption{ + dashboard.sharedCrossHair(), + } + + if len(dashboard.Tags) != 0 { + opts = append(opts, Tags(dashboard.Tags)) + } + + if dashboard.AutoRefresh != "" { + opts = append(opts, AutoRefresh(dashboard.AutoRefresh)) + } + + return NewDashboardBuilder(dashboard.Title, opts...), nil +} + +func (dashboard *dashboardYaml) sharedCrossHair() DashboardBuilderOption { + if dashboard.SharedCrosshair { + return SharedCrossHair() + } + + return DefaultTooltip() +} From 878aba96942ab4d15f15474790a68a9a4e5f2a2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Thu, 19 Mar 2020 23:18:00 +0100 Subject: [PATCH 02/24] Vendor cleanup --- cmd/yaml/example.yaml | 2 +- go.sum | 1 + .../github.com/stretchr/testify/suite/doc.go | 65 ------- .../stretchr/testify/suite/interfaces.go | 46 ----- .../stretchr/testify/suite/suite.go | 166 ------------------ vendor/modules.txt | 1 - yaml.go | 11 +- 7 files changed, 12 insertions(+), 280 deletions(-) delete mode 100644 vendor/github.com/stretchr/testify/suite/doc.go delete mode 100644 vendor/github.com/stretchr/testify/suite/interfaces.go delete mode 100644 vendor/github.com/stretchr/testify/suite/suite.go diff --git a/cmd/yaml/example.yaml b/cmd/yaml/example.yaml index df165628..23164ec3 100644 --- a/cmd/yaml/example.yaml +++ b/cmd/yaml/example.yaml @@ -2,4 +2,4 @@ title: Awesome dashboard editable: true shared_crosshair: true tags: [generated, yaml] -autorefresh: 10s \ No newline at end of file +auto_refresh: 10s \ No newline at end of file diff --git a/go.sum b/go.sum index 749e6c7f..d3b48c54 100644 --- a/go.sum +++ b/go.sum @@ -11,6 +11,7 @@ github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDF github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/vendor/github.com/stretchr/testify/suite/doc.go b/vendor/github.com/stretchr/testify/suite/doc.go deleted file mode 100644 index f91a245d..00000000 --- a/vendor/github.com/stretchr/testify/suite/doc.go +++ /dev/null @@ -1,65 +0,0 @@ -// Package suite contains logic for creating testing suite structs -// and running the methods on those structs as tests. The most useful -// piece of this package is that you can create setup/teardown methods -// on your testing suites, which will run before/after the whole suite -// or individual tests (depending on which interface(s) you -// implement). -// -// A testing suite is usually built by first extending the built-in -// suite functionality from suite.Suite in testify. Alternatively, -// you could reproduce that logic on your own if you wanted (you -// just need to implement the TestingSuite interface from -// suite/interfaces.go). -// -// After that, you can implement any of the interfaces in -// suite/interfaces.go to add setup/teardown functionality to your -// suite, and add any methods that start with "Test" to add tests. -// Methods that do not match any suite interfaces and do not begin -// with "Test" will not be run by testify, and can safely be used as -// helper methods. -// -// Once you've built your testing suite, you need to run the suite -// (using suite.Run from testify) inside any function that matches the -// identity that "go test" is already looking for (i.e. -// func(*testing.T)). -// -// Regular expression to select test suites specified command-line -// argument "-run". Regular expression to select the methods -// of test suites specified command-line argument "-m". -// Suite object has assertion methods. -// -// A crude example: -// // Basic imports -// import ( -// "testing" -// "github.com/stretchr/testify/assert" -// "github.com/stretchr/testify/suite" -// ) -// -// // Define the suite, and absorb the built-in basic suite -// // functionality from testify - including a T() method which -// // returns the current testing context -// type ExampleTestSuite struct { -// suite.Suite -// VariableThatShouldStartAtFive int -// } -// -// // Make sure that VariableThatShouldStartAtFive is set to five -// // before each test -// func (suite *ExampleTestSuite) SetupTest() { -// suite.VariableThatShouldStartAtFive = 5 -// } -// -// // All methods that begin with "Test" are run as tests within a -// // suite. -// func (suite *ExampleTestSuite) TestExample() { -// assert.Equal(suite.T(), 5, suite.VariableThatShouldStartAtFive) -// suite.Equal(5, suite.VariableThatShouldStartAtFive) -// } -// -// // In order for 'go test' to run this suite, we need to create -// // a normal test function and pass our suite to suite.Run -// func TestExampleTestSuite(t *testing.T) { -// suite.Run(t, new(ExampleTestSuite)) -// } -package suite diff --git a/vendor/github.com/stretchr/testify/suite/interfaces.go b/vendor/github.com/stretchr/testify/suite/interfaces.go deleted file mode 100644 index b37cb040..00000000 --- a/vendor/github.com/stretchr/testify/suite/interfaces.go +++ /dev/null @@ -1,46 +0,0 @@ -package suite - -import "testing" - -// TestingSuite can store and return the current *testing.T context -// generated by 'go test'. -type TestingSuite interface { - T() *testing.T - SetT(*testing.T) -} - -// SetupAllSuite has a SetupSuite method, which will run before the -// tests in the suite are run. -type SetupAllSuite interface { - SetupSuite() -} - -// SetupTestSuite has a SetupTest method, which will run before each -// test in the suite. -type SetupTestSuite interface { - SetupTest() -} - -// TearDownAllSuite has a TearDownSuite method, which will run after -// all the tests in the suite have been run. -type TearDownAllSuite interface { - TearDownSuite() -} - -// TearDownTestSuite has a TearDownTest method, which will run after -// each test in the suite. -type TearDownTestSuite interface { - TearDownTest() -} - -// BeforeTest has a function to be executed right before the test -// starts and receives the suite and test names as input -type BeforeTest interface { - BeforeTest(suiteName, testName string) -} - -// AfterTest has a function to be executed right after the test -// finishes and receives the suite and test names as input -type AfterTest interface { - AfterTest(suiteName, testName string) -} diff --git a/vendor/github.com/stretchr/testify/suite/suite.go b/vendor/github.com/stretchr/testify/suite/suite.go deleted file mode 100644 index d708d7d7..00000000 --- a/vendor/github.com/stretchr/testify/suite/suite.go +++ /dev/null @@ -1,166 +0,0 @@ -package suite - -import ( - "flag" - "fmt" - "os" - "reflect" - "regexp" - "runtime/debug" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -var allTestsFilter = func(_, _ string) (bool, error) { return true, nil } -var matchMethod = flag.String("testify.m", "", "regular expression to select tests of the testify suite to run") - -// Suite is a basic testing suite with methods for storing and -// retrieving the current *testing.T context. -type Suite struct { - *assert.Assertions - require *require.Assertions - t *testing.T -} - -// T retrieves the current *testing.T context. -func (suite *Suite) T() *testing.T { - return suite.t -} - -// SetT sets the current *testing.T context. -func (suite *Suite) SetT(t *testing.T) { - suite.t = t - suite.Assertions = assert.New(t) - suite.require = require.New(t) -} - -// Require returns a require context for suite. -func (suite *Suite) Require() *require.Assertions { - if suite.require == nil { - suite.require = require.New(suite.T()) - } - return suite.require -} - -// Assert returns an assert context for suite. Normally, you can call -// `suite.NoError(expected, actual)`, but for situations where the embedded -// methods are overridden (for example, you might want to override -// assert.Assertions with require.Assertions), this method is provided so you -// can call `suite.Assert().NoError()`. -func (suite *Suite) Assert() *assert.Assertions { - if suite.Assertions == nil { - suite.Assertions = assert.New(suite.T()) - } - return suite.Assertions -} - -func failOnPanic(t *testing.T) { - r := recover() - if r != nil { - t.Errorf("test panicked: %v\n%s", r, debug.Stack()) - t.FailNow() - } -} - -// Run provides suite functionality around golang subtests. It should be -// called in place of t.Run(name, func(t *testing.T)) in test suite code. -// The passed-in func will be executed as a subtest with a fresh instance of t. -// Provides compatibility with go test pkg -run TestSuite/TestName/SubTestName. -func (suite *Suite) Run(name string, subtest func()) bool { - oldT := suite.T() - defer suite.SetT(oldT) - return oldT.Run(name, func(t *testing.T) { - suite.SetT(t) - subtest() - }) -} - -// Run takes a testing suite and runs all of the tests attached -// to it. -func Run(t *testing.T, suite TestingSuite) { - suite.SetT(t) - defer failOnPanic(t) - - suiteSetupDone := false - - methodFinder := reflect.TypeOf(suite) - tests := []testing.InternalTest{} - for index := 0; index < methodFinder.NumMethod(); index++ { - method := methodFinder.Method(index) - ok, err := methodFilter(method.Name) - if err != nil { - fmt.Fprintf(os.Stderr, "testify: invalid regexp for -m: %s\n", err) - os.Exit(1) - } - if !ok { - continue - } - if !suiteSetupDone { - if setupAllSuite, ok := suite.(SetupAllSuite); ok { - setupAllSuite.SetupSuite() - } - defer func() { - if tearDownAllSuite, ok := suite.(TearDownAllSuite); ok { - tearDownAllSuite.TearDownSuite() - } - }() - suiteSetupDone = true - } - test := testing.InternalTest{ - Name: method.Name, - F: func(t *testing.T) { - parentT := suite.T() - suite.SetT(t) - defer failOnPanic(t) - - if setupTestSuite, ok := suite.(SetupTestSuite); ok { - setupTestSuite.SetupTest() - } - if beforeTestSuite, ok := suite.(BeforeTest); ok { - beforeTestSuite.BeforeTest(methodFinder.Elem().Name(), method.Name) - } - defer func() { - if afterTestSuite, ok := suite.(AfterTest); ok { - afterTestSuite.AfterTest(methodFinder.Elem().Name(), method.Name) - } - if tearDownTestSuite, ok := suite.(TearDownTestSuite); ok { - tearDownTestSuite.TearDownTest() - } - suite.SetT(parentT) - }() - method.Func.Call([]reflect.Value{reflect.ValueOf(suite)}) - }, - } - tests = append(tests, test) - } - runTests(t, tests) -} - -func runTests(t testing.TB, tests []testing.InternalTest) { - r, ok := t.(runner) - if !ok { // backwards compatibility with Go 1.6 and below - if !testing.RunTests(allTestsFilter, tests) { - t.Fail() - } - return - } - - for _, test := range tests { - r.Run(test.Name, test.F) - } -} - -// Filtering method according to set regular expression -// specified command-line argument -m -func methodFilter(name string) (bool, error) { - if ok, _ := regexp.MatchString("^Test", name); !ok { - return false, nil - } - return regexp.MatchString(*matchMethod, name) -} - -type runner interface { - Run(name string, f func(t *testing.T)) bool -} diff --git a/vendor/modules.txt b/vendor/modules.txt index 7fb12945..99e65346 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -11,6 +11,5 @@ github.com/rainycape/unidecode # github.com/stretchr/testify v1.4.0 github.com/stretchr/testify/assert github.com/stretchr/testify/require -github.com/stretchr/testify/suite # gopkg.in/yaml.v2 v2.2.2 gopkg.in/yaml.v2 diff --git a/yaml.go b/yaml.go index 834170e8..2497a21a 100644 --- a/yaml.go +++ b/yaml.go @@ -26,11 +26,12 @@ type dashboardYaml struct { Editable bool `yaml:"editable"` SharedCrosshair bool `yaml:"shared_crosshair"` Tags []string `yaml:"tags"` - AutoRefresh string `yaml:"autorefresh"` + AutoRefresh string `yaml:"auto_refresh"` } func (dashboard *dashboardYaml) toDashboardBuilder() (DashboardBuilder, error) { opts := []DashboardBuilderOption{ + dashboard.editable(), dashboard.sharedCrossHair(), } @@ -52,3 +53,11 @@ func (dashboard *dashboardYaml) sharedCrossHair() DashboardBuilderOption { return DefaultTooltip() } + +func (dashboard *dashboardYaml) editable() DashboardBuilderOption { + if dashboard.Editable { + return Editable() + } + + return ReadOnly() +} From 072f4b7783e8a74cde4275ef7da179c315a59afa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Thu, 19 Mar 2020 23:25:12 +0100 Subject: [PATCH 03/24] End to end example --- cmd/yaml/main.go | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/cmd/yaml/main.go b/cmd/yaml/main.go index 32c9f5a2..5b88b67f 100644 --- a/cmd/yaml/main.go +++ b/cmd/yaml/main.go @@ -2,16 +2,18 @@ package main import ( "bytes" + "context" "fmt" "io/ioutil" + "net/http" "os" "github.com/K-Phoen/grabana" ) func main() { - if len(os.Args) != 2 { - fmt.Fprint(os.Stderr, "Usage: go run -mod=vendor main.go file\n") + if len(os.Args) != 4 { + fmt.Fprint(os.Stderr, "Usage: go run -mod=vendor main.go file http://grafana-host:3000 api-key\n") os.Exit(1) } @@ -21,12 +23,38 @@ func main() { os.Exit(1) } - builder, err := grabana.UnmarshalYAML(bytes.NewBuffer(content)) + dashboard, err := grabana.UnmarshalYAML(bytes.NewBuffer(content)) if err != nil { fmt.Fprintf(os.Stderr, "Could not parse file: %s\n", err) os.Exit(1) } - json, _ := builder.MarshalJSON() + json, _ := dashboard.MarshalJSON() fmt.Printf("Dashboard JSON: %s\n", json) + + ctx := context.Background() + client := grabana.NewClient(&http.Client{}, os.Args[2], os.Args[3]) + + // create the folder holding the dashboard for the service + folder, err := client.GetFolderByTitle(ctx, "Test Folder") + if err != nil && err != grabana.ErrFolderNotFound { + fmt.Printf("Could not create folder: %s\n", err) + os.Exit(1) + } + if folder == nil { + folder, err = client.CreateFolder(ctx, "Test Folder") + if err != nil { + fmt.Printf("Could not create folder: %s\n", err) + os.Exit(1) + } + + fmt.Printf("Folder created (id: %d, uid: %s)\n", folder.ID, folder.UID) + } + + if _, err := client.UpsertDashboard(ctx, folder, dashboard); err != nil { + fmt.Printf("Could not create dashboard: %s\n", err) + os.Exit(1) + } + + fmt.Println("The deed is done.") } From 7bbe3eb9836c37a7251abd25ce32ab76e2831776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Thu, 19 Mar 2020 23:31:25 +0100 Subject: [PATCH 04/24] Support tag annotations --- cmd/yaml/example.yaml | 9 ++++++++- dashboard.go | 2 +- yaml.go | 15 ++++++++++----- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/cmd/yaml/example.yaml b/cmd/yaml/example.yaml index 23164ec3..12d56d14 100644 --- a/cmd/yaml/example.yaml +++ b/cmd/yaml/example.yaml @@ -1,5 +1,12 @@ title: Awesome dashboard + editable: true shared_crosshair: true tags: [generated, yaml] -auto_refresh: 10s \ No newline at end of file +auto_refresh: 10s + +tags_annotations: + - name: Deployments + datasource: '-- Grafana --' + color: '#5794F2' + tags: ['deploy', 'production'] \ No newline at end of file diff --git a/dashboard.go b/dashboard.go index 775e7132..63cff1c6 100644 --- a/dashboard.go +++ b/dashboard.go @@ -16,7 +16,7 @@ import ( type TagAnnotation struct { Name string Datasource string - IconColor string + IconColor string `yaml:"color"` Tags []string } diff --git a/yaml.go b/yaml.go index 2497a21a..44acfc3c 100644 --- a/yaml.go +++ b/yaml.go @@ -22,11 +22,12 @@ func UnmarshalYAML(input io.Reader) (DashboardBuilder, error) { } type dashboardYaml struct { - Title string `yaml:"title"` - Editable bool `yaml:"editable"` - SharedCrosshair bool `yaml:"shared_crosshair"` - Tags []string `yaml:"tags"` - AutoRefresh string `yaml:"auto_refresh"` + Title string + Editable bool + SharedCrosshair bool `yaml:"shared_crosshair"` + Tags []string + AutoRefresh string `yaml:"auto_refresh"` + TagsAnnotation []TagAnnotation `yaml:"tags_annotations"` } func (dashboard *dashboardYaml) toDashboardBuilder() (DashboardBuilder, error) { @@ -43,6 +44,10 @@ func (dashboard *dashboardYaml) toDashboardBuilder() (DashboardBuilder, error) { opts = append(opts, AutoRefresh(dashboard.AutoRefresh)) } + for _, tagAnnotation := range dashboard.TagsAnnotation { + opts = append(opts, TagsAnnotation(tagAnnotation)) + } + return NewDashboardBuilder(dashboard.Title, opts...), nil } From a8b65de4b152ed2234ad4e39ab701168dc504560 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Thu, 19 Mar 2020 23:48:34 +0100 Subject: [PATCH 05/24] Basic support for interval variables --- cmd/yaml/example.yaml | 12 +++++++++--- yaml.go | 45 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/cmd/yaml/example.yaml b/cmd/yaml/example.yaml index 12d56d14..6e06f3dd 100644 --- a/cmd/yaml/example.yaml +++ b/cmd/yaml/example.yaml @@ -7,6 +7,12 @@ auto_refresh: 10s tags_annotations: - name: Deployments - datasource: '-- Grafana --' - color: '#5794F2' - tags: ['deploy', 'production'] \ No newline at end of file + datasource: "-- Grafana --" + color: "#5794F2" + tags: ["deploy", "production"] + +variables: + - type: interval + name: interval + label: Interval + values: ["30s", "1m", "5m", "10m", "30m", "1h", "6h", "12h"] \ No newline at end of file diff --git a/yaml.go b/yaml.go index 44acfc3c..d48af2fd 100644 --- a/yaml.go +++ b/yaml.go @@ -4,6 +4,7 @@ import ( "fmt" "io" + "github.com/K-Phoen/grabana/variable/interval" "gopkg.in/yaml.v2" ) @@ -26,11 +27,21 @@ type dashboardYaml struct { Editable bool SharedCrosshair bool `yaml:"shared_crosshair"` Tags []string - AutoRefresh string `yaml:"auto_refresh"` - TagsAnnotation []TagAnnotation `yaml:"tags_annotations"` + AutoRefresh string `yaml:"auto_refresh"` + + TagsAnnotation []TagAnnotation `yaml:"tags_annotations"` + Variables []dashboardVariable +} + +type dashboardVariable struct { + Type string + Name string + Label string + Values []string } func (dashboard *dashboardYaml) toDashboardBuilder() (DashboardBuilder, error) { + emptyDashboard := DashboardBuilder{} opts := []DashboardBuilderOption{ dashboard.editable(), dashboard.sharedCrossHair(), @@ -48,6 +59,15 @@ func (dashboard *dashboardYaml) toDashboardBuilder() (DashboardBuilder, error) { opts = append(opts, TagsAnnotation(tagAnnotation)) } + for _, variable := range dashboard.Variables { + opt, err := variable.toOption() + if err != nil { + return emptyDashboard, err + } + + opts = append(opts, opt) + } + return NewDashboardBuilder(dashboard.Title, opts...), nil } @@ -66,3 +86,24 @@ func (dashboard *dashboardYaml) editable() DashboardBuilderOption { return ReadOnly() } + +func (variable *dashboardVariable) toOption() (DashboardBuilderOption, error) { + switch variable.Type { + case "interval": + return variable.asInterval(), nil + } + + return nil, fmt.Errorf("unknown dashboard variable type '%s'", variable.Type) +} + +func (variable *dashboardVariable) asInterval() DashboardBuilderOption { + opts := []interval.Option{ + interval.Values(variable.Values), + } + + if variable.Label != "" { + opts = append(opts, interval.Label(variable.Label)) + } + + return VariableAsInterval(variable.Name, opts...) +} From c7e951bcfb81f4618e5dcbc3d3fd1696d5f25236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Thu, 19 Mar 2020 23:53:26 +0100 Subject: [PATCH 06/24] Basic support for query variables --- cmd/yaml/example.yaml | 7 ++++++- yaml.go | 32 +++++++++++++++++++++++++++----- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/cmd/yaml/example.yaml b/cmd/yaml/example.yaml index 6e06f3dd..938f32dd 100644 --- a/cmd/yaml/example.yaml +++ b/cmd/yaml/example.yaml @@ -15,4 +15,9 @@ variables: - type: interval name: interval label: Interval - values: ["30s", "1m", "5m", "10m", "30m", "1h", "6h", "12h"] \ No newline at end of file + values: ["30s", "1m", "5m", "10m", "30m", "1h", "6h", "12h"] + - type: query + name: status + label: HTTP status + datasource: prometheus-default + request: "label_values(prometheus_http_requests_total, code)" \ No newline at end of file diff --git a/yaml.go b/yaml.go index d48af2fd..c988ad9d 100644 --- a/yaml.go +++ b/yaml.go @@ -5,6 +5,7 @@ import ( "io" "github.com/K-Phoen/grabana/variable/interval" + "github.com/K-Phoen/grabana/variable/query" "gopkg.in/yaml.v2" ) @@ -17,8 +18,6 @@ func UnmarshalYAML(input io.Reader) (DashboardBuilder, error) { return DashboardBuilder{}, err } - fmt.Printf("parsed: %#v\n", parsed) - return parsed.toDashboardBuilder() } @@ -34,10 +33,16 @@ type dashboardYaml struct { } type dashboardVariable struct { - Type string - Name string - Label string + Type string + Name string + Label string + + // used for "interval" and "const" Values []string + + // used for "query" + Datasource string + Request string } func (dashboard *dashboardYaml) toDashboardBuilder() (DashboardBuilder, error) { @@ -91,6 +96,8 @@ func (variable *dashboardVariable) toOption() (DashboardBuilderOption, error) { switch variable.Type { case "interval": return variable.asInterval(), nil + case "query": + return variable.asQuery(), nil } return nil, fmt.Errorf("unknown dashboard variable type '%s'", variable.Type) @@ -107,3 +114,18 @@ func (variable *dashboardVariable) asInterval() DashboardBuilderOption { return VariableAsInterval(variable.Name, opts...) } + +func (variable *dashboardVariable) asQuery() DashboardBuilderOption { + opts := []query.Option{ + query.Request(variable.Request), + } + + if variable.Datasource != "" { + opts = append(opts, query.DataSource(variable.Datasource)) + } + if variable.Label != "" { + opts = append(opts, query.Label(variable.Label)) + } + + return VariableAsQuery(variable.Name, opts...) +} From fd7cd38923ee6d290a5df3d769fb0afa71ba8486 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Fri, 20 Mar 2020 00:00:40 +0100 Subject: [PATCH 07/24] Basic support for const variables --- cmd/yaml/example.yaml | 14 +++++++++++++- yaml.go | 29 ++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/cmd/yaml/example.yaml b/cmd/yaml/example.yaml index 938f32dd..5c40588c 100644 --- a/cmd/yaml/example.yaml +++ b/cmd/yaml/example.yaml @@ -20,4 +20,16 @@ variables: name: status label: HTTP status datasource: prometheus-default - request: "label_values(prometheus_http_requests_total, code)" \ No newline at end of file + request: "label_values(prometheus_http_requests_total, code)" + - type: const + name: percentile + label: Percentile + default: 80 + values_map: + 50th: "50" + 75th: "75" + 80th: "80" + 85th: "85" + 90th: "90" + 95th: "95" + 99th: "99" \ No newline at end of file diff --git a/yaml.go b/yaml.go index c988ad9d..f6eb7791 100644 --- a/yaml.go +++ b/yaml.go @@ -4,6 +4,7 @@ import ( "fmt" "io" + "github.com/K-Phoen/grabana/variable/constant" "github.com/K-Phoen/grabana/variable/interval" "github.com/K-Phoen/grabana/variable/query" "gopkg.in/yaml.v2" @@ -37,9 +38,15 @@ type dashboardVariable struct { Name string Label string - // used for "interval" and "const" + // used for "interval", "const" and "custom" + Default string + + // used for "interval" Values []string + // used for "const" and "custom" + ValuesMap map[string]string `yaml:"values_map"` + // used for "query" Datasource string Request string @@ -98,6 +105,8 @@ func (variable *dashboardVariable) toOption() (DashboardBuilderOption, error) { return variable.asInterval(), nil case "query": return variable.asQuery(), nil + case "const": + return variable.asConst(), nil } return nil, fmt.Errorf("unknown dashboard variable type '%s'", variable.Type) @@ -111,6 +120,9 @@ func (variable *dashboardVariable) asInterval() DashboardBuilderOption { if variable.Label != "" { opts = append(opts, interval.Label(variable.Label)) } + if variable.Default != "" { + opts = append(opts, interval.Default(variable.Default)) + } return VariableAsInterval(variable.Name, opts...) } @@ -129,3 +141,18 @@ func (variable *dashboardVariable) asQuery() DashboardBuilderOption { return VariableAsQuery(variable.Name, opts...) } + +func (variable *dashboardVariable) asConst() DashboardBuilderOption { + opts := []constant.Option{ + constant.Values(variable.ValuesMap), + } + + if variable.Default != "" { + opts = append(opts, constant.Default(variable.Default)) + } + if variable.Label != "" { + opts = append(opts, constant.Label(variable.Label)) + } + + return VariableAsConst(variable.Name, opts...) +} From ae2349dd6503fca2af3226358a91900696954a51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Fri, 20 Mar 2020 00:03:41 +0100 Subject: [PATCH 08/24] Basic support for custom variables --- cmd/yaml/example.yaml | 8 +++++++- yaml.go | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/cmd/yaml/example.yaml b/cmd/yaml/example.yaml index 5c40588c..2dcfef91 100644 --- a/cmd/yaml/example.yaml +++ b/cmd/yaml/example.yaml @@ -32,4 +32,10 @@ variables: 85th: "85" 90th: "90" 95th: "95" - 99th: "99" \ No newline at end of file + 99th: "99" + - type: custom + name: vX + default: v2 + values_map: + v1: v1 + v2: v2 diff --git a/yaml.go b/yaml.go index f6eb7791..511f37a3 100644 --- a/yaml.go +++ b/yaml.go @@ -5,6 +5,7 @@ import ( "io" "github.com/K-Phoen/grabana/variable/constant" + "github.com/K-Phoen/grabana/variable/custom" "github.com/K-Phoen/grabana/variable/interval" "github.com/K-Phoen/grabana/variable/query" "gopkg.in/yaml.v2" @@ -107,6 +108,8 @@ func (variable *dashboardVariable) toOption() (DashboardBuilderOption, error) { return variable.asQuery(), nil case "const": return variable.asConst(), nil + case "custom": + return variable.asCustom(), nil } return nil, fmt.Errorf("unknown dashboard variable type '%s'", variable.Type) @@ -156,3 +159,18 @@ func (variable *dashboardVariable) asConst() DashboardBuilderOption { return VariableAsConst(variable.Name, opts...) } + +func (variable *dashboardVariable) asCustom() DashboardBuilderOption { + opts := []custom.Option{ + custom.Values(variable.ValuesMap), + } + + if variable.Default != "" { + opts = append(opts, custom.Default(variable.Default)) + } + if variable.Label != "" { + opts = append(opts, custom.Label(variable.Label)) + } + + return VariableAsCustom(variable.Name, opts...) +} From 8b68a35e1e2ea86aac8f75dd88bef930f4235470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Fri, 20 Mar 2020 00:40:50 +0100 Subject: [PATCH 09/24] Basic support for graph panels --- cmd/yaml/example.yaml | 20 ++++++++ yaml.go | 110 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) diff --git a/cmd/yaml/example.yaml b/cmd/yaml/example.yaml index 2dcfef91..b1045a66 100644 --- a/cmd/yaml/example.yaml +++ b/cmd/yaml/example.yaml @@ -39,3 +39,23 @@ variables: values_map: v1: v1 v2: v2 + +rows: + - name: Prometheus + panels: + - graph: + title: HTTP Rate + height: 400px + datasource: prometheus-default + targets: + - prometheus: + query: "rate(promhttp_metric_handler_requests_total[$interval])" + legend: "{{handler}} - {{ code }}" + - graph: + title: Heap allocations + height: 400px + datasource: prometheus-default + targets: + - prometheus: + query: "go_memstats_heap_alloc_bytes" + ref: A \ No newline at end of file diff --git a/yaml.go b/yaml.go index 511f37a3..c98bea84 100644 --- a/yaml.go +++ b/yaml.go @@ -4,6 +4,9 @@ import ( "fmt" "io" + "github.com/K-Phoen/grabana/graph" + "github.com/K-Phoen/grabana/row" + "github.com/K-Phoen/grabana/target/prometheus" "github.com/K-Phoen/grabana/variable/constant" "github.com/K-Phoen/grabana/variable/custom" "github.com/K-Phoen/grabana/variable/interval" @@ -32,6 +35,8 @@ type dashboardYaml struct { TagsAnnotation []TagAnnotation `yaml:"tags_annotations"` Variables []dashboardVariable + + Rows []dashboardRow } type dashboardVariable struct { @@ -81,6 +86,15 @@ func (dashboard *dashboardYaml) toDashboardBuilder() (DashboardBuilder, error) { opts = append(opts, opt) } + for _, row := range dashboard.Rows { + opt, err := row.toOption() + if err != nil { + return emptyDashboard, err + } + + opts = append(opts, opt) + } + return NewDashboardBuilder(dashboard.Title, opts...), nil } @@ -174,3 +188,99 @@ func (variable *dashboardVariable) asCustom() DashboardBuilderOption { return VariableAsCustom(variable.Name, opts...) } + +type dashboardRow struct { + Name string + Panels []dashboardPanel +} + +func (r dashboardRow) toOption() (DashboardBuilderOption, error) { + opts := []row.Option{} + + for _, panel := range r.Panels { + opt, err := panel.toOption() + if err != nil { + return nil, err + } + + opts = append(opts, opt) + } + + return Row(r.Name, opts...), nil +} + +type dashboardPanel struct { + Graph *dashboardGraph +} + +func (panel dashboardPanel) toOption() (row.Option, error) { + if panel.Graph != nil { + return panel.Graph.toOption() + } + + return nil, fmt.Errorf("panel not configured") +} + +type dashboardGraph struct { + Title string + Span float32 + Height string + Datasource string + Targets []target +} + +func (graphPanel dashboardGraph) toOption() (row.Option, error) { + opts := []graph.Option{} + + if graphPanel.Span != 0 { + opts = append(opts, graph.Span(graphPanel.Span)) + } + if graphPanel.Height != "" { + opts = append(opts, graph.Height(graphPanel.Height)) + } + if graphPanel.Datasource != "" { + opts = append(opts, graph.DataSource(graphPanel.Datasource)) + } + + for _, t := range graphPanel.Targets { + opt, err := graphPanel.target(t) + if err != nil { + return nil, err + } + + opts = append(opts, opt) + } + + return row.WithGraph(graphPanel.Title, opts...), nil +} + +func (graphPanel *dashboardGraph) target(t target) (graph.Option, error) { + if t.Prometheus != nil { + return graph.WithPrometheusTarget(t.Prometheus.Query, t.Prometheus.toOptions()...), nil + } + + return nil, fmt.Errorf("target not configured") +} + +type target struct { + Prometheus *prometheusTarget +} + +type prometheusTarget struct { + Query string + Legend string + Ref string +} + +func (t prometheusTarget) toOptions() []prometheus.Option { + var opts []prometheus.Option + + if t.Legend != "" { + opts = append(opts, prometheus.Legend(t.Legend)) + } + if t.Ref != "" { + opts = append(opts, prometheus.Legend(t.Ref)) + } + + return opts +} From 80e43676d0c32cd2798e7c22030b0b6f2a50afdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Fri, 20 Mar 2020 16:34:01 +0100 Subject: [PATCH 10/24] Add basic support for table panels --- cmd/yaml/example.yaml | 14 ++++++++++- yaml.go | 56 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/cmd/yaml/example.yaml b/cmd/yaml/example.yaml index b1045a66..5ba09abe 100644 --- a/cmd/yaml/example.yaml +++ b/cmd/yaml/example.yaml @@ -58,4 +58,16 @@ rows: targets: - prometheus: query: "go_memstats_heap_alloc_bytes" - ref: A \ No newline at end of file + ref: A + - table: + title: Threads + datasource: prometheus-default + targets: + - prometheus: + query: "go_threads" + hidden_columns: ["Time"] + time_series_aggregations: + - label: AVG + type: avg + - label: Current + type: current diff --git a/yaml.go b/yaml.go index c98bea84..b030574c 100644 --- a/yaml.go +++ b/yaml.go @@ -6,6 +6,7 @@ import ( "github.com/K-Phoen/grabana/graph" "github.com/K-Phoen/grabana/row" + "github.com/K-Phoen/grabana/table" "github.com/K-Phoen/grabana/target/prometheus" "github.com/K-Phoen/grabana/variable/constant" "github.com/K-Phoen/grabana/variable/custom" @@ -211,12 +212,16 @@ func (r dashboardRow) toOption() (DashboardBuilderOption, error) { type dashboardPanel struct { Graph *dashboardGraph + Table *dashboardTable } func (panel dashboardPanel) toOption() (row.Option, error) { if panel.Graph != nil { return panel.Graph.toOption() } + if panel.Table != nil { + return panel.Table.toOption() + } return nil, fmt.Errorf("panel not configured") } @@ -284,3 +289,54 @@ func (t prometheusTarget) toOptions() []prometheus.Option { return opts } + +type dashboardTable struct { + Title string + Span float32 + Height string + Datasource string + Targets []target + HiddenColumns []string `yaml:"hidden_columns"` + TimeSeriesAggregations []table.Aggregation `yaml:"time_series_aggregations"` +} + +func (tablePanel dashboardTable) toOption() (row.Option, error) { + opts := []table.Option{} + + if tablePanel.Span != 0 { + opts = append(opts, table.Span(tablePanel.Span)) + } + if tablePanel.Height != "" { + opts = append(opts, table.Height(tablePanel.Height)) + } + if tablePanel.Datasource != "" { + opts = append(opts, table.DataSource(tablePanel.Datasource)) + } + + for _, t := range tablePanel.Targets { + opt, err := tablePanel.target(t) + if err != nil { + return nil, err + } + + opts = append(opts, opt) + } + + for _, column := range tablePanel.HiddenColumns { + opts = append(opts, table.HideColumn(column)) + } + + if len(tablePanel.TimeSeriesAggregations) != 0 { + opts = append(opts, table.AsTimeSeriesAggregations(tablePanel.TimeSeriesAggregations)) + } + + return row.WithTable(tablePanel.Title, opts...), nil +} + +func (tablePanel *dashboardTable) target(t target) (table.Option, error) { + if t.Prometheus != nil { + return table.WithPrometheusTarget(t.Prometheus.Query, t.Prometheus.toOptions()...), nil + } + + return nil, fmt.Errorf("target not configured") +} From 68b3b4ffa06f53200364a6193dff76ce6c96da1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Fri, 20 Mar 2020 17:03:16 +0100 Subject: [PATCH 11/24] Add basic support for singlestat panels --- cmd/yaml/example.yaml | 9 ++++++ yaml.go | 73 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/cmd/yaml/example.yaml b/cmd/yaml/example.yaml index 5ba09abe..42cbdadb 100644 --- a/cmd/yaml/example.yaml +++ b/cmd/yaml/example.yaml @@ -71,3 +71,12 @@ rows: type: avg - label: Current type: current + - single_stat: + title: Heap Allocations + datasource: prometheus-default + targets: + - prometheus: + query: "go_memstats_heap_alloc_bytes" + unit: bytes + thresholds: ["26000000", "28000000"] + color: ["value"] diff --git a/yaml.go b/yaml.go index b030574c..a3f5e7f3 100644 --- a/yaml.go +++ b/yaml.go @@ -6,6 +6,7 @@ import ( "github.com/K-Phoen/grabana/graph" "github.com/K-Phoen/grabana/row" + "github.com/K-Phoen/grabana/singlestat" "github.com/K-Phoen/grabana/table" "github.com/K-Phoen/grabana/target/prometheus" "github.com/K-Phoen/grabana/variable/constant" @@ -211,8 +212,9 @@ func (r dashboardRow) toOption() (DashboardBuilderOption, error) { } type dashboardPanel struct { - Graph *dashboardGraph - Table *dashboardTable + Graph *dashboardGraph + Table *dashboardTable + SingleStat *dashboardSingleStat `yaml:"single_stat"` } func (panel dashboardPanel) toOption() (row.Option, error) { @@ -222,6 +224,9 @@ func (panel dashboardPanel) toOption() (row.Option, error) { if panel.Table != nil { return panel.Table.toOption() } + if panel.SingleStat != nil { + return panel.SingleStat.toOption() + } return nil, fmt.Errorf("panel not configured") } @@ -340,3 +345,67 @@ func (tablePanel *dashboardTable) target(t target) (table.Option, error) { return nil, fmt.Errorf("target not configured") } + +type dashboardSingleStat struct { + Title string + Span float32 + Height string + Datasource string + Unit string + Targets []target + Thresholds [2]string + Colors [3]string + Color []string +} + +func (singleStatPanel dashboardSingleStat) toOption() (row.Option, error) { + opts := []singlestat.Option{} + + if singleStatPanel.Span != 0 { + opts = append(opts, singlestat.Span(singleStatPanel.Span)) + } + if singleStatPanel.Height != "" { + opts = append(opts, singlestat.Height(singleStatPanel.Height)) + } + if singleStatPanel.Datasource != "" { + opts = append(opts, singlestat.DataSource(singleStatPanel.Datasource)) + } + if singleStatPanel.Unit != "" { + opts = append(opts, singlestat.Unit(singleStatPanel.Unit)) + } + if singleStatPanel.Thresholds[0] != "" { + opts = append(opts, singlestat.Thresholds(singleStatPanel.Thresholds)) + } + if singleStatPanel.Colors[0] != "" { + opts = append(opts, singlestat.Colors(singleStatPanel.Colors)) + } + + for _, colorTarget := range singleStatPanel.Color { + if colorTarget == "value" { + opts = append(opts, singlestat.ColorValue()) + } else if colorTarget == "background" { + opts = append(opts, singlestat.ColorBackground()) + } else { + return nil, fmt.Errorf("invalid coloring target '%s'", colorTarget) + } + } + + for _, t := range singleStatPanel.Targets { + opt, err := singleStatPanel.target(t) + if err != nil { + return nil, err + } + + opts = append(opts, opt) + } + + return row.WithSingleStat(singleStatPanel.Title, opts...), nil +} + +func (singleStatPanel dashboardSingleStat) target(t target) (singlestat.Option, error) { + if t.Prometheus != nil { + return singlestat.WithPrometheusTarget(t.Prometheus.Query, t.Prometheus.toOptions()...), nil + } + + return nil, fmt.Errorf("target not configured") +} From 35e458a6fc8e962f9449fab4efd5fe65dd792dbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Fri, 20 Mar 2020 17:31:31 +0100 Subject: [PATCH 12/24] Add basic support for text panels --- cmd/example/main.go | 2 +- cmd/yaml/example.yaml | 9 +++++++++ yaml.go | 39 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/cmd/example/main.go b/cmd/example/main.go index 3b443576..2661a725 100644 --- a/cmd/example/main.go +++ b/cmd/example/main.go @@ -153,7 +153,7 @@ func main() { ), row.WithText( "Some awesome html?", - text.HTML("lalalala"), + text.HTML("Some awesome html?"), ), ), ) diff --git a/cmd/yaml/example.yaml b/cmd/yaml/example.yaml index 42cbdadb..d758d9de 100644 --- a/cmd/yaml/example.yaml +++ b/cmd/yaml/example.yaml @@ -80,3 +80,12 @@ rows: unit: bytes thresholds: ["26000000", "28000000"] color: ["value"] + + - name: "Some text, because it might be useful" + panels: + - text: + title: Some awesome text? + markdown: "Markdown syntax help: [commonmark.org/help](https://commonmark.org/help/)\n${percentile}" + - text: + title: Some awesome html? + markdown: "Some awesome html?" diff --git a/yaml.go b/yaml.go index a3f5e7f3..301cee64 100644 --- a/yaml.go +++ b/yaml.go @@ -9,6 +9,7 @@ import ( "github.com/K-Phoen/grabana/singlestat" "github.com/K-Phoen/grabana/table" "github.com/K-Phoen/grabana/target/prometheus" + "github.com/K-Phoen/grabana/text" "github.com/K-Phoen/grabana/variable/constant" "github.com/K-Phoen/grabana/variable/custom" "github.com/K-Phoen/grabana/variable/interval" @@ -215,6 +216,7 @@ type dashboardPanel struct { Graph *dashboardGraph Table *dashboardTable SingleStat *dashboardSingleStat `yaml:"single_stat"` + Text *dashboardText } func (panel dashboardPanel) toOption() (row.Option, error) { @@ -227,6 +229,9 @@ func (panel dashboardPanel) toOption() (row.Option, error) { if panel.SingleStat != nil { return panel.SingleStat.toOption() } + if panel.Text != nil { + return panel.Text.toOption() + } return nil, fmt.Errorf("panel not configured") } @@ -381,11 +386,12 @@ func (singleStatPanel dashboardSingleStat) toOption() (row.Option, error) { } for _, colorTarget := range singleStatPanel.Color { - if colorTarget == "value" { + switch colorTarget { + case "value": opts = append(opts, singlestat.ColorValue()) - } else if colorTarget == "background" { + case "background": opts = append(opts, singlestat.ColorBackground()) - } else { + default: return nil, fmt.Errorf("invalid coloring target '%s'", colorTarget) } } @@ -409,3 +415,30 @@ func (singleStatPanel dashboardSingleStat) target(t target) (singlestat.Option, return nil, fmt.Errorf("target not configured") } + +type dashboardText struct { + Title string + Span float32 + Height string + HTML string + Markdown string +} + +func (textPanel dashboardText) toOption() (row.Option, error) { + opts := []text.Option{} + + if textPanel.Span != 0 { + opts = append(opts, text.Span(textPanel.Span)) + } + if textPanel.Height != "" { + opts = append(opts, text.Height(textPanel.Height)) + } + if textPanel.Markdown != "" { + opts = append(opts, text.Markdown(textPanel.Markdown)) + } + if textPanel.HTML != "" { + opts = append(opts, text.HTML(textPanel.HTML)) + } + + return row.WithText(textPanel.Title, opts...), nil +} From c0cce281d7cd288cc1fabb7e30727eeaff32904d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Sat, 21 Mar 2020 15:01:27 +0100 Subject: [PATCH 13/24] Fix small bug --- cmd/yaml/example.yaml | 3 ++- yaml.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cmd/yaml/example.yaml b/cmd/yaml/example.yaml index d758d9de..7d00dc1c 100644 --- a/cmd/yaml/example.yaml +++ b/cmd/yaml/example.yaml @@ -58,6 +58,7 @@ rows: targets: - prometheus: query: "go_memstats_heap_alloc_bytes" + legend: "{{job}}" ref: A - table: title: Threads @@ -76,7 +77,7 @@ rows: datasource: prometheus-default targets: - prometheus: - query: "go_memstats_heap_alloc_bytes" + query: 'go_memstats_heap_alloc_bytes{job="prometheus"}' unit: bytes thresholds: ["26000000", "28000000"] color: ["value"] diff --git a/yaml.go b/yaml.go index 301cee64..1a06d2a7 100644 --- a/yaml.go +++ b/yaml.go @@ -294,7 +294,7 @@ func (t prometheusTarget) toOptions() []prometheus.Option { opts = append(opts, prometheus.Legend(t.Legend)) } if t.Ref != "" { - opts = append(opts, prometheus.Legend(t.Ref)) + opts = append(opts, prometheus.Ref(t.Ref)) } return opts From cceb8dab809fdf088d0d9de0e306a4f2435b3ca2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Sun, 22 Mar 2020 14:31:58 +0100 Subject: [PATCH 14/24] Move YAML related code into its own package --- README.md | 6 +- client.go | 12 +- client_test.go | 5 +- cmd/example/main.go | 23 +- cmd/yaml/main.go | 6 +- dashboard.go => dashboard/dashboard.go | 85 ++-- .../dashboard_test.go | 30 +- decoder/dashboard.go | 101 ++++ decoder/graph.go | 49 ++ decoder/row.go | 26 + decoder/singlestat.go | 73 +++ decoder/table.go | 59 +++ decoder/target.go | 28 ++ decoder/text.go | 33 ++ decoder/variables.go | 105 +++++ decoder/yaml.go | 20 + doc.go | 2 +- yaml.go | 444 ------------------ 18 files changed, 581 insertions(+), 526 deletions(-) rename dashboard.go => dashboard/dashboard.go (65%) rename dashboard_test.go => dashboard/dashboard_test.go (75%) create mode 100644 decoder/dashboard.go create mode 100644 decoder/graph.go create mode 100644 decoder/row.go create mode 100644 decoder/singlestat.go create mode 100644 decoder/table.go create mode 100644 decoder/target.go create mode 100644 decoder/text.go create mode 100644 decoder/variables.go create mode 100644 decoder/yaml.go delete mode 100644 yaml.go diff --git a/README.md b/README.md index 8739b0bb..5e9acb55 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ for you. Dashboard configuration: ```go -dashboard := grabana.NewDashboardBuilder( +builder := dashboard.New( "Awesome dashboard", grabana.AutoRefresh("5s"), grabana.Tags([]string{"generated"}), @@ -45,7 +45,7 @@ Dashboard creation: ```go ctx := context.Background() -client := grabana.NewClient(&http.Client{}, os.Args[1], os.Args[2]) +client := grabana.NewClient(&http.Client{}, grafanaHost, grafanaAPIToken) // create the folder holding the dashboard for the service folder, err := client.GetFolderByTitle(ctx, "Test Folder") @@ -63,7 +63,7 @@ if folder == nil { fmt.Printf("Folder created (id: %d, uid: %s)\n", folder.ID, folder.UID) } -if _, err := client.UpsertDashboard(ctx, folder, dashboard); err != nil { +if _, err := client.UpsertDashboard(ctx, folder, builder); err != nil { fmt.Printf("Could not create dashboard: %s\n", err) os.Exit(1) } diff --git a/client.go b/client.go index f16e00e0..f651449c 100644 --- a/client.go +++ b/client.go @@ -12,7 +12,7 @@ import ( "strings" "github.com/K-Phoen/grabana/alert" - + "github.com/K-Phoen/grabana/dashboard" "github.com/grafana-tools/sdk" ) @@ -155,13 +155,13 @@ func (client *Client) GetAlertChannelByName(ctx context.Context, name string) (* } // UpsertDashboard creates or replaces a dashboard, in the given folder. -func (client *Client) UpsertDashboard(ctx context.Context, folder *Folder, builder DashboardBuilder) (*Dashboard, error) { +func (client *Client) UpsertDashboard(ctx context.Context, folder *Folder, builder dashboard.Builder) (*Dashboard, error) { buf, err := json.Marshal(struct { Dashboard *sdk.Board `json:"dashboard"` FolderID uint `json:"folderId"` Overwrite bool `json:"overwrite"` }{ - Dashboard: builder.board, + Dashboard: builder.Internal(), FolderID: folder.ID, Overwrite: true, }) @@ -185,12 +185,12 @@ func (client *Client) UpsertDashboard(ctx context.Context, folder *Folder, build return nil, fmt.Errorf("could not create dashboard: %s", body) } - var dashboard Dashboard - if err := decodeJSON(resp.Body, &dashboard); err != nil { + var model Dashboard + if err := decodeJSON(resp.Body, &model); err != nil { return nil, err } - return &dashboard, nil + return &model, nil } func (client Client) postJSON(ctx context.Context, path string, body []byte) (*http.Response, error) { diff --git a/client_test.go b/client_test.go index 89f5d345..801a7472 100644 --- a/client_test.go +++ b/client_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + builder "github.com/K-Phoen/grabana/dashboard" "github.com/stretchr/testify/require" ) @@ -208,7 +209,7 @@ func TestGetAlertChannelByNameCanFail(t *testing.T) { func TestDashboardsCanBeCreated(t *testing.T) { req := require.New(t) - dashboard := NewDashboardBuilder("Dashboard name") + dashboard := builder.New("Dashboard name") ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, `{ "id": 1, @@ -231,7 +232,7 @@ func TestDashboardsCanBeCreated(t *testing.T) { func TestDashboardsCreationCanFail(t *testing.T) { req := require.New(t) - dashboard := NewDashboardBuilder("Dashboard name") + dashboard := builder.New("Dashboard name") ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) fmt.Fprintln(w, `{ diff --git a/cmd/example/main.go b/cmd/example/main.go index 2661a725..868cb47a 100644 --- a/cmd/example/main.go +++ b/cmd/example/main.go @@ -9,6 +9,7 @@ import ( "github.com/K-Phoen/grabana" "github.com/K-Phoen/grabana/alert" "github.com/K-Phoen/grabana/axis" + "github.com/K-Phoen/grabana/dashboard" "github.com/K-Phoen/grabana/graph" "github.com/K-Phoen/grabana/row" "github.com/K-Phoen/grabana/singlestat" @@ -52,27 +53,27 @@ func main() { os.Exit(1) } - dashboard := grabana.NewDashboardBuilder( + builder := dashboard.New( "Awesome dashboard", - grabana.AutoRefresh("5s"), - grabana.Tags([]string{"generated"}), - grabana.TagsAnnotation(grabana.TagAnnotation{ + dashboard.AutoRefresh("5s"), + dashboard.Tags([]string{"generated"}), + dashboard.TagsAnnotation(dashboard.TagAnnotation{ Name: "Deployments", Datasource: "-- Grafana --", IconColor: "#5794F2", Tags: []string{"deploy", "production"}, }), - grabana.VariableAsInterval( + dashboard.VariableAsInterval( "interval", interval.Values([]string{"30s", "1m", "5m", "10m", "30m", "1h", "6h", "12h"}), ), - grabana.VariableAsQuery( + dashboard.VariableAsQuery( "status", query.DataSource("prometheus-default"), query.Request("label_values(prometheus_http_requests_total, code)"), query.Sort(query.NumericalAsc), ), - grabana.VariableAsConst( + dashboard.VariableAsConst( "percentile", constant.Label("Percentile"), constant.Values(map[string]string{ @@ -86,7 +87,7 @@ func main() { }), constant.Default("80"), ), - grabana.VariableAsCustom( + dashboard.VariableAsCustom( "vX", custom.Multi(), custom.IncludeAll(), @@ -96,7 +97,7 @@ func main() { }), custom.Default("v2"), ), - grabana.Row( + dashboard.Row( "Prometheus", row.WithGraph( "HTTP Rate", @@ -145,7 +146,7 @@ func main() { singlestat.Thresholds([2]string{"26000000", "28000000"}), ), ), - grabana.Row( + dashboard.Row( "Some text, because it might be useful", row.WithText( "Some awesome text?", @@ -157,7 +158,7 @@ func main() { ), ), ) - if _, err := client.UpsertDashboard(ctx, folder, dashboard); err != nil { + if _, err := client.UpsertDashboard(ctx, folder, builder); err != nil { fmt.Printf("Could not create dashboard: %s\n", err) os.Exit(1) } diff --git a/cmd/yaml/main.go b/cmd/yaml/main.go index 5b88b67f..93b09afe 100644 --- a/cmd/yaml/main.go +++ b/cmd/yaml/main.go @@ -9,6 +9,7 @@ import ( "os" "github.com/K-Phoen/grabana" + "github.com/K-Phoen/grabana/decoder" ) func main() { @@ -23,15 +24,12 @@ func main() { os.Exit(1) } - dashboard, err := grabana.UnmarshalYAML(bytes.NewBuffer(content)) + dashboard, err := decoder.UnmarshalYAML(bytes.NewBuffer(content)) if err != nil { fmt.Fprintf(os.Stderr, "Could not parse file: %s\n", err) os.Exit(1) } - json, _ := dashboard.MarshalJSON() - fmt.Printf("Dashboard JSON: %s\n", json) - ctx := context.Background() client := grabana.NewClient(&http.Client{}, os.Args[2], os.Args[3]) diff --git a/dashboard.go b/dashboard/dashboard.go similarity index 65% rename from dashboard.go rename to dashboard/dashboard.go index 63cff1c6..198dd2a5 100644 --- a/dashboard.go +++ b/dashboard/dashboard.go @@ -1,4 +1,4 @@ -package grabana +package dashboard import ( "encoding/json" @@ -20,40 +20,40 @@ type TagAnnotation struct { Tags []string } -// DashboardBuilderOption represents an option that can be used to configure a +// Option represents an option that can be used to configure a // dashboard. -type DashboardBuilderOption func(dashboard *DashboardBuilder) +type Option func(dashboard *Builder) -// DashboardBuilder is the main builder used to configure dashboards. -type DashboardBuilder struct { +// Builder is the main builder used to configure dashboards. +type Builder struct { board *sdk.Board } -// NewDashboardBuilder creates a new dashboard builder. -func NewDashboardBuilder(title string, options ...DashboardBuilderOption) DashboardBuilder { +// New creates a new dashboard builder. +func New(title string, options ...Option) Builder { board := sdk.NewBoard(title) board.ID = 0 board.Timezone = "" - builder := &DashboardBuilder{board: board} + builder := &Builder{board: board} - for _, opt := range append(dashboardDefaults(), options...) { + for _, opt := range append(defaults(), options...) { opt(builder) } return *builder } -func dashboardDefaults() []DashboardBuilderOption { - return []DashboardBuilderOption{ +func defaults() []Option { + return []Option{ defaultTimePicker(), defaultTime(), SharedCrossHair(), } } -func defaultTime() DashboardBuilderOption { - return func(builder *DashboardBuilder) { +func defaultTime() Option { + return func(builder *Builder) { builder.board.Time = sdk.Time{ From: "now-3h", To: "now", @@ -61,8 +61,8 @@ func defaultTime() DashboardBuilderOption { } } -func defaultTimePicker() DashboardBuilderOption { - return func(builder *DashboardBuilder) { +func defaultTimePicker() Option { + return func(builder *Builder) { builder.board.Timepicker = sdk.Timepicker{ RefreshIntervals: []string{"5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"}, TimeOptions: []string{"5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"}, @@ -76,15 +76,20 @@ func defaultTimePicker() DashboardBuilderOption { // which your configuration management tool of choice can then feed into // Grafana's dashboard via its provisioning support. // See https://grafana.com/docs/grafana/latest/administration/provisioning/#dashboards -func (builder *DashboardBuilder) MarshalJSON() ([]byte, error) { +func (builder *Builder) MarshalJSON() ([]byte, error) { return json.Marshal(builder.board) } +// Internal. +func (builder *Builder) Internal() *sdk.Board { + return builder.board +} + // VariableAsConst adds a templated variable, defined as a set of constant // values. // See https://grafana.com/docs/grafana/latest/reference/templating/#variable-types -func VariableAsConst(name string, options ...constant.Option) DashboardBuilderOption { - return func(builder *DashboardBuilder) { +func VariableAsConst(name string, options ...constant.Option) Option { + return func(builder *Builder) { templatedVar := constant.New(name, options...) builder.board.Templating.List = append(builder.board.Templating.List, templatedVar.Builder) @@ -94,8 +99,8 @@ func VariableAsConst(name string, options ...constant.Option) DashboardBuilderOp // VariableAsCustom adds a templated variable, defined as a set of custom // values. // See https://grafana.com/docs/grafana/latest/reference/templating/#variable-types -func VariableAsCustom(name string, options ...custom.Option) DashboardBuilderOption { - return func(builder *DashboardBuilder) { +func VariableAsCustom(name string, options ...custom.Option) Option { + return func(builder *Builder) { templatedVar := custom.New(name, options...) builder.board.Templating.List = append(builder.board.Templating.List, templatedVar.Builder) @@ -104,8 +109,8 @@ func VariableAsCustom(name string, options ...custom.Option) DashboardBuilderOpt // VariableAsInterval adds a templated variable, defined as an interval. // See https://grafana.com/docs/grafana/latest/reference/templating/#variable-types -func VariableAsInterval(name string, options ...interval.Option) DashboardBuilderOption { - return func(builder *DashboardBuilder) { +func VariableAsInterval(name string, options ...interval.Option) Option { + return func(builder *Builder) { templatedVar := interval.New(name, options...) builder.board.Templating.List = append(builder.board.Templating.List, templatedVar.Builder) @@ -114,8 +119,8 @@ func VariableAsInterval(name string, options ...interval.Option) DashboardBuilde // VariableAsQuery adds a templated variable, defined as a query. // See https://grafana.com/docs/grafana/latest/reference/templating/#variable-types -func VariableAsQuery(name string, options ...query.Option) DashboardBuilderOption { - return func(builder *DashboardBuilder) { +func VariableAsQuery(name string, options ...query.Option) Option { + return func(builder *Builder) { templatedVar := query.New(name, options...) builder.board.Templating.List = append(builder.board.Templating.List, templatedVar.Builder) @@ -123,15 +128,15 @@ func VariableAsQuery(name string, options ...query.Option) DashboardBuilderOptio } // Row adds a row to the dashboard. -func Row(title string, options ...row.Option) DashboardBuilderOption { - return func(builder *DashboardBuilder) { +func Row(title string, options ...row.Option) Option { + return func(builder *Builder) { row.New(builder.board, title, options...) } } // TagsAnnotation adds a new source of annotation for the dashboard. -func TagsAnnotation(annotation TagAnnotation) DashboardBuilderOption { - return func(builder *DashboardBuilder) { +func TagsAnnotation(annotation TagAnnotation) Option { + return func(builder *Builder) { builder.board.Annotations.List = append(builder.board.Annotations.List, sdk.Annotation{ Name: annotation.Name, Datasource: &annotation.Datasource, @@ -144,43 +149,43 @@ func TagsAnnotation(annotation TagAnnotation) DashboardBuilderOption { } // Editable marks the dashboard as editable. -func Editable() DashboardBuilderOption { - return func(builder *DashboardBuilder) { +func Editable() Option { + return func(builder *Builder) { builder.board.Editable = true } } // ReadOnly marks the dashboard as non-editable. -func ReadOnly() DashboardBuilderOption { - return func(builder *DashboardBuilder) { +func ReadOnly() Option { + return func(builder *Builder) { builder.board.Editable = false } } // SharedCrossHair configures the graph tooltip to be shared across panels. -func SharedCrossHair() DashboardBuilderOption { - return func(builder *DashboardBuilder) { +func SharedCrossHair() Option { + return func(builder *Builder) { builder.board.SharedCrosshair = true } } // DefaultTooltip configures the graph tooltip NOT to be shared across panels. -func DefaultTooltip() DashboardBuilderOption { - return func(builder *DashboardBuilder) { +func DefaultTooltip() Option { + return func(builder *Builder) { builder.board.SharedCrosshair = false } } // Tags adds the given set of tags to the dashboard. -func Tags(tags []string) DashboardBuilderOption { - return func(builder *DashboardBuilder) { +func Tags(tags []string) Option { + return func(builder *Builder) { builder.board.Tags = tags } } // AutoRefresh defines the auto-refresh interval for the dashboard. -func AutoRefresh(interval string) DashboardBuilderOption { - return func(builder *DashboardBuilder) { +func AutoRefresh(interval string) Option { + return func(builder *Builder) { builder.board.Refresh = &sdk.BoolString{Flag: true, Value: interval} } } diff --git a/dashboard_test.go b/dashboard/dashboard_test.go similarity index 75% rename from dashboard_test.go rename to dashboard/dashboard_test.go index c1986854..03c1d676 100644 --- a/dashboard_test.go +++ b/dashboard/dashboard_test.go @@ -1,4 +1,4 @@ -package grabana +package dashboard import ( "encoding/json" @@ -17,7 +17,7 @@ func requireJSON(t *testing.T, payload []byte) { func TestNewDashboardsCanBeCreated(t *testing.T) { req := require.New(t) - panel := NewDashboardBuilder("My dashboard") + panel := New("My dashboard") req.Equal(uint(0), panel.board.ID) req.Equal("My dashboard", panel.board.Title) @@ -32,7 +32,7 @@ func TestNewDashboardsCanBeCreated(t *testing.T) { func TestDashboardCanBeMarshalledIntoJSON(t *testing.T) { req := require.New(t) - builder := NewDashboardBuilder("Awesome dashboard") + builder := New("Awesome dashboard") dashboardJSON, err := builder.MarshalJSON() req.NoError(err) @@ -42,7 +42,7 @@ func TestDashboardCanBeMarshalledIntoJSON(t *testing.T) { func TestDashboardCanBeMadeEditable(t *testing.T) { req := require.New(t) - panel := NewDashboardBuilder("", Editable()) + panel := New("", Editable()) req.True(panel.board.Editable) } @@ -50,7 +50,7 @@ func TestDashboardCanBeMadeEditable(t *testing.T) { func TestDashboardCanBeMadeReadOnly(t *testing.T) { req := require.New(t) - panel := NewDashboardBuilder("", ReadOnly()) + panel := New("", ReadOnly()) req.False(panel.board.Editable) } @@ -58,7 +58,7 @@ func TestDashboardCanBeMadeReadOnly(t *testing.T) { func TestDashboardCanHaveASharedCrossHair(t *testing.T) { req := require.New(t) - panel := NewDashboardBuilder("", SharedCrossHair()) + panel := New("", SharedCrossHair()) req.True(panel.board.SharedCrosshair) } @@ -66,7 +66,7 @@ func TestDashboardCanHaveASharedCrossHair(t *testing.T) { func TestDashboardCanHaveADefaultTooltip(t *testing.T) { req := require.New(t) - panel := NewDashboardBuilder("", DefaultTooltip()) + panel := New("", DefaultTooltip()) req.False(panel.board.SharedCrosshair) } @@ -74,7 +74,7 @@ func TestDashboardCanHaveADefaultTooltip(t *testing.T) { func TestDashboardCanBeAutoRefreshed(t *testing.T) { req := require.New(t) - panel := NewDashboardBuilder("", AutoRefresh("5s")) + panel := New("", AutoRefresh("5s")) req.True(panel.board.Refresh.Flag) req.Equal("5s", panel.board.Refresh.Value) @@ -84,7 +84,7 @@ func TestDashboardCanHaveTags(t *testing.T) { req := require.New(t) tags := []string{"generated", "grabana"} - panel := NewDashboardBuilder("", Tags(tags)) + panel := New("", Tags(tags)) req.Len(panel.board.Tags, 2) req.ElementsMatch(tags, panel.board.Tags) @@ -93,7 +93,7 @@ func TestDashboardCanHaveTags(t *testing.T) { func TestDashboardCanHaveVariablesAsConstants(t *testing.T) { req := require.New(t) - panel := NewDashboardBuilder("", VariableAsConst("percentile")) + panel := New("", VariableAsConst("percentile")) req.Len(panel.board.Templating.List, 1) } @@ -101,7 +101,7 @@ func TestDashboardCanHaveVariablesAsConstants(t *testing.T) { func TestDashboardCanHaveVariablesAsCustom(t *testing.T) { req := require.New(t) - panel := NewDashboardBuilder("", VariableAsCustom("vX")) + panel := New("", VariableAsCustom("vX")) req.Len(panel.board.Templating.List, 1) } @@ -109,7 +109,7 @@ func TestDashboardCanHaveVariablesAsCustom(t *testing.T) { func TestDashboardCanHaveVariablesAsInterval(t *testing.T) { req := require.New(t) - panel := NewDashboardBuilder("", VariableAsInterval("interval")) + panel := New("", VariableAsInterval("interval")) req.Len(panel.board.Templating.List, 1) } @@ -117,7 +117,7 @@ func TestDashboardCanHaveVariablesAsInterval(t *testing.T) { func TestDashboardCanHaveVariablesAsQuery(t *testing.T) { req := require.New(t) - panel := NewDashboardBuilder("", VariableAsQuery("status")) + panel := New("", VariableAsQuery("status")) req.Len(panel.board.Templating.List, 1) } @@ -125,7 +125,7 @@ func TestDashboardCanHaveVariablesAsQuery(t *testing.T) { func TestDashboardCanHaveRows(t *testing.T) { req := require.New(t) - panel := NewDashboardBuilder("", Row("Prometheus")) + panel := New("", Row("Prometheus")) req.Len(panel.board.Rows, 1) } @@ -133,7 +133,7 @@ func TestDashboardCanHaveRows(t *testing.T) { func TestDashboardCanHaveAnnotationsFromTags(t *testing.T) { req := require.New(t) - panel := NewDashboardBuilder("", TagsAnnotation(TagAnnotation{})) + panel := New("", TagsAnnotation(TagAnnotation{})) req.Len(panel.board.Annotations.List, 1) } diff --git a/decoder/dashboard.go b/decoder/dashboard.go new file mode 100644 index 00000000..06a97e83 --- /dev/null +++ b/decoder/dashboard.go @@ -0,0 +1,101 @@ +package decoder + +import ( + "fmt" + + "github.com/K-Phoen/grabana/dashboard" + "github.com/K-Phoen/grabana/row" +) + +type dashboardModel struct { + Title string + Editable bool + SharedCrosshair bool `yaml:"shared_crosshair"` + Tags []string + AutoRefresh string `yaml:"auto_refresh"` + + TagsAnnotation []dashboard.TagAnnotation `yaml:"tags_annotations"` + Variables []dashboardVariable + + Rows []dashboardRow +} + +func (d *dashboardModel) toDashboardBuilder() (dashboard.Builder, error) { + emptyDashboard := dashboard.Builder{} + opts := []dashboard.Option{ + d.editable(), + d.sharedCrossHair(), + } + + if len(d.Tags) != 0 { + opts = append(opts, dashboard.Tags(d.Tags)) + } + + if d.AutoRefresh != "" { + opts = append(opts, dashboard.AutoRefresh(d.AutoRefresh)) + } + + for _, tagAnnotation := range d.TagsAnnotation { + opts = append(opts, dashboard.TagsAnnotation(tagAnnotation)) + } + + for _, variable := range d.Variables { + opt, err := variable.toOption() + if err != nil { + return emptyDashboard, err + } + + opts = append(opts, opt) + } + + for _, row := range d.Rows { + opt, err := row.toOption() + if err != nil { + return emptyDashboard, err + } + + opts = append(opts, opt) + } + + return dashboard.New(d.Title, opts...), nil +} + +func (d *dashboardModel) sharedCrossHair() dashboard.Option { + if d.SharedCrosshair { + return dashboard.SharedCrossHair() + } + + return dashboard.DefaultTooltip() +} + +func (d *dashboardModel) editable() dashboard.Option { + if d.Editable { + return dashboard.Editable() + } + + return dashboard.ReadOnly() +} + +type dashboardPanel struct { + Graph *dashboardGraph + Table *dashboardTable + SingleStat *dashboardSingleStat `yaml:"single_stat"` + Text *dashboardText +} + +func (panel dashboardPanel) toOption() (row.Option, error) { + if panel.Graph != nil { + return panel.Graph.toOption() + } + if panel.Table != nil { + return panel.Table.toOption() + } + if panel.SingleStat != nil { + return panel.SingleStat.toOption() + } + if panel.Text != nil { + return panel.Text.toOption(), nil + } + + return nil, fmt.Errorf("panel not configured") +} diff --git a/decoder/graph.go b/decoder/graph.go new file mode 100644 index 00000000..f32c9a8c --- /dev/null +++ b/decoder/graph.go @@ -0,0 +1,49 @@ +package decoder + +import ( + "fmt" + + "github.com/K-Phoen/grabana/graph" + "github.com/K-Phoen/grabana/row" +) + +type dashboardGraph struct { + Title string + Span float32 + Height string + Datasource string + Targets []target +} + +func (graphPanel dashboardGraph) toOption() (row.Option, error) { + opts := []graph.Option{} + + if graphPanel.Span != 0 { + opts = append(opts, graph.Span(graphPanel.Span)) + } + if graphPanel.Height != "" { + opts = append(opts, graph.Height(graphPanel.Height)) + } + if graphPanel.Datasource != "" { + opts = append(opts, graph.DataSource(graphPanel.Datasource)) + } + + for _, t := range graphPanel.Targets { + opt, err := graphPanel.target(t) + if err != nil { + return nil, err + } + + opts = append(opts, opt) + } + + return row.WithGraph(graphPanel.Title, opts...), nil +} + +func (graphPanel *dashboardGraph) target(t target) (graph.Option, error) { + if t.Prometheus != nil { + return graph.WithPrometheusTarget(t.Prometheus.Query, t.Prometheus.toOptions()...), nil + } + + return nil, fmt.Errorf("target not configured") +} diff --git a/decoder/row.go b/decoder/row.go new file mode 100644 index 00000000..e80b8ed1 --- /dev/null +++ b/decoder/row.go @@ -0,0 +1,26 @@ +package decoder + +import ( + "github.com/K-Phoen/grabana/dashboard" + "github.com/K-Phoen/grabana/row" +) + +type dashboardRow struct { + Name string + Panels []dashboardPanel +} + +func (r dashboardRow) toOption() (dashboard.Option, error) { + opts := []row.Option{} + + for _, panel := range r.Panels { + opt, err := panel.toOption() + if err != nil { + return nil, err + } + + opts = append(opts, opt) + } + + return dashboard.Row(r.Name, opts...), nil +} diff --git a/decoder/singlestat.go b/decoder/singlestat.go new file mode 100644 index 00000000..195f0127 --- /dev/null +++ b/decoder/singlestat.go @@ -0,0 +1,73 @@ +package decoder + +import ( + "fmt" + + "github.com/K-Phoen/grabana/row" + "github.com/K-Phoen/grabana/singlestat" +) + +type dashboardSingleStat struct { + Title string + Span float32 + Height string + Datasource string + Unit string + Targets []target + Thresholds [2]string + Colors [3]string + Color []string +} + +func (singleStatPanel dashboardSingleStat) toOption() (row.Option, error) { + opts := []singlestat.Option{} + + if singleStatPanel.Span != 0 { + opts = append(opts, singlestat.Span(singleStatPanel.Span)) + } + if singleStatPanel.Height != "" { + opts = append(opts, singlestat.Height(singleStatPanel.Height)) + } + if singleStatPanel.Datasource != "" { + opts = append(opts, singlestat.DataSource(singleStatPanel.Datasource)) + } + if singleStatPanel.Unit != "" { + opts = append(opts, singlestat.Unit(singleStatPanel.Unit)) + } + if singleStatPanel.Thresholds[0] != "" { + opts = append(opts, singlestat.Thresholds(singleStatPanel.Thresholds)) + } + if singleStatPanel.Colors[0] != "" { + opts = append(opts, singlestat.Colors(singleStatPanel.Colors)) + } + + for _, colorTarget := range singleStatPanel.Color { + switch colorTarget { + case "value": + opts = append(opts, singlestat.ColorValue()) + case "background": + opts = append(opts, singlestat.ColorBackground()) + default: + return nil, fmt.Errorf("invalid coloring target '%s'", colorTarget) + } + } + + for _, t := range singleStatPanel.Targets { + opt, err := singleStatPanel.target(t) + if err != nil { + return nil, err + } + + opts = append(opts, opt) + } + + return row.WithSingleStat(singleStatPanel.Title, opts...), nil +} + +func (singleStatPanel dashboardSingleStat) target(t target) (singlestat.Option, error) { + if t.Prometheus != nil { + return singlestat.WithPrometheusTarget(t.Prometheus.Query, t.Prometheus.toOptions()...), nil + } + + return nil, fmt.Errorf("target not configured") +} diff --git a/decoder/table.go b/decoder/table.go new file mode 100644 index 00000000..88416fbf --- /dev/null +++ b/decoder/table.go @@ -0,0 +1,59 @@ +package decoder + +import ( + "fmt" + + "github.com/K-Phoen/grabana/row" + "github.com/K-Phoen/grabana/table" +) + +type dashboardTable struct { + Title string + Span float32 + Height string + Datasource string + Targets []target + HiddenColumns []string `yaml:"hidden_columns"` + TimeSeriesAggregations []table.Aggregation `yaml:"time_series_aggregations"` +} + +func (tablePanel dashboardTable) toOption() (row.Option, error) { + opts := []table.Option{} + + if tablePanel.Span != 0 { + opts = append(opts, table.Span(tablePanel.Span)) + } + if tablePanel.Height != "" { + opts = append(opts, table.Height(tablePanel.Height)) + } + if tablePanel.Datasource != "" { + opts = append(opts, table.DataSource(tablePanel.Datasource)) + } + + for _, t := range tablePanel.Targets { + opt, err := tablePanel.target(t) + if err != nil { + return nil, err + } + + opts = append(opts, opt) + } + + for _, column := range tablePanel.HiddenColumns { + opts = append(opts, table.HideColumn(column)) + } + + if len(tablePanel.TimeSeriesAggregations) != 0 { + opts = append(opts, table.AsTimeSeriesAggregations(tablePanel.TimeSeriesAggregations)) + } + + return row.WithTable(tablePanel.Title, opts...), nil +} + +func (tablePanel *dashboardTable) target(t target) (table.Option, error) { + if t.Prometheus != nil { + return table.WithPrometheusTarget(t.Prometheus.Query, t.Prometheus.toOptions()...), nil + } + + return nil, fmt.Errorf("target not configured") +} diff --git a/decoder/target.go b/decoder/target.go new file mode 100644 index 00000000..1588779b --- /dev/null +++ b/decoder/target.go @@ -0,0 +1,28 @@ +package decoder + +import ( + "github.com/K-Phoen/grabana/target/prometheus" +) + +type target struct { + Prometheus *prometheusTarget +} + +type prometheusTarget struct { + Query string + Legend string + Ref string +} + +func (t prometheusTarget) toOptions() []prometheus.Option { + var opts []prometheus.Option + + if t.Legend != "" { + opts = append(opts, prometheus.Legend(t.Legend)) + } + if t.Ref != "" { + opts = append(opts, prometheus.Ref(t.Ref)) + } + + return opts +} diff --git a/decoder/text.go b/decoder/text.go new file mode 100644 index 00000000..206e0c3c --- /dev/null +++ b/decoder/text.go @@ -0,0 +1,33 @@ +package decoder + +import ( + "github.com/K-Phoen/grabana/row" + "github.com/K-Phoen/grabana/text" +) + +type dashboardText struct { + Title string + Span float32 + Height string + HTML string + Markdown string +} + +func (textPanel dashboardText) toOption() row.Option { + opts := []text.Option{} + + if textPanel.Span != 0 { + opts = append(opts, text.Span(textPanel.Span)) + } + if textPanel.Height != "" { + opts = append(opts, text.Height(textPanel.Height)) + } + if textPanel.Markdown != "" { + opts = append(opts, text.Markdown(textPanel.Markdown)) + } + if textPanel.HTML != "" { + opts = append(opts, text.HTML(textPanel.HTML)) + } + + return row.WithText(textPanel.Title, opts...) +} diff --git a/decoder/variables.go b/decoder/variables.go new file mode 100644 index 00000000..f6b04cc2 --- /dev/null +++ b/decoder/variables.go @@ -0,0 +1,105 @@ +package decoder + +import ( + "fmt" + + "github.com/K-Phoen/grabana/dashboard" + "github.com/K-Phoen/grabana/variable/constant" + "github.com/K-Phoen/grabana/variable/custom" + "github.com/K-Phoen/grabana/variable/interval" + "github.com/K-Phoen/grabana/variable/query" +) + +type dashboardVariable struct { + Type string + Name string + Label string + + // used for "interval", "const" and "custom" + Default string + + // used for "interval" + Values []string + + // used for "const" and "custom" + ValuesMap map[string]string `yaml:"values_map"` + + // used for "query" + Datasource string + Request string +} + +func (variable *dashboardVariable) toOption() (dashboard.Option, error) { + switch variable.Type { + case "interval": + return variable.asInterval(), nil + case "query": + return variable.asQuery(), nil + case "const": + return variable.asConst(), nil + case "custom": + return variable.asCustom(), nil + } + + return nil, fmt.Errorf("unknown dashboard variable type '%s'", variable.Type) +} + +func (variable *dashboardVariable) asInterval() dashboard.Option { + opts := []interval.Option{ + interval.Values(variable.Values), + } + + if variable.Label != "" { + opts = append(opts, interval.Label(variable.Label)) + } + if variable.Default != "" { + opts = append(opts, interval.Default(variable.Default)) + } + + return dashboard.VariableAsInterval(variable.Name, opts...) +} + +func (variable *dashboardVariable) asQuery() dashboard.Option { + opts := []query.Option{ + query.Request(variable.Request), + } + + if variable.Datasource != "" { + opts = append(opts, query.DataSource(variable.Datasource)) + } + if variable.Label != "" { + opts = append(opts, query.Label(variable.Label)) + } + + return dashboard.VariableAsQuery(variable.Name, opts...) +} + +func (variable *dashboardVariable) asConst() dashboard.Option { + opts := []constant.Option{ + constant.Values(variable.ValuesMap), + } + + if variable.Default != "" { + opts = append(opts, constant.Default(variable.Default)) + } + if variable.Label != "" { + opts = append(opts, constant.Label(variable.Label)) + } + + return dashboard.VariableAsConst(variable.Name, opts...) +} + +func (variable *dashboardVariable) asCustom() dashboard.Option { + opts := []custom.Option{ + custom.Values(variable.ValuesMap), + } + + if variable.Default != "" { + opts = append(opts, custom.Default(variable.Default)) + } + if variable.Label != "" { + opts = append(opts, custom.Label(variable.Label)) + } + + return dashboard.VariableAsCustom(variable.Name, opts...) +} diff --git a/decoder/yaml.go b/decoder/yaml.go new file mode 100644 index 00000000..7d91b44b --- /dev/null +++ b/decoder/yaml.go @@ -0,0 +1,20 @@ +package decoder + +import ( + "io" + + builder "github.com/K-Phoen/grabana/dashboard" + "gopkg.in/yaml.v2" +) + +func UnmarshalYAML(input io.Reader) (builder.Builder, error) { + decoder := yaml.NewDecoder(input) + decoder.SetStrict(true) + + parsed := &dashboardModel{} + if err := decoder.Decode(parsed); err != nil { + return builder.Builder{}, err + } + + return parsed.toDashboardBuilder() +} diff --git a/doc.go b/doc.go index dbc19844..5f6baeed 100644 --- a/doc.go +++ b/doc.go @@ -5,7 +5,7 @@ If you are looking for a way to version your dashboards configuration or automate tedious and error-prone creation of dashboards, this library is meant for you. - dashboard := grabana.NewDashboardBuilder( + builder := dashboard.New( "Awesome dashboard", grabana.VariableAsInterval( "interval", diff --git a/yaml.go b/yaml.go deleted file mode 100644 index 1a06d2a7..00000000 --- a/yaml.go +++ /dev/null @@ -1,444 +0,0 @@ -package grabana - -import ( - "fmt" - "io" - - "github.com/K-Phoen/grabana/graph" - "github.com/K-Phoen/grabana/row" - "github.com/K-Phoen/grabana/singlestat" - "github.com/K-Phoen/grabana/table" - "github.com/K-Phoen/grabana/target/prometheus" - "github.com/K-Phoen/grabana/text" - "github.com/K-Phoen/grabana/variable/constant" - "github.com/K-Phoen/grabana/variable/custom" - "github.com/K-Phoen/grabana/variable/interval" - "github.com/K-Phoen/grabana/variable/query" - "gopkg.in/yaml.v2" -) - -func UnmarshalYAML(input io.Reader) (DashboardBuilder, error) { - decoder := yaml.NewDecoder(input) - decoder.SetStrict(true) - - parsed := &dashboardYaml{} - if err := decoder.Decode(parsed); err != nil { - return DashboardBuilder{}, err - } - - return parsed.toDashboardBuilder() -} - -type dashboardYaml struct { - Title string - Editable bool - SharedCrosshair bool `yaml:"shared_crosshair"` - Tags []string - AutoRefresh string `yaml:"auto_refresh"` - - TagsAnnotation []TagAnnotation `yaml:"tags_annotations"` - Variables []dashboardVariable - - Rows []dashboardRow -} - -type dashboardVariable struct { - Type string - Name string - Label string - - // used for "interval", "const" and "custom" - Default string - - // used for "interval" - Values []string - - // used for "const" and "custom" - ValuesMap map[string]string `yaml:"values_map"` - - // used for "query" - Datasource string - Request string -} - -func (dashboard *dashboardYaml) toDashboardBuilder() (DashboardBuilder, error) { - emptyDashboard := DashboardBuilder{} - opts := []DashboardBuilderOption{ - dashboard.editable(), - dashboard.sharedCrossHair(), - } - - if len(dashboard.Tags) != 0 { - opts = append(opts, Tags(dashboard.Tags)) - } - - if dashboard.AutoRefresh != "" { - opts = append(opts, AutoRefresh(dashboard.AutoRefresh)) - } - - for _, tagAnnotation := range dashboard.TagsAnnotation { - opts = append(opts, TagsAnnotation(tagAnnotation)) - } - - for _, variable := range dashboard.Variables { - opt, err := variable.toOption() - if err != nil { - return emptyDashboard, err - } - - opts = append(opts, opt) - } - - for _, row := range dashboard.Rows { - opt, err := row.toOption() - if err != nil { - return emptyDashboard, err - } - - opts = append(opts, opt) - } - - return NewDashboardBuilder(dashboard.Title, opts...), nil -} - -func (dashboard *dashboardYaml) sharedCrossHair() DashboardBuilderOption { - if dashboard.SharedCrosshair { - return SharedCrossHair() - } - - return DefaultTooltip() -} - -func (dashboard *dashboardYaml) editable() DashboardBuilderOption { - if dashboard.Editable { - return Editable() - } - - return ReadOnly() -} - -func (variable *dashboardVariable) toOption() (DashboardBuilderOption, error) { - switch variable.Type { - case "interval": - return variable.asInterval(), nil - case "query": - return variable.asQuery(), nil - case "const": - return variable.asConst(), nil - case "custom": - return variable.asCustom(), nil - } - - return nil, fmt.Errorf("unknown dashboard variable type '%s'", variable.Type) -} - -func (variable *dashboardVariable) asInterval() DashboardBuilderOption { - opts := []interval.Option{ - interval.Values(variable.Values), - } - - if variable.Label != "" { - opts = append(opts, interval.Label(variable.Label)) - } - if variable.Default != "" { - opts = append(opts, interval.Default(variable.Default)) - } - - return VariableAsInterval(variable.Name, opts...) -} - -func (variable *dashboardVariable) asQuery() DashboardBuilderOption { - opts := []query.Option{ - query.Request(variable.Request), - } - - if variable.Datasource != "" { - opts = append(opts, query.DataSource(variable.Datasource)) - } - if variable.Label != "" { - opts = append(opts, query.Label(variable.Label)) - } - - return VariableAsQuery(variable.Name, opts...) -} - -func (variable *dashboardVariable) asConst() DashboardBuilderOption { - opts := []constant.Option{ - constant.Values(variable.ValuesMap), - } - - if variable.Default != "" { - opts = append(opts, constant.Default(variable.Default)) - } - if variable.Label != "" { - opts = append(opts, constant.Label(variable.Label)) - } - - return VariableAsConst(variable.Name, opts...) -} - -func (variable *dashboardVariable) asCustom() DashboardBuilderOption { - opts := []custom.Option{ - custom.Values(variable.ValuesMap), - } - - if variable.Default != "" { - opts = append(opts, custom.Default(variable.Default)) - } - if variable.Label != "" { - opts = append(opts, custom.Label(variable.Label)) - } - - return VariableAsCustom(variable.Name, opts...) -} - -type dashboardRow struct { - Name string - Panels []dashboardPanel -} - -func (r dashboardRow) toOption() (DashboardBuilderOption, error) { - opts := []row.Option{} - - for _, panel := range r.Panels { - opt, err := panel.toOption() - if err != nil { - return nil, err - } - - opts = append(opts, opt) - } - - return Row(r.Name, opts...), nil -} - -type dashboardPanel struct { - Graph *dashboardGraph - Table *dashboardTable - SingleStat *dashboardSingleStat `yaml:"single_stat"` - Text *dashboardText -} - -func (panel dashboardPanel) toOption() (row.Option, error) { - if panel.Graph != nil { - return panel.Graph.toOption() - } - if panel.Table != nil { - return panel.Table.toOption() - } - if panel.SingleStat != nil { - return panel.SingleStat.toOption() - } - if panel.Text != nil { - return panel.Text.toOption() - } - - return nil, fmt.Errorf("panel not configured") -} - -type dashboardGraph struct { - Title string - Span float32 - Height string - Datasource string - Targets []target -} - -func (graphPanel dashboardGraph) toOption() (row.Option, error) { - opts := []graph.Option{} - - if graphPanel.Span != 0 { - opts = append(opts, graph.Span(graphPanel.Span)) - } - if graphPanel.Height != "" { - opts = append(opts, graph.Height(graphPanel.Height)) - } - if graphPanel.Datasource != "" { - opts = append(opts, graph.DataSource(graphPanel.Datasource)) - } - - for _, t := range graphPanel.Targets { - opt, err := graphPanel.target(t) - if err != nil { - return nil, err - } - - opts = append(opts, opt) - } - - return row.WithGraph(graphPanel.Title, opts...), nil -} - -func (graphPanel *dashboardGraph) target(t target) (graph.Option, error) { - if t.Prometheus != nil { - return graph.WithPrometheusTarget(t.Prometheus.Query, t.Prometheus.toOptions()...), nil - } - - return nil, fmt.Errorf("target not configured") -} - -type target struct { - Prometheus *prometheusTarget -} - -type prometheusTarget struct { - Query string - Legend string - Ref string -} - -func (t prometheusTarget) toOptions() []prometheus.Option { - var opts []prometheus.Option - - if t.Legend != "" { - opts = append(opts, prometheus.Legend(t.Legend)) - } - if t.Ref != "" { - opts = append(opts, prometheus.Ref(t.Ref)) - } - - return opts -} - -type dashboardTable struct { - Title string - Span float32 - Height string - Datasource string - Targets []target - HiddenColumns []string `yaml:"hidden_columns"` - TimeSeriesAggregations []table.Aggregation `yaml:"time_series_aggregations"` -} - -func (tablePanel dashboardTable) toOption() (row.Option, error) { - opts := []table.Option{} - - if tablePanel.Span != 0 { - opts = append(opts, table.Span(tablePanel.Span)) - } - if tablePanel.Height != "" { - opts = append(opts, table.Height(tablePanel.Height)) - } - if tablePanel.Datasource != "" { - opts = append(opts, table.DataSource(tablePanel.Datasource)) - } - - for _, t := range tablePanel.Targets { - opt, err := tablePanel.target(t) - if err != nil { - return nil, err - } - - opts = append(opts, opt) - } - - for _, column := range tablePanel.HiddenColumns { - opts = append(opts, table.HideColumn(column)) - } - - if len(tablePanel.TimeSeriesAggregations) != 0 { - opts = append(opts, table.AsTimeSeriesAggregations(tablePanel.TimeSeriesAggregations)) - } - - return row.WithTable(tablePanel.Title, opts...), nil -} - -func (tablePanel *dashboardTable) target(t target) (table.Option, error) { - if t.Prometheus != nil { - return table.WithPrometheusTarget(t.Prometheus.Query, t.Prometheus.toOptions()...), nil - } - - return nil, fmt.Errorf("target not configured") -} - -type dashboardSingleStat struct { - Title string - Span float32 - Height string - Datasource string - Unit string - Targets []target - Thresholds [2]string - Colors [3]string - Color []string -} - -func (singleStatPanel dashboardSingleStat) toOption() (row.Option, error) { - opts := []singlestat.Option{} - - if singleStatPanel.Span != 0 { - opts = append(opts, singlestat.Span(singleStatPanel.Span)) - } - if singleStatPanel.Height != "" { - opts = append(opts, singlestat.Height(singleStatPanel.Height)) - } - if singleStatPanel.Datasource != "" { - opts = append(opts, singlestat.DataSource(singleStatPanel.Datasource)) - } - if singleStatPanel.Unit != "" { - opts = append(opts, singlestat.Unit(singleStatPanel.Unit)) - } - if singleStatPanel.Thresholds[0] != "" { - opts = append(opts, singlestat.Thresholds(singleStatPanel.Thresholds)) - } - if singleStatPanel.Colors[0] != "" { - opts = append(opts, singlestat.Colors(singleStatPanel.Colors)) - } - - for _, colorTarget := range singleStatPanel.Color { - switch colorTarget { - case "value": - opts = append(opts, singlestat.ColorValue()) - case "background": - opts = append(opts, singlestat.ColorBackground()) - default: - return nil, fmt.Errorf("invalid coloring target '%s'", colorTarget) - } - } - - for _, t := range singleStatPanel.Targets { - opt, err := singleStatPanel.target(t) - if err != nil { - return nil, err - } - - opts = append(opts, opt) - } - - return row.WithSingleStat(singleStatPanel.Title, opts...), nil -} - -func (singleStatPanel dashboardSingleStat) target(t target) (singlestat.Option, error) { - if t.Prometheus != nil { - return singlestat.WithPrometheusTarget(t.Prometheus.Query, t.Prometheus.toOptions()...), nil - } - - return nil, fmt.Errorf("target not configured") -} - -type dashboardText struct { - Title string - Span float32 - Height string - HTML string - Markdown string -} - -func (textPanel dashboardText) toOption() (row.Option, error) { - opts := []text.Option{} - - if textPanel.Span != 0 { - opts = append(opts, text.Span(textPanel.Span)) - } - if textPanel.Height != "" { - opts = append(opts, text.Height(textPanel.Height)) - } - if textPanel.Markdown != "" { - opts = append(opts, text.Markdown(textPanel.Markdown)) - } - if textPanel.HTML != "" { - opts = append(opts, text.HTML(textPanel.HTML)) - } - - return row.WithText(textPanel.Title, opts...), nil -} From 9ce8dabbaa898658dc45881f35ae80c35a99a2b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Sun, 22 Mar 2020 17:18:15 +0100 Subject: [PATCH 15/24] Bootstrap tests for the YAML decoder --- cmd/yaml/example.yaml | 2 +- decoder/dashboard_test.go | 233 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 decoder/dashboard_test.go diff --git a/cmd/yaml/example.yaml b/cmd/yaml/example.yaml index 7d00dc1c..23ff660b 100644 --- a/cmd/yaml/example.yaml +++ b/cmd/yaml/example.yaml @@ -89,4 +89,4 @@ rows: markdown: "Markdown syntax help: [commonmark.org/help](https://commonmark.org/help/)\n${percentile}" - text: title: Some awesome html? - markdown: "Some awesome html?" + html: "Some awesome html?" diff --git a/decoder/dashboard_test.go b/decoder/dashboard_test.go new file mode 100644 index 00000000..28d4acc7 --- /dev/null +++ b/decoder/dashboard_test.go @@ -0,0 +1,233 @@ +package decoder + +import ( + "bytes" + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +type testCase struct { + name string + yaml string + expectedGrafanaJSON string +} + +func TestUnmarshalYAML(t *testing.T) { + testCases := []struct { + name string + yaml string + expectedGrafanaJSON string + }{ + generalOptions(), + tagAnnotations(), + textPanel(), + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + req := require.New(t) + + builder, err := UnmarshalYAML(bytes.NewBufferString(testCase.yaml)) + req.NoError(err) + + json, err := builder.MarshalJSON() + req.NoError(err) + + fmt.Printf("json:\n%s\n", json) + + req.JSONEq(testCase.expectedGrafanaJSON, string(json)) + }) + } +} + +func generalOptions() testCase { + yaml := `title: Awesome dashboard + +editable: true +shared_crosshair: true +tags: [generated, yaml] +auto_refresh: 10s +` + json := `{ + "slug": "", + "title": "Awesome dashboard", + "originalTitle": "", + "tags": ["generated", "yaml"], + "style": "dark", + "timezone": "", + "editable": true, + "hideControls": false, + "sharedCrosshair": true, + "templating": {"list": null}, + "annotations": {"list": null}, + "links": null, + "panels": null, + "rows": [], + "refresh": "10s", + "time": {"from": "now-3h", "to": "now"}, + "timepicker": { + "refresh_intervals": ["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"], + "time_options": ["5m","15m","1h","6h","12h","24h","2d","7d","30d"] + }, + "schemaVersion": 0, + "version": 0 +}` + + return testCase{ + name: "general options", + yaml: yaml, + expectedGrafanaJSON: json, + } +} + +func tagAnnotations() testCase { + yaml := `title: Awesome dashboard + +tags_annotations: + - name: Deployments + datasource: "-- Grafana --" + color: "#5794F2" + tags: ["deploy", "production"] +` + json := `{ + "slug": "", + "title": "Awesome dashboard", + "originalTitle": "", + "tags": null, + "style": "dark", + "timezone": "", + "editable": false, + "hideControls": false, + "sharedCrosshair": false, + "templating": { + "list": null + }, + "annotations": { + "list": [{ + "datasource": "-- Grafana --", + "enable": true, + "iconColor": "#5794F2", + "iconSize": 0, + "name": "Deployments", + "query": "", + "showLine": false, + "lineColor": "", + "tags": ["deploy", "production"], + "tagsField": "", + "textField": "", + "type": "tags" + }] + }, + "links": null, + "panels": null, + "rows": [], + "time": {"from": "now-3h", "to": "now"}, + "timepicker": { + "refresh_intervals": ["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"], + "time_options": ["5m","15m","1h","6h","12h","24h","2d","7d","30d"] + }, + "schemaVersion": 0, + "version": 0 +}` + + return testCase{ + name: "tag annotations", + yaml: yaml, + expectedGrafanaJSON: json, + } +} + +func textPanel() testCase { + yaml := `title: Awesome dashboard + +rows: + - name: Test row + panels: + - text: + title: Some markdown? + markdown: "*markdown*" + - text: + title: Some html? + html: "Some awesome html" +` + json := `{ + "slug": "", + "title": "Awesome dashboard", + "originalTitle": "", + "tags": null, + "style": "dark", + "timezone": "", + "editable": false, + "hideControls": false, + "sharedCrosshair": false, + "templating": {"list": null}, + "annotations": {"list": null}, + "links": null, + "panels": null, + "rows": [ + { + "title": "Test row", + "collapse": false, + "editable": true, + "height": "250px", + "repeat": null, + "showTitle": true, + "panels": [ + { + "type": "text", + "mode": "markdown", + "content": "*markdown*", + "editable": false, + "error": false, + "gridPos": {}, + "id": 1, + "isNew": false, + "pageSize": 0, + "scroll": false, + "renderer": "flot", + "showHeader": false, + "sort": {"col": 0, "desc": false}, + "span": 6, + "styles": null, + "title": "Some markdown?", + "transparent": false + }, + { + "type": "text", + "mode": "html", + "content": "Some awesome html", + "editable": false, + "error": false, + "gridPos": {}, + "id": 2, + "isNew": false, + "scroll": false, + "pageSize": 0, + "renderer": "flot", + "showHeader": false, + "sort": {"col": 0, "desc": false}, + "span": 6, + "styles": null, + "title": "Some html?", + "transparent": false + } + ] + } + ], + "time": {"from": "now-3h", "to": "now"}, + "timepicker": { + "refresh_intervals": ["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"], + "time_options": ["5m","15m","1h","6h","12h","24h","2d","7d","30d"] + }, + "schemaVersion": 0, + "version": 0 +}` + + return testCase{ + name: "single row with text panels", + yaml: yaml, + expectedGrafanaJSON: json, + } +} From 56635d9bbf8e771e12db6c5eaa1f5b36d448fcdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Sun, 22 Mar 2020 17:20:56 +0100 Subject: [PATCH 16/24] Fix linter issues --- decoder/dashboard_test.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/decoder/dashboard_test.go b/decoder/dashboard_test.go index 28d4acc7..c5eab5f4 100644 --- a/decoder/dashboard_test.go +++ b/decoder/dashboard_test.go @@ -26,10 +26,12 @@ func TestUnmarshalYAML(t *testing.T) { } for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { + tc := testCase + + t.Run(tc.name, func(t *testing.T) { req := require.New(t) - builder, err := UnmarshalYAML(bytes.NewBufferString(testCase.yaml)) + builder, err := UnmarshalYAML(bytes.NewBufferString(tc.yaml)) req.NoError(err) json, err := builder.MarshalJSON() @@ -37,7 +39,7 @@ func TestUnmarshalYAML(t *testing.T) { fmt.Printf("json:\n%s\n", json) - req.JSONEq(testCase.expectedGrafanaJSON, string(json)) + req.JSONEq(tc.expectedGrafanaJSON, string(json)) }) } } From ec7fb536821d84798f9be5062766a8505ced31d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Sun, 22 Mar 2020 17:36:58 +0100 Subject: [PATCH 17/24] Add a few tests for graph panels --- decoder/dashboard.go | 4 +- decoder/dashboard_test.go | 136 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 2 deletions(-) diff --git a/decoder/dashboard.go b/decoder/dashboard.go index 06a97e83..ef0ee13b 100644 --- a/decoder/dashboard.go +++ b/decoder/dashboard.go @@ -48,8 +48,8 @@ func (d *dashboardModel) toDashboardBuilder() (dashboard.Builder, error) { opts = append(opts, opt) } - for _, row := range d.Rows { - opt, err := row.toOption() + for _, r := range d.Rows { + opt, err := r.toOption() if err != nil { return emptyDashboard, err } diff --git a/decoder/dashboard_test.go b/decoder/dashboard_test.go index c5eab5f4..a0c580ad 100644 --- a/decoder/dashboard_test.go +++ b/decoder/dashboard_test.go @@ -23,6 +23,7 @@ func TestUnmarshalYAML(t *testing.T) { generalOptions(), tagAnnotations(), textPanel(), + graphPanel(), } for _, testCase := range testCases { @@ -148,9 +149,13 @@ rows: - name: Test row panels: - text: + height: 400px + span: 6 title: Some markdown? markdown: "*markdown*" - text: + height: 400px + span: 6 title: Some html? html: "Some awesome html" ` @@ -192,6 +197,7 @@ rows: "showHeader": false, "sort": {"col": 0, "desc": false}, "span": 6, + "height": "400px", "styles": null, "title": "Some markdown?", "transparent": false @@ -211,6 +217,7 @@ rows: "showHeader": false, "sort": {"col": 0, "desc": false}, "span": 6, + "height": "400px", "styles": null, "title": "Some html?", "transparent": false @@ -233,3 +240,132 @@ rows: expectedGrafanaJSON: json, } } + +func graphPanel() testCase { + yaml := `title: Awesome dashboard + +rows: + - name: Test row + panels: + - graph: + title: Heap allocations + height: 400px + span: 4 + datasource: prometheus-default + targets: + - prometheus: + query: "go_memstats_heap_alloc_bytes" + legend: "{{job}}" + ref: A +` + json := `{ + "slug": "", + "title": "Awesome dashboard", + "originalTitle": "", + "tags": null, + "style": "dark", + "timezone": "", + "editable": false, + "hideControls": false, + "sharedCrosshair": false, + "templating": {"list": null}, + "annotations": {"list": null}, + "links": null, + "panels": null, + "rows": [ + { + "title": "Test row", + "collapse": false, + "editable": true, + "height": "250px", + "repeat": null, + "showTitle": true, + "panels": [ + { + "type": "graph", + "datasource": "prometheus-default", + "editable": true, + "error": false, + "height": "400px", + "gridPos": {}, + "id": 3, + "isNew": false, + "renderer": "flot", + "span": 4, + "fill": 1, + "title": "Heap allocations", + "aliasColors": {}, + "bars": false, + "points": false, + "stack": false, + "steppedLine": false, + "lines": true, + "linewidth": 1, + "pointradius": 5, + "percentage": false, + "nullPointMode": "null as zero", + "legend": { + "alignAsTable": false, + "avg": false, + "current": false, + "hideEmpty": true, + "hideZero": true, + "max": false, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": false + }, + "targets": [ + { + "refId": "A", + "expr": "go_memstats_heap_alloc_bytes", + "legendFormat": "{{job}}", + "format": "time_series" + } + ], + "tooltip": { + "shared": true, + "value_type": "", + "sort": 2 + }, + "x-axis": true, + "y-axis": true, + "xaxis": { + "format": "time", + "logBase": 1, + "show": true + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": false + } + ], + "transparent": false + } + ] + } + ], + "time": {"from": "now-3h", "to": "now"}, + "timepicker": { + "refresh_intervals": ["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"], + "time_options": ["5m","15m","1h","6h","12h","24h","2d","7d","30d"] + }, + "schemaVersion": 0, + "version": 0 +}` + + return testCase{ + name: "single row with single graph panel", + yaml: yaml, + expectedGrafanaJSON: json, + } +} From 76284c5bc6911e463a70c1b10e0dcc748bd39aeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Sun, 22 Mar 2020 17:39:17 +0100 Subject: [PATCH 18/24] Add test for malformed yaml --- decoder/dashboard_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/decoder/dashboard_test.go b/decoder/dashboard_test.go index a0c580ad..7da55b65 100644 --- a/decoder/dashboard_test.go +++ b/decoder/dashboard_test.go @@ -14,6 +14,12 @@ type testCase struct { expectedGrafanaJSON string } +func TestUnmarshalYAMLWithInvalidInput(t *testing.T) { + _, err := UnmarshalYAML(bytes.NewBufferString("")) + + require.Error(t, err) +} + func TestUnmarshalYAML(t *testing.T) { testCases := []struct { name string From 79a4cf701ea336d4f94511c8e4b1f86b3034b89e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Sun, 22 Mar 2020 17:46:29 +0100 Subject: [PATCH 19/24] Add a few tests for singlestat panels --- decoder/dashboard_test.go | 125 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/decoder/dashboard_test.go b/decoder/dashboard_test.go index 7da55b65..7817f011 100644 --- a/decoder/dashboard_test.go +++ b/decoder/dashboard_test.go @@ -30,6 +30,7 @@ func TestUnmarshalYAML(t *testing.T) { tagAnnotations(), textPanel(), graphPanel(), + singleStatPanel(), } for _, testCase := range testCases { @@ -375,3 +376,127 @@ rows: expectedGrafanaJSON: json, } } + +func singleStatPanel() testCase { + yaml := `title: Awesome dashboard + +rows: + - name: Test row + panels: + - single_stat: + title: Heap Allocations + height: 400px + span: 4 + datasource: prometheus-default + targets: + - prometheus: + query: 'go_memstats_heap_alloc_bytes{job="prometheus"}' + unit: bytes + thresholds: ["26000000", "28000000"] + color: ["value", "background"] + colors: ["green", "yellow", "red"] +` + json := `{ + "slug": "", + "title": "Awesome dashboard", + "originalTitle": "", + "tags": null, + "style": "dark", + "timezone": "", + "editable": false, + "hideControls": false, + "sharedCrosshair": false, + "templating": {"list": null}, + "annotations": {"list": null}, + "links": null, + "panels": null, + "rows": [ + { + "title": "Test row", + "collapse": false, + "editable": true, + "height": "250px", + "repeat": null, + "showTitle": true, + "panels": [ + { + "datasource": "prometheus-default", + "editable": true, + "error": false, + "gridPos": {}, + "id": 4, + "isNew": false, + "renderer": "flot", + "span": 4, + "height": "400px", + "title": "Heap Allocations", + "transparent": false, + "type": "singlestat", + "colors": [ + "green", + "yellow", + "red" + ], + "colorValue": true, + "colorBackground": true, + "decimals": 0, + "format": "bytes", + "gauge": { + "maxValue": 0, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": false + }, + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "nullPointMode": "", + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "lineColor": "rgb(31, 120, 193)" + }, + "targets": [ + { + "refId": "", + "expr": "go_memstats_heap_alloc_bytes{job=\"prometheus\"}", + "format": "time_series" + } + ], + "thresholds": "26000000,28000000", + "valueFontSize": "100%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + } + ] + } + ], + "time": {"from": "now-3h", "to": "now"}, + "timepicker": { + "refresh_intervals": ["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"], + "time_options": ["5m","15m","1h","6h","12h","24h","2d","7d","30d"] + }, + "schemaVersion": 0, + "version": 0 +}` + + return testCase{ + name: "single row with single graph panel", + yaml: yaml, + expectedGrafanaJSON: json, + } +} From a2d1252349fddc7229f0c90adcdb10c3b31d4338 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Sun, 22 Mar 2020 17:52:53 +0100 Subject: [PATCH 20/24] Add a few tests for table panels --- decoder/dashboard_test.go | 109 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/decoder/dashboard_test.go b/decoder/dashboard_test.go index 7817f011..1c9f699a 100644 --- a/decoder/dashboard_test.go +++ b/decoder/dashboard_test.go @@ -31,6 +31,7 @@ func TestUnmarshalYAML(t *testing.T) { textPanel(), graphPanel(), singleStatPanel(), + tablePanel(), } for _, testCase := range testCases { @@ -500,3 +501,111 @@ rows: expectedGrafanaJSON: json, } } + +func tablePanel() testCase { + yaml := `title: Awesome dashboard + +rows: + - name: Test row + panels: + - table: + title: Threads + height: 400px + span: 4 + datasource: prometheus-default + targets: + - prometheus: + query: "go_threads" + hidden_columns: ["Time"] + time_series_aggregations: + - label: AVG + type: avg + - label: Current + type: current +` + json := `{ + "slug": "", + "title": "Awesome dashboard", + "originalTitle": "", + "tags": null, + "style": "dark", + "timezone": "", + "editable": false, + "hideControls": false, + "sharedCrosshair": false, + "templating": {"list": null}, + "annotations": {"list": null}, + "links": null, + "panels": null, + "rows": [ + { + "title": "Test row", + "collapse": false, + "editable": true, + "height": "250px", + "repeat": null, + "showTitle": true, + "panels": [ + { + "datasource": "prometheus-default", + "editable": true, + "error": false, + "gridPos": {}, + "height": "400px", + "id": 5, + "isNew": false, + "renderer": "flot", + "span": 4, + "title": "Threads", + "transparent": false, + "type": "table", + "columns": [ + { + "text": "AVG", + "value": "avg" + }, + { + "text": "Current", + "value": "current" + } + ], + "styles": [ + { + "alias": "", + "pattern": "/.*/", + "type": "string" + }, + { + "alias": null, + "pattern": "Time", + "type": "hidden" + } + ], + "transform": "timeseries_aggregations", + "targets": [ + { + "refId": "", + "expr": "go_threads", + "format": "time_series" + } + ], + "scroll": false + } + ] + } + ], + "time": {"from": "now-3h", "to": "now"}, + "timepicker": { + "refresh_intervals": ["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"], + "time_options": ["5m","15m","1h","6h","12h","24h","2d","7d","30d"] + }, + "schemaVersion": 0, + "version": 0 +}` + + return testCase{ + name: "single row with single graph panel", + yaml: yaml, + expectedGrafanaJSON: json, + } +} From 56e55598d149894471701f83578f141c5131eaa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Sun, 22 Mar 2020 18:16:25 +0100 Subject: [PATCH 21/24] Add a few tests for dashboard variables --- cmd/yaml/example.yaml | 54 +++++----- decoder/dashboard_test.go | 164 +++++++++++++++++++++++++++++ decoder/variables.go | 108 +++++++++++-------- variable/constant/constant.go | 3 + variable/custom/custom.go | 3 + variable/interval/interval.go | 3 + variable/interval/interval_test.go | 2 +- 7 files changed, 265 insertions(+), 72 deletions(-) diff --git a/cmd/yaml/example.yaml b/cmd/yaml/example.yaml index 23ff660b..8d0c7270 100644 --- a/cmd/yaml/example.yaml +++ b/cmd/yaml/example.yaml @@ -12,33 +12,33 @@ tags_annotations: tags: ["deploy", "production"] variables: - - type: interval - name: interval - label: Interval - values: ["30s", "1m", "5m", "10m", "30m", "1h", "6h", "12h"] - - type: query - name: status - label: HTTP status - datasource: prometheus-default - request: "label_values(prometheus_http_requests_total, code)" - - type: const - name: percentile - label: Percentile - default: 80 - values_map: - 50th: "50" - 75th: "75" - 80th: "80" - 85th: "85" - 90th: "90" - 95th: "95" - 99th: "99" - - type: custom - name: vX - default: v2 - values_map: - v1: v1 - v2: v2 + - interval: + name: interval + label: Interval + values: ["30s", "1m", "5m", "10m", "30m", "1h", "6h", "12h"] + - query: + name: status + label: HTTP status + datasource: prometheus-default + request: "label_values(prometheus_http_requests_total, code)" + - const: + name: percentile + label: Percentile + default: 80 + values_map: + 50th: "50" + 75th: "75" + 80th: "80" + 85th: "85" + 90th: "90" + 95th: "95" + 99th: "99" + - custom: + name: vX + default: v2 + values_map: + v1: v1 + v2: v2 rows: - name: Prometheus diff --git a/decoder/dashboard_test.go b/decoder/dashboard_test.go index 1c9f699a..4ada3012 100644 --- a/decoder/dashboard_test.go +++ b/decoder/dashboard_test.go @@ -28,6 +28,7 @@ func TestUnmarshalYAML(t *testing.T) { }{ generalOptions(), tagAnnotations(), + variables(), textPanel(), graphPanel(), singleStatPanel(), @@ -150,6 +151,169 @@ tags_annotations: } } +func variables() testCase { + yaml := `title: Awesome dashboard + +variables: + - interval: + name: interval + label: Interval + default: 30s + values: ["30s", "1m", "5m", "10m", "30m", "1h", "6h", "12h"] + - query: + name: status + label: HTTP status + datasource: prometheus-default + request: "label_values(prometheus_http_requests_total, code)" + - const: + name: percentile + label: Percentile + default: 50 + values_map: + 50th: "50" + - custom: + name: vX + label: vX + default: v2 + values_map: + v1: v1 + v2: v2 +` + json := `{ + "slug": "", + "title": "Awesome dashboard", + "originalTitle": "", + "tags": null, + "style": "dark", + "timezone": "", + "editable": false, + "hideControls": false, + "sharedCrosshair": false, + "templating": { + "list": [ + { + "name": "interval", + "type": "interval", + "datasource": null, + "refresh": false, + "options": null, + "includeAll": false, + "allFormat": "", + "allValue": "", + "multi": false, + "multiFormat": "", + "query": "10m,12h,1h,1m,30m,30s,5m,6h", + "regex": "", + "current": { + "text": "30s", + "value": "30s" + }, + "label": "Interval", + "hide": 0, + "sort": 0 + }, + { + "name": "status", + "type": "query", + "datasource": "prometheus-default", + "refresh": 1, + "options": null, + "includeAll": false, + "allFormat": "", + "allValue": "", + "multi": false, + "multiFormat": "", + "query": "label_values(prometheus_http_requests_total, code)", + "regex": "", + "current": { + "text": "", + "value": null + }, + "label": "HTTP status", + "hide": 0, + "sort": 0 + }, + { + "name": "percentile", + "type": "constant", + "datasource": null, + "refresh": false, + "options": [ + { + "selected": false, + "text": "50th", + "value": "50" + } + ], + "includeAll": false, + "allFormat": "", + "allValue": "", + "multi": false, + "multiFormat": "", + "query": "50", + "regex": "", + "current": { + "text": "50th", + "value": "50" + }, + "label": "Percentile", + "hide": 0, + "sort": 0 + }, + { + "name": "vX", + "type": "custom", + "datasource": null, + "refresh": false, + "options": [ + { + "text": "v1", + "value": "v1", + "selected": false + }, + { + "text": "v2", + "value": "v2", + "selected": false + } + ], + "includeAll": false, + "allFormat": "", + "allValue": "", + "multi": false, + "multiFormat": "", + "query": "v1,v2", + "regex": "", + "current": { + "text": "v2", + "value": "v2" + }, + "label": "vX", + "hide": 0, + "sort": 0 + } + ] + }, + "annotations": {"list": null}, + "links": null, + "panels": null, + "rows": [], + "time": {"from": "now-3h", "to": "now"}, + "timepicker": { + "refresh_intervals": ["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"], + "time_options": ["5m","15m","1h","6h","12h","24h","2d","7d","30d"] + }, + "schemaVersion": 0, + "version": 0 +}` + + return testCase{ + name: "variables", + yaml: yaml, + expectedGrafanaJSON: json, + } +} + func textPanel() testCase { yaml := `title: Awesome dashboard diff --git a/decoder/variables.go b/decoder/variables.go index f6b04cc2..b710b74f 100644 --- a/decoder/variables.go +++ b/decoder/variables.go @@ -3,48 +3,46 @@ package decoder import ( "fmt" + "github.com/K-Phoen/grabana/variable/interval" + "github.com/K-Phoen/grabana/dashboard" "github.com/K-Phoen/grabana/variable/constant" "github.com/K-Phoen/grabana/variable/custom" - "github.com/K-Phoen/grabana/variable/interval" "github.com/K-Phoen/grabana/variable/query" ) type dashboardVariable struct { - Type string - Name string - Label string - - // used for "interval", "const" and "custom" - Default string - - // used for "interval" - Values []string + Interval *variableInterval + Custom *variableCustom + Query *variableQuery + Const *variableConst +} - // used for "const" and "custom" - ValuesMap map[string]string `yaml:"values_map"` +func (variable *dashboardVariable) toOption() (dashboard.Option, error) { + if variable.Query != nil { + return variable.Query.toOption(), nil + } + if variable.Interval != nil { + return variable.Interval.toOption(), nil + } + if variable.Const != nil { + return variable.Const.toOption(), nil + } + if variable.Custom != nil { + return variable.Custom.toOption(), nil + } - // used for "query" - Datasource string - Request string + return nil, fmt.Errorf("unconfigured variable") } -func (variable *dashboardVariable) toOption() (dashboard.Option, error) { - switch variable.Type { - case "interval": - return variable.asInterval(), nil - case "query": - return variable.asQuery(), nil - case "const": - return variable.asConst(), nil - case "custom": - return variable.asCustom(), nil - } - - return nil, fmt.Errorf("unknown dashboard variable type '%s'", variable.Type) +type variableInterval struct { + Name string + Label string + Default string + Values []string } -func (variable *dashboardVariable) asInterval() dashboard.Option { +func (variable *variableInterval) toOption() dashboard.Option { opts := []interval.Option{ interval.Values(variable.Values), } @@ -59,22 +57,36 @@ func (variable *dashboardVariable) asInterval() dashboard.Option { return dashboard.VariableAsInterval(variable.Name, opts...) } -func (variable *dashboardVariable) asQuery() dashboard.Option { - opts := []query.Option{ - query.Request(variable.Request), +type variableCustom struct { + Name string + Label string + Default string + ValuesMap map[string]string `yaml:"values_map"` +} + +func (variable *variableCustom) toOption() dashboard.Option { + opts := []custom.Option{ + custom.Values(variable.ValuesMap), } - if variable.Datasource != "" { - opts = append(opts, query.DataSource(variable.Datasource)) + if variable.Default != "" { + opts = append(opts, custom.Default(variable.Default)) } if variable.Label != "" { - opts = append(opts, query.Label(variable.Label)) + opts = append(opts, custom.Label(variable.Label)) } - return dashboard.VariableAsQuery(variable.Name, opts...) + return dashboard.VariableAsCustom(variable.Name, opts...) +} + +type variableConst struct { + Name string + Label string + Default string + ValuesMap map[string]string `yaml:"values_map"` } -func (variable *dashboardVariable) asConst() dashboard.Option { +func (variable *variableConst) toOption() dashboard.Option { opts := []constant.Option{ constant.Values(variable.ValuesMap), } @@ -89,17 +101,25 @@ func (variable *dashboardVariable) asConst() dashboard.Option { return dashboard.VariableAsConst(variable.Name, opts...) } -func (variable *dashboardVariable) asCustom() dashboard.Option { - opts := []custom.Option{ - custom.Values(variable.ValuesMap), +type variableQuery struct { + Name string + Label string + + Datasource string + Request string +} + +func (variable *variableQuery) toOption() dashboard.Option { + opts := []query.Option{ + query.Request(variable.Request), } - if variable.Default != "" { - opts = append(opts, custom.Default(variable.Default)) + if variable.Datasource != "" { + opts = append(opts, query.DataSource(variable.Datasource)) } if variable.Label != "" { - opts = append(opts, custom.Label(variable.Label)) + opts = append(opts, query.Label(variable.Label)) } - return dashboard.VariableAsCustom(variable.Name, opts...) + return dashboard.VariableAsQuery(variable.Name, opts...) } diff --git a/variable/constant/constant.go b/variable/constant/constant.go index 34236d41..f4d453c8 100644 --- a/variable/constant/constant.go +++ b/variable/constant/constant.go @@ -1,6 +1,7 @@ package constant import ( + "sort" "strings" "github.com/grafana-tools/sdk" @@ -19,6 +20,8 @@ func (values ValuesMap) asQuery() string { valuesList = append(valuesList, value) } + sort.Strings(valuesList) + return strings.Join(valuesList, ",") } diff --git a/variable/custom/custom.go b/variable/custom/custom.go index 37e6dfb9..d90e624d 100644 --- a/variable/custom/custom.go +++ b/variable/custom/custom.go @@ -1,6 +1,7 @@ package custom import ( + "sort" "strings" "github.com/grafana-tools/sdk" @@ -19,6 +20,8 @@ func (values ValuesMap) asQuery() string { valuesList = append(valuesList, value) } + sort.Strings(valuesList) + return strings.Join(valuesList, ",") } diff --git a/variable/interval/interval.go b/variable/interval/interval.go index 09186796..adad05bb 100644 --- a/variable/interval/interval.go +++ b/variable/interval/interval.go @@ -1,6 +1,7 @@ package interval import ( + "sort" "strings" "github.com/grafana-tools/sdk" @@ -35,6 +36,8 @@ func New(name string, options ...Option) *Interval { // Values sets the possible values for the variable. func Values(values ValuesList) Option { return func(interval *Interval) { + sort.Strings(values) + interval.Builder.Query = strings.Join(values, ",") } } diff --git a/variable/interval/interval_test.go b/variable/interval/interval_test.go index f8af4129..768b993a 100644 --- a/variable/interval/interval_test.go +++ b/variable/interval/interval_test.go @@ -31,7 +31,7 @@ func TestValuesCanBeSet(t *testing.T) { panel := New("", Values(values)) - req.Equal("30s,1m,5m,10m,30m,1h,6h,12h", panel.Builder.Query) + req.Equal("10m,12h,1h,1m,30m,30s,5m,6h", panel.Builder.Query) } func TestDefaultValueCanBeSet(t *testing.T) { From 79703e90a88b74ce292e0e3466374b14f42f1ab0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Sun, 22 Mar 2020 18:37:01 +0100 Subject: [PATCH 22/24] Add a few tests for error cases --- decoder/dashboard.go | 4 +- decoder/dashboard_test.go | 100 ++++++++++++++++++++++++++++++++++++-- decoder/graph.go | 4 +- decoder/singlestat.go | 6 ++- decoder/table.go | 4 +- decoder/target.go | 4 ++ decoder/variables.go | 4 +- 7 files changed, 113 insertions(+), 13 deletions(-) diff --git a/decoder/dashboard.go b/decoder/dashboard.go index ef0ee13b..96baac59 100644 --- a/decoder/dashboard.go +++ b/decoder/dashboard.go @@ -7,6 +7,8 @@ import ( "github.com/K-Phoen/grabana/row" ) +var ErrPanelNotConfigured = fmt.Errorf("panel not configured") + type dashboardModel struct { Title string Editable bool @@ -97,5 +99,5 @@ func (panel dashboardPanel) toOption() (row.Option, error) { return panel.Text.toOption(), nil } - return nil, fmt.Errorf("panel not configured") + return nil, ErrPanelNotConfigured } diff --git a/decoder/dashboard_test.go b/decoder/dashboard_test.go index 4ada3012..59612268 100644 --- a/decoder/dashboard_test.go +++ b/decoder/dashboard_test.go @@ -2,7 +2,6 @@ package decoder import ( "bytes" - "fmt" "testing" "github.com/stretchr/testify/require" @@ -47,13 +46,108 @@ func TestUnmarshalYAML(t *testing.T) { json, err := builder.MarshalJSON() req.NoError(err) - fmt.Printf("json:\n%s\n", json) - req.JSONEq(tc.expectedGrafanaJSON, string(json)) }) } } +func TestUnmarshalYAMLWithInvalidPanel(t *testing.T) { + payload := ` +rows: + - name: Prometheus + panels: + - {}` + + _, err := UnmarshalYAML(bytes.NewBufferString(payload)) + + require.Error(t, err) + require.Equal(t, ErrPanelNotConfigured, err) +} + +func TestUnmarshalYAMLWithInvalidVariable(t *testing.T) { + payload := ` +variables: + - {}` + + _, err := UnmarshalYAML(bytes.NewBufferString(payload)) + + require.Error(t, err) + require.Equal(t, ErrVariableNotConfigured, err) +} + +func TestUnmarshalYAMLWithNoTargetTable(t *testing.T) { + payload := ` +rows: + - name: Prometheus + panels: + - table: + title: Threads + targets: + - {} +` + + _, err := UnmarshalYAML(bytes.NewBufferString(payload)) + + require.Error(t, err) + require.Equal(t, ErrTargetNotConfigured, err) +} + +func TestUnmarshalYAMLWithNoTargetSingleStat(t *testing.T) { + payload := ` +rows: + - name: Prometheus + panels: + - single_stat: + title: Threads + targets: + - {} +` + + _, err := UnmarshalYAML(bytes.NewBufferString(payload)) + + require.Error(t, err) + require.Equal(t, ErrTargetNotConfigured, err) +} + +func TestUnmarshalYAMLWithSingleStatAndInvalidColoringTarget(t *testing.T) { + payload := ` +rows: + - name: Prometheus + panels: + - single_stat: + title: Heap Allocations + datasource: prometheus-default + targets: + - prometheus: + query: 'go_memstats_heap_alloc_bytes{job="prometheus"}' + unit: bytes + thresholds: ["26000000", "28000000"] + color: ["value", "invalid target"] +` + + _, err := UnmarshalYAML(bytes.NewBufferString(payload)) + + require.Error(t, err) + require.Equal(t, ErrInvalidColoringTarget, err) +} + +func TestUnmarshalYAMLWithNoTargetSingleGraph(t *testing.T) { + payload := ` +rows: + - name: Prometheus + panels: + - graph: + title: Threads + targets: + - {} +` + + _, err := UnmarshalYAML(bytes.NewBufferString(payload)) + + require.Error(t, err) + require.Equal(t, ErrTargetNotConfigured, err) +} + func generalOptions() testCase { yaml := `title: Awesome dashboard diff --git a/decoder/graph.go b/decoder/graph.go index f32c9a8c..354a4ce5 100644 --- a/decoder/graph.go +++ b/decoder/graph.go @@ -1,8 +1,6 @@ package decoder import ( - "fmt" - "github.com/K-Phoen/grabana/graph" "github.com/K-Phoen/grabana/row" ) @@ -45,5 +43,5 @@ func (graphPanel *dashboardGraph) target(t target) (graph.Option, error) { return graph.WithPrometheusTarget(t.Prometheus.Query, t.Prometheus.toOptions()...), nil } - return nil, fmt.Errorf("target not configured") + return nil, ErrTargetNotConfigured } diff --git a/decoder/singlestat.go b/decoder/singlestat.go index 195f0127..08321f20 100644 --- a/decoder/singlestat.go +++ b/decoder/singlestat.go @@ -7,6 +7,8 @@ import ( "github.com/K-Phoen/grabana/singlestat" ) +var ErrInvalidColoringTarget = fmt.Errorf("invalid coloring target") + type dashboardSingleStat struct { Title string Span float32 @@ -48,7 +50,7 @@ func (singleStatPanel dashboardSingleStat) toOption() (row.Option, error) { case "background": opts = append(opts, singlestat.ColorBackground()) default: - return nil, fmt.Errorf("invalid coloring target '%s'", colorTarget) + return nil, ErrInvalidColoringTarget } } @@ -69,5 +71,5 @@ func (singleStatPanel dashboardSingleStat) target(t target) (singlestat.Option, return singlestat.WithPrometheusTarget(t.Prometheus.Query, t.Prometheus.toOptions()...), nil } - return nil, fmt.Errorf("target not configured") + return nil, ErrTargetNotConfigured } diff --git a/decoder/table.go b/decoder/table.go index 88416fbf..6c49b4a5 100644 --- a/decoder/table.go +++ b/decoder/table.go @@ -1,8 +1,6 @@ package decoder import ( - "fmt" - "github.com/K-Phoen/grabana/row" "github.com/K-Phoen/grabana/table" ) @@ -55,5 +53,5 @@ func (tablePanel *dashboardTable) target(t target) (table.Option, error) { return table.WithPrometheusTarget(t.Prometheus.Query, t.Prometheus.toOptions()...), nil } - return nil, fmt.Errorf("target not configured") + return nil, ErrTargetNotConfigured } diff --git a/decoder/target.go b/decoder/target.go index 1588779b..2211bd86 100644 --- a/decoder/target.go +++ b/decoder/target.go @@ -1,9 +1,13 @@ package decoder import ( + "fmt" + "github.com/K-Phoen/grabana/target/prometheus" ) +var ErrTargetNotConfigured = fmt.Errorf("target not configured") + type target struct { Prometheus *prometheusTarget } diff --git a/decoder/variables.go b/decoder/variables.go index b710b74f..4e5b3c38 100644 --- a/decoder/variables.go +++ b/decoder/variables.go @@ -11,6 +11,8 @@ import ( "github.com/K-Phoen/grabana/variable/query" ) +var ErrVariableNotConfigured = fmt.Errorf("variable not configured") + type dashboardVariable struct { Interval *variableInterval Custom *variableCustom @@ -32,7 +34,7 @@ func (variable *dashboardVariable) toOption() (dashboard.Option, error) { return variable.Custom.toOption(), nil } - return nil, fmt.Errorf("unconfigured variable") + return nil, ErrVariableNotConfigured } type variableInterval struct { From b337b34c0407c2f284ff51f7fdbb1b6c9841db48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Sun, 22 Mar 2020 18:53:35 +0100 Subject: [PATCH 23/24] Allow DefaultAll and IncludeAll in YAML-defined query variables --- decoder/dashboard_test.go | 16 ++++++++++++---- decoder/variables.go | 9 +++++++++ variable/query/query.go | 2 +- variable/query/query_test.go | 2 +- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/decoder/dashboard_test.go b/decoder/dashboard_test.go index 59612268..fd535c4f 100644 --- a/decoder/dashboard_test.go +++ b/decoder/dashboard_test.go @@ -258,6 +258,8 @@ variables: name: status label: HTTP status datasource: prometheus-default + include_all: true + default_all: true request: "label_values(prometheus_http_requests_total, code)" - const: name: percentile @@ -311,8 +313,14 @@ variables: "type": "query", "datasource": "prometheus-default", "refresh": 1, - "options": null, - "includeAll": false, + "options": [ + { + "text": "All", + "value": "$__all", + "selected": false + } + ], + "includeAll": true, "allFormat": "", "allValue": "", "multi": false, @@ -320,8 +328,8 @@ variables: "query": "label_values(prometheus_http_requests_total, code)", "regex": "", "current": { - "text": "", - "value": null + "text": "All", + "value": "$__all" }, "label": "HTTP status", "hide": 0, diff --git a/decoder/variables.go b/decoder/variables.go index 4e5b3c38..500286d1 100644 --- a/decoder/variables.go +++ b/decoder/variables.go @@ -109,6 +109,9 @@ type variableQuery struct { Datasource string Request string + + IncludeAll bool `yaml:"include_all"` + DefaultAll bool `yaml:"default_all"` } func (variable *variableQuery) toOption() dashboard.Option { @@ -122,6 +125,12 @@ func (variable *variableQuery) toOption() dashboard.Option { if variable.Label != "" { opts = append(opts, query.Label(variable.Label)) } + if variable.IncludeAll { + opts = append(opts, query.IncludeAll()) + } + if variable.DefaultAll { + opts = append(opts, query.DefaultAll()) + } return dashboard.VariableAsQuery(variable.Name, opts...) } diff --git a/variable/query/query.go b/variable/query/query.go index 2d3409d8..7061aa46 100644 --- a/variable/query/query.go +++ b/variable/query/query.go @@ -146,6 +146,6 @@ func IncludeAll() Option { // DefaultAll selects "All" values by default. func DefaultAll() Option { return func(query *Query) { - query.Builder.Current = sdk.Current{Text: "All", Value: "$_all"} + query.Builder.Current = sdk.Current{Text: "All", Value: "$__all"} } } diff --git a/variable/query/query_test.go b/variable/query/query_test.go index 49972bf8..604718f0 100644 --- a/variable/query/query_test.go +++ b/variable/query/query_test.go @@ -63,7 +63,7 @@ func TestAnAllValuesCanBeTheDefault(t *testing.T) { panel := New("", DefaultAll()) req.Equal("All", panel.Builder.Current.Text) - req.Equal("$_all", panel.Builder.Current.Value) + req.Equal("$__all", panel.Builder.Current.Value) } func TestValuesCanBeFilteredByRegex(t *testing.T) { From 5572be2820d2538775410ca5938e1820df673049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Sun, 22 Mar 2020 19:00:05 +0100 Subject: [PATCH 24/24] Allow sparkline configuration in YAML-defined singlestat panels --- decoder/dashboard_test.go | 32 ++++++++++++++++++++++---------- decoder/singlestat.go | 12 ++++++++++++ 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/decoder/dashboard_test.go b/decoder/dashboard_test.go index fd535c4f..b43913dd 100644 --- a/decoder/dashboard_test.go +++ b/decoder/dashboard_test.go @@ -109,6 +109,22 @@ rows: require.Equal(t, ErrTargetNotConfigured, err) } +func TestUnmarshalYAMLWithNoTInvalidSparklineModeSingleStat(t *testing.T) { + payload := ` +rows: + - name: Prometheus + panels: + - single_stat: + title: Threads + sparkline: unknown-mode +` + + _, err := UnmarshalYAML(bytes.NewBufferString(payload)) + + require.Error(t, err) + require.Equal(t, ErrInvalidSparkLineMode, err) +} + func TestUnmarshalYAMLWithSingleStatAndInvalidColoringTarget(t *testing.T) { payload := ` rows: @@ -270,10 +286,9 @@ variables: - custom: name: vX label: vX - default: v2 + default: v1 values_map: v1: v1 - v2: v2 ` json := `{ "slug": "", @@ -372,11 +387,6 @@ variables: "text": "v1", "value": "v1", "selected": false - }, - { - "text": "v2", - "value": "v2", - "selected": false } ], "includeAll": false, @@ -384,11 +394,11 @@ variables: "allValue": "", "multi": false, "multiFormat": "", - "query": "v1,v2", + "query": "v1", "regex": "", "current": { - "text": "v2", - "value": "v2" + "text": "v1", + "value": "v1" }, "label": "vX", "hide": 0, @@ -659,6 +669,7 @@ rows: - prometheus: query: 'go_memstats_heap_alloc_bytes{job="prometheus"}' unit: bytes + sparkline: bottom thresholds: ["26000000", "28000000"] color: ["value", "background"] colors: ["green", "yellow", "red"] @@ -728,6 +739,7 @@ rows: ], "nullPointMode": "", "sparkline": { + "show": true, "fillColor": "rgba(31, 118, 189, 0.18)", "lineColor": "rgb(31, 120, 193)" }, diff --git a/decoder/singlestat.go b/decoder/singlestat.go index 08321f20..15f0f091 100644 --- a/decoder/singlestat.go +++ b/decoder/singlestat.go @@ -8,6 +8,7 @@ import ( ) var ErrInvalidColoringTarget = fmt.Errorf("invalid coloring target") +var ErrInvalidSparkLineMode = fmt.Errorf("invalid sparkline mode") type dashboardSingleStat struct { Title string @@ -15,6 +16,7 @@ type dashboardSingleStat struct { Height string Datasource string Unit string + SparkLine string `yaml:"sparkline"` Targets []target Thresholds [2]string Colors [3]string @@ -43,6 +45,16 @@ func (singleStatPanel dashboardSingleStat) toOption() (row.Option, error) { opts = append(opts, singlestat.Colors(singleStatPanel.Colors)) } + switch singleStatPanel.SparkLine { + case "bottom": + opts = append(opts, singlestat.SparkLine()) + case "full": + opts = append(opts, singlestat.FullSparkLine()) + case "": + default: + return nil, ErrInvalidSparkLineMode + } + for _, colorTarget := range singleStatPanel.Color { switch colorTarget { case "value":