Merge remote-tracking branch 'apache/master' into feature/graphql-client
diff --git a/.asf.yaml b/.asf.yaml
index 3f2645a..c1f6dfa 100644
--- a/.asf.yaml
+++ b/.asf.yaml
@@ -1,3 +1,20 @@
+#
+# 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.
+#
+
 github:
   description: Apache SkyWalking CLI
   homepage: https://skywalking.apache.org/
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index bdc008c..1abc12e 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -23,22 +23,22 @@
     runs-on: ubuntu-latest
     steps:
 
-    - name: Set up Go 1.13
-      uses: actions/setup-go@v1
-      with:
-        go-version: 1.13
-      id: go
+      - name: Set up Go 1.13
+        uses: actions/setup-go@v1
+        with:
+          go-version: 1.13
+        id: go
 
-    - name: Check out code into the Go module directory
-      uses: actions/checkout@v1
+      - name: Check out code into the Go module directory
+        uses: actions/checkout@v1
 
-    - name: Get dependencies
-      run: |
-        go get -v -t -d ./...
-        if [ -f Gopkg.toml ]; then
-            curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
-            dep ensure
-        fi
+      - name: Get dependencies
+        run: |
+          go get -v -t -d ./...
+          if [ -f Gopkg.toml ]; then
+              curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
+              dep ensure
+          fi
 
-    - name: Build
-      run: make
+      - name: Build
+        run: make
diff --git a/Makefile b/Makefile
index 03d2d59..3273175 100644
--- a/Makefile
+++ b/Makefile
@@ -19,4 +19,7 @@
 
 build:
 	GO_BUILD_FLAGS="-v" ./scripts/build
-	./bin/swctl --version
\ No newline at end of file
+	./bin/swctl --version
+
+clean:
+	-rm -rf ./bin
\ No newline at end of file
diff --git a/commands/interceptor.go b/commands/interceptor.go
new file mode 100644
index 0000000..6837529
--- /dev/null
+++ b/commands/interceptor.go
@@ -0,0 +1,71 @@
+/*
+ * 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/model.go b/commands/model.go
new file mode 100644
index 0000000..9da2fba
--- /dev/null
+++ b/commands/model.go
@@ -0,0 +1,49 @@
+/*
+ * 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 (
+	"fmt"
+	"github.com/apache/skywalking-cli/graphql/schema"
+	"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
+		}
+	}
+	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, ", "))
+}
+
+func (s StepEnumValue) String() string {
+	return s.Selected.String()
+}
diff --git a/commands/service/list.go b/commands/service/list.go
new file mode 100644
index 0000000..e2c91af
--- /dev/null
+++ b/commands/service/list.go
@@ -0,0 +1,73 @@
+/*
+ * 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 service
+
+import (
+	"encoding/json"
+	"fmt"
+	"github.com/apache/skywalking-cli/commands"
+	"github.com/apache/skywalking-cli/graphql/client"
+	"github.com/apache/skywalking-cli/graphql/schema"
+	"github.com/urfave/cli"
+)
+
+var ListCommand = cli.Command{
+	Name:      "list",
+	ShortName: "ls",
+	Usage:     "List all available services",
+	Flags: []cli.Flag{
+		cli.StringFlag{
+			Name:  "start",
+			Usage: "Query start time",
+		},
+		cli.StringFlag{
+			Name:  "end",
+			Usage: "Query end time",
+		},
+		cli.GenericFlag{
+			Name: "step",
+			Value: &commands.StepEnumValue{
+				Enum:     schema.AllStep,
+				Default:  schema.StepMinute,
+				Selected: schema.StepMinute,
+			},
+		},
+	},
+	Before: commands.BeforeChain([]cli.BeforeFunc{
+		commands.SetUpDuration,
+	}),
+	Action: func(ctx *cli.Context) error {
+		end := ctx.String("end")
+		start := ctx.String("start")
+		step := ctx.Generic("step")
+		services := client.Services(schema.Duration{
+			Start: start,
+			End:   end,
+			Step:  step.(*commands.StepEnumValue).Selected,
+		})
+
+		if bytes, e := json.Marshal(services); e != nil {
+			return e
+		} else {
+			fmt.Printf("%v\n", string(bytes))
+		}
+
+		return nil
+	},
+}
diff --git a/commands/service/service.go b/commands/service/service.go
new file mode 100644
index 0000000..5760adf
--- /dev/null
+++ b/commands/service/service.go
@@ -0,0 +1,32 @@
+/*
+ * 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 service
+
+import (
+	"github.com/urfave/cli"
+)
+
+var Command = cli.Command{
+	Name:      "service",
+	ShortName: "s",
+	Usage:     "Service related sub-command",
+	Subcommands: cli.Commands{
+		ListCommand,
+	},
+}
diff --git a/config/config.go b/config/config.go
new file mode 100644
index 0000000..bc33da2
--- /dev/null
+++ b/config/config.go
@@ -0,0 +1,25 @@
+/*
+ * 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 config
+
+var Config struct {
+	Global struct {
+		BaseUrl string `yaml:"base-url"`
+	}
+}
diff --git a/go.mod b/go.mod
index 8d79e73..062d750 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,8 @@
 go 1.13
 
 require (
-	github.com/lytics/logrus v0.0.0-20170528191427-4389a17ed024 // indirect
+	github.com/machinebox/graphql v0.2.2
+	github.com/pkg/errors v0.8.1 // indirect
 	github.com/sirupsen/logrus v1.4.2
 	github.com/urfave/cli v1.22.1
 )
diff --git a/go.sum b/go.sum
index 7860264..7627702 100644
--- a/go.sum
+++ b/go.sum
@@ -5,6 +5,10 @@
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/lytics/logrus v0.0.0-20170528191427-4389a17ed024 h1:QaKVrqyQRNPbdBNCpiU0Ei3iDQko3qoiUUXMiTWhzZM=
 github.com/lytics/logrus v0.0.0-20170528191427-4389a17ed024/go.mod h1:SkQviJ2s7rFyzyuxdVp6osZceHOabU91ZhKsEXF0RWg=
+github.com/machinebox/graphql v0.2.2 h1:dWKpJligYKhYKO5A2gvNhkJdQMNZeChZYyBbrZkBZfo=
+github.com/machinebox/graphql v0.2.2/go.mod h1:F+kbVMHuwrQ5tYgU9JXlnskM8nOaFxCAEolaQybkjWA=
+github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -19,4 +23,5 @@
 golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
 golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/graphql/client/client.go b/graphql/client/client.go
new file mode 100644
index 0000000..10a680a
--- /dev/null
+++ b/graphql/client/client.go
@@ -0,0 +1,51 @@
+/*
+ * 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 client
+
+import (
+	"context"
+	"github.com/apache/skywalking-cli/config"
+	"github.com/apache/skywalking-cli/graphql/schema"
+	"github.com/apache/skywalking-cli/logger"
+	"github.com/machinebox/graphql"
+)
+
+func Services(duration schema.Duration) []schema.Service {
+	ctx := context.Background()
+	client := graphql.NewClient(config.Config.Global.BaseUrl)
+	client.Log = func(msg string) {
+		logger.Log.Debugln(msg)
+	}
+
+	var services map[string][]schema.Service
+	request := graphql.NewRequest(`
+		query ($duration: Duration!) {
+			services: getAllServices(duration: $duration) {
+				id name
+			}
+		}
+	`)
+	request.Var("duration", duration)
+	if err := client.Run(ctx, request, &services); err != nil {
+		logger.Log.Fatalln(err)
+		panic(err)
+	}
+
+	return services["services"]
+}
diff --git a/graphql/schema/schema.go b/graphql/schema/schema.go
new file mode 100644
index 0000000..d3848d5
--- /dev/null
+++ b/graphql/schema/schema.go
@@ -0,0 +1,750 @@
+/*
+ * 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 schema
+
+import (
+	"fmt"
+	"io"
+	"strconv"
+)
+
+type AlarmMessage struct {
+	StartTime int64  `json:"startTime"`
+	Scope     *Scope `json:"scope"`
+	ID        string `json:"id"`
+	Message   string `json:"message"`
+}
+
+type AlarmTrend struct {
+	NumOfAlarm []*int `json:"numOfAlarm"`
+}
+
+type Alarms struct {
+	Msgs  []*AlarmMessage `json:"msgs"`
+	Total int             `json:"total"`
+}
+
+type Attribute struct {
+	Name  string `json:"name"`
+	Value string `json:"value"`
+}
+
+type BasicTrace struct {
+	SegmentID     string   `json:"segmentId"`
+	EndpointNames []string `json:"endpointNames"`
+	Duration      int      `json:"duration"`
+	Start         string   `json:"start"`
+	IsError       *bool    `json:"isError"`
+	TraceIds      []string `json:"traceIds"`
+}
+
+type BatchMetricConditions struct {
+	Name string   `json:"name"`
+	Ids  []string `json:"ids"`
+}
+
+type Call struct {
+	Source           string        `json:"source"`
+	SourceComponents []string      `json:"sourceComponents"`
+	Target           string        `json:"target"`
+	TargetComponents []string      `json:"targetComponents"`
+	ID               string        `json:"id"`
+	DetectPoints     []DetectPoint `json:"detectPoints"`
+}
+
+type ClusterBrief struct {
+	NumOfService  int `json:"numOfService"`
+	NumOfEndpoint int `json:"numOfEndpoint"`
+	NumOfDatabase int `json:"numOfDatabase"`
+	NumOfCache    int `json:"numOfCache"`
+	NumOfMq       int `json:"numOfMQ"`
+}
+
+type Database struct {
+	ID   string `json:"id"`
+	Name string `json:"name"`
+	Type string `json:"type"`
+}
+
+type Duration struct {
+	Start string `json:"start"`
+	End   string `json:"end"`
+	Step  Step   `json:"step"`
+}
+
+type Endpoint struct {
+	ID   string `json:"id"`
+	Name string `json:"name"`
+}
+
+type EndpointInfo struct {
+	ID          string `json:"id"`
+	Name        string `json:"name"`
+	ServiceID   string `json:"serviceId"`
+	ServiceName string `json:"serviceName"`
+}
+
+type IntValues struct {
+	Values []*KVInt `json:"values"`
+}
+
+type KVInt struct {
+	ID    string `json:"id"`
+	Value int64  `json:"value"`
+}
+
+type KeyValue struct {
+	Key   string  `json:"key"`
+	Value *string `json:"value"`
+}
+
+type Log struct {
+	ServiceName         *string     `json:"serviceName"`
+	ServiceID           *string     `json:"serviceId"`
+	ServiceInstanceName *string     `json:"serviceInstanceName"`
+	ServiceInstanceID   *string     `json:"serviceInstanceId"`
+	EndpointName        *string     `json:"endpointName"`
+	EndpointID          *string     `json:"endpointId"`
+	TraceID             *string     `json:"traceId"`
+	Timestamp           string      `json:"timestamp"`
+	IsError             *bool       `json:"isError"`
+	StatusCode          *string     `json:"statusCode"`
+	ContentType         ContentType `json:"contentType"`
+	Content             *string     `json:"content"`
+}
+
+type LogEntity struct {
+	Time int64       `json:"time"`
+	Data []*KeyValue `json:"data"`
+}
+
+type LogQueryCondition struct {
+	MetricName        *string     `json:"metricName"`
+	ServiceID         *string     `json:"serviceId"`
+	ServiceInstanceID *string     `json:"serviceInstanceId"`
+	EndpointID        *string     `json:"endpointId"`
+	TraceID           *string     `json:"traceId"`
+	QueryDuration     *Duration   `json:"queryDuration"`
+	State             LogState    `json:"state"`
+	StateCode         *string     `json:"stateCode"`
+	Paging            *Pagination `json:"paging"`
+}
+
+type Logs struct {
+	Logs  []*Log `json:"logs"`
+	Total int    `json:"total"`
+}
+
+type MetricCondition struct {
+	Name string  `json:"name"`
+	ID   *string `json:"id"`
+}
+
+type Node struct {
+	ID     string  `json:"id"`
+	Name   string  `json:"name"`
+	Type   *string `json:"type"`
+	IsReal bool    `json:"isReal"`
+}
+
+type Pagination struct {
+	PageNum   *int  `json:"pageNum"`
+	PageSize  int   `json:"pageSize"`
+	NeedTotal *bool `json:"needTotal"`
+}
+
+type Ref struct {
+	TraceID         string  `json:"traceId"`
+	ParentSegmentID string  `json:"parentSegmentId"`
+	ParentSpanID    int     `json:"parentSpanId"`
+	Type            RefType `json:"type"`
+}
+
+type Service struct {
+	ID   string `json:"id"`
+	Name string `json:"name"`
+}
+
+type ServiceInstance struct {
+	ID           string       `json:"id"`
+	Name         string       `json:"name"`
+	Attributes   []*Attribute `json:"attributes"`
+	Language     Language     `json:"language"`
+	InstanceUUID string       `json:"instanceUUID"`
+}
+
+type Span struct {
+	TraceID      string       `json:"traceId"`
+	SegmentID    string       `json:"segmentId"`
+	SpanID       int          `json:"spanId"`
+	ParentSpanID int          `json:"parentSpanId"`
+	Refs         []*Ref       `json:"refs"`
+	ServiceCode  string       `json:"serviceCode"`
+	StartTime    int64        `json:"startTime"`
+	EndTime      int64        `json:"endTime"`
+	EndpointName *string      `json:"endpointName"`
+	Type         string       `json:"type"`
+	Peer         *string      `json:"peer"`
+	Component    *string      `json:"component"`
+	IsError      *bool        `json:"isError"`
+	Layer        *string      `json:"layer"`
+	Tags         []*KeyValue  `json:"tags"`
+	Logs         []*LogEntity `json:"logs"`
+}
+
+type Thermodynamic struct {
+	Nodes     [][]*int `json:"nodes"`
+	AxisYStep int      `json:"axisYStep"`
+}
+
+type TimeInfo struct {
+	Timezone         *string `json:"timezone"`
+	CurrentTimestamp *int64  `json:"currentTimestamp"`
+}
+
+type TopNEntity struct {
+	Name  string `json:"name"`
+	ID    string `json:"id"`
+	Value int64  `json:"value"`
+}
+
+type TopNRecord struct {
+	Statement *string `json:"statement"`
+	Latency   int64   `json:"latency"`
+	TraceID   *string `json:"traceId"`
+}
+
+type TopNRecordsCondition struct {
+	ServiceID  string    `json:"serviceId"`
+	MetricName string    `json:"metricName"`
+	TopN       int       `json:"topN"`
+	Order      Order     `json:"order"`
+	Duration   *Duration `json:"duration"`
+}
+
+type Topology struct {
+	Nodes []*Node `json:"nodes"`
+	Calls []*Call `json:"calls"`
+}
+
+type Trace struct {
+	Spans []*Span `json:"spans"`
+}
+
+type TraceBrief struct {
+	Traces []*BasicTrace `json:"traces"`
+	Total  int           `json:"total"`
+}
+
+type TraceQueryCondition struct {
+	ServiceID         *string     `json:"serviceId"`
+	ServiceInstanceID *string     `json:"serviceInstanceId"`
+	TraceID           *string     `json:"traceId"`
+	EndpointID        *string     `json:"endpointId"`
+	EndpointName      *string     `json:"endpointName"`
+	QueryDuration     *Duration   `json:"queryDuration"`
+	MinTraceDuration  *int        `json:"minTraceDuration"`
+	MaxTraceDuration  *int        `json:"maxTraceDuration"`
+	TraceState        TraceState  `json:"traceState"`
+	QueryOrder        QueryOrder  `json:"queryOrder"`
+	Paging            *Pagination `json:"paging"`
+}
+
+type ContentType string
+
+const (
+	ContentTypeText ContentType = "TEXT"
+	ContentTypeJSON ContentType = "JSON"
+	ContentTypeNone ContentType = "NONE"
+)
+
+var AllContentType = []ContentType{
+	ContentTypeText,
+	ContentTypeJSON,
+	ContentTypeNone,
+}
+
+func (e ContentType) IsValid() bool {
+	switch e {
+	case ContentTypeText, ContentTypeJSON, ContentTypeNone:
+		return true
+	}
+	return false
+}
+
+func (e ContentType) String() string {
+	return string(e)
+}
+
+func (e *ContentType) UnmarshalGQL(v interface{}) error {
+	str, ok := v.(string)
+	if !ok {
+		return fmt.Errorf("enums must be strings")
+	}
+
+	*e = ContentType(str)
+	if !e.IsValid() {
+		return fmt.Errorf("%s is not a valid ContentType", str)
+	}
+	return nil
+}
+
+func (e ContentType) MarshalGQL(w io.Writer) {
+	fmt.Fprint(w, strconv.Quote(e.String()))
+}
+
+type DetectPoint string
+
+const (
+	DetectPointClient DetectPoint = "CLIENT"
+	DetectPointServer DetectPoint = "SERVER"
+	DetectPointProxy  DetectPoint = "PROXY"
+)
+
+var AllDetectPoint = []DetectPoint{
+	DetectPointClient,
+	DetectPointServer,
+	DetectPointProxy,
+}
+
+func (e DetectPoint) IsValid() bool {
+	switch e {
+	case DetectPointClient, DetectPointServer, DetectPointProxy:
+		return true
+	}
+	return false
+}
+
+func (e DetectPoint) String() string {
+	return string(e)
+}
+
+func (e *DetectPoint) UnmarshalGQL(v interface{}) error {
+	str, ok := v.(string)
+	if !ok {
+		return fmt.Errorf("enums must be strings")
+	}
+
+	*e = DetectPoint(str)
+	if !e.IsValid() {
+		return fmt.Errorf("%s is not a valid DetectPoint", str)
+	}
+	return nil
+}
+
+func (e DetectPoint) MarshalGQL(w io.Writer) {
+	fmt.Fprint(w, strconv.Quote(e.String()))
+}
+
+type Language string
+
+const (
+	LanguageUnknown Language = "UNKNOWN"
+	LanguageJava    Language = "JAVA"
+	LanguageDotnet  Language = "DOTNET"
+	LanguageNodejs  Language = "NODEJS"
+	LanguagePython  Language = "PYTHON"
+	LanguageRuby    Language = "RUBY"
+)
+
+var AllLanguage = []Language{
+	LanguageUnknown,
+	LanguageJava,
+	LanguageDotnet,
+	LanguageNodejs,
+	LanguagePython,
+	LanguageRuby,
+}
+
+func (e Language) IsValid() bool {
+	switch e {
+	case LanguageUnknown, LanguageJava, LanguageDotnet, LanguageNodejs, LanguagePython, LanguageRuby:
+		return true
+	}
+	return false
+}
+
+func (e Language) String() string {
+	return string(e)
+}
+
+func (e *Language) UnmarshalGQL(v interface{}) error {
+	str, ok := v.(string)
+	if !ok {
+		return fmt.Errorf("enums must be strings")
+	}
+
+	*e = Language(str)
+	if !e.IsValid() {
+		return fmt.Errorf("%s is not a valid Language", str)
+	}
+	return nil
+}
+
+func (e Language) MarshalGQL(w io.Writer) {
+	fmt.Fprint(w, strconv.Quote(e.String()))
+}
+
+type LogState string
+
+const (
+	LogStateAll     LogState = "ALL"
+	LogStateSuccess LogState = "SUCCESS"
+	LogStateError   LogState = "ERROR"
+)
+
+var AllLogState = []LogState{
+	LogStateAll,
+	LogStateSuccess,
+	LogStateError,
+}
+
+func (e LogState) IsValid() bool {
+	switch e {
+	case LogStateAll, LogStateSuccess, LogStateError:
+		return true
+	}
+	return false
+}
+
+func (e LogState) String() string {
+	return string(e)
+}
+
+func (e *LogState) UnmarshalGQL(v interface{}) error {
+	str, ok := v.(string)
+	if !ok {
+		return fmt.Errorf("enums must be strings")
+	}
+
+	*e = LogState(str)
+	if !e.IsValid() {
+		return fmt.Errorf("%s is not a valid LogState", str)
+	}
+	return nil
+}
+
+func (e LogState) MarshalGQL(w io.Writer) {
+	fmt.Fprint(w, strconv.Quote(e.String()))
+}
+
+type NodeType string
+
+const (
+	NodeTypeService  NodeType = "SERVICE"
+	NodeTypeEndpoint NodeType = "ENDPOINT"
+	NodeTypeUser     NodeType = "USER"
+)
+
+var AllNodeType = []NodeType{
+	NodeTypeService,
+	NodeTypeEndpoint,
+	NodeTypeUser,
+}
+
+func (e NodeType) IsValid() bool {
+	switch e {
+	case NodeTypeService, NodeTypeEndpoint, NodeTypeUser:
+		return true
+	}
+	return false
+}
+
+func (e NodeType) String() string {
+	return string(e)
+}
+
+func (e *NodeType) UnmarshalGQL(v interface{}) error {
+	str, ok := v.(string)
+	if !ok {
+		return fmt.Errorf("enums must be strings")
+	}
+
+	*e = NodeType(str)
+	if !e.IsValid() {
+		return fmt.Errorf("%s is not a valid NodeType", str)
+	}
+	return nil
+}
+
+func (e NodeType) MarshalGQL(w io.Writer) {
+	fmt.Fprint(w, strconv.Quote(e.String()))
+}
+
+type Order string
+
+const (
+	OrderAsc Order = "ASC"
+	OrderDes Order = "DES"
+)
+
+var AllOrder = []Order{
+	OrderAsc,
+	OrderDes,
+}
+
+func (e Order) IsValid() bool {
+	switch e {
+	case OrderAsc, OrderDes:
+		return true
+	}
+	return false
+}
+
+func (e Order) String() string {
+	return string(e)
+}
+
+func (e *Order) UnmarshalGQL(v interface{}) error {
+	str, ok := v.(string)
+	if !ok {
+		return fmt.Errorf("enums must be strings")
+	}
+
+	*e = Order(str)
+	if !e.IsValid() {
+		return fmt.Errorf("%s is not a valid Order", str)
+	}
+	return nil
+}
+
+func (e Order) MarshalGQL(w io.Writer) {
+	fmt.Fprint(w, strconv.Quote(e.String()))
+}
+
+type QueryOrder string
+
+const (
+	QueryOrderByStartTime QueryOrder = "BY_START_TIME"
+	QueryOrderByDuration  QueryOrder = "BY_DURATION"
+)
+
+var AllQueryOrder = []QueryOrder{
+	QueryOrderByStartTime,
+	QueryOrderByDuration,
+}
+
+func (e QueryOrder) IsValid() bool {
+	switch e {
+	case QueryOrderByStartTime, QueryOrderByDuration:
+		return true
+	}
+	return false
+}
+
+func (e QueryOrder) String() string {
+	return string(e)
+}
+
+func (e *QueryOrder) UnmarshalGQL(v interface{}) error {
+	str, ok := v.(string)
+	if !ok {
+		return fmt.Errorf("enums must be strings")
+	}
+
+	*e = QueryOrder(str)
+	if !e.IsValid() {
+		return fmt.Errorf("%s is not a valid QueryOrder", str)
+	}
+	return nil
+}
+
+func (e QueryOrder) MarshalGQL(w io.Writer) {
+	fmt.Fprint(w, strconv.Quote(e.String()))
+}
+
+type RefType string
+
+const (
+	RefTypeCrossProcess RefType = "CROSS_PROCESS"
+	RefTypeCrossThread  RefType = "CROSS_THREAD"
+)
+
+var AllRefType = []RefType{
+	RefTypeCrossProcess,
+	RefTypeCrossThread,
+}
+
+func (e RefType) IsValid() bool {
+	switch e {
+	case RefTypeCrossProcess, RefTypeCrossThread:
+		return true
+	}
+	return false
+}
+
+func (e RefType) String() string {
+	return string(e)
+}
+
+func (e *RefType) UnmarshalGQL(v interface{}) error {
+	str, ok := v.(string)
+	if !ok {
+		return fmt.Errorf("enums must be strings")
+	}
+
+	*e = RefType(str)
+	if !e.IsValid() {
+		return fmt.Errorf("%s is not a valid RefType", str)
+	}
+	return nil
+}
+
+func (e RefType) MarshalGQL(w io.Writer) {
+	fmt.Fprint(w, strconv.Quote(e.String()))
+}
+
+type Scope string
+
+const (
+	ScopeService                 Scope = "Service"
+	ScopeServiceInstance         Scope = "ServiceInstance"
+	ScopeEndpoint                Scope = "Endpoint"
+	ScopeServiceRelation         Scope = "ServiceRelation"
+	ScopeServiceInstanceRelation Scope = "ServiceInstanceRelation"
+	ScopeEndpointRelation        Scope = "EndpointRelation"
+)
+
+var AllScope = []Scope{
+	ScopeService,
+	ScopeServiceInstance,
+	ScopeEndpoint,
+	ScopeServiceRelation,
+	ScopeServiceInstanceRelation,
+	ScopeEndpointRelation,
+}
+
+func (e Scope) IsValid() bool {
+	switch e {
+	case ScopeService, ScopeServiceInstance, ScopeEndpoint, ScopeServiceRelation, ScopeServiceInstanceRelation, ScopeEndpointRelation:
+		return true
+	}
+	return false
+}
+
+func (e Scope) String() string {
+	return string(e)
+}
+
+func (e *Scope) UnmarshalGQL(v interface{}) error {
+	str, ok := v.(string)
+	if !ok {
+		return fmt.Errorf("enums must be strings")
+	}
+
+	*e = Scope(str)
+	if !e.IsValid() {
+		return fmt.Errorf("%s is not a valid Scope", str)
+	}
+	return nil
+}
+
+func (e Scope) MarshalGQL(w io.Writer) {
+	fmt.Fprint(w, strconv.Quote(e.String()))
+}
+
+type Step string
+
+const (
+	StepMonth  Step = "MONTH"
+	StepDay    Step = "DAY"
+	StepHour   Step = "HOUR"
+	StepMinute Step = "MINUTE"
+	StepSecond Step = "SECOND"
+)
+
+var AllStep = []Step{
+	StepMonth,
+	StepDay,
+	StepHour,
+	StepMinute,
+	StepSecond,
+}
+
+func (e Step) IsValid() bool {
+	switch e {
+	case StepMonth, StepDay, StepHour, StepMinute, StepSecond:
+		return true
+	}
+	return false
+}
+
+func (e Step) String() string {
+	return string(e)
+}
+
+func (e *Step) UnmarshalGQL(v interface{}) error {
+	str, ok := v.(string)
+	if !ok {
+		return fmt.Errorf("enums must be strings")
+	}
+
+	*e = Step(str)
+	if !e.IsValid() {
+		return fmt.Errorf("%s is not a valid Step", str)
+	}
+	return nil
+}
+
+func (e Step) MarshalGQL(w io.Writer) {
+	fmt.Fprint(w, strconv.Quote(e.String()))
+}
+
+type TraceState string
+
+const (
+	TraceStateAll     TraceState = "ALL"
+	TraceStateSuccess TraceState = "SUCCESS"
+	TraceStateError   TraceState = "ERROR"
+)
+
+var AllTraceState = []TraceState{
+	TraceStateAll,
+	TraceStateSuccess,
+	TraceStateError,
+}
+
+func (e TraceState) IsValid() bool {
+	switch e {
+	case TraceStateAll, TraceStateSuccess, TraceStateError:
+		return true
+	}
+	return false
+}
+
+func (e TraceState) String() string {
+	return string(e)
+}
+
+func (e *TraceState) UnmarshalGQL(v interface{}) error {
+	str, ok := v.(string)
+	if !ok {
+		return fmt.Errorf("enums must be strings")
+	}
+
+	*e = TraceState(str)
+	if !e.IsValid() {
+		return fmt.Errorf("%s is not a valid TraceState", str)
+	}
+	return nil
+}
+
+func (e TraceState) MarshalGQL(w io.Writer) {
+	fmt.Fprint(w, strconv.Quote(e.String()))
+}
diff --git a/scripts/build b/scripts/build
index 2e48690..7a20563 100755
--- a/scripts/build
+++ b/scripts/build
@@ -20,7 +20,7 @@
 cli_build() {
   out="bin"
   CGO_ENABLED=0 go build $GO_BUILD_FLAGS \
-  -o "${out}/swctl" cmd/* || return
+  -o "${out}/swctl" swctl/* || return
 }
 
 if echo "$0" | grep "build$" > /dev/null; then
diff --git a/swctl/main.go b/swctl/main.go
new file mode 100644
index 0000000..5619a28
--- /dev/null
+++ b/swctl/main.go
@@ -0,0 +1,86 @@
+/*
+ * 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 main
+
+import (
+	"encoding/json"
+	"github.com/apache/skywalking-cli/commands/service"
+	"github.com/apache/skywalking-cli/config"
+	"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
+
+func init() {
+	log = logger.Log
+}
+
+func main() {
+	app := cli.NewApp()
+	app.Usage = "The CLI (Command Line Interface) for Apache SkyWalking."
+	app.Flags = []cli.Flag{
+		cli.StringFlag{
+			Name:  "config",
+			Value: "./settings.yml",
+			Usage: "load configuration `FILE`, default to ./settings.yml",
+		},
+		cli.BoolFlag{
+			Name:     "debug",
+			Required: false,
+			Usage:    "enable debug mode, will print more detailed logs",
+		},
+	}
+	app.Commands = []cli.Command{
+		service.Command,
+	}
+
+	app.Before = BeforeCommand
+
+	if err := app.Run(os.Args); err != nil {
+		log.Fatalln(err)
+	}
+}
+
+func BeforeCommand(c *cli.Context) error {
+	if c.Bool("debug") {
+		log.SetLevel(logrus.DebugLevel)
+		log.Debugln("Debug mode is enabled")
+	}
+
+	configFile := c.String("config")
+	log.Debugln("Using configuration file:", configFile)
+
+	if bytes, err := ioutil.ReadFile(configFile); err != nil {
+		return err
+	} else if err := yaml.Unmarshal(bytes, &config.Config); err != nil {
+		return err
+	}
+
+	if bytes, err := json.Marshal(config.Config); err == nil {
+		log.Debugln("Configurations: ", string(bytes))
+	}
+
+	return nil
+}