Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/products.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions products/cloudwatch/internal/cloudwatch/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package cloudwatch

import (
"github.com/spf13/cobra"

"github.com/ucloud/ucloud-cli/pkg/cli"
)

// NewCommand builds the `cloudwatch` root command and mounts its public verbs.
func NewCommand(ctx *cli.Context) *cobra.Command {
cmd := &cobra.Command{
Use: "cloudwatch",
Short: "Discover and query CloudWatch metrics",
Long: "List monitored products and metrics, then query metric data.",
}
cmd.AddCommand(newListProducts(ctx))
cmd.AddCommand(newListMetrics(ctx))
cmd.AddCommand(newQueryMetricData(ctx))
return cmd
}
66 changes: 66 additions & 0 deletions products/cloudwatch/internal/cloudwatch/completion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package cloudwatch

import (
"fmt"

"github.com/spf13/cobra"

"github.com/ucloud/ucloud-cli/pkg/cli"
"github.com/ucloud/ucloud-cli/pkg/command"
)

var (
calcMethodValues = []string{"raw", "max", "min", "avg", "sum"}
periodValues = []string{"60", "300", "3600", "21600", "86400"}
)

// productKeyCandidates returns the live product-key list for --product
// completion by calling ListMonitorProduct — the authoritative, dynamic
// source (products are added/retired over time; a hardcoded list would go
// stale). No request fields are required (empty Filter matches everything).
func productKeyCandidates(ctx *cli.Context) func() []string {
return func() []string {
client := newGenericClient(ctx)
req := client.NewGenericRequest()
out, err := invoke(client, req, map[string]interface{}{
"Action": "ListMonitorProduct",
})
if err != nil {
return nil
}
var resp listMonitorProductResp
if err := decodeData(out, &resp); err != nil {
return nil
}
keys := make([]string, 0, len(resp.List))
for _, p := range resp.List {
keys = append(keys, p.ProductKey)
}
return keys
}
}

func registerQueryMetricDataCompletions(cmd *cobra.Command) {
command.SetFlagValues(cmd, "calc-method", calcMethodValues...)
command.SetFlagValues(cmd, "period", periodValues...)
}

func validateEnum(name, value string, allowed []string) error {
for _, candidate := range allowed {
if value == candidate {
return nil
}
}
return fmt.Errorf("%s must be one of: %s", name, joinEnumValues(allowed))
}

func joinEnumValues(values []string) string {
result := ""
for i, value := range values {
if i > 0 {
result += ", "
}
result += value
}
return result
}
59 changes: 59 additions & 0 deletions products/cloudwatch/internal/cloudwatch/invoke.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package cloudwatch

import (
"encoding/json"
"fmt"

sdkcloudwatch "github.com/ucloud/ucloud-sdk-go/services/cloudwatch"
"github.com/ucloud/ucloud-sdk-go/ucloud/request"

"github.com/ucloud/ucloud-cli/pkg/cli"
)

// newGenericClient returns the authed CloudWatch service client used purely as
// the carrier for GenericInvoke. The SDK does provide a strongly-typed
// CloudWatchClient (services/cloudwatch), but this product intentionally does
// NOT call its typed methods — it only borrows the client (and the platform
// credential/signature/handler chain cli.NewServiceClient wires up) to send
// generic Action requests, exactly like `ucloud api` and the mysql product do
// for actions with no typed SDK method. Using the cloudwatch client (rather
// than uaccount) makes the call site read as "this is a CloudWatch request"
// without coupling to the SDK's generated request/response types.
func newGenericClient(ctx *cli.Context) *sdkcloudwatch.CloudWatchClient {
return cli.NewServiceClient(ctx, sdkcloudwatch.NewClient)
}

// invoke sends req with the given payload and returns the decoded SkymFlameAPI
// envelope payload (map: {Action, TraceId, RetCode, Message, Data, TotalCount?}).
//
// Business errors (envelope RetCode != 0) do NOT need to be checked here: the
// SDK's built-in errorHandler (ucloud/handlers.go, registered by default on
// every *ucloud.Client) already inspects resp.GetRetCode() after every
// InvokeAction call and converts a non-zero RetCode into a uerr.Error, which
// GenericInvoke returns as err. So `err != nil` from GenericInvoke already
// covers both transport errors (network/timeout/signature) and business
// errors — callers only need ctx.HandleError(err); there is nothing left for
// product code to inspect on the payload for error purposes.
func invoke(client *sdkcloudwatch.CloudWatchClient, req request.GenericRequest, payload map[string]interface{}) (map[string]interface{}, error) {
if err := req.SetPayload(payload); err != nil {
return nil, fmt.Errorf("set payload: %w", err)
}
resp, err := client.GenericInvoke(req)
if err != nil {
return nil, err
}
return resp.GetPayload(), nil
}

// decodeData decodes the envelope's Data field into out (a pointer to the
// caller's local response struct) by re-marshaling the interface{} value.
func decodeData(payload map[string]interface{}, out interface{}) error {
raw, err := json.Marshal(payload["Data"])
if err != nil {
return fmt.Errorf("marshal Data: %w", err)
}
if err := json.Unmarshal(raw, out); err != nil {
return fmt.Errorf("unmarshal Data: %w", err)
}
return nil
}
97 changes: 97 additions & 0 deletions products/cloudwatch/internal/cloudwatch/list_metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package cloudwatch

import (
"github.com/spf13/cobra"

"github.com/ucloud/ucloud-cli/pkg/cli"
"github.com/ucloud/ucloud-cli/pkg/command"
)

// getProductMetricResp is the local decode target for the GetProductMetrics
// envelope Data. Fields mirror SkymFlameAPI dto.GetProductMetricListResp
// (release branch) — only what the CLI renders is declared.
type getProductMetricResp struct {
Total int64 `json:"Total"`
List []metricItem `json:"List"`
}

type metricItem struct {
Metric string `json:"Metric"`
MetricName string `json:"MetricName"`
MetricChName string `json:"MetricChName"`
FrequencyMs int32 `json:"FrequencyMs"`
Unit *unitItem `json:"Unit"`
}

type unitItem struct {
UnitChName string `json:"UnitChName"`
UnitName string `json:"UnitName"`
}

func newListMetrics(ctx *cli.Context) *cobra.Command {
var product, monitorType string
client := newGenericClient(ctx)
req := client.NewGenericRequest()

cmd := &cobra.Command{
Use: "list-metrics",
Short: "List metrics for a product",
Long: "List the metrics available for one monitored product.",
Example: ` # List all UHost metrics
ucloud cloudwatch list-metrics --product uhost

# List only basic UHost metrics
ucloud cloudwatch list-metrics --product uhost --monitor-type basic`,
Args: cobra.NoArgs,
Run: func(c *cobra.Command, args []string) {
payload := map[string]interface{}{
"Action": "GetProductMetrics",
"ProductKey": product,
}
if monitorType != "" {
payload["MonitorType"] = monitorType
}
out, err := invoke(client, req, payload)
if err != nil {
ctx.HandleError(err)
return
}
var resp getProductMetricResp
if err := decodeData(out, &resp); err != nil {
ctx.HandleError(err)
return
}
rows := make([]MetricRow, 0, len(resp.List))
for _, m := range resp.List {
name := m.MetricChName
if name == "" {
name = m.MetricName
}
unit := ""
if m.Unit != nil {
unit = m.Unit.UnitChName
if unit == "" {
unit = m.Unit.UnitName
}
}
rows = append(rows, MetricRow{
Metric: m.Metric,
MetricName: name,
Unit: unit,
FrequencyMs: m.FrequencyMs,
})
}
ctx.PrintList(rows)
},
}

flags := cmd.Flags()
flags.SortFlags = false
flags.StringVar(&product, "product", "", "Required. Product key returned by list-products, for example uhost")
flags.StringVar(&monitorType, "monitor-type", "", "Optional. Metric type filter; omit to list all types")
cmd.MarkFlagRequired("product")

command.SetCompletion(cmd, "product", productKeyCandidates(ctx))

return cmd
}
64 changes: 64 additions & 0 deletions products/cloudwatch/internal/cloudwatch/list_products.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package cloudwatch

import (
"github.com/spf13/cobra"

"github.com/ucloud/ucloud-cli/pkg/cli"
)

type monitorProductItem struct {
ProductKey string `json:"ProductKey"`
ProductName string `json:"ProductName"`
ProductChName string `json:"ProductChName"`
IsSupportHighPrecision bool `json:"IsSupportHighPrecision"`
}

type listMonitorProductResp struct {
Total int `json:"Total"`
List []monitorProductItem `json:"List"`
}

func newListProducts(ctx *cli.Context) *cobra.Command {
client := newGenericClient(ctx)
req := client.NewGenericRequest()

cmd := &cobra.Command{
Use: "list-products",
Short: "List monitored products",
Long: "List products that can be queried with CloudWatch.",
Example: ` # List monitored products
ucloud cloudwatch list-products
# Print only product keys as JSON
ucloud cloudwatch list-products --output json | jq -r '.[].Product'`,
Args: cobra.NoArgs,
Run: func(c *cobra.Command, args []string) {
out, err := invoke(client, req, map[string]interface{}{
"Action": "ListMonitorProduct",
})
if err != nil {
ctx.HandleError(err)
return
}

var resp listMonitorProductResp
if err := decodeData(out, &resp); err != nil {
ctx.HandleError(err)
return
}

rows := make([]MonitorProductRow, 0, len(resp.List))
for _, p := range resp.List {
rows = append(rows, MonitorProductRow{
Product: p.ProductKey,
ProductName: p.ProductName,
ProductChName: p.ProductChName,
IsSupportHighPrecision: p.IsSupportHighPrecision,
})
}
ctx.PrintList(rows)
},
}

return cmd
}
Loading
Loading