This commit is contained in:
Brad Beam 2025-03-05 22:37:21 +08:00 committed by GitHub
commit 9dd0c5f683
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 130 additions and 71 deletions

View file

@ -34,6 +34,7 @@ import (
"github.com/alecthomas/kingpin/v2"
"github.com/google/pprof/profile"
"github.com/prometheus/client_golang/api"
v1 "github.com/prometheus/client_golang/api/prometheus/v1"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/testutil/promlint"
"github.com/prometheus/common/expfmt"
@ -162,7 +163,7 @@ func main() {
agentMode := checkConfigCmd.Flag("agent", "Check config file for Prometheus in Agent mode.").Bool()
queryCmd := app.Command("query", "Run query against a Prometheus server.")
queryCmdFmt := queryCmd.Flag("format", "Output format of the query.").Short('o').Default("promql").Enum("promql", "json")
queryCmdFmt := queryCmd.Flag("format", "Output format of the query: promql, json, unittest. Unittest output is experimental.").Short('o').Default("promql").Enum("promql", "json", "unittest")
queryCmd.Flag("http.config.file", "HTTP client configuration file for promtool to connect to Prometheus.").PlaceHolder("<filename>").ExistingFileVar(&httpConfigFilePath)
queryInstantCmd := queryCmd.Command("instant", "Run instant query.")
@ -310,12 +311,31 @@ func main() {
parsedCmd := kingpin.MustParse(app.Parse(os.Args[1:]))
queryRange := v1.Range{}
var err error
switch parsedCmd {
case queryInstantCmd.FullCommand():
queryRange, err = newQueryRange(*queryInstantTime, "", time.Second)
case queryRangeCmd.FullCommand():
queryRange, err = newQueryRange(*queryRangeBegin, *queryRangeEnd, *queryRangeStep)
case querySeriesCmd.FullCommand():
queryRange, err = newQueryRange(*querySeriesBegin, *querySeriesEnd, 0)
case queryLabelsCmd.FullCommand():
queryRange, err = newQueryRange(*queryLabelsBegin, *queryLabelsEnd, 0)
}
if err != nil {
kingpin.Fatalf("Failed to parse query range: %v", err)
}
var p printer
switch *queryCmdFmt {
case "json":
p = &jsonPrinter{}
case "promql":
p = &promqlPrinter{}
case "unittest":
p = &unittestPrinter{Step: model.Duration(queryRange.Step)}
}
if httpConfigFilePath != "" {
@ -372,13 +392,13 @@ func main() {
os.Exit(PushMetrics(remoteWriteURL, httpRoundTripper, *pushMetricsHeaders, *pushMetricsTimeout, *pushMetricsLabels, *metricFiles...))
case queryInstantCmd.FullCommand():
os.Exit(QueryInstant(serverURL, httpRoundTripper, *queryInstantExpr, *queryInstantTime, p))
os.Exit(QueryInstant(serverURL, httpRoundTripper, *queryInstantExpr, queryRange, p))
case queryRangeCmd.FullCommand():
os.Exit(QueryRange(serverURL, httpRoundTripper, *queryRangeHeaders, *queryRangeExpr, *queryRangeBegin, *queryRangeEnd, *queryRangeStep, p))
os.Exit(QueryRange(serverURL, httpRoundTripper, *queryRangeHeaders, *queryRangeExpr, queryRange, p))
case querySeriesCmd.FullCommand():
os.Exit(QuerySeries(serverURL, httpRoundTripper, *querySeriesMatch, *querySeriesBegin, *querySeriesEnd, p))
os.Exit(QuerySeries(serverURL, httpRoundTripper, *querySeriesMatch, queryRange, p))
case debugPprofCmd.FullCommand():
os.Exit(debugPprof(*debugPprofServer))
@ -390,7 +410,7 @@ func main() {
os.Exit(debugAll(*debugAllServer))
case queryLabelsCmd.FullCommand():
os.Exit(QueryLabels(serverURL, httpRoundTripper, *queryLabelsMatch, *queryLabelsName, *queryLabelsBegin, *queryLabelsEnd, p))
os.Exit(QueryLabels(serverURL, httpRoundTripper, *queryLabelsMatch, *queryLabelsName, queryRange, p))
case testRulesCmd.FullCommand():
results := io.Discard
@ -1214,6 +1234,64 @@ func (j *jsonPrinter) printLabelValues(v model.LabelValues) {
json.NewEncoder(os.Stdout).Encode(v)
}
type unittestPrinter struct {
Step model.Duration
}
func (u *unittestPrinter) printValue(v model.Value) {
samples := make(map[string][]string)
switch modelType := v.(type) {
case model.Vector:
for _, samplePair := range modelType {
metricName := samplePair.Metric.String()
if _, ok := samples[metricName]; !ok {
samples[metricName] = []string{}
}
if samplePair.Histogram != nil {
panic("histograms are not supported")
}
samples[metricName] = append(samples[metricName], strconv.FormatFloat(float64(samplePair.Value), 'f', -1, 64))
}
case model.Matrix:
for _, stream := range modelType {
metricName := stream.Metric.String()
if _, ok := samples[metricName]; !ok {
samples[metricName] = []string{}
}
if len(stream.Histograms) > 0 {
panic("histograms are not supported")
}
for _, samplePair := range stream.Values {
samples[metricName] = append(samples[metricName], strconv.FormatFloat(float64(samplePair.Value), 'f', -1, 64))
}
}
}
inputSeries := make([]series, 0, len(samples))
for metric, value := range samples {
inputSeries = append(inputSeries, series{Series: metric, Values: strings.Join(value, " ")})
}
if err := yaml.NewEncoder(os.Stdout).Encode(unitTestFile{Tests: []testGroup{{Interval: u.Step, InputSeries: inputSeries}}}); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(failureExitCode)
}
}
func (u *unittestPrinter) printSeries(v []model.LabelSet) {}
func (u *unittestPrinter) printLabelValues(v model.LabelValues) {}
// importRules backfills recording rules from the files provided. The output are blocks of data
// at the outputDir location.
func importRules(url *url.URL, roundTripper http.RoundTripper, start, end, outputDir string, evalInterval, maxBlockDuration time.Duration, files ...string) error {

View file

@ -68,14 +68,17 @@ func TestQueryRange(t *testing.T) {
require.NoError(t, err)
p := &promqlPrinter{}
exitCode := QueryRange(urlObject, http.DefaultTransport, map[string]string{}, "up", "0", "300", 0, p)
qr, err := newQueryRange("0", "300", 0)
require.NoError(t, err)
exitCode := QueryRange(urlObject, http.DefaultTransport, map[string]string{}, "up", qr, p)
require.Equal(t, "/api/v1/query_range", getRequest().URL.Path)
form := getRequest().Form
require.Equal(t, "up", form.Get("query"))
require.Equal(t, "1", form.Get("step"))
require.Equal(t, 0, exitCode)
exitCode = QueryRange(urlObject, http.DefaultTransport, map[string]string{}, "up", "0", "300", 10*time.Millisecond, p)
qr, err = newQueryRange("0", "300", 10*time.Millisecond)
exitCode = QueryRange(urlObject, http.DefaultTransport, map[string]string{}, "up", qr, p)
require.Equal(t, "/api/v1/query_range", getRequest().URL.Path)
form = getRequest().Form
require.Equal(t, "up", form.Get("query"))
@ -92,7 +95,9 @@ func TestQueryInstant(t *testing.T) {
require.NoError(t, err)
p := &promqlPrinter{}
exitCode := QueryInstant(urlObject, http.DefaultTransport, "up", "300", p)
qr, err := newQueryRange("300", "", time.Second)
require.NoError(t, err)
exitCode := QueryInstant(urlObject, http.DefaultTransport, "up", qr, p)
require.Equal(t, "/api/v1/query", getRequest().URL.Path)
form := getRequest().Form
require.Equal(t, "up", form.Get("query"))

View file

@ -61,25 +61,20 @@ func newAPI(url *url.URL, roundTripper http.RoundTripper, headers map[string]str
}
// QueryInstant performs an instant query against a Prometheus server.
func QueryInstant(url *url.URL, roundTripper http.RoundTripper, query, evalTime string, p printer) int {
func QueryInstant(url *url.URL, roundTripper http.RoundTripper, query string, qr v1.Range, p printer) int {
api, err := newAPI(url, roundTripper, nil)
if err != nil {
fmt.Fprintln(os.Stderr, "error creating API client:", err)
return failureExitCode
}
eTime := time.Now()
if evalTime != "" {
eTime, err = parseTime(evalTime)
if err != nil {
fmt.Fprintln(os.Stderr, "error parsing evaluation time:", err)
return failureExitCode
}
if qr.Start.Equal(minTime) {
qr.Start = time.Now()
}
// Run query against client.
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
val, _, err := api.Query(ctx, query, eTime) // Ignoring warnings for now.
val, _, err := api.Query(ctx, query, qr.Start) // Ignoring warnings for now.
cancel()
if err != nil {
return handleAPIError(err)
@ -91,50 +86,24 @@ func QueryInstant(url *url.URL, roundTripper http.RoundTripper, query, evalTime
}
// QueryRange performs a range query against a Prometheus server.
func QueryRange(url *url.URL, roundTripper http.RoundTripper, headers map[string]string, query, start, end string, step time.Duration, p printer) int {
func QueryRange(url *url.URL, roundTripper http.RoundTripper, headers map[string]string, query string, qr v1.Range, p printer) int {
api, err := newAPI(url, roundTripper, headers)
if err != nil {
fmt.Fprintln(os.Stderr, "error creating API client:", err)
return failureExitCode
}
var stime, etime time.Time
if end == "" {
etime = time.Now()
} else {
etime, err = parseTime(end)
if err != nil {
fmt.Fprintln(os.Stderr, "error parsing end time:", err)
return failureExitCode
}
if qr.End.Equal(maxTime) {
qr.End = time.Now()
}
if start == "" {
stime = etime.Add(-5 * time.Minute)
} else {
stime, err = parseTime(start)
if err != nil {
fmt.Fprintln(os.Stderr, "error parsing start time:", err)
return failureExitCode
}
}
if !stime.Before(etime) {
fmt.Fprintln(os.Stderr, "start time is not before end time")
return failureExitCode
}
if step == 0 {
resolution := math.Max(math.Floor(etime.Sub(stime).Seconds()/250), 1)
// Convert seconds to nanoseconds such that time.Duration parses correctly.
step = time.Duration(resolution) * time.Second
if qr.Start.Equal(minTime) {
qr.Start = qr.End.Add(-5 * time.Minute)
}
// Run query against client.
r := v1.Range{Start: stime, End: etime, Step: step}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
val, _, err := api.QueryRange(ctx, query, r) // Ignoring warnings for now.
val, _, err := api.QueryRange(ctx, query, qr) // Ignoring warnings for now.
cancel()
if err != nil {
@ -146,22 +115,16 @@ func QueryRange(url *url.URL, roundTripper http.RoundTripper, headers map[string
}
// QuerySeries queries for a series against a Prometheus server.
func QuerySeries(url *url.URL, roundTripper http.RoundTripper, matchers []string, start, end string, p printer) int {
func QuerySeries(url *url.URL, roundTripper http.RoundTripper, matchers []string, qr v1.Range, p printer) int {
api, err := newAPI(url, roundTripper, nil)
if err != nil {
fmt.Fprintln(os.Stderr, "error creating API client:", err)
return failureExitCode
}
stime, etime, err := parseStartTimeAndEndTime(start, end)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return failureExitCode
}
// Run query against client.
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
val, _, err := api.Series(ctx, matchers, stime, etime) // Ignoring warnings for now.
val, _, err := api.Series(ctx, matchers, qr.Start, qr.End) // Ignoring warnings for now.
cancel()
if err != nil {
@ -173,22 +136,16 @@ func QuerySeries(url *url.URL, roundTripper http.RoundTripper, matchers []string
}
// QueryLabels queries for label values against a Prometheus server.
func QueryLabels(url *url.URL, roundTripper http.RoundTripper, matchers []string, name, start, end string, p printer) int {
func QueryLabels(url *url.URL, roundTripper http.RoundTripper, matchers []string, name string, qr v1.Range, p printer) int {
api, err := newAPI(url, roundTripper, nil)
if err != nil {
fmt.Fprintln(os.Stderr, "error creating API client:", err)
return failureExitCode
}
stime, etime, err := parseStartTimeAndEndTime(start, end)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return failureExitCode
}
// Run query against client.
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
val, warn, err := api.LabelValues(ctx, name, matchers, stime, etime)
val, warn, err := api.LabelValues(ctx, name, matchers, qr.Start, qr.End)
cancel()
for _, v := range warn {
@ -213,12 +170,31 @@ func handleAPIError(err error) int {
return failureExitCode
}
func newQueryRange(start, end string, step time.Duration) (v1.Range, error) {
qr := v1.Range{}
var err error
qr.Start, qr.End, err = parseStartTimeAndEndTime(start, end)
if !qr.Start.Before(qr.End) {
return qr, errors.New("start time is not before end time")
}
if step == 0 {
resolution := math.Max(math.Floor(qr.End.Sub(qr.Start).Seconds()/250), 1)
// Convert seconds to nanoseconds such that time.Duration parses correctly.
step = time.Duration(resolution) * time.Second
}
qr.Step = step
return qr, err
}
var minTime = time.Now().Add(-9999 * time.Hour)
var maxTime = time.Now().Add(9999 * time.Hour)
func parseStartTimeAndEndTime(start, end string) (time.Time, time.Time, error) {
var (
minTime = time.Now().Add(-9999 * time.Hour)
maxTime = time.Now().Add(9999 * time.Hour)
err error
)
var err error
stime := minTime
etime := maxTime

View file

@ -215,7 +215,7 @@ Run query against a Prometheus server.
| Flag | Description | Default |
| --- | --- | --- |
| <code class="text-nowrap">-o</code>, <code class="text-nowrap">--format</code> | Output format of the query. | `promql` |
| <code class="text-nowrap">-o</code>, <code class="text-nowrap">--format</code> | Output format of the query: promql, json, unittest. Unittest output is experimental. | `promql` |
| <code class="text-nowrap">--http.config.file</code> | HTTP client configuration file for promtool to connect to Prometheus. | |