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
commit 8d4c95f758cf7cce163119cb2656b04ad5849824 Author: kezhenxu94 <kezhenx...@163.com> AuthorDate: Sat Nov 9 12:34:35 2019 +0800 Set up CI flow --- .github/workflows/go.yml | 12 +- commands/interceptor.go | 71 ------------ commands/interceptor/duration.go | 121 +++++++++++++++++++++ commands/interceptor/duration_test.go | 98 +++++++++++++++++ .../log.go => commands/interceptor/interceptor.go | 25 ++--- commands/{model.go => model/step.go} | 5 +- commands/service/list.go | 24 ++-- config/config.go | 2 +- go.mod | 1 + graphql/client/client.go | 2 +- logger/log.go | 4 +- swctl/main.go | 14 +-- commands/model.go => util/io.go | 39 +++---- 13 files changed, 287 insertions(+), 131 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 1abc12e..de9ad27 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -40,5 +40,15 @@ jobs: dep ensure fi + - name: Lint + run: | + curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $(go env GOPATH)/bin v1.21.0 + $(go env GOPATH)/bin/golangci-lint run -v ./... + + - name: Test + run: | + go test ./... -coverprofile=coverage.txt -covermode=atomic + bash <(curl -s https://codecov.io/bash) + - name: Build - run: make + run: make clean && make diff --git a/commands/interceptor.go b/commands/interceptor.go deleted file mode 100644 index 6837529..0000000 --- a/commands/interceptor.go +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Licensed to the 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. - * The 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 commands - -import ( - "github.com/apache/skywalking-cli/graphql/schema" - "github.com/apache/skywalking-cli/logger" - "github.com/urfave/cli" - "time" -) - -// Convenient function to chain up multiple cli.BeforeFunc -func BeforeChain(beforeFunctions []cli.BeforeFunc) cli.BeforeFunc { - return func(ctx *cli.Context) error { - for _, beforeFunc := range beforeFunctions { - if err := beforeFunc(ctx); err != nil { - return err - } - } - return nil - } -} - -var StepFormats = map[schema.Step]string{ - schema.StepMonth: "2006-01-02", - schema.StepDay: "2006-01-02", - schema.StepHour: "2006-01-02 15", - schema.StepMinute: "2006-01-02 1504", - schema.StepSecond: "2006-01-02 1504", -} - -// Set the duration if not set, and format it according to -// the given step -func SetUpDuration(ctx *cli.Context) error { - step := ctx.Generic("step").(*StepEnumValue).Selected - end := ctx.String("end") - if len(end) == 0 { - end = time.Now().Format(StepFormats[step]) - logger.Log.Debugln("Missing --end, defaults to", end) - if err := ctx.Set("end", end); err != nil { - return err - } - } - - start := ctx.String("start") - if len(start) == 0 { - start = time.Now().Add(-15 * time.Minute).Format(StepFormats[step]) - logger.Log.Debugln("Missing --start, defaults to", start) - if err := ctx.Set("start", start); err != nil { - return err - } - } - - return nil -} diff --git a/commands/interceptor/duration.go b/commands/interceptor/duration.go new file mode 100644 index 0000000..55c4b86 --- /dev/null +++ b/commands/interceptor/duration.go @@ -0,0 +1,121 @@ +/* + * Licensed to the 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. + * The 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 ( + "github.com/apache/skywalking-cli/graphql/schema" + "github.com/apache/skywalking-cli/logger" + "github.com/urfave/cli" + "time" +) + +var stepFormats = map[schema.Step]string{ + schema.StepSecond: "2006-01-02 1504", + schema.StepMinute: "2006-01-02 1504", + schema.StepHour: "2006-01-02 15", + schema.StepDay: "2006-01-02", + schema.StepMonth: "2006-01-02", +} + +var supportedTimeLayouts = []string{ + "2006-01-02 150400", + "2006-01-02 1504", + "2006-01-02 15", + "2006-01-02", + "2006-01", +} + +func tryParseTime(unparsed string, parsed *time.Time) error { + var possibleError error = nil + for _, layout := range supportedTimeLayouts { + t, err := time.Parse(layout, unparsed) + if err == nil { + *parsed = t + return nil + } + possibleError = err + } + return possibleError +} + +// DurationInterceptor sets the duration if absent, and formats it accordingly, +// see ParseDuration +func DurationInterceptor(ctx *cli.Context) error { + start := ctx.String("start") + end := ctx.String("end") + + startTime, endTime, step := ParseDuration(start, end) + + if err := ctx.Set("start", startTime.Format(stepFormats[step])); err != nil { + return err + } else if err := ctx.Set("end", endTime.Format(stepFormats[step])); err != nil { + return err + } else if err := ctx.Set("step", step.String()); err != nil { + return err + } + return nil +} + +// ParseDuration parses the `start` and `end` to a triplet, (startTime, endTime, step) +// if --start and --end are both absent, then: start := now - 30min; end := now +// if --start is given, --end is absent, then: end := now +// if --start is absent, --end is given, then: start := end - 30min +// 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 string, end string) (time.Time, time.Time, schema.Step) { + now := time.Now().UTC() + + startTime := now + endTime := now + logger.Log.Debugln("Start time:", start, "end time:", end) + if len(start) == 0 && len(end) == 0 { // both absent + startTime = now.Add(-30 * time.Minute) + endTime = now + } else if len(end) == 0 { // start is present + if err := tryParseTime(start, &startTime); err != nil { + logger.Log.Fatalln("Unsupported time format:", start, err) + } + } else if len(start) == 0 { // end is present + if err := tryParseTime(end, &endTime); err != nil { + logger.Log.Fatalln("Unsupported time format:", end, err) + } + } else { // both are present + if err := tryParseTime(start, &startTime); err != nil { + logger.Log.Fatalln("Unsupported time format:", start, err) + } + if err := tryParseTime(end, &endTime); err != nil { + logger.Log.Fatalln("Unsupported time format:", end, err) + } + } + duration := endTime.Sub(startTime) + step := schema.StepSecond + if duration.Hours() >= 24*30 { // time range > 1 month + step = schema.StepMonth + } else if duration.Hours() > 24 { // time range > 1 day + step = schema.StepDay + } else if duration.Minutes() > 60 { // time range > 1 hour + step = schema.StepHour + } else if duration.Seconds() > 60 { // time range > 1 minute + step = schema.StepMinute + } else if duration.Seconds() <= 0 { // illegal + logger.Log.Fatalln("end time must be later than start time, end time:", endTime, ", start time:", startTime) + } + return startTime, endTime, step +} diff --git a/commands/interceptor/duration_test.go b/commands/interceptor/duration_test.go new file mode 100644 index 0000000..7879cfd --- /dev/null +++ b/commands/interceptor/duration_test.go @@ -0,0 +1,98 @@ +/* + * Licensed to the 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. + * The 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 ( + "github.com/apache/skywalking-cli/graphql/schema" + "reflect" + "testing" + "time" +) + +func TestParseDuration(t *testing.T) { + now := time.Now().UTC() + + type args struct { + start string + end string + } + timeFormat := "2006-01-02 1504" + tests := []struct { + name string + args args + wantedStartTime time.Time + wantedEndTime time.Time + wantedStep schema.Step + }{ + { + name: "Should set current time if start is absent", + args: args{ + start: "", + end: now.Add(10 * time.Minute).Format(timeFormat), + }, + wantedStartTime: now, + wantedEndTime: now.Add(10 * time.Minute), + wantedStep: schema.StepMinute, + }, + { + name: "Should set current time if end is absent", + args: args{ + start: now.Add(-10 * time.Minute).Format(timeFormat), + end: "", + }, + wantedStartTime: now.Add(-10 * time.Minute), + wantedEndTime: now, + wantedStep: schema.StepMinute, + }, + { + name: "Should keep both if both are present", + args: args{ + start: now.Add(-10 * time.Minute).Format(timeFormat), + end: now.Add(10 * time.Minute).Format(timeFormat), + }, + wantedStartTime: now.Add(-10 * time.Minute), + wantedEndTime: now.Add(10 * time.Minute), + wantedStep: schema.StepMinute, + }, + { + name: "Should set both if both are absent", + args: args{ + start: "", + end: "", + }, + wantedStartTime: now.Add(-30 * time.Minute), + wantedEndTime: now, + wantedStep: schema.StepMinute, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotStartTime, gotEndTime, gotStep := ParseDuration(tt.args.start, tt.args.end) + if !reflect.DeepEqual(gotStartTime.Truncate(time.Minute), tt.wantedStartTime.Truncate(time.Minute)) { + t.Errorf("ParseDuration() got start time = %v, wanted start time %v", gotStartTime.Truncate(time.Minute), tt.wantedStartTime.Truncate(time.Minute)) + } + if !reflect.DeepEqual(gotEndTime.Truncate(time.Minute), tt.wantedEndTime.Truncate(time.Minute)) { + t.Errorf("ParseDuration() got end time = %v, wanted end time %v", gotEndTime.Truncate(time.Minute), tt.wantedEndTime.Truncate(time.Minute)) + } + if gotStep != tt.wantedStep { + t.Errorf("ParseDuration() got step = %v, wanted step %v", gotStep, tt.wantedStep) + } + }) + } +} diff --git a/logger/log.go b/commands/interceptor/interceptor.go similarity index 69% copy from logger/log.go copy to commands/interceptor/interceptor.go index 4a5d482..111b73b 100644 --- a/logger/log.go +++ b/commands/interceptor/interceptor.go @@ -16,23 +16,20 @@ * */ -package logger +package interceptor import ( - "os" - - "github.com/sirupsen/logrus" + "github.com/urfave/cli" ) -var Log *logrus.Logger - -func init() { - if Log == nil { - Log = logrus.New() +// BeforeChain is a convenient function to chain up multiple cli.BeforeFunc +func BeforeChain(beforeFunctions []cli.BeforeFunc) cli.BeforeFunc { + return func(ctx *cli.Context) error { + for _, beforeFunc := range beforeFunctions { + if err := beforeFunc(ctx); err != nil { + return err + } + } + return nil } - Log.SetOutput(os.Stdout) - Log.SetFormatter(&logrus.TextFormatter{ - FullTimestamp: true, - TimestampFormat: "2006-01-02 15:04:05", - }) } diff --git a/commands/model.go b/commands/model/step.go similarity index 89% copy from commands/model.go copy to commands/model/step.go index 9da2fba..eed3b3f 100644 --- a/commands/model.go +++ b/commands/model/step.go @@ -16,7 +16,7 @@ * */ -package commands +package model import ( "fmt" @@ -24,12 +24,14 @@ import ( "strings" ) +// StepEnumValue defines the values domain of --step option type StepEnumValue struct { Enum []schema.Step Default schema.Step Selected schema.Step } +// Set the --step value, from raw string to StepEnumValue func (s *StepEnumValue) Set(value string) error { for _, enum := range s.Enum { if enum.String() == value { @@ -44,6 +46,7 @@ func (s *StepEnumValue) Set(value string) error { return fmt.Errorf("allowed steps are %s", strings.Join(steps, ", ")) } +// String representation of the step func (s StepEnumValue) String() string { return s.Selected.String() } diff --git a/commands/service/list.go b/commands/service/list.go index e2c91af..257bc3a 100644 --- a/commands/service/list.go +++ b/commands/service/list.go @@ -21,7 +21,8 @@ package service import ( "encoding/json" "fmt" - "github.com/apache/skywalking-cli/commands" + "github.com/apache/skywalking-cli/commands/interceptor" + "github.com/apache/skywalking-cli/commands/model" "github.com/apache/skywalking-cli/graphql/client" "github.com/apache/skywalking-cli/graphql/schema" "github.com/urfave/cli" @@ -34,23 +35,24 @@ var ListCommand = cli.Command{ Flags: []cli.Flag{ cli.StringFlag{ Name: "start", - Usage: "Query start time", + Usage: "query start `TIME`", }, cli.StringFlag{ Name: "end", - Usage: "Query end time", + Usage: "query end `TIME`", }, cli.GenericFlag{ - Name: "step", - Value: &commands.StepEnumValue{ + Name: "step", + Hidden: true, + Value: &model.StepEnumValue{ Enum: schema.AllStep, Default: schema.StepMinute, Selected: schema.StepMinute, }, }, }, - Before: commands.BeforeChain([]cli.BeforeFunc{ - commands.SetUpDuration, + Before: interceptor.BeforeChain([]cli.BeforeFunc{ + interceptor.DurationInterceptor, }), Action: func(ctx *cli.Context) error { end := ctx.String("end") @@ -59,13 +61,13 @@ var ListCommand = cli.Command{ services := client.Services(schema.Duration{ Start: start, End: end, - Step: step.(*commands.StepEnumValue).Selected, + Step: step.(*model.StepEnumValue).Selected, }) - if bytes, e := json.Marshal(services); e != nil { - return e - } else { + if bytes, e := json.Marshal(services); e == nil { fmt.Printf("%v\n", string(bytes)) + } else { + return e } return nil diff --git a/config/config.go b/config/config.go index bc33da2..0565fee 100644 --- a/config/config.go +++ b/config/config.go @@ -20,6 +20,6 @@ package config var Config struct { Global struct { - BaseUrl string `yaml:"base-url"` + BaseURL string `yaml:"base-url"` } } diff --git a/go.mod b/go.mod index 062d750..ad56fce 100644 --- a/go.mod +++ b/go.mod @@ -7,4 +7,5 @@ require ( github.com/pkg/errors v0.8.1 // indirect github.com/sirupsen/logrus v1.4.2 github.com/urfave/cli v1.22.1 + gopkg.in/yaml.v2 v2.2.2 ) diff --git a/graphql/client/client.go b/graphql/client/client.go index 0b050e3..d74f5c3 100644 --- a/graphql/client/client.go +++ b/graphql/client/client.go @@ -28,7 +28,7 @@ import ( func Services(duration schema.Duration) []schema.Service { ctx := context.Background() - client := graphql.NewClient(config.Config.Global.BaseUrl) + client := graphql.NewClient(config.Config.Global.BaseURL) client.Log = func(msg string) { logger.Log.Debugln(msg) } diff --git a/logger/log.go b/logger/log.go index 4a5d482..ce1cda7 100644 --- a/logger/log.go +++ b/logger/log.go @@ -32,7 +32,7 @@ func init() { } Log.SetOutput(os.Stdout) Log.SetFormatter(&logrus.TextFormatter{ - FullTimestamp: true, - TimestampFormat: "2006-01-02 15:04:05", + DisableTimestamp: true, + DisableLevelTruncation: true, }) } diff --git a/swctl/main.go b/swctl/main.go index 5619a28..2f3f967 100644 --- a/swctl/main.go +++ b/swctl/main.go @@ -22,13 +22,13 @@ import ( "encoding/json" "github.com/apache/skywalking-cli/commands/service" "github.com/apache/skywalking-cli/config" + "github.com/apache/skywalking-cli/logger" + "github.com/apache/skywalking-cli/util" "github.com/sirupsen/logrus" "github.com/urfave/cli" "gopkg.in/yaml.v2" "io/ioutil" "os" - - "github.com/apache/skywalking-cli/logger" ) var log *logrus.Logger @@ -43,8 +43,8 @@ func main() { app.Flags = []cli.Flag{ cli.StringFlag{ Name: "config", - Value: "./settings.yml", - Usage: "load configuration `FILE`, default to ./settings.yml", + Value: "~/.skywalking.yml", + Usage: "load configuration `FILE`", }, cli.BoolFlag{ Name: "debug", @@ -56,20 +56,20 @@ func main() { service.Command, } - app.Before = BeforeCommand + app.Before = beforeCommand if err := app.Run(os.Args); err != nil { log.Fatalln(err) } } -func BeforeCommand(c *cli.Context) error { +func beforeCommand(c *cli.Context) error { if c.Bool("debug") { log.SetLevel(logrus.DebugLevel) log.Debugln("Debug mode is enabled") } - configFile := c.String("config") + configFile := util.ExpandFilePath(c.String("config")) log.Debugln("Using configuration file:", configFile) if bytes, err := ioutil.ReadFile(configFile); err != nil { diff --git a/commands/model.go b/util/io.go similarity index 59% rename from commands/model.go rename to util/io.go index 9da2fba..5156856 100644 --- a/commands/model.go +++ b/util/io.go @@ -16,34 +16,29 @@ * */ -package commands +package util import ( - "fmt" - "github.com/apache/skywalking-cli/graphql/schema" + "github.com/apache/skywalking-cli/logger" + "os/user" "strings" ) -type StepEnumValue struct { - Enum []schema.Step - Default schema.Step - Selected schema.Step -} - -func (s *StepEnumValue) Set(value string) error { - for _, enum := range s.Enum { - if enum.String() == value { - s.Selected = enum - return nil - } +// UserHomeDir returns the current user's home directory absolute path, +// which is usually represented as `~` in most shells +func UserHomeDir() string { + if currentUser, err := user.Current(); err != nil { + logger.Log.Warnln("Cannot obtain user home directory") + } else { + return currentUser.HomeDir } - steps := make([]string, len(schema.AllStep)) - for i, step := range schema.AllStep { - steps[i] = step.String() - } - return fmt.Errorf("allowed steps are %s", strings.Join(steps, ", ")) + return "" } -func (s StepEnumValue) String() string { - return s.Selected.String() +// ExpandFilePath expands the leading `~` to absolute path +func ExpandFilePath(path string) string { + if strings.HasPrefix(path, "~") { + return strings.Replace(path, "~", UserHomeDir(), 1) + } + return path }