From 45a5f9807126dc6ea9fe06e17e7aafbe212436d2 Mon Sep 17 00:00:00 2001 From: kezhenxu94 Date: Tue, 26 Oct 2021 19:42:30 +0800 Subject: [PATCH] Allow setting `start` `end` with relative time (#128) --- internal/commands/interceptor/duration.go | 48 +++++++++++++---------- internal/flags/duration.go | 20 +++++++--- pkg/display/graph/dashboard/global.go | 14 +++++-- 3 files changed, 52 insertions(+), 30 deletions(-) diff --git a/internal/commands/interceptor/duration.go b/internal/commands/interceptor/duration.go index 9f8e627f..ea9f2914 100644 --- a/internal/commands/interceptor/duration.go +++ b/internal/commands/interceptor/duration.go @@ -18,6 +18,7 @@ package interceptor import ( + "fmt" "strconv" "time" @@ -28,9 +29,10 @@ import ( "github.com/urfave/cli/v2" "github.com/apache/skywalking-cli/internal/logger" + "github.com/apache/skywalking-cli/internal/model" ) -func TryParseTime(unparsed string) (api.Step, time.Time, error) { +func TryParseTime(unparsed string, userStep api.Step) (api.Step, time.Time, error) { var possibleError error for step, layout := range utils.StepFormats { t, err := time.Parse(layout, unparsed) @@ -39,7 +41,11 @@ func TryParseTime(unparsed string) (api.Step, time.Time, error) { } possibleError = err } - return api.StepSecond, time.Time{}, possibleError + duration, err := time.ParseDuration(unparsed) + if err == nil { + return userStep, time.Now().Add(duration), nil + } + return userStep, time.Time{}, fmt.Errorf("the given time %v is neither absolute time nor relative time: %+v %+v", unparsed, possibleError, err) } // DurationInterceptor sets the duration if absent, and formats it accordingly, @@ -47,9 +53,20 @@ func TryParseTime(unparsed string) (api.Step, time.Time, error) { func DurationInterceptor(ctx *cli.Context) error { start := ctx.String("start") end := ctx.String("end") - timezone := ctx.String("timezone") + userStep := ctx.Generic("step") + if timezone := ctx.String("timezone"); timezone != "" { + if offset, err := strconv.Atoi(timezone); err == nil { + // `offset` is in form of "+1300", while `time.FixedZone` takes offset in seconds + time.Local = time.FixedZone("", offset/100*60*60) + } + } + + var s api.Step + if userStep != nil { + s = userStep.(*model.StepEnumValue).Selected + } - startTime, endTime, step, dt := ParseDuration(start, end, timezone) + startTime, endTime, step, dt := ParseDuration(start, end, s) if err := ctx.Set("start", startTime.Format(utils.StepFormats[step])); err != nil { return err @@ -71,20 +88,11 @@ func DurationInterceptor(ctx *cli.Context) error { // then: end := now + 30 units, where unit is the precision of `start`, (hours, minutes, etc.) // if --start is absent, --end is given, // then: start := end - 30 units, where unit is the precision of `end`, (hours, minutes, etc.) -func ParseDuration(start, end, timezone string) (startTime, endTime time.Time, step api.Step, dt utils.DurationType) { - logger.Log.Debugln("Start time:", start, "end time:", end, "timezone:", timezone) +func ParseDuration(start, end string, userStep api.Step) (startTime, endTime time.Time, step api.Step, dt utils.DurationType) { + logger.Log.Debugln("Start time:", start, "end time:", end, "timezone:", time.Local) now := time.Now() - if timezone != "" { - if offset, err := strconv.Atoi(timezone); err == nil { - // `offset` is in form of "+1300", while `time.FixedZone` takes offset in seconds - now = now.In(time.FixedZone("", offset/100*60*60)) - - logger.Log.Debugln("Now:", now, "with server timezone:", timezone) - } - } - // both are absent if start == "" && end == "" { return now.Add(-30 * time.Minute), now, api.StepMinute, utils.BothAbsent @@ -94,23 +102,21 @@ func ParseDuration(start, end, timezone string) (startTime, endTime time.Time, s // both are present if len(start) > 0 && len(end) > 0 { - start, end = AlignPrecision(start, end) - - if _, startTime, err = TryParseTime(start); err != nil { + if userStep, startTime, err = TryParseTime(start, userStep); err != nil { logger.Log.Fatalln("Unsupported time format:", start, err) } - if step, endTime, err = TryParseTime(end); err != nil { + if step, endTime, err = TryParseTime(end, userStep); err != nil { logger.Log.Fatalln("Unsupported time format:", end, err) } return startTime, endTime, step, utils.BothPresent } else if end == "" { // end is absent - if step, startTime, err = TryParseTime(start); err != nil { + if step, startTime, err = TryParseTime(start, userStep); err != nil { logger.Log.Fatalln("Unsupported time format:", start, err) } return startTime, startTime.Add(30 * utils.StepDuration[step]), step, utils.EndAbsent } else { // start is absent - if step, endTime, err = TryParseTime(end); err != nil { + if step, endTime, err = TryParseTime(end, userStep); err != nil { logger.Log.Fatalln("Unsupported time format:", end, err) } return endTime.Add(-30 * utils.StepDuration[step]), endTime, step, utils.StartAbsent diff --git a/internal/flags/duration.go b/internal/flags/duration.go index f4a3f674..1e3793a9 100644 --- a/internal/flags/duration.go +++ b/internal/flags/duration.go @@ -25,8 +25,9 @@ import ( "github.com/apache/skywalking-cli/internal/model" ) -var startEndUsage = `"start" and "end" specify a time range during which the query is preformed, - they are both optional and their default values follow the rules below: +var startEndUsage = `"start" and "end" specify a time range during which the query is preformed, + they can be absolute time like "2019-01-01 12", "2019-01-01 1213", or relative time (to the + current time) like "-30m", "30m". They are both optional and their default values follow the rules below: 1. when "start" and "end" are both absent, "start = now - 30 minutes" and "end = now", namely past 30 minutes; 2. when "start" and "end" are both present, they are aligned to the same precision by @@ -39,7 +40,16 @@ var startEndUsage = `"start" and "end" specify a time range during which the que 4. when "start" is present and "end" is absent, will determine the precision of "start" and then use the precision to calculate "end" (plus 30 units), e.g. "start = 2019-11-09 1204", the precision is "MINUTE", so "end = start + 30 minutes = 2019-11-09 1234", - and if "start = 2019-11-08 06", the precision is "HOUR", so "end = start + 30HOUR = 2019-11-09 12".` + and if "start = 2019-11-08 06", the precision is "HOUR", so "end = start + 30HOUR = 2019-11-09 12". + Examples: + 1. Query the metrics from 20 minutes ago to 10 minutes ago + $ swctl metrics linear --name=service_resp_time --service-name business-zone::projectB --start "-20m" --end "-10m" + 2. Query the metrics from 1 hour ago to 10 minutes ago + $ swctl metrics linear --name=service_resp_time --service-name business-zone::projectB --start "-1h" --end "-10m" + 3. Query the metrics from 1 hour ago to now + $ swctl metrics linear --name=service_resp_time --service-name business-zone::projectB --start "-1h" --end "0m" + 4. Query the metrics from "2021-10-26 1047" to "2021-10-26 1127" + $ swctl metrics linear --name=service_resp_time --service-name business-zone::projectB --start "2021-10-26 1047" --end "2021-10-26 1127"` // DurationFlags are healthcheck flags that involves a duration, composed // by a start time, an end time, and a step, which is commonly used @@ -54,8 +64,8 @@ var DurationFlags = []cli.Flag{ Usage: `end time of the query duration. Check the usage of "start"`, }, &cli.GenericFlag{ - Name: "step", - Hidden: true, + Name: "step", + Usage: `time step between start time and end time, should be one of SECOND, MINUTE, HOUR, DAY`, Value: &model.StepEnumValue{ Enum: api.AllStep, Default: api.StepMinute, diff --git a/pkg/display/graph/dashboard/global.go b/pkg/display/graph/dashboard/global.go index 52a01c3d..b7f86823 100644 --- a/pkg/display/graph/dashboard/global.go +++ b/pkg/display/graph/dashboard/global.go @@ -38,6 +38,7 @@ import ( "github.com/mum4k/termdash/terminal/terminalapi" "github.com/urfave/cli/v2" + "github.com/apache/skywalking-cli/internal/model" "github.com/apache/skywalking-cli/pkg/display/graph/gauge" "github.com/apache/skywalking-cli/pkg/display/graph/heatmap" "github.com/apache/skywalking-cli/pkg/display/graph/linear" @@ -88,6 +89,7 @@ var template *dashboard.GlobalTemplate var allWidgets *widgets var initStartStr string +var initStep = api.StepMinute var initEndStr string var curStartTime time.Time @@ -131,7 +133,7 @@ func newLayoutButtons(c *container.Container) ([]*button.Button, error) { return nil, err } - buttons[int(lt)] = b + buttons[lt] = b } return buttons, nil @@ -317,11 +319,15 @@ func refresh(con context.Context, ctx *cli.Context, interval time.Duration) { initStartStr = ctx.String("start") initEndStr = ctx.String("end") - _, start, err := interceptor.TryParseTime(initStartStr) + if s := ctx.Generic("step"); s != nil { + initStep = s.(*model.StepEnumValue).Selected + } + + _, start, err := interceptor.TryParseTime(initStartStr, initStep) if err != nil { return } - _, end, err := interceptor.TryParseTime(initEndStr) + _, end, err := interceptor.TryParseTime(initEndStr, initStep) if err != nil { return } @@ -355,7 +361,7 @@ func refresh(con context.Context, ctx *cli.Context, interval time.Duration) { // If the duration doesn't change, an error will be returned, and the dashboard will not refresh. // Otherwise, a new duration will be returned, which is used to get the latest global data. func updateDuration(interval time.Duration) (api.Duration, error) { - step, _, err := interceptor.TryParseTime(initStartStr) + step, _, err := interceptor.TryParseTime(initStartStr, initStep) if err != nil { return api.Duration{}, err }