Skip to content

Commit 7fc4b39

Browse files
committed
add optional upload log
1 parent 9aa3588 commit 7fc4b39

12 files changed

Lines changed: 298 additions & 100 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export VERSION=0.1.24
1+
export VERSION=0.1.25
22

33
.PHONY : install
44
install:

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ English | [简体中文](./README-CN.md)
22

33
## UCloud CLI
44

5-
![](./docs/_static/ucloud_cli_demo.gif)
5+
[![asciicast](https://asciinema.org/a/CIm32OoqRxO22lGUd5N3On435.svg?autoplay=1&speed=3)](https://asciinema.org/a/CIm32OoqRxO22lGUd5N3On435)
66

77
The UCloud CLI provides a unified command line interface to UCloud services. It works on [ucloud-sdk-go](https://github.com/ucloud/ucloud-sdk-go) based on UCloud OpenAPI and supports Linux, macOS and Windows.
88
https://docs.ucloud.cn/software/cli/index

base/config.go

Lines changed: 83 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const DefaultBaseURL = "https://api.ucloud.cn/"
3636
const DefaultProfile = "default"
3737

3838
//Version 版本号
39-
const Version = "0.1.24"
39+
const Version = "0.1.25"
4040

4141
//ConfigIns 配置实例, 程序加载时生成
4242
var ConfigIns = &AggConfig{
@@ -79,14 +79,15 @@ type GlobalFlag struct {
7979

8080
//CLIConfig cli_config element
8181
type CLIConfig struct {
82-
ProjectID string `json:"project_id"`
83-
Region string `json:"region"`
84-
Zone string `json:"zone"`
85-
BaseURL string `json:"base_url"`
86-
Timeout int `json:"timeout_sec"`
87-
Profile string `json:"profile"`
88-
Active bool `json:"active"` //是否生效
89-
MaxRetryTimes *int `json:"max_retry_times"`
82+
ProjectID string `json:"project_id"`
83+
Region string `json:"region"`
84+
Zone string `json:"zone"`
85+
BaseURL string `json:"base_url"`
86+
Timeout int `json:"timeout_sec"`
87+
Profile string `json:"profile"`
88+
Active bool `json:"active"` //是否生效
89+
MaxRetryTimes *int `json:"max_retry_times"`
90+
AgreeUploadLog bool `json:"agree_upload_log"`
9091
}
9192

9293
//CredentialConfig credential element
@@ -98,16 +99,17 @@ type CredentialConfig struct {
9899

99100
//AggConfig 聚合配置 config+credential
100101
type AggConfig struct {
101-
Profile string `json:"profile"`
102-
Active bool `json:"active"`
103-
ProjectID string `json:"project_id"`
104-
Region string `json:"region"`
105-
Zone string `json:"zone"`
106-
BaseURL string `json:"base_url"`
107-
Timeout int `json:"timeout_sec"`
108-
PublicKey string `json:"public_key"`
109-
PrivateKey string `json:"private_key"`
110-
MaxRetryTimes *int `json:"max_retry_times"`
102+
Profile string `json:"profile"`
103+
Active bool `json:"active"`
104+
ProjectID string `json:"project_id"`
105+
Region string `json:"region"`
106+
Zone string `json:"zone"`
107+
BaseURL string `json:"base_url"`
108+
Timeout int `json:"timeout_sec"`
109+
PublicKey string `json:"public_key"`
110+
PrivateKey string `json:"private_key"`
111+
MaxRetryTimes *int `json:"max_retry_times"`
112+
AgreeUploadLog bool `json:"agree_upload_log"`
111113
}
112114

113115
//ConfigPublicKey 输入公钥
@@ -136,6 +138,22 @@ func (p *AggConfig) ConfigPrivateKey() error {
136138
return nil
137139
}
138140

141+
//ConfigUploadLog agree upload log or not
142+
func (p *AggConfig) ConfigUploadLog() error {
143+
var input string
144+
fmt.Print("Do you agree to upload log in local file ~/.ucloud/cli.log to help ucloud-cli get better(yes/no):")
145+
_, err := fmt.Scanf("%s\n", &input)
146+
if err != nil {
147+
HandleError(err)
148+
return err
149+
}
150+
151+
if str := strings.ToLower(input); str == "y" || str == "ye" || str == "yes" {
152+
p.AgreeUploadLog = true
153+
}
154+
return nil
155+
}
156+
139157
//GetClientConfig 用来生成sdkClient
140158
func (p *AggConfig) GetClientConfig(isDebug bool) *sdk.Config {
141159
clientConfig := &sdk.Config{
@@ -169,6 +187,7 @@ func (p *AggConfig) copyToCLIConfig(target *CLIConfig) {
169187
target.Zone = p.Zone
170188
target.Active = p.Active
171189
target.MaxRetryTimes = p.MaxRetryTimes
190+
target.AgreeUploadLog = p.AgreeUploadLog
172191
}
173192

174193
func (p *AggConfig) copyToCredentialConfig(target *CredentialConfig) {
@@ -217,7 +236,7 @@ func NewAggConfigManager(cfgFile, credFile *os.File) (*AggConfigManager, error)
217236
//Append config to list, override if already exist the same profile
218237
func (p *AggConfigManager) Append(config *AggConfig) error {
219238
if _, ok := p.configs[config.Profile]; ok {
220-
return fmt.Errorf("profile %s exists already", config.Profile)
239+
return fmt.Errorf("profile [%s] exists already", config.Profile)
221240
}
222241

223242
if config.Active && config.Profile != p.activeProfile {
@@ -279,16 +298,17 @@ func (p *AggConfigManager) Load() error {
279298
}
280299

281300
p.configs[profile] = &AggConfig{
282-
PrivateKey: cred.PrivateKey,
283-
PublicKey: cred.PublicKey,
284-
Profile: config.Profile,
285-
ProjectID: config.ProjectID,
286-
Region: config.Region,
287-
Zone: config.Zone,
288-
BaseURL: config.BaseURL,
289-
Timeout: config.Timeout,
290-
Active: config.Active,
291-
MaxRetryTimes: config.MaxRetryTimes,
301+
PrivateKey: cred.PrivateKey,
302+
PublicKey: cred.PublicKey,
303+
Profile: config.Profile,
304+
ProjectID: config.ProjectID,
305+
Region: config.Region,
306+
Zone: config.Zone,
307+
BaseURL: config.BaseURL,
308+
Timeout: config.Timeout,
309+
Active: config.Active,
310+
MaxRetryTimes: config.MaxRetryTimes,
311+
AgreeUploadLog: config.AgreeUploadLog,
292312
}
293313
}
294314

@@ -465,6 +485,37 @@ func LoadUserInfo() (*uaccount.UserInfo, error) {
465485
return &user, nil
466486
}
467487

488+
//GetUserInfo from local file and remote api
489+
func GetUserInfo() (*uaccount.UserInfo, error) {
490+
user, err := LoadUserInfo()
491+
if err == nil {
492+
return user, nil
493+
}
494+
495+
req := BizClient.NewGetUserInfoRequest()
496+
resp, err := BizClient.GetUserInfo(req)
497+
498+
if err != nil {
499+
return nil, err
500+
}
501+
502+
if len(resp.DataSet) == 1 {
503+
user = &resp.DataSet[0]
504+
bytes, err := json.Marshal(user)
505+
if err != nil {
506+
return nil, err
507+
}
508+
fileFullPath := GetConfigDir() + "/user.json"
509+
err = ioutil.WriteFile(fileFullPath, bytes, 0600)
510+
if err != nil {
511+
return nil, err
512+
}
513+
} else {
514+
return nil, fmt.Errorf("GetUserInfo DataSet length: %d", len(resp.DataSet))
515+
}
516+
return user, nil
517+
}
518+
468519
//OldConfig 0.1.7以及之前版本的配置struct
469520
type OldConfig struct {
470521
PublicKey string `json:"public_key"`
@@ -585,18 +636,15 @@ func InitConfig() {
585636
HandleError(err)
586637
}
587638
} else {
588-
var ok bool
589-
ins, ok = AggConfigListIns.GetAggConfigByProfile(Global.Profile)
590-
if !ok {
591-
LogError("Profile %s does not exist", Global.Profile)
592-
}
639+
ins, _ = AggConfigListIns.GetAggConfigByProfile(Global.Profile)
593640
}
594641

595642
if ins != nil {
596643
ConfigIns = ins
597644
} else {
598645
ins = ConfigIns
599646
}
647+
logCmd()
600648

601649
bc, err := GetBizClient(ConfigIns)
602650
if err != nil {

base/log.go

Lines changed: 114 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,30 @@
11
package base
22

33
import (
4+
"bytes"
5+
"encoding/json"
46
"fmt"
7+
"net/http"
58
"os"
9+
"runtime"
610
"strings"
711
"sync"
12+
"time"
813

14+
uuid "github.com/satori/go.uuid"
915
log "github.com/sirupsen/logrus"
16+
1017
"github.com/ucloud/ucloud-sdk-go/ucloud/request"
18+
"github.com/ucloud/ucloud-sdk-go/ucloud/version"
1119
)
1220

21+
const DefaultDasURL = "https://das-rpt.ucloud.cn/log"
22+
1323
//Logger 日志
1424
var logger *log.Logger
1525
var mu sync.Mutex
1626
var out = Cxt.GetWriter()
27+
var tracer = Tracer{DefaultDasURL}
1728

1829
func initConfigDir() {
1930
if _, err := os.Stat(GetLogFileDir()); os.IsNotExist(err) {
@@ -23,6 +34,7 @@ func initConfigDir() {
2334
}
2435
}
2536
}
37+
2638
func initLog() error {
2739
initConfigDir()
2840
file, err := os.OpenFile(GetLogFilePath(), os.O_APPEND|os.O_CREATE|os.O_RDWR, 0644)
@@ -33,10 +45,21 @@ func initLog() error {
3345
logger.SetNoLock()
3446
logger.AddHook(NewLogRotateHook(file))
3547
logger.SetOutput(file)
36-
LogInfo(fmt.Sprintf("command: %s", strings.Join(os.Args, " ")))
48+
3749
return nil
3850
}
3951

52+
func logCmd() {
53+
args := make([]string, len(os.Args))
54+
copy(args, os.Args)
55+
for idx, arg := range args {
56+
if strings.Contains(arg, "password") && idx <= len(args)-2 {
57+
args[idx+1] = strings.Repeat("*", 8)
58+
}
59+
}
60+
LogInfo(fmt.Sprintf("command: %s", strings.Join(args, " ")))
61+
}
62+
4063
//GetLogger return point of logger
4164
func GetLogger() *log.Logger {
4265
return logger
@@ -54,21 +77,47 @@ func GetLogFilePath() string {
5477

5578
//LogInfo 记录日志
5679
func LogInfo(logs ...string) {
80+
_, ok := os.LookupEnv("COMP_LINE")
81+
if ok {
82+
return
83+
}
5784
mu.Lock()
5885
defer mu.Unlock()
5986
goID := curGoroutineID()
6087
for _, line := range logs {
61-
logger.WithField("GoroutineID", goID).Info(line)
88+
logger.WithField("goroutine_id", goID).Info(line)
89+
}
90+
if ConfigIns.AgreeUploadLog {
91+
UploadLogs(logs, "info", goID)
6292
}
6393
}
6494

6595
//LogError 记录日志
6696
func LogError(logs ...string) {
97+
_, ok := os.LookupEnv("COMP_LINE")
98+
if ok {
99+
return
100+
}
101+
mu.Lock()
102+
defer mu.Unlock()
67103
goID := curGoroutineID()
68104
for _, line := range logs {
69-
logger.WithField("GoroutineID", goID).Error(line)
105+
logger.WithField("goroutine_id", goID).Error(line)
70106
fmt.Fprintln(out, line)
71107
}
108+
if ConfigIns.AgreeUploadLog {
109+
UploadLogs(logs, "error", goID)
110+
}
111+
}
112+
113+
//UploadLogs send logs to das server
114+
func UploadLogs(logs []string, level string, goID int64) {
115+
var lines []string
116+
for _, log := range logs {
117+
line := fmt.Sprintf("time=%s level=%s goroutine_id=%d msg=%s", time.Now().Format(time.RFC3339Nano), level, goID, log)
118+
lines = append(lines, line)
119+
}
120+
tracer.Send(lines)
72121
}
73122

74123
//LogRotateHook rotate log file
@@ -143,3 +192,65 @@ func ToQueryMap(req request.Common) map[string]string {
143192
delete(reqMap, "Password")
144193
return reqMap
145194
}
195+
196+
//Tracer upload log to server if allowed
197+
type Tracer struct {
198+
DasUrl string
199+
}
200+
201+
func (t Tracer) wrapLogs(log []string) ([]byte, error) {
202+
dataSet := make([]map[string]interface{}, 0)
203+
dataItem := map[string]interface{}{
204+
"level": "info",
205+
"topic": "api",
206+
"log": log,
207+
}
208+
dataSet = append(dataSet, dataItem)
209+
reqUUID := uuid.NewV4()
210+
sessionID := uuid.NewV4()
211+
user, err := GetUserInfo()
212+
if err != nil {
213+
return nil, err
214+
}
215+
payload := map[string]interface{}{
216+
"aid": "iywtleaa",
217+
"uuid": reqUUID,
218+
"sid": sessionID,
219+
"ds": dataSet,
220+
"cs": map[string]interface{}{
221+
"uname": user.UserEmail,
222+
},
223+
}
224+
marshaled, err := json.Marshal(payload)
225+
if err != nil {
226+
return nil, fmt.Errorf("cannot to marshal log: %s", err)
227+
}
228+
return marshaled, nil
229+
}
230+
231+
//Send logs to server
232+
func (t Tracer) Send(logs []string) error {
233+
body, err := t.wrapLogs(logs)
234+
if err != nil {
235+
return err
236+
}
237+
for i := 0; i < len(body); i++ {
238+
body[i] = ^body[i]
239+
}
240+
241+
client := &http.Client{}
242+
ua := fmt.Sprintf("GO/%s GO-SDK/%s %s", runtime.Version(), version.Version, ClientConfig.UserAgent)
243+
req, err := http.NewRequest("POST", t.DasUrl, bytes.NewReader(body))
244+
req.Header.Add("Origin", "https://sdk.ucloud.cn")
245+
req.Header.Add("User-Agent", ua)
246+
resp, err := client.Do(req)
247+
if err != nil {
248+
return err
249+
}
250+
defer resp.Body.Close()
251+
if resp.StatusCode != 200 {
252+
return fmt.Errorf("send logs failed: status %d %s", resp.StatusCode, resp.Status)
253+
}
254+
255+
return nil
256+
}

base/util.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ func GetConfigDir() string {
112112
//HandleBizError 处理RetCode != 0 的业务异常
113113
func HandleBizError(resp response.Common) error {
114114
format := "Something wrong. RetCode:%d. Message:%s\n"
115-
Cxt.Printf(format, resp.GetRetCode(), resp.GetMessage())
115+
LogError(fmt.Sprintf(format, resp.GetRetCode(), resp.GetMessage()))
116116
return fmt.Errorf(format, resp.GetRetCode(), resp.GetMessage())
117117
}
118118

@@ -625,7 +625,7 @@ func Confirm(yes bool, text string) bool {
625625
}
626626
sure, err := ux.Prompt(text)
627627
if err != nil {
628-
Cxt.Println(err)
628+
LogError(err.Error())
629629
return false
630630
}
631631
return sure

0 commit comments

Comments
 (0)