You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by wu...@apache.org on 2019/12/31 13:05:45 UTC

[skywalking-cli] branch master updated: Make use of server timezone API when possible (#26)

This is an automated email from the ASF dual-hosted git repository.

wusheng pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/skywalking-cli.git


The following commit(s) were added to refs/heads/master by this push:
     new 9e3365a  Make use of server timezone API when possible (#26)
9e3365a is described below

commit 9e3365a7c14e7661a4068ea6f423864859c2eebe
Author: kezhenxu94 <ke...@apache.org>
AuthorDate: Tue Dec 31 21:05:37 2019 +0800

    Make use of server timezone API when possible (#26)
    
    * Make use of server timezone API when possible
    
    * Update duration.go
    
    * Add documentation for timezone option
---
 README.md                                          | 22 +++++++++-
 commands/instance/list.go                          |  1 +
 commands/instance/search.go                        |  1 +
 commands/interceptor/duration.go                   | 25 +++++++----
 commands/interceptor/duration_test.go              |  2 +-
 commands/interceptor/timezone.go                   | 51 ++++++++++++++++++++++
 commands/metrics/linear/linear-metrics.go          |  1 +
 commands/metrics/single/single-metrics.go          |  1 +
 commands/service/list.go                           |  1 +
 graphql/client/client.go                           | 23 ++++++----
 .../list.go => graphql/metadata/metadata.go        | 42 +++++++-----------
 swctl/main.go                                      |  6 +++
 12 files changed, 131 insertions(+), 45 deletions(-)

diff --git a/README.md b/README.md
index 32162e0..00f225c 100644
--- a/README.md
+++ b/README.md
@@ -48,7 +48,7 @@ There're some common options that are shared by multiple commands, and they foll
 
 <details>
 
-<summary>--start, --end</summary>
+<summary>--start, --end, --timezone</summary>
 
 `--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:
@@ -64,6 +64,12 @@ and if `end = 2019-11-09 12`, the precision is `HOUR`, so `start = end - 30HOUR
 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`;
 
+`--timezone` specifies the timezone where `--start` `--end` are based, in the form of `+0800`:
+
+- if `--timezone` is given in the command line option, then it's used directly;
+- else if the backend support the timezone API (since 6.5.0), CLI will try to get the timezone from backend, and use it;
+- otherwise, the CLI will use the current timezone in the current machine; 
+
 </details>
 
 ## All available commands
@@ -302,6 +308,20 @@ $ ./bin/swctl service ls projectC | jq '.[0].id' | xargs ./bin/swctl endpoint ls
 
 </details>
 
+<details>
+
+<summary>Automatically convert to server side timezone</summary>
+
+if your backend nodes are deployed in docker and the timezone is UTC, you may not want to convert your timezone to UTC every time you type a command, `--timezone` comes to your rescue.
+
+```shell
+$ ./bin/swctl --debug --timezone="0" service ls
+```
+
+`--timezone="+1200"` and `--timezone="-0900"` are also valid usage.
+
+</details>
+
 # Contributing
 For developers who want to contribute to this project, see [Contribution Guide](CONTRIBUTING.md)
 
diff --git a/commands/instance/list.go b/commands/instance/list.go
index 9f84be7..c20ccb4 100644
--- a/commands/instance/list.go
+++ b/commands/instance/list.go
@@ -34,6 +34,7 @@ var ListCommand = cli.Command{
 	Usage:     "List all available instance by given --service-id or --service-name parameter",
 	Flags:     append(flags.DurationFlags, flags.InstanceServiceIDFlags...),
 	Before: interceptor.BeforeChain([]cli.BeforeFunc{
+		interceptor.TimezoneInterceptor,
 		interceptor.DurationInterceptor,
 	}),
 	Action: func(ctx *cli.Context) error {
diff --git a/commands/instance/search.go b/commands/instance/search.go
index 856273f..b9e2ac9 100644
--- a/commands/instance/search.go
+++ b/commands/instance/search.go
@@ -35,6 +35,7 @@ var SearchCommand = cli.Command{
 	Usage: "Filter the instance from the existing service instance list",
 	Flags: append(flags.DurationFlags, append(flags.SearchRegexFlags, flags.InstanceServiceIDFlags...)...),
 	Before: interceptor.BeforeChain([]cli.BeforeFunc{
+		interceptor.TimezoneInterceptor,
 		interceptor.DurationInterceptor,
 	}),
 	Action: func(ctx *cli.Context) error {
diff --git a/commands/interceptor/duration.go b/commands/interceptor/duration.go
index 186c3fe..3eb355a 100644
--- a/commands/interceptor/duration.go
+++ b/commands/interceptor/duration.go
@@ -18,6 +18,7 @@
 package interceptor
 
 import (
+	"strconv"
 	"time"
 
 	"github.com/urfave/cli"
@@ -43,8 +44,9 @@ func tryParseTime(unparsed string) (schema.Step, time.Time, error) {
 func DurationInterceptor(ctx *cli.Context) error {
 	start := ctx.String("start")
 	end := ctx.String("end")
+	timezone := ctx.GlobalString("timezone")
 
-	startTime, endTime, step := ParseDuration(start, end)
+	startTime, endTime, step := ParseDuration(start, end, timezone)
 
 	if err := ctx.Set("start", startTime.Format(schema.StepFormats[step])); err != nil {
 		return err
@@ -56,21 +58,28 @@ func DurationInterceptor(ctx *cli.Context) error {
 	return nil
 }
 
-// ParseDuration parses the `start` and `end` to a triplet, (startTime, endTime, step)
+// ParseDuration parses the `start` and `end` to a triplet, (startTime, endTime, step),
+// based on the given `timezone`, however, if the given `timezone` is empty, UTC becomes the default timezone.
 // if --start and --end are both absent,
 //   then: start := now - 30min; end := now
 // if --start is given, --end is absent,
 //   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 unis, where unit is the precision of `end`, (hours, minutes, etc.)
-// NOTE that when either(both) `start` or `end` is(are) given, there is no timezone info
-// in the format, (e.g. 2019-11-09 1001), so they'll be considered as UTC-based,
-// and generate the missing `start`(`end`) based on the same timezone, UTC
-func ParseDuration(start, end string) (startTime, endTime time.Time, step schema.Step) {
-	logger.Log.Debugln("Start time:", start, "end time:", end)
+//   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 schema.Step) {
+	logger.Log.Debugln("Start time:", start, "end time:", end, "timezone:", timezone)
 
 	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, schema.StepMinute
diff --git a/commands/interceptor/duration_test.go b/commands/interceptor/duration_test.go
index e8720ae..b4f8c90 100644
--- a/commands/interceptor/duration_test.go
+++ b/commands/interceptor/duration_test.go
@@ -82,7 +82,7 @@ func TestParseDuration(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			gotStartTime, gotEndTime, gotStep := ParseDuration(tt.args.start, tt.args.end)
+			gotStartTime, gotEndTime, gotStep := ParseDuration(tt.args.start, tt.args.end, "")
 			current := gotStartTime.Truncate(time.Minute).Format(timeFormat)
 			spec := tt.wantedStartTime.Truncate(time.Minute).Format(timeFormat)
 			if !reflect.DeepEqual(current, spec) {
diff --git a/commands/interceptor/timezone.go b/commands/interceptor/timezone.go
new file mode 100644
index 0000000..c724276
--- /dev/null
+++ b/commands/interceptor/timezone.go
@@ -0,0 +1,51 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (ASF) licenses this file to you under
+// the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package interceptor
+
+import (
+	"strconv"
+
+	"github.com/urfave/cli"
+
+	"github.com/apache/skywalking-cli/graphql/metadata"
+	"github.com/apache/skywalking-cli/logger"
+)
+
+// TimezoneInterceptor sets the server timezone if the server supports the API,
+// otherwise, sets to local timezone
+func TimezoneInterceptor(ctx *cli.Context) error {
+	// If there is timezone given by the user in command line, use it directly
+	if ctx.GlobalString("timezone") != "" {
+		return nil
+	}
+
+	serverTimeInfo, err := metadata.ServerTimeInfo(ctx)
+
+	if err != nil {
+		logger.Log.Debugf("Failed to get server time info: %v\n", err)
+		return nil
+	}
+
+	if timezone := serverTimeInfo.Timezone; timezone != nil {
+		if _, err := strconv.Atoi(*timezone); err == nil {
+			return ctx.GlobalSet("timezone", *timezone)
+		}
+	}
+
+	return nil
+}
diff --git a/commands/metrics/linear/linear-metrics.go b/commands/metrics/linear/linear-metrics.go
index f34289a..ceda447 100644
--- a/commands/metrics/linear/linear-metrics.go
+++ b/commands/metrics/linear/linear-metrics.go
@@ -47,6 +47,7 @@ var Command = cli.Command{
 		},
 	),
 	Before: interceptor.BeforeChain([]cli.BeforeFunc{
+		interceptor.TimezoneInterceptor,
 		interceptor.DurationInterceptor,
 	}),
 	Action: func(ctx *cli.Context) error {
diff --git a/commands/metrics/single/single-metrics.go b/commands/metrics/single/single-metrics.go
index ded58ac..99817da 100644
--- a/commands/metrics/single/single-metrics.go
+++ b/commands/metrics/single/single-metrics.go
@@ -49,6 +49,7 @@ var Command = cli.Command{
 		},
 	),
 	Before: interceptor.BeforeChain([]cli.BeforeFunc{
+		interceptor.TimezoneInterceptor,
 		interceptor.DurationInterceptor,
 	}),
 	Action: func(ctx *cli.Context) error {
diff --git a/commands/service/list.go b/commands/service/list.go
index feacb89..8400a47 100644
--- a/commands/service/list.go
+++ b/commands/service/list.go
@@ -36,6 +36,7 @@ var ListCommand = cli.Command{
 	Description: "list all services if no <service name> is given, otherwise, only list the given service",
 	Flags:       flags.DurationFlags,
 	Before: interceptor.BeforeChain([]cli.BeforeFunc{
+		interceptor.TimezoneInterceptor,
 		interceptor.DurationInterceptor,
 	}),
 	Action: func(ctx *cli.Context) error {
diff --git a/graphql/client/client.go b/graphql/client/client.go
index d33df92..a0766ef 100644
--- a/graphql/client/client.go
+++ b/graphql/client/client.go
@@ -37,7 +37,14 @@ func newClient(cliCtx *cli.Context) (client *graphql.Client) {
 	return
 }
 
-func executeQuery(cliCtx *cli.Context, request *graphql.Request, response interface{}) {
+func ExecuteQuery(cliCtx *cli.Context, request *graphql.Request, response interface{}) error {
+	client := newClient(cliCtx)
+	ctx := context.Background()
+	err := client.Run(ctx, request, response)
+	return err
+}
+
+func ExecuteQueryOrFail(cliCtx *cli.Context, request *graphql.Request, response interface{}) {
 	client := newClient(cliCtx)
 	ctx := context.Background()
 	if err := client.Run(ctx, request, response); err != nil {
@@ -56,7 +63,7 @@ func Services(cliCtx *cli.Context, duration schema.Duration) []schema.Service {
 	`)
 	request.Var("duration", duration)
 
-	executeQuery(cliCtx, request, &response)
+	ExecuteQueryOrFail(cliCtx, request, &response)
 	return response["services"]
 }
 
@@ -73,7 +80,7 @@ func SearchEndpoints(cliCtx *cli.Context, serviceID, keyword string, limit int)
 	request.Var("keyword", keyword)
 	request.Var("limit", limit)
 
-	executeQuery(cliCtx, request, &response)
+	ExecuteQueryOrFail(cliCtx, request, &response)
 	return response["endpoints"]
 }
 
@@ -88,7 +95,7 @@ func GetEndpointInfo(cliCtx *cli.Context, endpointID string) schema.Endpoint {
 	`)
 	request.Var("endpointId", endpointID)
 
-	executeQuery(cliCtx, request, &response)
+	ExecuteQueryOrFail(cliCtx, request, &response)
 	return response["endpoint"]
 }
 
@@ -111,7 +118,7 @@ func Instances(cliCtx *cli.Context, serviceID string, duration schema.Duration)
 	request.Var("serviceId", serviceID)
 	request.Var("duration", duration)
 
-	executeQuery(cliCtx, request, &response)
+	ExecuteQueryOrFail(cliCtx, request, &response)
 	return response["instances"]
 }
 
@@ -126,7 +133,7 @@ func SearchService(cliCtx *cli.Context, serviceCode string) (service schema.Serv
 	`)
 	request.Var("serviceCode", serviceCode)
 
-	executeQuery(cliCtx, request, &response)
+	ExecuteQueryOrFail(cliCtx, request, &response)
 	service = response["service"]
 	if service.ID == "" {
 		return service, fmt.Errorf("no such service [%s]", serviceCode)
@@ -147,7 +154,7 @@ func LinearIntValues(ctx *cli.Context, condition schema.MetricCondition, duratio
 	request.Var("metric", condition)
 	request.Var("duration", duration)
 
-	executeQuery(ctx, request, &response)
+	ExecuteQueryOrFail(ctx, request, &response)
 
 	values := metricsToMap(duration, response["metrics"].Values)
 
@@ -167,7 +174,7 @@ func IntValues(ctx *cli.Context, condition schema.BatchMetricConditions, duratio
 	request.Var("metric", condition)
 	request.Var("duration", duration)
 
-	executeQuery(ctx, request, &response)
+	ExecuteQueryOrFail(ctx, request, &response)
 
 	return response["metrics"].Values
 }
diff --git a/commands/instance/list.go b/graphql/metadata/metadata.go
similarity index 51%
copy from commands/instance/list.go
copy to graphql/metadata/metadata.go
index 9f84be7..55a1bc7 100644
--- a/commands/instance/list.go
+++ b/graphql/metadata/metadata.go
@@ -15,40 +15,28 @@
 // specific language governing permissions and limitations
 // under the License.
 
-package instance
+package metadata
 
 import (
+	"github.com/machinebox/graphql"
 	"github.com/urfave/cli"
 
-	"github.com/apache/skywalking-cli/commands/flags"
-	"github.com/apache/skywalking-cli/commands/interceptor"
-	"github.com/apache/skywalking-cli/commands/model"
-	"github.com/apache/skywalking-cli/display"
 	"github.com/apache/skywalking-cli/graphql/client"
 	"github.com/apache/skywalking-cli/graphql/schema"
 )
 
-var ListCommand = cli.Command{
-	Name:      "list",
-	ShortName: "ls",
-	Usage:     "List all available instance by given --service-id or --service-name parameter",
-	Flags:     append(flags.DurationFlags, flags.InstanceServiceIDFlags...),
-	Before: interceptor.BeforeChain([]cli.BeforeFunc{
-		interceptor.DurationInterceptor,
-	}),
-	Action: func(ctx *cli.Context) error {
-		serviceID := verifyAndSwitchServiceParameter(ctx)
+func ServerTimeInfo(cliCtx *cli.Context) (schema.TimeInfo, error) {
+	request := graphql.NewRequest(`
+		query {
+			timeInfo: getTimeInfo {
+				timezone, currentTimestamp
+			}
+		}
+	`)
 
-		end := ctx.String("end")
-		start := ctx.String("start")
-		step := ctx.Generic("step")
-
-		instances := client.Instances(ctx, serviceID, schema.Duration{
-			Start: start,
-			End:   end,
-			Step:  step.(*model.StepEnumValue).Selected,
-		})
-
-		return display.Display(ctx, instances)
-	},
+	var response map[string]schema.TimeInfo
+	if err := client.ExecuteQuery(cliCtx, request, &response); err != nil {
+		return schema.TimeInfo{}, err
+	}
+	return response["timeInfo"], nil
 }
diff --git a/swctl/main.go b/swctl/main.go
index 113c2bb..0cf3177 100644
--- a/swctl/main.go
+++ b/swctl/main.go
@@ -60,6 +60,12 @@ func main() {
 			Usage:    "base `url` of the OAP backend graphql",
 			Value:    "http://127.0.0.1:12800/graphql",
 		}),
+		altsrc.NewStringFlag(cli.StringFlag{
+			Name:     "timezone",
+			Required: false,
+			Hidden:   true,
+			Usage:    "the timezone of the server side",
+		}),
 		altsrc.NewBoolFlag(cli.BoolFlag{
 			Name:     "debug",
 			Required: false,