提交 9e3365a7 编写于 作者: K kezhenxu94 提交者: wu-sheng

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
上级 5d2b49dd
......@@ -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)
......
......@@ -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 {
......
......@@ -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 {
......
......@@ -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
......
......@@ -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) {
......
// 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
}
......@@ -47,6 +47,7 @@ var Command = cli.Command{
},
),
Before: interceptor.BeforeChain([]cli.BeforeFunc{
interceptor.TimezoneInterceptor,
interceptor.DurationInterceptor,
}),
Action: func(ctx *cli.Context) error {
......
......@@ -49,6 +49,7 @@ var Command = cli.Command{
},
),
Before: interceptor.BeforeChain([]cli.BeforeFunc{
interceptor.TimezoneInterceptor,
interceptor.DurationInterceptor,
}),
Action: func(ctx *cli.Context) error {
......
......@@ -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 {
......
......@@ -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
}
......
// 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 metadata
import (
"github.com/machinebox/graphql"
"github.com/urfave/cli"
"github.com/apache/skywalking-cli/graphql/client"
"github.com/apache/skywalking-cli/graphql/schema"
)
func ServerTimeInfo(cliCtx *cli.Context) (schema.TimeInfo, error) {
request := graphql.NewRequest(`
query {
timeInfo: getTimeInfo {
timezone, currentTimestamp
}
}
`)
var response map[string]schema.TimeInfo
if err := client.ExecuteQuery(cliCtx, request, &response); err != nil {
return schema.TimeInfo{}, err
}
return response["timeInfo"], nil
}
......@@ -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,
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册