From ac4a3cba6427f75b9c6a2a6eaa04c292e5caa333 Mon Sep 17 00:00:00 2001 From: mashiike Date: Sat, 13 Nov 2021 12:41:16 +0900 Subject: [PATCH 1/9] use urfave/cli/v2 for subcommands --- cmd/shimesaba/main.go | 189 ++++++++++++++++++++++++------------------ go.mod | 4 + go.sum | 4 + 3 files changed, 115 insertions(+), 82 deletions(-) diff --git a/cmd/shimesaba/main.go b/cmd/shimesaba/main.go index b51b37f..968192e 100644 --- a/cmd/shimesaba/main.go +++ b/cmd/shimesaba/main.go @@ -2,12 +2,12 @@ package main import ( "context" - "flag" + "errors" "fmt" "log" "os" "os/signal" - "runtime" + "sort" "strings" "syscall" @@ -15,30 +15,12 @@ import ( "github.com/handlename/ssmwrap" "github.com/mashiike/shimesaba" "github.com/mashiike/shimesaba/internal/logger" + "github.com/urfave/cli/v2" ) -type stringSlice []string - -func (i *stringSlice) String() string { - return fmt.Sprintf("%v", *i) -} -func (i *stringSlice) Set(v string) error { - if strings.ContainsRune(v, ',') { - *i = append(*i, strings.Split(v, ",")...) - } else { - *i = append(*i, v) - } - return nil -} - var ( - Version = "current" - mackerelAPIKey string - debug bool - dryRun bool - backfill uint - version bool - configFiles stringSlice + Version = "current" + app *shimesaba.App ) func main() { @@ -50,72 +32,115 @@ func main() { Retries: 3, }) } - - flag.Var(&configFiles, "config", "config file path, can set multiple") - flag.StringVar(&mackerelAPIKey, "mackerel-apikey", "", "for access mackerel API") - flag.BoolVar(&debug, "debug", false, "output debug log") - flag.BoolVar(&dryRun, "dry-run", false, "report output stdout and not put mackerel") - flag.BoolVar(&version, "version", false, "show version") - flag.UintVar(&backfill, "backfill", 3, "generate report before n point") - flag.VisitAll(envToFlag) - flag.Parse() - - minLevel := "info" - if debug { - minLevel = "debug" - } - logger.Setup(os.Stderr, minLevel) - if version { - log.Printf("[info] shimesaba version : %s", Version) - log.Printf("[info] go runtime version: %s", runtime.Version()) - return + cliApp := &cli.App{ + Name: "shimesaba", + Usage: "A commandline tool for tracking SLO/ErrorBudget using Mackerel as an SLI measurement service.", + Flags: []cli.Flag{ + &cli.StringSliceFlag{ + Name: "config", + Aliases: []string{"c"}, + Usage: "config file path, can set multiple", + Required: true, + EnvVars: []string{"CONFIG", "SHIMESABA_CONFIG"}, + }, + &cli.StringFlag{ + Name: "mackerel-apikey", + Aliases: []string{"k"}, + Usage: "for access mackerel API", + Required: true, + DefaultText: "*********", + EnvVars: []string{"MACKEREL_APIKEY", "SHIMESABA_MACKEREL_APIKEY"}, + }, + &cli.BoolFlag{ + Name: "debug", + Usage: "output debug log", + EnvVars: []string{"SHIMESABA_DEBUG"}, + }, + }, + Commands: []*cli.Command{ + { + Name: "run", + Usage: "run shimesaba. this is main feature", + Action: func(c *cli.Context) error { + if c.Int("backfill") <= 0 { + return errors.New("backfill count must positive value") + } + opts := []shimesaba.RunOption{ + shimesaba.DryRunOption(c.Bool("dry-run")), + shimesaba.BackfillOption(c.Int("backfill")), + } + handler := func(ctx context.Context) error { + return app.Run(ctx, opts...) + } + if isLabmda() { + lambda.Start(handler) + return nil + } + return handler(c.Context) + }, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "dry-run", + Usage: "report output stdout and not put mackerel", + EnvVars: []string{"SHIMESABA_DRY_RUN"}, + }, + &cli.IntFlag{ + Name: "backfill", + DefaultText: "3", + Value: 3, + Usage: "generate report before n point", + EnvVars: []string{"BACKFILL", "SHIMESABA_BACKFILL"}, + }, + }, + }, + }, } - if ssmwrapErr != nil { - logger.Setup(os.Stderr, "info") - log.Printf("[error] ssmwrap.Export failed: %s\n", ssmwrapErr) - os.Exit(1) - } - if backfill == 0 { - log.Println("[error] backfill count must positive avlue") - os.Exit(1) - } - cfg := shimesaba.NewDefaultConfig() - if err := cfg.Load(configFiles...); err != nil { - log.Println("[error]", err) - os.Exit(1) - } - if err := cfg.ValidateVersion(Version); err != nil { - log.Println("[error]", err) - os.Exit(1) - } - app, err := shimesaba.New(mackerelAPIKey, cfg) - if err != nil { - log.Println("[error]", err) - os.Exit(1) + sort.Sort(cli.FlagsByName(cliApp.Flags)) + sort.Sort(cli.CommandsByName(cliApp.Commands)) + cliApp.Version = Version + cliApp.EnableBashCompletion = true + cliApp.Before = func(c *cli.Context) error { + minLevel := "info" + if c.Bool("debug") { + minLevel = "debug" + } + logger.Setup(os.Stderr, minLevel) + switch c.Args().First() { + case "help", "h", "version": + return nil + default: + } + if ssmwrapErr != nil { + return fmt.Errorf("ssmwrap.Export failed: %w", ssmwrapErr) + } + cfg := shimesaba.NewDefaultConfig() + if err := cfg.Load(c.StringSlice("config")...); err != nil { + return err + } + if err := cfg.ValidateVersion(Version); err != nil { + return err + } + var err error + app, err = shimesaba.New(c.String("mackerel-apikey"), cfg) + if err != nil { + return err + } + return nil } - if strings.HasPrefix(os.Getenv("AWS_EXECUTION_ENV"), "AWS_Lambda") || - os.Getenv("AWS_LAMBDA_RUNTIME_API") != "" { - lambda.Start(lambdaHandler(app)) - return + if isLabmda() { + if len(os.Args) <= 1 { + os.Args = append(os.Args, "run") + } } ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT, syscall.SIGHUP) defer cancel() - if err := app.Run(ctx, shimesaba.DryRunOption(dryRun), shimesaba.BackfillOption(int(backfill))); err != nil { - log.Println("[error]", err) - os.Exit(1) + if err := cliApp.RunContext(ctx, os.Args); err != nil { + log.Printf("[error] %s", err) } } -func lambdaHandler(app *shimesaba.App) func(context.Context) error { - return func(ctx context.Context) error { - return app.Run(ctx, shimesaba.DryRunOption(dryRun), shimesaba.BackfillOption(int(backfill))) - } -} - -func envToFlag(f *flag.Flag) { - name := strings.ToUpper(strings.Replace(f.Name, "-", "_", -1)) - if s, ok := os.LookupEnv(name); ok { - f.Value.Set(s) - } +func isLabmda() bool { + return strings.HasPrefix(os.Getenv("AWS_EXECUTION_ENV"), "AWS_Lambda") || + os.Getenv("AWS_LAMBDA_RUNTIME_API") != "" } diff --git a/go.mod b/go.mod index 5bb4b56..9020f84 100644 --- a/go.mod +++ b/go.mod @@ -14,17 +14,21 @@ require ( github.com/mashiike/evaluator v0.2.0 github.com/shogo82148/go-retry v1.1.1 github.com/stretchr/testify v1.7.0 + github.com/urfave/cli/v2 v2.2.0 ) require ( github.com/BurntSushi/toml v0.3.1 // indirect github.com/aws/aws-sdk-go v1.30.19 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/jmespath/go-jmespath v0.3.0 // indirect github.com/mattn/go-colorable v0.1.9 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/russross/blackfriday/v2 v2.0.1 // indirect + github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 // indirect golang.org/x/tools v0.1.7 // indirect diff --git a/go.sum b/go.sum index 386ea0d..3ca85c0 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,7 @@ github.com/aws/aws-lambda-go v1.27.0/go.mod h1:jJmlefzPfGnckuHdXX7/80O3BvUUi12XO github.com/aws/aws-sdk-go v1.30.19 h1:vRwsYgbUvC25Cb3oKXTyTYk3R5n1LRVk8zbvL4inWsc= github.com/aws/aws-sdk-go v1.30.19/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -41,15 +42,18 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shogo82148/go-retry v1.1.1 h1:BfUEVHTNDSjYxoRPC+c/ht5Sy6qdwl+0kFhhubeh4Fo= github.com/shogo82148/go-retry v1.1.1/go.mod h1:TPSFDcc2rlx2D/yfhi8BBOlsHhVBjjJoMvxG7iFHUbI= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4= github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= From 7e64a853d9cc3893f8ee95e70b29929a536f8978 Mon Sep 17 00:00:00 2001 From: mashiike Date: Sat, 13 Nov 2021 17:01:48 +0900 Subject: [PATCH 2/9] dashboard init command --- app.go | 4 ++++ cmd/shimesaba/main.go | 30 ++++++++++++++++++++++++++++++ config.go | 20 ++++++++++++-------- dashboard.go | 32 ++++++++++++++++++++++++++++++++ mackerel.go | 37 +++++++++++++++++++++++++++++++++++++ 5 files changed, 115 insertions(+), 8 deletions(-) create mode 100644 dashboard.go diff --git a/app.go b/app.go index b7c7e37..c5c3b5e 100644 --- a/app.go +++ b/app.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "log" + "path/filepath" "sort" "time" @@ -22,6 +23,8 @@ type App struct { maxTimeFrame time.Duration maxCalculate time.Duration + + dashboardPath string } //New creates an app @@ -55,6 +58,7 @@ func NewWithMackerelClient(client MackerelClient, cfg *Config) (*App, error) { definitions: definitions, maxTimeFrame: maxTimeFrame, maxCalculate: maxCalculate, + dashboardPath: filepath.Join(cfg.configFilePath, cfg.Dashboard), } return app, nil } diff --git a/cmd/shimesaba/main.go b/cmd/shimesaba/main.go index 968192e..a4786ae 100644 --- a/cmd/shimesaba/main.go +++ b/cmd/shimesaba/main.go @@ -62,6 +62,10 @@ func main() { Name: "run", Usage: "run shimesaba. this is main feature", Action: func(c *cli.Context) error { + if c.Args().First() == "help" { + cli.ShowAppHelp(c) + return nil + } if c.Int("backfill") <= 0 { return errors.New("backfill count must positive value") } @@ -93,6 +97,32 @@ func main() { }, }, }, + { + Name: "dashboard", + Usage: "manage mackerel dashboard for SLI/SLO", + Subcommands: []*cli.Command{ + { + Name: "init", + Usage: "import an existing mackerel dashboard", + UsageText: "shimesaba dashboard [global options] init ", + Action: func(c *cli.Context) error { + if c.NArg() < 1 { + cli.ShowAppHelp(c) + return errors.New("dashboard_id is required") + } + if c.Args().First() == "help" { + cli.ShowAppHelp(c) + return nil + } + return app.DashboardInit(c.Context, c.Args().First()) + }, + }, + { + Name: "build", + Usage: "create or update mackerel dashboard", + }, + }, + }, }, } sort.Sort(cli.FlagsByName(cliApp.Flags)) diff --git a/config.go b/config.go index 73377ae..94e8777 100644 --- a/config.go +++ b/config.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "log" + "path/filepath" "strings" "time" @@ -21,19 +22,21 @@ type Config struct { Metrics MetricConfigs `yaml:"metrics" json:"metrics"` Definitions DefinitionConfigs `yaml:"definitions" json:"definitions"` + Dashboard string `json:"dashboard,omitempty" yaml:"dashboard,omitempty"` + configFilePath string versionConstraints gv.Constraints } //MetricConfig handles metric information obtained from Mackerel type MetricConfig struct { - ID string `yaml:"id" json:"id"` - Type MetricType `yaml:"type" json:"type"` - Name string `yaml:"name" json:"name"` - ServiceName string `yaml:"service_name" json:"service_name"` - Roles []string `yaml:"roles" json:"roles"` - HostName string `yaml:"host_name" json:"host_name"` - AggregationInterval string `yaml:"aggregation_interval" json:"aggregation_interval"` - AggregationMethod string `json:"aggregation_method" yaml:"aggregation_method"` + ID string `yaml:"id,omitempty" json:"id,omitempty"` + Type MetricType `yaml:"type,omitempty" json:"type,omitempty"` + Name string `yaml:"name,omitempty" json:"name,omitempty"` + ServiceName string `yaml:"service_name,omitempty" json:"service_name,omitempty"` + Roles []string `yaml:"roles,omitempty" json:"roles,omitempty"` + HostName string `yaml:"host_name,omitempty" json:"host_name,omitempty"` + AggregationInterval string `yaml:"aggregation_interval,omitempty" json:"aggregation_interval,omitempty"` + AggregationMethod string `json:"aggregation_method,omitempty" yaml:"aggregation_method,omitempty"` aggregationInterval time.Duration } @@ -366,6 +369,7 @@ func (c *Config) Load(paths ...string) error { if err := gc.LoadWithEnv(c, paths...); err != nil { return err } + c.configFilePath = filepath.Dir(paths[len(paths)-1]) return c.Restrict() } diff --git a/dashboard.go b/dashboard.go new file mode 100644 index 0000000..64443fc --- /dev/null +++ b/dashboard.go @@ -0,0 +1,32 @@ +package shimesaba + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log" + "os" +) + +func (app *App) DashboardInit(ctx context.Context, dashboardIDOrURL string) error { + if app.dashboardPath == "" { + return errors.New("dashboard file path is not configured") + } + dashboard, err := app.repo.FindDashboard(dashboardIDOrURL) + if err != nil { + return err + } + fp, err := os.OpenFile(app.dashboardPath, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + return fmt.Errorf("can not open file: %w", err) + } + defer fp.Close() + encoder := json.NewEncoder(fp) + encoder.SetIndent("", " ") + if err := encoder.Encode(dashboard); err != nil { + return fmt.Errorf("dashboard encode failed: %w", err) + } + log.Printf("[info] dashboard url_path `%s` write to `%s`", dashboard.URLPath, app.dashboardPath) + return nil +} diff --git a/mackerel.go b/mackerel.go index c705f22..827ca63 100644 --- a/mackerel.go +++ b/mackerel.go @@ -2,6 +2,7 @@ package shimesaba import ( "context" + "errors" "fmt" "log" "time" @@ -17,6 +18,9 @@ type MackerelClient interface { FetchHostMetricValues(hostID string, metricName string, from int64, to int64) ([]mackerel.MetricValue, error) FetchServiceMetricValues(serviceName string, metricName string, from int64, to int64) ([]mackerel.MetricValue, error) PostServiceMetricValues(serviceName string, metricValues []*mackerel.MetricValue) error + + FindDashboards() ([]*mackerel.Dashboard, error) + FindDashboard(dashboardID string) (*mackerel.Dashboard, error) } // Repository handles reading and writing data @@ -205,3 +209,36 @@ func newMackerelMetricValuesFromReport(report *Report) []*mackerel.MetricValue { }) return values } + +//Dashboard is alieas of mackerel.Dashboard +type Dashboard = mackerel.Dashboard + +// FindDashboard get Mackerel Dashboard +func (repo *Repository) FindDashboard(dashboardIDOrURL string) (*Dashboard, error) { + dashboards, _ := repo.client.FindDashboards() + var id string + for _, d := range dashboards { + if d.ID == dashboardIDOrURL { + id = d.ID + break + } + if d.URLPath == dashboardIDOrURL { + id = d.ID + break + } + } + if id == "" { + return nil, errors.New("dashboard not found") + } + + //Get Widgets + dashboard, err := repo.client.FindDashboard(id) + if err != nil { + return nil, err + } + + dashboard.ID = "" + dashboard.CreatedAt = 0 + dashboard.UpdatedAt = 0 + return dashboard, nil +} From de6b1cf284e0306b2b9a490ecebbce7316ecfb80 Mon Sep 17 00:00:00 2001 From: mashiike Date: Sun, 14 Nov 2021 23:29:24 +0900 Subject: [PATCH 3/9] dashboard build command --- app.go | 55 +++++++------------------- cmd/shimesaba/main.go | 27 +++++++++---- dashboard.go | 89 ++++++++++++++++++++++++++++++++++++++++++- go.mod | 1 + go.sum | 20 +++++++++- mackerel.go | 54 ++++++++++++++++++++------ options.go | 20 ++++++++++ 7 files changed, 203 insertions(+), 63 deletions(-) create mode 100644 options.go diff --git a/app.go b/app.go index c5c3b5e..5133ee6 100644 --- a/app.go +++ b/app.go @@ -21,9 +21,9 @@ type App struct { metricConfigs MetricConfigs definitions []*Definition - maxTimeFrame time.Duration - maxCalculate time.Duration - + maxTimeFrame time.Duration + maxCalculate time.Duration + cfgPath string dashboardPath string } @@ -58,58 +58,29 @@ func NewWithMackerelClient(client MackerelClient, cfg *Config) (*App, error) { definitions: definitions, maxTimeFrame: maxTimeFrame, maxCalculate: maxCalculate, + cfgPath: cfg.configFilePath, dashboardPath: filepath.Join(cfg.configFilePath, cfg.Dashboard), } return app, nil } -type runConfig struct { - dryRun bool - backfill int -} - -//RunOption is an App.Run option -type RunOption interface { - apply(*runConfig) -} - -type runOptionFunc func(*runConfig) - -func (f runOptionFunc) apply(rc *runConfig) { - f(rc) -} - -//DryRunOption is an option to output the calculated error budget as standard without posting it to Mackerel. -func DryRunOption(dryRun bool) RunOption { - return runOptionFunc(func(rc *runConfig) { - rc.dryRun = dryRun - }) -} - -//BackfillOption specifies how many points of data to calculate retroactively from the current time. -func BackfillOption(count int) RunOption { - return runOptionFunc(func(rc *runConfig) { - rc.backfill = count - }) -} - //Run performs the calculation of the error bar calculation -func (app *App) Run(ctx context.Context, opts ...RunOption) error { +func (app *App) Run(ctx context.Context, optFns ...func(*Options)) error { log.Printf("[info] start run") - rc := &runConfig{ + opts := &Options{ backfill: 3, dryRun: false, } - for _, opt := range opts { - opt.apply(rc) + for _, optFn := range optFns { + optFn(opts) } - if rc.backfill <= 0 { + if opts.backfill <= 0 { return errors.New("backfill must over 0") } log.Println("[debug]", app.metricConfigs) now := flextime.Now() startAt := now.Truncate(app.maxCalculate). - Add(-(time.Duration(rc.backfill))*app.maxCalculate - app.maxTimeFrame). + Add(-(time.Duration(opts.backfill))*app.maxCalculate - app.maxTimeFrame). Truncate(app.maxCalculate) log.Printf("[info] fetch metric range %s ~ %s", startAt, now) metrics, err := app.repo.FetchMetrics(ctx, app.metricConfigs, startAt, now) @@ -123,17 +94,17 @@ func (app *App) Run(ctx context.Context, opts ...RunOption) error { if err != nil { return fmt.Errorf("objective[%s] create report failed: %w", d.ID(), err) } - if len(reports) > rc.backfill { + if len(reports) > opts.backfill { sort.Slice(reports, func(i, j int) bool { return reports[i].DataPoint.Before(reports[j].DataPoint) }) - n := len(reports) - rc.backfill + n := len(reports) - opts.backfill if n < 0 { n = 0 } reports = reports[n:] } - if rc.dryRun { + if opts.dryRun { log.Printf("[info] dryrun! output stdout reports[%s]\n", d.ID()) bs, err := json.MarshalIndent(reports, "", " ") if err != nil { diff --git a/cmd/shimesaba/main.go b/cmd/shimesaba/main.go index a4786ae..55abf6b 100644 --- a/cmd/shimesaba/main.go +++ b/cmd/shimesaba/main.go @@ -66,15 +66,12 @@ func main() { cli.ShowAppHelp(c) return nil } - if c.Int("backfill") <= 0 { - return errors.New("backfill count must positive value") - } - opts := []shimesaba.RunOption{ + optFns := []func(*shimesaba.Options){ shimesaba.DryRunOption(c.Bool("dry-run")), shimesaba.BackfillOption(c.Int("backfill")), } handler := func(ctx context.Context) error { - return app.Run(ctx, opts...) + return app.Run(ctx, optFns...) } if isLabmda() { lambda.Start(handler) @@ -118,8 +115,23 @@ func main() { }, }, { - Name: "build", - Usage: "create or update mackerel dashboard", + Name: "build", + Usage: "create or update mackerel dashboard", + UsageText: "shimesaba dashboard [global options] build", + Action: func(c *cli.Context) error { + if c.Args().First() == "help" { + cli.ShowAppHelp(c) + return nil + } + return app.DashboardBuild(c.Context, shimesaba.DryRunOption(c.Bool("dry-run"))) + }, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "dry-run", + Usage: "dry run", + EnvVars: []string{"SHIMESABA_DRY_RUN"}, + }, + }, }, }, }, @@ -135,6 +147,7 @@ func main() { minLevel = "debug" } logger.Setup(os.Stderr, minLevel) + log.Println("[debug] set log level ", minLevel) switch c.Args().First() { case "help", "h", "version": return nil diff --git a/dashboard.go b/dashboard.go index 64443fc..ed4e759 100644 --- a/dashboard.go +++ b/dashboard.go @@ -1,12 +1,19 @@ package shimesaba import ( + "bytes" "context" "encoding/json" "errors" "fmt" + "io" "log" "os" + "path/filepath" + "text/template" + + jsonnet "github.com/google/go-jsonnet" + gc "github.com/kayac/go-config" ) func (app *App) DashboardInit(ctx context.Context, dashboardIDOrURL string) error { @@ -22,11 +29,89 @@ func (app *App) DashboardInit(ctx context.Context, dashboardIDOrURL string) erro return fmt.Errorf("can not open file: %w", err) } defer fp.Close() - encoder := json.NewEncoder(fp) + if err := app.writeDashboard(fp, dashboard); err != nil { + return err + } + log.Printf("[info] dashboard url_path `%s` write to `%s`", dashboard.URLPath, app.dashboardPath) + return nil +} + +func (app *App) writeDashboard(w io.Writer, dashboard *Dashboard) error { + encoder := json.NewEncoder(w) encoder.SetIndent("", " ") if err := encoder.Encode(dashboard); err != nil { return fmt.Errorf("dashboard encode failed: %w", err) } - log.Printf("[info] dashboard url_path `%s` write to `%s`", dashboard.URLPath, app.dashboardPath) return nil } + +func (app *App) DashboardBuild(ctx context.Context, optFns ...func(*Options)) error { + opts := &Options{ + dryRun: false, + } + for _, optFn := range optFns { + optFn(opts) + } + dashboard, err := app.loadDashbaord() + if err != nil { + return err + } + if opts.dryRun { + var buf bytes.Buffer + if err := app.writeDashboard(&buf, dashboard); err != nil { + return err + } + log.Printf("[info] build dashboard **dry run** %s", buf.String()) + return nil + } + return app.repo.SaveDashboard(ctx, dashboard) +} + +func (app *App) loadDashbaord() (*Dashboard, error) { + if app.dashboardPath == "" { + return nil, errors.New("dashboard file path is not configured") + } + loader := gc.New() + definitions := make(map[string]interface{}, len(app.definitions)) + for _, def := range app.definitions { + objectives := make([]string, 0, len(def.objectives)) + for _, obj := range def.objectives { + objectives = append(objectives, obj.String()) + } + definitions[def.id] = map[string]interface{}{ + "Objectives": objectives, + } + } + data := map[string]interface{}{ + "Metric": app.metricConfigs, + "Definitions": definitions, + } + loader.Data = data + funcs := template.FuncMap{ + "file": func(path string) string { + bs, err := loader.ReadWithEnv(filepath.Join(app.cfgPath, path)) + if err != nil { + panic(err) + } + return string(bs) + }, + } + loader.Funcs(funcs) + var dashboard Dashboard + switch filepath.Ext(app.dashboardPath) { + case ".jsonnet": + vm := jsonnet.MakeVM() + jsonStr, err := vm.EvaluateFile(app.dashboardPath) + if err != nil { + return nil, err + } + if err := loader.LoadWithEnvJSONBytes(&dashboard, []byte(jsonStr)); err != nil { + return nil, err + } + case ".json": + if err := loader.LoadWithEnvJSON(&dashboard, app.dashboardPath); err != nil { + return nil, err + } + } + return &dashboard, nil +} diff --git a/go.mod b/go.mod index 9020f84..6515619 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/aws/aws-lambda-go v1.27.0 github.com/fatih/color v1.13.0 github.com/fujiwara/logutils v1.1.0 + github.com/google/go-jsonnet v0.17.0 github.com/handlename/ssmwrap v1.1.1 github.com/hashicorp/go-version v1.3.0 github.com/kayac/go-config v0.6.0 diff --git a/go.sum b/go.sum index 3ca85c0..5f73923 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsr github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fujiwara/logutils v1.1.0 h1:JAYmqW40d/ZjzouB01sfZiaTxwNe4hwmB6lLajZqm1s= @@ -19,6 +20,8 @@ github.com/fujiwara/logutils v1.1.0/go.mod h1:pdb/Uk70rjQWEmFm/OvYH7OG8meZt1fEIq github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-jsonnet v0.17.0 h1:/9NIEfhK1NQRKl3sP2536b2+x5HnZMdql7x3yK/l8JY= +github.com/google/go-jsonnet v0.17.0/go.mod h1:sOcuej3UW1vpPTZOr8L7RQimqai1a57bt5j22LzGZCw= github.com/handlename/ssmwrap v1.1.1 h1:mLv6b7Sq/PhA2cjFH/sbSu4g/VoTWNgdsFCqJ3fHzxE= github.com/handlename/ssmwrap v1.1.1/go.mod h1:vF1fjedJ5a0CQ+JBmdGLHwznLHGLvI9sy5X6N1itFMU= github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw= @@ -27,14 +30,22 @@ github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2 github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= github.com/kayac/go-config v0.6.0 h1:Y4l9tsWrUCvT1id8tbO4aT4SdGxbYqd8lqSe5l1GrK0= github.com/kayac/go-config v0.6.0/go.mod h1:5C4ZN+sMjYpEX0bi+AcgF6g0hZYVdzZiV16TEyzAzfk= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mackerelio/mackerel-client-go v0.19.0 h1:DkYVD07fmklFTMKLaHcjtkU53Nt+nhvXNUSEeKfRSZs= github.com/mackerelio/mackerel-client-go v0.19.0/go.mod h1:/GNOj+y1eFsd3CK8c6IQ/uS38/GT0+NWImk5YGJs5Lk= github.com/mashiike/evaluator v0.2.0 h1:PyvkiiCTyBDuucnsCj/CFSz3LS363bh/pa7jLXT/9Rw= github.com/mashiike/evaluator v0.2.0/go.mod h1:0ku9oXYoUPrhJ8tp/S7QGoPEpMgcXjjLF1e9h0U+VtU= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= @@ -44,11 +55,14 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shogo82148/go-retry v1.1.1 h1:BfUEVHTNDSjYxoRPC+c/ht5Sy6qdwl+0kFhhubeh4Fo= github.com/shogo82148/go-retry v1.1.1/go.mod h1:TPSFDcc2rlx2D/yfhi8BBOlsHhVBjjJoMvxG7iFHUbI= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= @@ -73,7 +87,9 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -95,9 +111,11 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -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/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/mackerel.go b/mackerel.go index 827ca63..f952d0a 100644 --- a/mackerel.go +++ b/mackerel.go @@ -21,6 +21,8 @@ type MackerelClient interface { FindDashboards() ([]*mackerel.Dashboard, error) FindDashboard(dashboardID string) (*mackerel.Dashboard, error) + CreateDashboard(param *mackerel.Dashboard) (*mackerel.Dashboard, error) + UpdateDashboard(dashboardID string, param *mackerel.Dashboard) (*mackerel.Dashboard, error) } // Repository handles reading and writing data @@ -213,24 +215,31 @@ func newMackerelMetricValuesFromReport(report *Report) []*mackerel.MetricValue { //Dashboard is alieas of mackerel.Dashboard type Dashboard = mackerel.Dashboard -// FindDashboard get Mackerel Dashboard -func (repo *Repository) FindDashboard(dashboardIDOrURL string) (*Dashboard, error) { - dashboards, _ := repo.client.FindDashboards() - var id string +var ErrDashboardNotFound = errors.New("dashboard not found") + +// FindDashboardID get Mackerel Dashboard ID from url or id +func (repo *Repository) FindDashboardID(dashboardIDOrURL string) (string, error) { + dashboards, err := repo.client.FindDashboards() + if err != nil { + return "", err + } for _, d := range dashboards { if d.ID == dashboardIDOrURL { - id = d.ID - break + return d.ID, nil } if d.URLPath == dashboardIDOrURL { - id = d.ID - break + return d.ID, nil } } - if id == "" { - return nil, errors.New("dashboard not found") - } + return "", ErrDashboardNotFound +} +// FindDashboard get Mackerel Dashboard +func (repo *Repository) FindDashboard(dashboardIDOrURL string) (*Dashboard, error) { + id, err := repo.FindDashboardID(dashboardIDOrURL) + if err != nil { + return nil, err + } //Get Widgets dashboard, err := repo.client.FindDashboard(id) if err != nil { @@ -242,3 +251,26 @@ func (repo *Repository) FindDashboard(dashboardIDOrURL string) (*Dashboard, erro dashboard.UpdatedAt = 0 return dashboard, nil } + +// SaveDashboard post Mackerel Dashboard +func (repo *Repository) SaveDashboard(ctx context.Context, dashboard *Dashboard) error { + id, err := repo.FindDashboardID(dashboard.URLPath) + if err == nil { + log.Printf("[debug] update dashboard id=%s url=%s", id, dashboard.URLPath) + after, err := repo.client.UpdateDashboard(id, dashboard) + if err != nil { + return err + } + log.Printf("[info] updated dashboard id=%s url=%s updated_at=%s", after.ID, after.URLPath, time.Unix(after.UpdatedAt, 0).String()) + } + if err == ErrDashboardNotFound { + log.Printf("[debug] create dashboard url=%s", dashboard.URLPath) + after, err := repo.client.CreateDashboard(dashboard) + if err != nil { + return err + } + log.Printf("[info] updated dashboard id=%s url=%s updated_at=%s", after.ID, after.URLPath, time.Unix(after.CreatedAt, 0).String()) + } + return err + +} diff --git a/options.go b/options.go new file mode 100644 index 0000000..a476062 --- /dev/null +++ b/options.go @@ -0,0 +1,20 @@ +package shimesaba + +type Options struct { + dryRun bool + backfill int +} + +//DryRunOption is an option to output the calculated error budget as standard without posting it to Mackerel. +func DryRunOption(dryRun bool) func(*Options) { + return func(opt *Options) { + opt.dryRun = dryRun + } +} + +//BackfillOption specifies how many points of data to calculate retroactively from the current time. +func BackfillOption(count int) func(*Options) { + return func(opt *Options) { + opt.backfill = count + } +} From ef31dbd03837d5f63ed0554e235fd279586cad0d Mon Sep 17 00:00:00 2001 From: mashiike Date: Sun, 14 Nov 2021 23:44:04 +0900 Subject: [PATCH 4/9] fix main cli --- cmd/shimesaba/main.go | 68 +++++++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/cmd/shimesaba/main.go b/cmd/shimesaba/main.go index 55abf6b..dc13d2c 100644 --- a/cmd/shimesaba/main.go +++ b/cmd/shimesaba/main.go @@ -19,13 +19,12 @@ import ( ) var ( - Version = "current" - app *shimesaba.App + Version = "current" + ssmwrapErr error ) func main() { paths := strings.Split(os.Getenv("SSMWRAP_PATHS"), ",") - var ssmwrapErr error if len(paths) > 0 { ssmwrapErr = ssmwrap.Export(ssmwrap.ExportOptions{ Paths: paths, @@ -37,17 +36,15 @@ func main() { Usage: "A commandline tool for tracking SLO/ErrorBudget using Mackerel as an SLI measurement service.", Flags: []cli.Flag{ &cli.StringSliceFlag{ - Name: "config", - Aliases: []string{"c"}, - Usage: "config file path, can set multiple", - Required: true, - EnvVars: []string{"CONFIG", "SHIMESABA_CONFIG"}, + Name: "config", + Aliases: []string{"c"}, + Usage: "config file path, can set multiple", + EnvVars: []string{"CONFIG", "SHIMESABA_CONFIG"}, }, &cli.StringFlag{ Name: "mackerel-apikey", Aliases: []string{"k"}, Usage: "for access mackerel API", - Required: true, DefaultText: "*********", EnvVars: []string{"MACKEREL_APIKEY", "SHIMESABA_MACKEREL_APIKEY"}, }, @@ -59,12 +56,13 @@ func main() { }, Commands: []*cli.Command{ { - Name: "run", - Usage: "run shimesaba. this is main feature", + Name: "run", + Usage: "run shimesaba. this is main feature", + UsageText: "shimesaba -config run [command options]", Action: func(c *cli.Context) error { - if c.Args().First() == "help" { - cli.ShowAppHelp(c) - return nil + app, err := buildApp(c) + if err != nil { + return err } optFns := []func(*shimesaba.Options){ shimesaba.DryRunOption(c.Bool("dry-run")), @@ -107,9 +105,9 @@ func main() { cli.ShowAppHelp(c) return errors.New("dashboard_id is required") } - if c.Args().First() == "help" { - cli.ShowAppHelp(c) - return nil + app, err := buildApp(c) + if err != nil { + return err } return app.DashboardInit(c.Context, c.Args().First()) }, @@ -119,9 +117,9 @@ func main() { Usage: "create or update mackerel dashboard", UsageText: "shimesaba dashboard [global options] build", Action: func(c *cli.Context) error { - if c.Args().First() == "help" { - cli.ShowAppHelp(c) - return nil + app, err := buildApp(c) + if err != nil { + return err } return app.DashboardBuild(c.Context, shimesaba.DryRunOption(c.Bool("dry-run"))) }, @@ -153,21 +151,7 @@ func main() { return nil default: } - if ssmwrapErr != nil { - return fmt.Errorf("ssmwrap.Export failed: %w", ssmwrapErr) - } - cfg := shimesaba.NewDefaultConfig() - if err := cfg.Load(c.StringSlice("config")...); err != nil { - return err - } - if err := cfg.ValidateVersion(Version); err != nil { - return err - } - var err error - app, err = shimesaba.New(c.String("mackerel-apikey"), cfg) - if err != nil { - return err - } + return nil } @@ -187,3 +171,17 @@ func isLabmda() bool { return strings.HasPrefix(os.Getenv("AWS_EXECUTION_ENV"), "AWS_Lambda") || os.Getenv("AWS_LAMBDA_RUNTIME_API") != "" } + +func buildApp(c *cli.Context) (*shimesaba.App, error) { + if ssmwrapErr != nil { + return nil, fmt.Errorf("ssmwrap.Export failed: %w", ssmwrapErr) + } + cfg := shimesaba.NewDefaultConfig() + if err := cfg.Load(c.StringSlice("config")...); err != nil { + return nil, err + } + if err := cfg.ValidateVersion(Version); err != nil { + return nil, err + } + return shimesaba.New(c.String("mackerel-apikey"), cfg) +} From 90df2cf4977f30b33c7de37eef4222af24827657 Mon Sep 17 00:00:00 2001 From: mashiike Date: Sun, 14 Nov 2021 23:45:45 +0900 Subject: [PATCH 5/9] delete debug log --- cmd/shimesaba/main.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/shimesaba/main.go b/cmd/shimesaba/main.go index dc13d2c..b57032a 100644 --- a/cmd/shimesaba/main.go +++ b/cmd/shimesaba/main.go @@ -145,7 +145,6 @@ func main() { minLevel = "debug" } logger.Setup(os.Stderr, minLevel) - log.Println("[debug] set log level ", minLevel) switch c.Args().First() { case "help", "h", "version": return nil From 992e0709bcbd02724468c89e23a36897ea1b7852 Mon Sep 17 00:00:00 2001 From: mashiike Date: Mon, 15 Nov 2021 00:23:13 +0900 Subject: [PATCH 6/9] more data --- dashboard.go | 7 +++++- internal/timeutils/stirng_test.go | 40 +++++++++++++++++++++++++++++++ internal/timeutils/string.go | 35 +++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 internal/timeutils/stirng_test.go create mode 100644 internal/timeutils/string.go diff --git a/dashboard.go b/dashboard.go index ed4e759..a4fc005 100644 --- a/dashboard.go +++ b/dashboard.go @@ -14,6 +14,7 @@ import ( jsonnet "github.com/google/go-jsonnet" gc "github.com/kayac/go-config" + "github.com/mashiike/shimesaba/internal/timeutils" ) func (app *App) DashboardInit(ctx context.Context, dashboardIDOrURL string) error { @@ -79,7 +80,11 @@ func (app *App) loadDashbaord() (*Dashboard, error) { objectives = append(objectives, obj.String()) } definitions[def.id] = map[string]interface{}{ - "Objectives": objectives, + "TimeFrame": timeutils.DurationString(def.timeFrame), + "ServiceName": def.serviceName, + "CalculateInterval": timeutils.DurationString(def.calculate), + "ErrorBudgetSize": def.errorBudgetSize, + "Objectives": objectives, } } data := map[string]interface{}{ diff --git a/internal/timeutils/stirng_test.go b/internal/timeutils/stirng_test.go new file mode 100644 index 0000000..bf1b6c1 --- /dev/null +++ b/internal/timeutils/stirng_test.go @@ -0,0 +1,40 @@ +package timeutils_test + +import ( + "testing" + "time" + + "github.com/mashiike/shimesaba/internal/timeutils" + "github.com/stretchr/testify/require" +) + +func TestDurationString(t *testing.T) { + cases := []struct { + d time.Duration + expected string + }{ + { + expected: "1m", + d: time.Minute, + }, + { + expected: "1h1m", + d: time.Hour + time.Minute, + }, + { + expected: "1d", + d: 24 * time.Hour, + }, + { + expected: "1d1m3s", + d: 24*time.Hour + time.Minute + 3*time.Second, + }, + } + + for _, c := range cases { + t.Run(c.expected, func(t *testing.T) { + actual := timeutils.DurationString(c.d) + require.EqualValues(t, c.expected, actual) + }) + } +} diff --git a/internal/timeutils/string.go b/internal/timeutils/string.go new file mode 100644 index 0000000..590dc5d --- /dev/null +++ b/internal/timeutils/string.go @@ -0,0 +1,35 @@ +package timeutils + +import ( + "fmt" + "strings" + "time" +) + +func DurationString(d time.Duration) string { + days := uint64(d.Truncate(24*time.Hour).Hours() / 24.0) + remining := d - d.Truncate(24*time.Hour) + hours := uint64(remining.Truncate(time.Hour).Hours()) + remining = remining - remining.Truncate(time.Hour) + minutes := uint64(remining.Truncate(time.Minute).Minutes()) + remining = remining - remining.Truncate(time.Minute) + seconds := uint64(remining.Truncate(time.Second).Seconds()) + return durationString(days, hours, minutes, seconds) +} + +func durationString(day, hours, minutes, seconds uint64) string { + var builder strings.Builder + if day > 0 { + fmt.Fprintf(&builder, "%dd", day) + } + if hours > 0 { + fmt.Fprintf(&builder, "%dh", hours) + } + if minutes > 0 { + fmt.Fprintf(&builder, "%dm", minutes) + } + if seconds > 0 { + fmt.Fprintf(&builder, "%ds", seconds) + } + return builder.String() +} From 6812cea8eaf93e257fd55ec21516ae1d021ee8aa Mon Sep 17 00:00:00 2001 From: mashiike Date: Mon, 15 Nov 2021 00:25:50 +0900 Subject: [PATCH 7/9] for markdown calcuratied error budeg size --- dashboard.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/dashboard.go b/dashboard.go index a4fc005..388861e 100644 --- a/dashboard.go +++ b/dashboard.go @@ -11,6 +11,7 @@ import ( "os" "path/filepath" "text/template" + "time" jsonnet "github.com/google/go-jsonnet" gc "github.com/kayac/go-config" @@ -80,11 +81,12 @@ func (app *App) loadDashbaord() (*Dashboard, error) { objectives = append(objectives, obj.String()) } definitions[def.id] = map[string]interface{}{ - "TimeFrame": timeutils.DurationString(def.timeFrame), - "ServiceName": def.serviceName, - "CalculateInterval": timeutils.DurationString(def.calculate), - "ErrorBudgetSize": def.errorBudgetSize, - "Objectives": objectives, + "TimeFrame": timeutils.DurationString(def.timeFrame), + "ServiceName": def.serviceName, + "CalculateInterval": timeutils.DurationString(def.calculate), + "ErrorBudgetSize": def.errorBudgetSize, + "ErrorBudgetSizeDuration": timeutils.DurationString(time.Duration(def.errorBudgetSize * float64(def.timeFrame))), + "Objectives": objectives, } } data := map[string]interface{}{ From a112b57059e3a3fc6046edb4d49cc1a552fa0984 Mon Sep 17 00:00:00 2001 From: mashiike Date: Mon, 15 Nov 2021 00:31:17 +0900 Subject: [PATCH 8/9] error budget size trunc min --- dashboard.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dashboard.go b/dashboard.go index 388861e..4bca728 100644 --- a/dashboard.go +++ b/dashboard.go @@ -85,7 +85,7 @@ func (app *App) loadDashbaord() (*Dashboard, error) { "ServiceName": def.serviceName, "CalculateInterval": timeutils.DurationString(def.calculate), "ErrorBudgetSize": def.errorBudgetSize, - "ErrorBudgetSizeDuration": timeutils.DurationString(time.Duration(def.errorBudgetSize * float64(def.timeFrame))), + "ErrorBudgetSizeDuration": timeutils.DurationString(time.Duration(def.errorBudgetSize * float64(def.timeFrame)).Truncate(time.Minute)), "Objectives": objectives, } } From 8b02c9d8365d640ecb318b7345cb17ea33303622 Mon Sep 17 00:00:00 2001 From: mashiike Date: Mon, 15 Nov 2021 00:31:25 +0900 Subject: [PATCH 9/9] update README.md --- README.md | 123 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 109 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index a220851..7d6649e 100644 --- a/README.md +++ b/README.md @@ -31,25 +31,53 @@ $ brew install mashiike/tap/shimesaba ### as CLI command ```console -$ shimesaba -config config.yaml -mackerel-apikey +$ shimesaba -config config.yaml -mackerel-apikey run ``` +```console +NAME: + shimesaba - A commandline tool for tracking SLO/ErrorBudget using Mackerel as an SLI measurement service. + +USAGE: + shimesaba [global options] command [command options] [arguments...] + +VERSION: + current + +COMMANDS: + dashboard manage mackerel dashboard for SLI/SLO + run run shimesaba. this is main feature + help, h Shows a list of commands or help for one command + +GLOBAL OPTIONS: + --config value, -c value config file path, can set multiple [$CONFIG, $SHIMESABA_CONFIG] + --debug output debug log (default: false) [$SHIMESABA_DEBUG] + --mackerel-apikey value, -k value for access mackerel API (default: *********) [$MACKEREL_APIKEY, $SHIMESABA_MACKEREL_APIKEY] + --help, -h show help (default: false) + --version, -v print the version (default: false) +2021/11/14 23:29:45 [error] Required flag "config" not set ``` -Usage of shimesaba: - -backfill uint - generate report before n point (default 3) - -config value - config file path, can set multiple - -debug - output debug log - -dry-run - report output stdout and not put mackerel - -mackerel-apikey string - for access mackerel API + +run command usage is follow +```console +$ shimesaba run --help +NAME: + main run - run shimesaba. this is main feature + +USAGE: + shimesaba -config run [command options] + +OPTIONS: + --dry-run report output stdout and not put mackerel (default: false) [$SHIMESABA_DRY_RUN] + --backfill value generate report before n point (default: 3) [$BACKFILL, $SHIMESABA_BACKFILL] + --help, -h show help (default: false) ``` + ### as AWS Lambda function -`shimesaba` binary also runs as AWS Lambda function. +`shimesaba` binary also runs as AWS Lambda function. +shimesaba implicitly behaves as a run command when run as a bootstrap with a Lambda Function + CLI options can be specified from environment variables. For example, when `MACKEREL_APIKEY` environment variable is set, the value is set to `-mackerel-apikey` option. @@ -60,7 +88,7 @@ Example Lambda functions configuration. "FunctionName": "shimesaba", "Environment": { "Variables": { - "CONFIG": "config.yaml", + "SHIMESABA_CONFIG": "config.yaml", "MACKEREL_APIKEY": "" } }, @@ -103,6 +131,8 @@ definitions: objectives: - expr: alb_p90_response_time <= 1.0 - expr: component_response_time <= 1.0 + +dashboard: dashboard.jsonnet ``` #### required_version @@ -236,6 +266,71 @@ It incorporates [github.com/handlename/ssmwrap](https://github.com/handlename/ss If you specify the path of the Parameter Store of AWS Systems Manager separated by commas, it will be output to the environment variable. Useful when used as a Lambda function. +### Usage Dashboard subcommand. + +This subcommand can only be used when acting as a CLI. +If the dashboard of the config file contains the dashboard definition file, you can manage the dashboard JSON using Go Template. + +For example, you can build a simple dashboard by defining a json file like the one below. + +dashboard.jsonnet +```jsonnet +local errorBudgetCounter(x, y, def_id, title) = { + type: 'value', + title: title, + layout: { + x: x, + y: y, + width: 10, + height: 5, + }, + metric: { + type: 'service', + name: 'shimesaba.error_budget.' + def_id, + serviceName: 'shimesaba', + }, + graph: null, + range: null, + fractionSize: 0, + suffix: 'min', +}; +{ + title: 'SLI/SLO', + urlPath: '4oequPJEwwd', + memo: '', + widgets: [ + errorBudgetCounter(0, 0, 'availability', ''), + errorBudgetCounter(10, 0, 'latency', ''), + { + type: 'markdown', + title: 'SLO Definitions', + layout: { + x: 20, + y: 0, + width: 5, + height: 20, + }, + markdown: '{{file `definitions.md` | json_escape }}', + }, + ], +} +``` + +definitions.md +```markdown +{{ range $def_id, $def := .Definitions }} +## SLO {{ $def_id }} + +- TimeFrame : {{ $def.TimeFrame }} +- ErrorBudgetSize: {{ $def.ErrorBudgetSizeDuration }} + + +{{ range $def.Objectives }} +- {{ . }} +{{ end }} +{{ end }} +``` + ## LICENSE MIT