From 6fb6d9ae8d988a7280b5c9767c89a8001223cd09 Mon Sep 17 00:00:00 2001 From: Xing Yahao Date: Mon, 17 Jul 2023 11:16:01 +0900 Subject: [PATCH] feat: show metrics in modal --- api/metrics.go | 24 ++++---- ui/components.go | 15 ++--- ui/container.go | 20 ++++++- ui/json.go | 1 + ui/modal.go | 148 ++++++++++++++++++++++++++++++---------------- ui/table.go | 55 ++++++++++------- ui/view.go | 7 ++- util/util.go | 18 ++++++ util/util_test.go | 34 +++++++++++ 9 files changed, 224 insertions(+), 98 deletions(-) diff --git a/api/metrics.go b/api/metrics.go index 3e79e8b..c531e97 100644 --- a/api/metrics.go +++ b/api/metrics.go @@ -45,12 +45,12 @@ func (store *Store) GetMetrics(cluster, service *string) (*MetricsData, error) { // --namespace AWS/ECS \ // --metric-name CPUUtilization \ // --statistics Average \ -// --start-time "$(date -u -v -5M +'%Y-%m-%dT%H:%M:%SZ')" \ +// --start-time "$(date -u -v -30M +'%Y-%m-%dT%H:%M:%SZ')" \ // --end-time "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ -// --period 60 \ -// --dimensions Name=ClusterName,Value={clusterName} Name=ServiceName,Value={serviceName} +// --period 1800 \ +// --dimensions Name=ClusterName,Value=${clusterName} Name=ServiceName,Value=${serviceName} // -// Get last 5 minute, granularity 60s CPUUtilization +// Get last 30 minute, granularity 1800 seconds CPUUtilization func (store *Store) getCPU(cluster, service *string) ([]types.Datapoint, error) { statisticsInput := store.getStatisticsInput(cluster, service) statisticsInput.MetricName = aws.String(CPU) @@ -70,12 +70,12 @@ func (store *Store) getCPU(cluster, service *string) ([]types.Datapoint, error) // --namespace AWS/ECS \ // --metric-name MemoryUtilization \ // --statistics Average \ -// --start-time "$(date -u -v -5M +'%Y-%m-%dT%H:%M:%SZ')" \ +// --start-time "$(date -u -v -30M +'%Y-%m-%dT%H:%M:%SZ')" \ // --end-time "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ -// --period 60 \ -// --dimensions Name=ClusterName,Value={clusterName} Name=ServiceName,Value={serviceName} +// --period 1800 \ +// --dimensions Name=ClusterName,Value=${clusterName} Name=ServiceName,Value=${serviceName} // -// Get last 5 minute, granularity 60s MemoryUtilization +// Get last 30 minute, granularity 1800 seconds CPUUtilization func (store *Store) getMemory(cluster, service *string) ([]types.Datapoint, error) { statisticsInput := store.getStatisticsInput(cluster, service) statisticsInput.MetricName = aws.String(Memory) @@ -92,10 +92,12 @@ func (store *Store) getMemory(cluster, service *string) ([]types.Datapoint, erro func (store *Store) getStatisticsInput(cluster, service *string) *cloudwatch.GetMetricStatisticsInput { store.getCloudWatchClient() + // period := 30 + // granularity := 1800 statistic := []types.Statistic{types.StatisticAverage} - fiveMinutesAgo := time.Now().Add(-5 * time.Minute) + halfHourAgo := time.Now().Add(-30 * time.Minute) now := time.Now() - period := int32(60) + period := int32(1800) dimensions := []types.Dimension{ { Name: aws.String("ClusterName"), @@ -110,7 +112,7 @@ func (store *Store) getStatisticsInput(cluster, service *string) *cloudwatch.Get MetricName: aws.String("CPUUtilization"), Namespace: aws.String("AWS/ECS"), Statistics: statistic, - StartTime: aws.Time(fiveMinutesAgo), + StartTime: aws.Time(halfHourAgo), EndTime: aws.Time(now), Period: aws.Int32(period), Dimensions: dimensions, diff --git a/ui/components.go b/ui/components.go index 2ce28cc..812c820 100644 --- a/ui/components.go +++ b/ui/components.go @@ -18,6 +18,14 @@ func (v *View) modal(p tview.Primitive, width, height int) tview.Primitive { AddItem(nil, 0, 1, false), width, 1, true). AddItem(nil, 0, 1, false) + // handle ESC key close modal + m.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + if event.Key() == tcell.KeyESC { + v.closeModal() + } + return event + }) + return m } @@ -35,13 +43,6 @@ func (v *View) styledForm(title string) *tview.Form { // build form title, input fields f.SetTitle(title).SetTitleAlign(tview.AlignLeft) - // handle ESC key close modal - f.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - if event.Key() == tcell.KeyESC { - v.closeModal() - } - return event - }) return f } diff --git a/ui/container.go b/ui/container.go index 842b2dd..be2c7e7 100644 --- a/ui/container.go +++ b/ui/container.go @@ -90,14 +90,28 @@ func (v *ContainerView) tableHandler() { }) v.table.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - key := event.Rune() - // simulate selected action(ssh) - if key == lKey || key == lKey-upperLowerDiff { + sshHandler := func() { selected := v.getCurrentSelection() containerName := *selected.container.Name v.ssh(containerName) } + + // handle right arrow key + if event.Key() == tcell.KeyRight { + sshHandler() + return event + } + + // handle l key + key := event.Rune() + switch key { + case lKey, lKey - upperLowerDiff: + + sshHandler() + case hKey, hKey - upperLowerDiff: + v.handleDone(0) + } return event }) } diff --git a/ui/json.go b/ui/json.go index 54eeca6..6613273 100644 --- a/ui/json.go +++ b/ui/json.go @@ -73,6 +73,7 @@ func (v *View) switchToServiceEvents() { v.showJsonPages(selected, "events") } +// Deprecated // Switch to Metrics get by cloudwatch func (v *View) switchToMetrics() { selected := v.getCurrentSelection() diff --git a/ui/modal.go b/ui/modal.go index 3104ae0..bd7b5ca 100644 --- a/ui/modal.go +++ b/ui/modal.go @@ -6,6 +6,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/ecs" + "github.com/keidarcy/e1s/util" "github.com/rivo/tview" ) @@ -21,7 +22,7 @@ func (v *View) showUpdateServiceModal() { v.app.Pages.AddPage(title, v.modal(content, 100, 15), true, true) } -// Show update service modal and handle submit event +// Show service auto scaling modal func (v *View) showAutoScaling() { if v.kind != ServicePage { return @@ -30,7 +31,19 @@ func (v *View) showAutoScaling() { if content == nil { return } - v.app.Pages.AddPage(title, v.modal(content, 100, 20), true, true) + v.app.Pages.AddPage(title, v.modal(content, 100, 25), true, true) +} + +// Show service metrics modal(Memory/CPU) +func (v *View) showMetrics() { + if v.kind != ServicePage { + return + } + content, title := v.serviceMetricsContent() + if content == nil { + return + } + v.app.Pages.AddPage(title, v.modal(content, 100, 15), true, true) } // Get service auto scaling form @@ -38,56 +51,53 @@ func (v *View) serviceAutoScalingContent() (*tview.Form, string) { if v.kind != ServicePage { return nil, "" } - return nil, "" - - // selected := v.getCurrentSelection() - // name := *selected.service.ServiceName - - // readonly := "[-:-:-](readonly) " - // title := " Auto scaling [purple::b](" + name + ")" + readonly - // f := v.styledForm(title) - // f.AddInputField("service ", name, len(name)+1, nil, nil) - - // serviceArn := selected.service.ServiceArn - - // if serviceArn == nil { - // f.AddTextView("no valid auto scaling configuration", "", 1, 1, false, false) - // return f, title - // } - - // serviceFullName := util.ArnToFullName(serviceArn) - // autoScaling, err := v.app.Store.GetAutoScaling(&serviceFullName) - // logger.Println(autoScaling) - // // empty auto scaling or empty - // if err != nil || (len(autoScaling.Targets) == 0 && len(autoScaling.Policies) == 0 && len(autoScaling.Activities) == 0) { - // f.AddTextView("no valid auto scaling configuration", "invalid", 10, 1, false, false) - // return f, title - // } - - // if len(autoScaling.Targets) == 1 { - // minCountLabel := "Minimum number of tasks" - // maxCountLabel := "Maximum number of tasks" - // f.AddTextView(minCountLabel, strconv.Itoa(int(*autoScaling.Targets[0].MinCapacity)), 10, 1, true, false) - // f.AddTextView(maxCountLabel, strconv.Itoa(int(*autoScaling.Targets[0].MaxCapacity)), 10, 1, true, false) - // } - - // if len(autoScaling.Policies) == 1 { - // policyNameLabel := "Policy name" - // metricNameLabel := "ECS service metric" - // targetValueLabel := "Target value" - // scaleOutPeriodLabel := "Scale-out cooldown period" - // scaleInPeriodLabel := "Scale-in cooldown period" - // noScaleInLabel := "Turn off scale-in" - // f.AddTextView(policyNameLabel, *autoScaling.Policies[0].PolicyName, 20, 1, true, false) - // f.AddTextView(metricNameLabel, string(autoScaling.Policies[0].TargetTrackingScalingPolicyConfiguration.PredefinedMetricSpecification.PredefinedMetricType), 20, 1, true, false) - // f.AddTextView(targetValueLabel, strconv.Itoa(int(*autoScaling.Policies[0].TargetTrackingScalingPolicyConfiguration.TargetValue)), 20, 1, true, false) - // f.AddTextView(scaleOutPeriodLabel, strconv.Itoa(int(*autoScaling.Policies[0].TargetTrackingScalingPolicyConfiguration.ScaleOutCooldown)), 20, 1, true, false) - // f.AddTextView(scaleInPeriodLabel, strconv.Itoa(int(*autoScaling.Policies[0].TargetTrackingScalingPolicyConfiguration.ScaleInCooldown)), 20, 1, true, false) - // f.AddTextView(noScaleInLabel, strconv.FormatBool(*autoScaling.Policies[0].TargetTrackingScalingPolicyConfiguration.DisableScaleIn), 20, 1, true, false) - // f.AddCheckbox(noScaleInLabel, false, nil) - // } - - // return f, title + + selected := v.getCurrentSelection() + name := *selected.service.ServiceName + + readonly := "[-:-:-](readonly) " + title := " Auto scaling [purple::b](" + name + ")" + readonly + f := v.styledForm(title) + f.AddInputField("Service ", name, len(name)+1, nil, nil) + + serviceArn := selected.service.ServiceArn + + if serviceArn == nil { + f.AddTextView("No valid auto scaling configuration", "", 1, 1, false, false) + return f, title + } + + serviceFullName := util.ArnToFullName(serviceArn) + autoScaling, err := v.app.Store.GetAutoScaling(&serviceFullName) + // empty auto scaling or empty + if err != nil || (len(autoScaling.Targets) == 0 && len(autoScaling.Policies) == 0 && len(autoScaling.Activities) == 0) { + f.AddTextView("No valid auto scaling configuration", "", 10, 1, false, false) + return f, title + } + + if len(autoScaling.Targets) == 1 { + minCountLabel := "Minimum number of tasks" + maxCountLabel := "Maximum number of tasks" + f.AddTextView(minCountLabel, strconv.Itoa(int(*autoScaling.Targets[0].MinCapacity)), 50, 1, true, false) + f.AddTextView(maxCountLabel, strconv.Itoa(int(*autoScaling.Targets[0].MaxCapacity)), 50, 1, true, false) + } + + if len(autoScaling.Policies) == 1 { + policyNameLabel := "Policy name" + metricNameLabel := "ECS service metric" + targetValueLabel := "Target value" + scaleOutPeriodLabel := "Scale-out cooldown period" + scaleInPeriodLabel := "Scale-in cooldown period" + noScaleInLabel := "Turn off scale-in" + f.AddTextView(policyNameLabel, *autoScaling.Policies[0].PolicyName, 20, 1, true, false) + f.AddTextView(metricNameLabel, string(autoScaling.Policies[0].TargetTrackingScalingPolicyConfiguration.PredefinedMetricSpecification.PredefinedMetricType), 50, 1, true, false) + f.AddTextView(targetValueLabel, strconv.Itoa(int(*autoScaling.Policies[0].TargetTrackingScalingPolicyConfiguration.TargetValue)), 50, 1, true, false) + f.AddTextView(scaleOutPeriodLabel, strconv.Itoa(int(*autoScaling.Policies[0].TargetTrackingScalingPolicyConfiguration.ScaleOutCooldown)), 50, 1, true, false) + f.AddTextView(scaleInPeriodLabel, strconv.Itoa(int(*autoScaling.Policies[0].TargetTrackingScalingPolicyConfiguration.ScaleInCooldown)), 50, 1, true, false) + f.AddTextView(noScaleInLabel, strconv.FormatBool(*autoScaling.Policies[0].TargetTrackingScalingPolicyConfiguration.DisableScaleIn), 50, 1, true, false) + } + + return f, title } // Get service update form @@ -162,3 +172,37 @@ func (v *View) serviceUpdateContent() (*tview.Form, string) { }) return f, title } + +// Get service metrics charts +func (v *View) serviceMetricsContent() (*tview.Form, string) { + if v.kind != ServicePage { + return nil, "" + } + + selected := v.getCurrentSelection() + cluster := v.app.cluster.ClusterName + service := *selected.service.ServiceName + + title := " Metrics [purple::b](" + service + ")" + + f := v.styledForm(title) + f.AddInputField("Service ", service, len(service)+1, nil, nil) + + metrics, err := v.app.Store.GetMetrics(cluster, &service) + + // empty Metrics or empty + if err != nil || (len(metrics.CPUUtilization) == 0 && len(metrics.MemoryUtilization) == 0) { + return f, title + } + if len(metrics.CPUUtilization) > 0 { + cpuLabel := "CPUUtilization" + f.AddTextView(cpuLabel, util.BuildMeterText(*metrics.CPUUtilization[0].Average), 50, 1, true, false) + } + + if len(metrics.MemoryUtilization) > 0 { + memLabel := "MemoryUtilization" + f.AddTextView(memLabel, util.BuildMeterText(*metrics.MemoryUtilization[0].Average), 50, 1, true, false) + } + + return f, title +} diff --git a/ui/table.go b/ui/table.go index ab6af19..6884f2e 100644 --- a/ui/table.go +++ b/ui/table.go @@ -80,31 +80,42 @@ func (v *View) handleSelected(row, column int) { // Handle keyboard input func (v *View) handleInputCapture(event *tcell.EventKey) *tcell.EventKey { - if event.Key() == tcell.KeyRune { - key := event.Rune() - if key == bKey || key == bKey-upperLowerDiff { - v.openInBrowser() - } else if key == dKey || key == dKey-upperLowerDiff { - v.switchToResourceJson() - } else if key == tKey || key == tKey-upperLowerDiff { - v.switchToTaskDefinition() - } else if key == rKey || key == rKey-upperLowerDiff { - v.switchToTaskDefinitionRevisions() - } else if key == wKey || key == wKey-upperLowerDiff { - v.switchToServiceEvents() - } else if key == mKey || key == mKey-upperLowerDiff { - v.switchToMetrics() - } else if key == aKey || key == aKey-upperLowerDiff { - v.showAutoScaling() - // } else if key == 'i' { - // v.switchToAutoScaling() - } else if key == eKey || key == eKey-upperLowerDiff { - v.showUpdateServiceModal() - } else if key == hKey || key == hKey-upperLowerDiff { + if event.Key() != tcell.KeyRune { + switch event.Key() { + case tcell.KeyLeft: + // Handle left arrow key v.handleDone(0) - } else if key == lKey || key == lKey-upperLowerDiff { + case tcell.KeyRight: + // Handle right arrow key v.handleSelected(0, 0) } + return event + } + + key := event.Rune() + switch key { + case bKey, bKey - upperLowerDiff: + v.openInBrowser() + case dKey, dKey - upperLowerDiff: + v.switchToResourceJson() + case tKey, tKey - upperLowerDiff: + v.switchToTaskDefinition() + case rKey, rKey - upperLowerDiff: + v.switchToTaskDefinitionRevisions() + case wKey, wKey - upperLowerDiff: + v.switchToServiceEvents() + case mKey, mKey - upperLowerDiff: + v.showMetrics() + case aKey, aKey - upperLowerDiff: + v.showAutoScaling() + // case 'i' { + // v.switchToAutoScaling() + case eKey, eKey - upperLowerDiff: + v.showUpdateServiceModal() + case hKey, hKey - upperLowerDiff: + v.handleDone(0) + case lKey, lKey - upperLowerDiff: + v.handleSelected(0, 0) } return event } diff --git a/ui/view.go b/ui/view.go index 7329f1c..c265e48 100644 --- a/ui/view.go +++ b/ui/view.go @@ -26,8 +26,8 @@ const ( describeTaskDefinition = "Describe task definition" describeTaskDefinitionRevisions = "Describe task definition revisions" describeServiceEvents = "Describe service events" - showAutoScaling = "Describe auto scaling" - showMetrics = "Describe metrics" + showAutoScaling = "Show auto scaling" + showMetrics = "Show metrics" updateService = "Update Service" openInBrowser = "Open in browser" @@ -210,7 +210,8 @@ func (v *View) addFooterItems() { // keep middle space keysLabel := tview.NewTextView(). - SetText(footerKeyFmt) + // SetText(footerKeyFmt) + SetText("") keysLabel.SetDynamicColors(true).SetTextAlign(tview.AlignCenter) v.footer.footer. AddItem(keysLabel, 0, 1, false) diff --git a/util/util.go b/util/util.go index 2b1d1ac..9338611 100644 --- a/util/util.go +++ b/util/util.go @@ -147,3 +147,21 @@ func OpenURL(url string) error { } return nil } + +func BuildMeterText(f float64) string { + const yesBlock = "█" + const noBlock = "▒" + i := int(f) + + yesNum := i / 5 + if yesNum == 0 { + yesNum++ + } + noNum := 20 - yesNum + meterVal := strings.Join([]string{ + strings.Repeat(yesBlock, yesNum), + strings.Repeat(noBlock, noNum), + }, "") + + return meterVal + " " + fmt.Sprintf("%.2f", f) + "%" +} diff --git a/util/util_test.go b/util/util_test.go index 31823a7..4847c76 100644 --- a/util/util_test.go +++ b/util/util_test.go @@ -68,3 +68,37 @@ func TestArnToURL(t *testing.T) { }) } } + +func TextBuildMeterText(t *testing.T) { + testCases := []struct { + name string + input float64 + want string + }{ + { + name: "value=2.123", + input: 2.123, + want: "█▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒", + }, + { + name: "value=12.123", + input: 12.123, + want: "██▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒", + }, + { + name: "value=2.123", + input: 52.123, + want: "██████████▒▒▒▒▒▒▒▒▒▒", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := BuildMeterText(tc.input) + if result != tc.want { + t.Errorf("input: %v, want: %v, results %v\n", tc.input, tc.want, result) + } + }) + } + +}