diff --git a/cmd/products.gen.go b/cmd/products.gen.go index bf8682ba6b..ba87a91d01 100644 --- a/cmd/products.gen.go +++ b/cmd/products.gen.go @@ -3,6 +3,7 @@ package cmd import ( "github.com/ucloud/ucloud-cli/pkg/cli" + "github.com/ucloud/ucloud-cli/products/cloudwatch" "github.com/ucloud/ucloud-cli/products/css" "github.com/ucloud/ucloud-cli/products/eip" "github.com/ucloud/ucloud-cli/products/firewall" @@ -39,6 +40,7 @@ import ( // registeredProducts returns the platform-registered products. func registeredProducts() []cli.Product { return []cli.Product{ + cloudwatch.New(), css.New(), eip.New(), firewall.New(), diff --git a/products/cloudwatch/internal/cloudwatch/cmd.go b/products/cloudwatch/internal/cloudwatch/cmd.go new file mode 100644 index 0000000000..593d277731 --- /dev/null +++ b/products/cloudwatch/internal/cloudwatch/cmd.go @@ -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 +} diff --git a/products/cloudwatch/internal/cloudwatch/completion.go b/products/cloudwatch/internal/cloudwatch/completion.go new file mode 100644 index 0000000000..a4b1665bb4 --- /dev/null +++ b/products/cloudwatch/internal/cloudwatch/completion.go @@ -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 +} diff --git a/products/cloudwatch/internal/cloudwatch/invoke.go b/products/cloudwatch/internal/cloudwatch/invoke.go new file mode 100644 index 0000000000..31a65ee890 --- /dev/null +++ b/products/cloudwatch/internal/cloudwatch/invoke.go @@ -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 +} diff --git a/products/cloudwatch/internal/cloudwatch/list_metrics.go b/products/cloudwatch/internal/cloudwatch/list_metrics.go new file mode 100644 index 0000000000..da3cd25bb9 --- /dev/null +++ b/products/cloudwatch/internal/cloudwatch/list_metrics.go @@ -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 +} diff --git a/products/cloudwatch/internal/cloudwatch/list_products.go b/products/cloudwatch/internal/cloudwatch/list_products.go new file mode 100644 index 0000000000..cdff0de485 --- /dev/null +++ b/products/cloudwatch/internal/cloudwatch/list_products.go @@ -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 +} diff --git a/products/cloudwatch/internal/cloudwatch/query_metric_data.go b/products/cloudwatch/internal/cloudwatch/query_metric_data.go new file mode 100644 index 0000000000..74bdb5aaea --- /dev/null +++ b/products/cloudwatch/internal/cloudwatch/query_metric_data.go @@ -0,0 +1,252 @@ +package cloudwatch + +import ( + "fmt" + "sort" + "strings" + "time" + + "github.com/spf13/cobra" + + "github.com/ucloud/ucloud-cli/internal/common" + "github.com/ucloud/ucloud-cli/pkg/cli" + "github.com/ucloud/ucloud-cli/pkg/command" +) + +// queryMetricDataResp is the local decode target for the QueryMetricDataSet +// envelope Data. Fields mirror SkymFlameAPI dto.QueryMetricDataResp (release). +type queryMetricDataResp struct { + List []metricInfo `json:"List"` + InvalidResourceIds []string `json:"InvalidResourceIds"` +} + +type metricInfo struct { + ErrCode int `json:"ErrCode"` + ErrMsg string `json:"ErrMsg"` + Metric string `json:"Metric"` + Results []metricValues `json:"Results"` +} + +type metricValues struct { + ResourceID string `json:"ResourceId"` + ResourceName string `json:"ResourceName"` + TagMap map[string]string `json:"TagMap"` + Values []metricPoint `json:"Values"` +} + +type metricPoint struct { + Timestamp int64 `json:"Timestamp"` + Value float64 `json:"Value"` +} + +// Note on request encoding: the SDK's generic form encoder expands maps and +// slices but rejects nested structs. MetricInfos is therefore built as a +// []map[string]interface{} (with each TagList entry also a map), not structs. + +// newQueryMetricData ucloud cloudwatch query-metric-data +func newQueryMetricData(ctx *cli.Context) *cobra.Command { + var product, calcMethod string + var resourceIDs, metrics []string + var startTime, endTime, period int64 + var tags []string + client := newGenericClient(ctx) + req := client.NewGenericRequest() + + cmd := &cobra.Command{ + Use: "query-metric-data", + Short: "Query metric data", + Long: "Query time-series data for one or more resources and metrics. Every resource is paired with every metric.", + Example: ` # Query one metric on one resource for the default last-hour window + ucloud cloudwatch query-metric-data --product uhost --resource-id uhost-xxx --metric uhost_cpu_used + + # Repeat --resource-id and --metric for multiple resources and metrics + ucloud cloudwatch query-metric-data --product uhost \ + --resource-id uhost-a --resource-id uhost-b \ + --metric uhost_cpu_used --metric uhost_mem_used \ + --tag env=prod --tag role=web --calc-method avg --period 300 + + # Values for the same tag key are OR-ed; different keys are AND-ed + ucloud cloudwatch query-metric-data --product uhost --resource-id uhost-a \ + --metric uhost_cpu_used --tag env=prod --tag env=staging --tag role=web`, + Args: cobra.NoArgs, + Run: func(c *cobra.Command, args []string) { + if req.GetProjectId() == "" { + ctx.HandleError(fmt.Errorf("project-id is required for query-metric-data; pass --project-id or configure a default project")) + return + } + // default time window: the last hour + now := time.Now().Unix() + if endTime == 0 { + endTime = now + } + if startTime == 0 { + startTime = endTime - 3600 + } + if startTime >= endTime { + ctx.HandleError(fmt.Errorf("start-time must be earlier than end-time")) + return + } + if err := validateEnum("calc-method", calcMethod, calcMethodValues); err != nil { + ctx.HandleError(err) + return + } + if period != 0 { + if err := validateEnum("period", fmt.Sprint(period), periodValues); err != nil { + ctx.HandleError(err) + return + } + } + + tagList, err := parseTags(tags) + if err != nil { + ctx.HandleError(err) + return + } + + // Cartesian product of --resource-id x --metric: one MetricInfos + // entry per (resource, metric) combination, all queried in the + // same request. The backend enforces its own cap on the total + // number of combinations (config.VM.ReqBatchMaxNum, not fixed at + // compile time) — CLI does not pre-validate a count, an + // over-limit request surfaces as a normal backend error via + // ctx.HandleError. + metricInfos := buildMetricInfos(ctx, resourceIDs, metrics, tagList) + + payload := map[string]interface{}{ + "Action": "QueryMetricDataSet", + "ProductKey": product, + "StartTime": startTime, + "EndTime": endTime, + "CalcMethod": calcMethod, + "MetricInfos": metricInfos, + } + if period != 0 { + payload["Period"] = period + } + out, err := invoke(client, req, payload) + if err != nil { + ctx.HandleError(err) + return + } + var resp queryMetricDataResp + if err := decodeData(out, &resp); err != nil { + ctx.HandleError(err) + return + } + + rows := make([]DataPointRow, 0) + for _, mi := range resp.List { + if mi.ErrCode != 0 { + ctx.LogWarn(fmt.Sprintf("metric %s error: %s", mi.Metric, mi.ErrMsg)) + continue + } + for _, mv := range mi.Results { + for _, pt := range mv.Values { + rows = append(rows, DataPointRow{ + ResourceID: mv.ResourceID, + ResourceName: mv.ResourceName, + Metric: mi.Metric, + Timestamp: common.FormatDateTime(int(pt.Timestamp)), + Value: pt.Value, + Tags: flattenTagMap(mv.TagMap), + }) + } + } + } + if len(resp.InvalidResourceIds) > 0 { + ctx.LogWarn(fmt.Sprintf("invalid resource ids: %v", resp.InvalidResourceIds)) + } + if len(rows) == 0 { + ctx.LogWarn("no data points in the given time range") + } + ctx.PrintList(rows) + }, + } + + flags := cmd.Flags() + flags.SortFlags = false + flags.StringVar(&product, "product", "", "Required. Product key returned by list-products, for example uhost") + flags.StringArrayVar(&resourceIDs, "resource-id", nil, "Required. Resource ID; repeat --resource-id to query multiple resources (values are not comma-split)") + flags.StringArrayVar(&metrics, "metric", nil, "Required. Metric key returned by list-metrics; repeat --metric to query multiple metrics (values are not comma-split)") + flags.Int64Var(&startTime, "start-time", 0, "Optional. Start time as Unix seconds; defaults to one hour before end-time") + flags.Int64Var(&endTime, "end-time", 0, "Optional. End time as Unix seconds; defaults to the current time") + flags.StringVar(&calcMethod, "calc-method", "raw", "Optional. Calculation method: raw, max, min, avg, or sum") + flags.Int64Var(&period, "period", 0, "Optional. Data interval in seconds: 60, 300, 3600, 21600, or 86400; omit to choose automatically") + flags.StringArrayVar(&tags, "tag", nil, "Optional. Tag filter as key=value; repeat --tag for multiple values or keys. Same-key values are OR-ed, different keys are AND-ed; commas in values are preserved") + cmd.MarkFlagRequired("product") + cmd.MarkFlagRequired("resource-id") + cmd.MarkFlagRequired("metric") + + ctx.BindProjectID(cmd, req) + cmd.Flags().Lookup("project-id").Usage = "Required. Project ID" + cmd.Flags().Lookup("project-id").DefValue = "" + ctx.BindRegion(cmd, req) + cmd.Flags().Lookup("region").Usage = "Optional. Region" + cmd.Flags().Lookup("region").DefValue = "" + command.SetCompletion(cmd, "product", productKeyCandidates(ctx)) + registerQueryMetricDataCompletions(cmd) + + return cmd +} + +func buildMetricInfos(ctx *cli.Context, resourceIDs, metrics []string, tagList []map[string]interface{}) []map[string]interface{} { + metricInfos := make([]map[string]interface{}, 0, len(resourceIDs)*len(metrics)) + for _, rid := range resourceIDs { + for _, metric := range metrics { + info := map[string]interface{}{ + "Metric": metric, + "ResourceId": ctx.PickResourceID(rid), + } + if len(tagList) > 0 { + info["TagList"] = tagList + } + metricInfos = append(metricInfos, info) + } + } + return metricInfos +} + +// parseTags converts repeated --tag "key=value" flags into TagList entries. +// Each entry is a map (not a struct) so the SDK generic form encoder can expand +// it into TagList.N.TagKey / TagList.N.TagValues.M form. Values for the same +// key are grouped into one entry (OR); separate keys remain separate entries +// (AND). +func parseTags(tags []string) ([]map[string]interface{}, error) { + if len(tags) == 0 { + return nil, nil + } + valuesByKey := make(map[string][]string, len(tags)) + keys := make([]string, 0, len(tags)) + for _, t := range tags { + kv := strings.SplitN(t, "=", 2) + if len(kv) != 2 || kv[0] == "" || kv[1] == "" { + return nil, fmt.Errorf("invalid --tag %q, want key=value; repeat --tag for multiple values", t) + } + if _, exists := valuesByKey[kv[0]]; !exists { + keys = append(keys, kv[0]) + } + valuesByKey[kv[0]] = append(valuesByKey[kv[0]], kv[1]) + } + list := make([]map[string]interface{}, 0, len(keys)) + for _, key := range keys { + list = append(list, map[string]interface{}{"TagKey": key, "TagValues": valuesByKey[key]}) + } + return list, nil +} + +// flattenTagMap renders a tag map as a stable "k=v, k2=v2" string. +func flattenTagMap(m map[string]string) string { + if len(m) == 0 { + return "" + } + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + parts := make([]string, 0, len(keys)) + for _, k := range keys { + parts = append(parts, k+"="+m[k]) + } + return strings.Join(parts, ", ") +} diff --git a/products/cloudwatch/internal/cloudwatch/rows.go b/products/cloudwatch/internal/cloudwatch/rows.go new file mode 100644 index 0000000000..b261b3231a --- /dev/null +++ b/products/cloudwatch/internal/cloudwatch/rows.go @@ -0,0 +1,30 @@ +package cloudwatch + +// MonitorProductRow is one product definition returned by ListMonitorProduct. +type MonitorProductRow struct { + Product string + ProductName string + ProductChName string + IsSupportHighPrecision bool +} + +// MetricRow is one table row for `cloudwatch list-metrics`. +// One row per metric of the queried product. +type MetricRow struct { + Metric string + MetricName string // MetricChName — Chinese display name, closer to console habit + Unit string // Unit.UnitChName (empty when Unit is nil) + FrequencyMs int32 +} + +// DataPointRow is one table row for `cloudwatch query-metric-data`. +// The nested response (metric → resource → point[]) is flattened so each row +// is a single (resource, metric, timestamp) sample. +type DataPointRow struct { + ResourceID string + ResourceName string + Metric string + Timestamp string // common.FormatDateTime(point.Timestamp) + Value float64 + Tags string // TagMap flattened to "k=v, k2=v2"; empty when no tags +} diff --git a/products/cloudwatch/product.go b/products/cloudwatch/product.go new file mode 100644 index 0000000000..3f6cf1049f --- /dev/null +++ b/products/cloudwatch/product.go @@ -0,0 +1,22 @@ +package cloudwatch + +import ( + "github.com/spf13/cobra" + + "github.com/ucloud/ucloud-cli/pkg/cli" + internalcloudwatch "github.com/ucloud/ucloud-cli/products/cloudwatch/internal/cloudwatch" +) + +type Product struct{} + +func New() cli.Product { return &Product{} } + +func (*Product) Metadata() cli.Metadata { + return cli.Metadata{Name: "cloudwatch", Commands: []string{"cloudwatch"}} +} + +func (*Product) NewCommand(ctx *cli.Context) []*cobra.Command { + return []*cobra.Command{internalcloudwatch.NewCommand(ctx)} +} + +var _ cli.Product = (*Product)(nil) diff --git a/products/cloudwatch/product.yaml b/products/cloudwatch/product.yaml new file mode 100644 index 0000000000..0e3303704d --- /dev/null +++ b/products/cloudwatch/product.yaml @@ -0,0 +1,7 @@ +# products/cloudwatch/product.yaml — cloudwatch 产品元数据(归属真源,owner 自治维护) +name: cloudwatch +owners: + - YAYALE-WA +commands: + - cloudwatch +enabled: true diff --git a/products/cloudwatch/testdata/cmdtree.golden b/products/cloudwatch/testdata/cmdtree.golden new file mode 100644 index 0000000000..497daa25f7 --- /dev/null +++ b/products/cloudwatch/testdata/cmdtree.golden @@ -0,0 +1,16 @@ +ucloud cloudwatch use=cloudwatch short=Discover and query CloudWatch metrics +ucloud cloudwatch list-metrics use=list-metrics short=List metrics for a product + flag=monitor-type short= default= required= + flag=product short= default= required=true +ucloud cloudwatch list-products use=list-products short=List monitored products +ucloud cloudwatch query-metric-data use=query-metric-data short=Query metric data + flag=calc-method short= default=raw required= + flag=end-time short= default=0 required= + flag=metric short= default=[] required=true + flag=period short= default=0 required= + flag=product short= default= required=true + flag=project-id short= default= required= + flag=region short= default= required= + flag=resource-id short= default=[] required=true + flag=start-time short= default=0 required= + flag=tag short= default=[] required= diff --git a/products/cloudwatch/testdata/completion.golden b/products/cloudwatch/testdata/completion.golden new file mode 100644 index 0000000000..9e5f2a2ffa --- /dev/null +++ b/products/cloudwatch/testdata/completion.golden @@ -0,0 +1,6 @@ +ucloud cloudwatch list-metrics product dynamic +ucloud cloudwatch query-metric-data calc-method static avg,max,min,raw,sum +ucloud cloudwatch query-metric-data period static 21600,300,3600,60,86400 +ucloud cloudwatch query-metric-data product dynamic +ucloud cloudwatch query-metric-data project-id dynamic +ucloud cloudwatch query-metric-data region dynamic