diff --git a/CHANGES.md b/CHANGES.md index 6de747b..a401f8a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,6 +17,7 @@ Release Notes. * Remove the oldest `queryTraceFromColdStage` query call in the `trace list` command by @mrproliu in https://github.com/apache/skywalking-cli/pull/225 * Add the sub-command `profiling pprof` for pprof query API by @JophieQu in https://github.com/apache/skywalking-cli/pull/226 * Add the `admin` command group for the OAP admin-server REST host (default port `17128`), with a new global `--admin-url` flag (derived from `--base-url` when unset). Covers every admin feature module: `admin preflight`; `admin cluster nodes`, `admin config dump|ttl`, `admin alarm rules|rule` (status); `admin inspect metrics|entities` (inspect); `admin ui-template list|get|create|update|disable` (ui-management); `admin runtime-rule list|bundled|get|add|inactivate|delete|dump` (runtime-rule); and `admin dsl-debug status|sessions|session start|get|stop` plus `admin oal files|file|rules|rule` (dsl-debugging). +* Add the sub-command `admin inspect values` to read the VALUES of metric(s) an OAP does not define locally (foreign metrics) by supplying their `{valueColumn, valueType}` metadata, via the new `POST /inspect/values` admin API; returns the native MQE result. ### Bug Fixes diff --git a/internal/commands/admin/inspect/inspect.go b/internal/commands/admin/inspect/inspect.go index 385807a..df9f1c3 100644 --- a/internal/commands/admin/inspect/inspect.go +++ b/internal/commands/admin/inspect/inspect.go @@ -19,6 +19,7 @@ package inspect import ( "fmt" + "strings" "github.com/urfave/cli/v2" @@ -37,6 +38,7 @@ var Command = &cli.Command{ Subcommands: []*cli.Command{ metricsCommand, entitiesCommand, + valuesCommand, }, } @@ -144,3 +146,96 @@ $ swctl admin inspect entities --metric meter_foo --value-column value --value-t return display.Display(ctx.Context, &displayable.Displayable{Data: entities}) }, } + +var valuesCommand = &cli.Command{ + Name: "values", + Usage: "Read the VALUES of metric(s) persisted by another OAP (POST /inspect/values)", + UsageText: `Evaluate an MQE expression over metric(s) the target OAP does not define locally, by +supplying each foreign metric's metadata. Returns the native MQE result (the same shape the UI +renders for catalog metrics). The entity scope is inferred from which name flags are set. + +Example — read a foreign service metric's value series: +$ swctl admin inspect values --expression meter_foo --service-name my-svc \ + --foreign-metric meter_foo,value,LONG`, + Flags: flags.Flags( + flags.DurationFlags, + []cli.Flag{ + &cli.StringFlag{ + Name: "expression", + Usage: "the MQE `expression` to evaluate", + Required: true, + }, + &cli.StringFlag{ + Name: "service-name", + Usage: "the `service` entity to query", + Required: true, + }, + &cli.StringFlag{ + Name: "service-instance-name", + Usage: "the `instance` to query (selects ServiceInstance scope)", + }, + &cli.StringFlag{ + Name: "endpoint-name", + Usage: "the `endpoint` to query (selects Endpoint scope)", + }, + &cli.BoolFlag{ + Name: "normal", + Usage: "whether the service is normal (agent-reported)", + Value: true, + }, + &cli.StringSliceFlag{ + Name: "foreign-metric", + Usage: "metadata for one foreign metric as `name,valueColumn,valueType` " + + "(repeatable — supply one per foreign metric in the expression)", + Required: true, + }, + }, + ), + Before: interceptor.BeforeChain( + interceptor.DurationInterceptor, + ), + Action: func(ctx *cli.Context) error { + step := ctx.Generic("step").(*model.StepEnumValue).Selected + + entity := inspect.QueryEntity{ + ServiceName: ctx.String("service-name"), + Normal: ctx.Bool("normal"), + } + switch { + case ctx.String("endpoint-name") != "": + entity.Scope = "Endpoint" + entity.EndpointName = ctx.String("endpoint-name") + case ctx.String("service-instance-name") != "": + entity.Scope = "ServiceInstance" + entity.ServiceInstanceName = ctx.String("service-instance-name") + default: + entity.Scope = "Service" + } + + var foreign []inspect.ForeignMetricInput + for _, fm := range ctx.StringSlice("foreign-metric") { + parts := strings.Split(fm, ",") + if len(parts) != 3 { + return fmt.Errorf("--foreign-metric must be name,valueColumn,valueType (got %q)", fm) + } + foreign = append(foreign, inspect.ForeignMetricInput{ + Name: parts[0], + ValueColumn: parts[1], + ValueType: parts[2], + }) + } + + result, err := inspect.ListValues(ctx.Context, &inspect.ValuesOptions{ + Expression: ctx.String("expression"), + Entity: entity, + Start: ctx.String("start"), + End: ctx.String("end"), + Step: string(step), + ForeignMetrics: foreign, + }) + if err != nil { + return preflight.Explain(ctx.Context, err, preflight.ModuleInspect, "SW_INSPECT") + } + return display.Display(ctx.Context, &displayable.Displayable{Data: result}) + }, +} diff --git a/pkg/admin/inspect/inspect.go b/pkg/admin/inspect/inspect.go index ef3ee20..8d80295 100644 --- a/pkg/admin/inspect/inspect.go +++ b/pkg/admin/inspect/inspect.go @@ -23,6 +23,7 @@ package inspect import ( "context" + "net/http" "net/url" "strconv" @@ -132,3 +133,85 @@ func ListEntities(ctx context.Context, opts *EntitiesOptions) (*Entities, error) err := client.GetJSON(ctx, "/inspect/entities", query, &out) return &out, err } + +// QueryEntity is the MQE query entity for a value query; its scope binds every foreign metric. +type QueryEntity struct { + Scope string `json:"scope,omitempty"` + ServiceName string `json:"serviceName,omitempty"` + Normal bool `json:"normal"` + ServiceInstanceName string `json:"serviceInstanceName,omitempty"` + EndpointName string `json:"endpointName,omitempty"` +} + +// ForeignMetricInput is caller-supplied metadata for one metric the target OAP does not define. +type ForeignMetricInput struct { + Name string `json:"name"` + ValueColumn string `json:"valueColumn"` + ValueType string `json:"valueType"` +} + +// ValuesOptions holds the parameters of POST /inspect/values. +type ValuesOptions struct { + Expression string + Entity QueryEntity + Start string + End string + Step string + ForeignMetrics []ForeignMetricInput +} + +// ExpressionResult is the native MQE result returned by POST /inspect/values, mirroring the +// GraphQL execExpression shape. +type ExpressionResult struct { + Type string `json:"type"` + Error string `json:"error,omitempty"` + Results []MQEValues `json:"results"` +} + +// MQEValues is one (optionally labeled) series of an ExpressionResult. +type MQEValues struct { + Metric MQEMetric `json:"metric"` + Values []MQEValue `json:"values"` +} + +// MQEMetric carries the label set of an MQEValues series. +type MQEMetric struct { + Labels []KeyValue `json:"labels"` +} + +// MQEValue is one time-bucket point. +type MQEValue struct { + ID string `json:"id"` + Value string `json:"value"` +} + +// KeyValue is a metric label. +type KeyValue struct { + Key string `json:"key"` + Value string `json:"value"` +} + +type valuesRequest struct { + Expression string `json:"expression"` + Entity QueryEntity `json:"entity"` + Start string `json:"start"` + End string `json:"end"` + Step string `json:"step"` + ForeignMetrics []ForeignMetricInput `json:"foreignMetrics"` +} + +// ListValues evaluates an MQE expression over foreign metric(s) — metrics the target OAP does not +// define locally — by supplying their metadata (POST /inspect/values). Returns the native MQE result. +func ListValues(ctx context.Context, opts *ValuesOptions) (*ExpressionResult, error) { + body := valuesRequest{ + Expression: opts.Expression, + Entity: opts.Entity, + Start: opts.Start, + End: opts.End, + Step: opts.Step, + ForeignMetrics: opts.ForeignMetrics, + } + var out ExpressionResult + err := client.SendJSON(ctx, http.MethodPost, "/inspect/values", nil, body, &out) + return &out, err +}