Add `admin inspect values` to read foreign metric values (#232)
The new sub-command POSTs to the OAP admin API `POST /inspect/values` to
evaluate an MQE expression over metric(s) the target OAP does not define
locally (foreign metrics), supplying each metric's {valueColumn, valueType}.
The entity scope is inferred from which name flag is set (--service-name /
--service-instance-name / --endpoint-name). Returns the native MQE result.diff --git a/CHANGES.md b/CHANGES.md
index 6de747b..a401f8a 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -17,6 +17,7 @@
* 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 @@
import (
"fmt"
+ "strings"
"github.com/urfave/cli/v2"
@@ -37,6 +38,7 @@
Subcommands: []*cli.Command{
metricsCommand,
entitiesCommand,
+ valuesCommand,
},
}
@@ -144,3 +146,96 @@
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 @@
import (
"context"
+ "net/http"
"net/url"
"strconv"
@@ -132,3 +133,85 @@
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
+}