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
+}