From 0c5e922665942bee4befff430670c7ae795e5c93 Mon Sep 17 00:00:00 2001 From: ClaudioGisch Date: Thu, 11 Apr 2024 10:27:59 -0300 Subject: [PATCH] Feat/prometheus autoscale (#223) * feat: keda prometheus autoscaler * test: keda prometheus autoscaler * bump: go-tsuruclient version --- go.mod | 2 +- go.sum | 4 +-- tsuru/client/apps.go | 16 ++++++++++ tsuru/client/apps_test.go | 53 ++++++++++++++++++++++---------- tsuru/client/autoscale.go | 28 +++++++++++++---- tsuru/client/autoscale_test.go | 55 +++++++++++++++++++++++++++++++++- 6 files changed, 134 insertions(+), 24 deletions(-) diff --git a/go.mod b/go.mod index 0524e533..e194b887 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/pmorie/go-open-service-broker-client v0.0.0-20180330214919-dca737037ce6 github.com/sabhiram/go-gitignore v0.0.0-20171017070213-362f9845770f github.com/tsuru/gnuflag v0.0.0-20151217162021-86b8c1b864aa - github.com/tsuru/go-tsuruclient v0.0.0-20240403173008-e8ea92a75f9b + github.com/tsuru/go-tsuruclient v0.0.0-20240409125509-22a1e08326f4 github.com/tsuru/tablecli v0.0.0-20190131152944-7ded8a3383c6 github.com/tsuru/tsuru v0.0.0-20240325190920-410c71393b77 golang.org/x/net v0.20.0 diff --git a/go.sum b/go.sum index 8a289b67..f7711c52 100644 --- a/go.sum +++ b/go.sum @@ -675,8 +675,8 @@ github.com/tsuru/config v0.0.0-20201023175036-375aaee8b560 h1:fniQ/BmYAHdnNmY333 github.com/tsuru/config v0.0.0-20201023175036-375aaee8b560/go.mod h1:mj6t8JKWU51GScTT50XRmDj65T5XhTyNvO5FUNV5zS4= github.com/tsuru/gnuflag v0.0.0-20151217162021-86b8c1b864aa h1:JlLQP1xa13a994p/Aau2e3K9xXYaHNoNvTDVIMHSUa4= github.com/tsuru/gnuflag v0.0.0-20151217162021-86b8c1b864aa/go.mod h1:UibOSvkMFKRe/eiwktAPAvQG8L+p8nYsECJvu3Dgw7I= -github.com/tsuru/go-tsuruclient v0.0.0-20240403173008-e8ea92a75f9b h1:De7rIp7qigWFMpdxukKcUPFDjacmwN4drji6i4X+hT0= -github.com/tsuru/go-tsuruclient v0.0.0-20240403173008-e8ea92a75f9b/go.mod h1:qwh/KJ6ypa2GISRI79XFOHhnSjGOe1cZVPHF3nfrf18= +github.com/tsuru/go-tsuruclient v0.0.0-20240409125509-22a1e08326f4 h1:MGmG6AxKP8XRe7nQqIQR+Tsb5tCzHnYpYk0tiuXVgxY= +github.com/tsuru/go-tsuruclient v0.0.0-20240409125509-22a1e08326f4/go.mod h1:qwh/KJ6ypa2GISRI79XFOHhnSjGOe1cZVPHF3nfrf18= github.com/tsuru/tablecli v0.0.0-20190131152944-7ded8a3383c6 h1:1XDdWFAjIbCSG1OjN9v9KdWhuM8UtYlFcfHe/Ldkchk= github.com/tsuru/tablecli v0.0.0-20190131152944-7ded8a3383c6/go.mod h1:ztYpOhW+u1k21FEqp7nZNgpWbr0dUKok5lgGCZi+1AQ= github.com/tsuru/tsuru v0.0.0-20240325190920-410c71393b77 h1:cuWFjNLaemdQZhojqJbb/rOXO97tlcPeLAHg/x+EQGk= diff --git a/tsuru/client/apps.go b/tsuru/client/apps.go index e830ddb4..86e90627 100644 --- a/tsuru/client/apps.go +++ b/tsuru/client/apps.go @@ -779,6 +779,14 @@ func (a *app) String(simplified bool) string { })) } + for _, prometheus := range as.Prometheus { + prometheusInfo := buildPrometheusInfo(prometheus) + autoScaleTable.AddRow(tablecli.Row([]string{ + "Prometheus", + prometheusInfo, + })) + } + autoScaleTables = append(autoScaleTables, autoScaleTable) } @@ -847,6 +855,14 @@ func buildScheduleInfo(schedule tsuru.AutoScaleSchedule) string { ) } +func buildPrometheusInfo(prometheus tsuru.AutoScalePrometheus) string { + thresholdValue := strconv.FormatFloat(prometheus.Threshold, 'f', -1, 64) + + return fmt.Sprintf("Name: %s\nQuery: %s\nThreshold: %s\nPrometheusAddress: %s", + prometheus.Name, prometheus.Query, thresholdValue, prometheus.PrometheusAddress, + ) +} + func (a *app) SimpleServicesView() string { sibs := make([]tsuru.AppServiceInstanceBinds, len(a.ServiceInstanceBinds)) copy(sibs, a.ServiceInstanceBinds) diff --git a/tsuru/client/apps_test.go b/tsuru/client/apps_test.go index 668b5541..0350e14f 100644 --- a/tsuru/client/apps_test.go +++ b/tsuru/client/apps_test.go @@ -1684,6 +1684,19 @@ func (s *S) TestAppInfoWithKEDAAutoScale(c *check.C) { "end":"0 15 * * *", "timezone":"UTC" } + ], + "prometheus": [ + { + "name":"my_metric_id", + "threshold":2, + "query":"my_query{app='my-app'}", + "prometheusAddress":"default.com" + },{ + "name":"my_metric_id_2", + "threshold":5, + "query":"my_query_2{app='my-app'}", + "prometheusAddress":"exemple.prometheus.com" + } ] }, { @@ -1730,21 +1743,31 @@ Units [process worker]: 1 Auto Scale: Process: web (v10), Min Units: 1, Max Units: 10 -+----------+---------------------------------+ -| Triggers | Trigger details | -+----------+---------------------------------+ -| CPU | Target: 50% | -+----------+---------------------------------+ -| Schedule | Start: At 06:00 AM (0 6 * * *) | -| | End: At 06:00 PM (0 18 * * *) | -| | Units: 2 | -| | Timezone: UTC | -+----------+---------------------------------+ -| Schedule | Start: At 12:00 PM (0 12 * * *) | -| | End: At 03:00 PM (0 15 * * *) | -| | Units: 3 | -| | Timezone: UTC | -+----------+---------------------------------+ ++------------+-------------------------------------------+ +| Triggers | Trigger details | ++------------+-------------------------------------------+ +| CPU | Target: 50% | ++------------+-------------------------------------------+ +| Schedule | Start: At 06:00 AM (0 6 * * *) | +| | End: At 06:00 PM (0 18 * * *) | +| | Units: 2 | +| | Timezone: UTC | ++------------+-------------------------------------------+ +| Schedule | Start: At 12:00 PM (0 12 * * *) | +| | End: At 03:00 PM (0 15 * * *) | +| | Units: 3 | +| | Timezone: UTC | ++------------+-------------------------------------------+ +| Prometheus | Name: my_metric_id | +| | Query: my_query{app='my-app'} | +| | Threshold: 2 | +| | PrometheusAddress: default.com | ++------------+-------------------------------------------+ +| Prometheus | Name: my_metric_id_2 | +| | Query: my_query_2{app='my-app'} | +| | Threshold: 5 | +| | PrometheusAddress: exemple.prometheus.com | ++------------+-------------------------------------------+ Process: worker (v10), Min Units: 2, Max Units: 5 +----------+--------------------------------+ diff --git a/tsuru/client/autoscale.go b/tsuru/client/autoscale.go index 18ed43a9..26aeef6e 100644 --- a/tsuru/client/autoscale.go +++ b/tsuru/client/autoscale.go @@ -29,15 +29,16 @@ func (i *int32Value) String() string { return fmt.Sprintf("%v", *i) } type AutoScaleSet struct { tsuruClientApp.AppNameMixIn - fs *gnuflag.FlagSet - autoscale tsuru.AutoScaleSpec - schedules cmd.StringSliceFlag + fs *gnuflag.FlagSet + autoscale tsuru.AutoScaleSpec + schedules cmd.StringSliceFlag + prometheus cmd.StringSliceFlag } func (c *AutoScaleSet) Info() *cmd.Info { return &cmd.Info{ Name: "unit-autoscale-set", - Usage: "unit autoscale set [-a/--app appname] [-p/--process processname] [--cpu targetCPU] [--min minUnits] [--max maxUnits] [--schedule scheduleWindow]", + Usage: "unit autoscale set [-a/--app appname] [-p/--process processname] [--cpu targetCPU] [--min minUnits] [--max maxUnits] [--schedule scheduleWindow] [--prometheus prometheusSettings]", Desc: ` # Sets an autoscale configuration: # Based on 50% of CPU utilization with min units 1 and max units 3 @@ -46,7 +47,11 @@ unit autoscale set -a my-app --cpu 50% --min 1 --max 3 # Based on a schedule window everyday from 6AM to 6PM UTC unit autoscale set -a my-app --min 1 --max 3 --schedule '{"minReplicas": 2, "start": "0 6 * * *", "end": "0 18 * * *"}' -# Combining both +# Based on a prometheus metric + +unit autoscale set -a my-app --min 1 --max 3 --prometheus '{"name": "my_metric_identification", "threshold": 10, "query":"sum(my_metric{tsuru_app=\"my_app\"})"}' + +# Combining unit autoscale set -a my-app --cpu 50% --min 1 --max 3 --schedule '{"minReplicas": 2, "start": "0 6 * * *", "end": "0 18 * * *"}' # When using more than one trigger (CPU + Schedule as an example), the number of units will be determined by the highest value @@ -71,6 +76,7 @@ func (c *AutoScaleSet) Flags() *gnuflag.FlagSet { c.fs.Var((*int32Value)(&c.autoscale.MaxUnits), "max", "Maximum Units") c.fs.Var(&c.schedules, "schedule", "Schedule window to up/down scale. Example: {\"minReplicas\": 2, \"start\": \"0 6 * * *\", \"end\": \"0 18 * * *\"}") + c.fs.Var(&c.prometheus, "prometheus", "Prometheus settings to up/down scale. Example: {\"name\": \"my_metric_identification\", \"threshold\": 10, \"query\":\"sum(my_metric{tsuru_app=\\\"my_app\\\"})\"}") } return c.fs } @@ -97,6 +103,18 @@ func (c *AutoScaleSet) Run(ctx *cmd.Context) error { c.autoscale.Schedules = schedules + prometheus := []tsuru.AutoScalePrometheus{} + for _, prometheusString := range c.prometheus { + var autoScalePrometheus tsuru.AutoScalePrometheus + if err = json.Unmarshal([]byte(prometheusString), &autoScalePrometheus); err != nil { + return err + } + + prometheus = append(prometheus, autoScalePrometheus) + } + + c.autoscale.Prometheus = prometheus + _, err = apiClient.AppApi.AutoScaleAdd(context.TODO(), appName, c.autoscale) if err != nil { return err diff --git a/tsuru/client/autoscale_test.go b/tsuru/client/autoscale_test.go index 6e7b556d..ba6941f9 100644 --- a/tsuru/client/autoscale_test.go +++ b/tsuru/client/autoscale_test.go @@ -53,7 +53,7 @@ func (s *S) TestAutoScaleSet(c *check.C) { c.Assert(stdout.String(), check.Equals, expected) } -func (s *S) TestKEDAAutoScaleSet(c *check.C) { +func (s *S) TestKEDAScheduleAutoScaleSet(c *check.C) { var stdout, stderr bytes.Buffer expected := "Unit auto scale successfully set.\n" context := cmd.Context{ @@ -106,6 +106,59 @@ func (s *S) TestKEDAAutoScaleSet(c *check.C) { c.Assert(stdout.String(), check.Equals, expected) } +func (s *S) TestKEDAPrometheusAutoScaleSet(c *check.C) { + var stdout, stderr bytes.Buffer + expected := "Unit auto scale successfully set.\n" + context := cmd.Context{ + Stdout: &stdout, + Stderr: &stderr, + Args: []string{}, + } + trans := cmdtest.ConditionalTransport{ + Transport: cmdtest.Transport{Message: "", Status: http.StatusOK}, + CondFunc: func(r *http.Request) bool { + c.Assert(r.URL.Path, check.Equals, "/1.9/apps/myapp/units/autoscale") + c.Assert(r.Method, check.Equals, "POST") + var ret tsuru.AutoScaleSpec + c.Assert(r.Header.Get("Content-Type"), check.Equals, "application/json") + data, err := io.ReadAll(r.Body) + c.Assert(err, check.IsNil) + err = json.Unmarshal(data, &ret) + c.Assert(err, check.IsNil) + c.Assert(ret, check.DeepEquals, tsuru.AutoScaleSpec{ + MinUnits: 1, + MaxUnits: 5, + Process: "proc1", + Prometheus: []tsuru.AutoScalePrometheus{ + { + Name: "prometheus_metric_1", + Threshold: 1, + Query: "my_metric_1(app='my-app')", + }, + { + Name: "prometheus_metric_2", + Threshold: 5, + Query: "my_metric_2(app='my-app')", + PrometheusAddress: "exemple.prometheus.com", + }, + }, + }) + return true + }, + } + s.setupFakeTransport(&trans) + command := AutoScaleSet{} + command.Info() + command.Flags().Parse(true, []string{ + "-a", "myapp", "-p", "proc1", "--min", "1", "--max", "5", + "--prometheus", "{\"name\": \"prometheus_metric_1\", \"threshold\": 1, \"query\": \"my_metric_1(app='my-app')\"}", + "--prometheus", "{\"name\": \"prometheus_metric_2\", \"threshold\": 5, \"query\": \"my_metric_2(app='my-app')\", \"prometheusAddress\": \"exemple.prometheus.com\"}", + }) + err := command.Run(&context) + c.Assert(err, check.IsNil) + c.Assert(stdout.String(), check.Equals, expected) +} + func (s *S) TestAutoScaleUnset(c *check.C) { var stdout, stderr bytes.Buffer expected := "Unit auto scale successfully unset.\n"