From 069b4e7ee6307ca4b9d8997175a621b27569529d Mon Sep 17 00:00:00 2001 From: Cezar Craciunoiu Date: Fri, 16 Aug 2024 16:09:01 +0300 Subject: [PATCH] feat(internal): Introduce 'metrics' command for instances Signed-off-by: Cezar Craciunoiu --- internal/cli/kraft/cloud/instance/instance.go | 2 + .../kraft/cloud/instance/metrics/metrics.go | 83 ++++++++++++++ internal/cli/kraft/cloud/utils/print.go | 103 ++++++++++++++++++ 3 files changed, 188 insertions(+) create mode 100644 internal/cli/kraft/cloud/instance/metrics/metrics.go diff --git a/internal/cli/kraft/cloud/instance/instance.go b/internal/cli/kraft/cloud/instance/instance.go index 660521800..de0ffa78c 100644 --- a/internal/cli/kraft/cloud/instance/instance.go +++ b/internal/cli/kraft/cloud/instance/instance.go @@ -17,6 +17,7 @@ import ( "kraftkit.sh/internal/cli/kraft/cloud/instance/get" "kraftkit.sh/internal/cli/kraft/cloud/instance/list" "kraftkit.sh/internal/cli/kraft/cloud/instance/logs" + "kraftkit.sh/internal/cli/kraft/cloud/instance/metrics" "kraftkit.sh/internal/cli/kraft/cloud/instance/remove" "kraftkit.sh/internal/cli/kraft/cloud/instance/start" "kraftkit.sh/internal/cli/kraft/cloud/instance/stop" @@ -42,6 +43,7 @@ func NewCmd() *cobra.Command { cmd.AddCommand(create.NewCmd()) cmd.AddCommand(list.NewCmd()) cmd.AddCommand(logs.NewCmd()) + cmd.AddCommand(metrics.NewCmd()) cmd.AddCommand(remove.NewCmd()) cmd.AddCommand(start.NewCmd()) cmd.AddCommand(get.NewCmd()) diff --git a/internal/cli/kraft/cloud/instance/metrics/metrics.go b/internal/cli/kraft/cloud/instance/metrics/metrics.go new file mode 100644 index 000000000..9a2f7378d --- /dev/null +++ b/internal/cli/kraft/cloud/instance/metrics/metrics.go @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2024, Unikraft GmbH and The KraftKit Authors. +// Licensed under the BSD-3-Clause License (the "License"). +// You may not use this file except in compliance with the License. + +package metrics + +import ( + "context" + "fmt" + + "github.com/MakeNowJust/heredoc" + "github.com/spf13/cobra" + + kraftcloud "sdk.kraft.cloud" + + "kraftkit.sh/cmdfactory" + "kraftkit.sh/config" + "kraftkit.sh/internal/cli/kraft/cloud/utils" +) + +type MetricsOptions struct { + Auth *config.AuthConfig `noattribute:"true"` + Client kraftcloud.KraftCloud `noattribute:"true"` + Metro string `noattribute:"true"` + Token string `noattribute:"true"` + Output string `long:"output" short:"o" usage:"Set output format. Options: table,yaml,json,list" default:"list"` +} + +func NewCmd() *cobra.Command { + cmd, err := cmdfactory.New(&MetricsOptions{}, cobra.Command{ + Short: "Return metrics for instances", + Use: "metrics [FLAGS] [UUID|NAME [UUID|NAME]...]", + Aliases: []string{"metric", "m", "meter"}, + Args: cobra.MinimumNArgs(1), + Example: heredoc.Doc(` + # Return metrics for an instance by UUID + $ kraft cloud instance metrics fd1684ea-7970-4994-92d6-61dcc7905f2b + + # Return metrics for an instance by name + $ kraft cloud instance metrics my-instance-431342 + `), + Annotations: map[string]string{ + cmdfactory.AnnotationHelpGroup: "kraftcloud-instance", + }, + }) + if err != nil { + panic(err) + } + + return cmd +} + +func (opts *MetricsOptions) Pre(cmd *cobra.Command, _ []string) error { + err := utils.PopulateMetroToken(cmd, &opts.Metro, &opts.Token) + if err != nil { + return fmt.Errorf("could not populate metro and token: %w", err) + } + + if !utils.IsValidOutputFormat(opts.Output) { + return fmt.Errorf("invalid output format: %s", opts.Output) + } + + return nil +} + +func (opts *MetricsOptions) Run(ctx context.Context, args []string) error { + auth, err := config.GetKraftCloudAuthConfig(ctx, opts.Token) + if err != nil { + return fmt.Errorf("could not retrieve credentials: %w", err) + } + + client := kraftcloud.NewInstancesClient( + kraftcloud.WithToken(config.GetKraftCloudTokenAuthConfig(*auth)), + ) + + resp, err := client.WithMetro(opts.Metro).Metrics(ctx, args...) + if err != nil { + return fmt.Errorf("could not get instance %s: %w", args, err) + } + + return utils.PrintMetrics(ctx, opts.Output, *resp) +} diff --git a/internal/cli/kraft/cloud/utils/print.go b/internal/cli/kraft/cloud/utils/print.go index 40813ad65..a4ef7018f 100644 --- a/internal/cli/kraft/cloud/utils/print.go +++ b/internal/cli/kraft/cloud/utils/print.go @@ -1070,6 +1070,109 @@ func PrintCertificates(ctx context.Context, format string, resp kcclient.Service return table.Render(iostreams.G(ctx).Out) } +// PrintMetrics pretty-prints the provided set of instances metrics or returns +// an error if unable to send to stdout via the provided context. +func PrintMetrics(ctx context.Context, format string, resp kcclient.ServiceResponse[kcinstances.MetricsResponseItem]) error { + if format == "raw" { + printRaw(ctx, resp) + return nil + } + + metrics, err := resp.AllOrErr() + if err != nil { + return err + } + + if err := iostreams.G(ctx).StartPager(); err != nil { + log.G(ctx).Errorf("error starting pager: %v", err) + } + + defer iostreams.G(ctx).StopPager() + + cs := iostreams.G(ctx).ColorScheme() + table, err := tableprinter.NewTablePrinter(ctx, + tableprinter.WithMaxWidth(iostreams.G(ctx).TerminalWidth()), + tableprinter.WithOutputFormatFromString(format), + ) + if err != nil { + return err + } + + // Header row + if format != "table" { + table.AddField("UUID", cs.Bold) + } + table.AddField("NAME", cs.Bold) + table.AddField("RSS", cs.Bold) + table.AddField("CPU TIME", cs.Bold) + if format != "table" { + table.AddField("RX SIZE", cs.Bold) + table.AddField("RX PACKETS", cs.Bold) + table.AddField("TX SIZE", cs.Bold) + table.AddField("TX PACKETS", cs.Bold) + table.AddField("CONNECTIONS", cs.Bold) + table.AddField("REQUESTS", cs.Bold) + table.AddField("QUEUED", cs.Bold) + } + table.AddField("TOTAL", cs.Bold) + table.EndRow() + + if config.G[config.KraftKit](ctx).NoColor { + instanceStateColor = instanceStateColorNil + } + + for _, metric := range metrics { + if metric.Message != "" { + // Header row + if format != "table" { + table.AddField(metric.UUID, nil) + } + table.AddField(metric.Name, nil) + table.AddField("", nil) // RSS + table.AddField("", nil) // CPU TIME + if format != "table" { + table.AddField("", nil) // RX SIZE + table.AddField("", nil) // RX PACKETS + table.AddField("", nil) // TX SIZE + table.AddField("", nil) // TX PACKETS + table.AddField("", nil) // CONNECTIONS + table.AddField("", nil) // REQUESTS + table.AddField("", cs.Bold) // QUEUED + } + table.AddField("", cs.Bold) // TOTAL + table.EndRow() + + continue + } + if format != "table" { + table.AddField(metric.UUID, nil) + } + + table.AddField(metric.Name, nil) + table.AddField(humanize.IBytes(metric.RSS), nil) + + duration, err := time.ParseDuration(fmt.Sprintf("%dms", metric.CPUTimeMs)) + if err != nil { + return fmt.Errorf("could not parse CPU time for '%s': %w", metric.UUID, err) + } + table.AddField(duration.String(), nil) + + if format != "table" { + table.AddField(humanize.IBytes(metric.RxBytes), nil) + table.AddField(fmt.Sprintf("%d", metric.RxPackets), nil) + table.AddField(humanize.IBytes(metric.TxBytes), nil) + table.AddField(fmt.Sprintf("%d", metric.TxPackets), nil) + table.AddField(fmt.Sprintf("%d", metric.Connections), nil) + table.AddField(fmt.Sprintf("%d", metric.Requests), nil) + table.AddField(fmt.Sprintf("%d", metric.Queued), nil) + } + table.AddField(fmt.Sprintf("%d", metric.Total), nil) + table.EndRow() + } + + return table.Render(iostreams.G(ctx).Out) +} + // PrettyPrintInstance outputs a single instance and information about it. func PrettyPrintInstance(ctx context.Context, instance kcinstances.GetResponseItem, service *kcservices.GetResponseItem, autoStart bool) { out := iostreams.G(ctx).Out