wenzuochao пре 6 година
родитељ
комит
af117b52c2

+ 3 - 2
README-CN.md

@@ -109,8 +109,9 @@ func main() {
 * [Proxy](docs/4-Proxy-CN.md)
 * [Timeout](docs/5-Timeout-CN.md)
 * [Debug](docs/6-Debug-CN.md)
-* [Concurrent](docs/7-Concurrent-CN.md)
-* [Asynchronous Call](docs/8-Asynchronous-CN.md)
+* [Logger](docs/7-Logger-CN.md)
+* [Concurrent](docs/8-Concurrent-CN.md)
+* [Asynchronous Call](docs/9-Asynchronous-CN.md)
 
 
 ## 问题

+ 3 - 2
README.md

@@ -112,8 +112,9 @@ func main() {
 * [Proxy](docs/4-Proxy-EN.md)
 * [Timeout](docs/5-Timeout-EN.md)
 * [Debug](docs/6-Debug-EN.md)
-* [Concurrent](docs/7-Concurrent-EN.md)
-* [Asynchronous Call](docs/8-Asynchronous-EN.md)
+* [Logger](docs/7-Logger-EN.md)
+* [Concurrent](docs/8-Concurrent-EN.md)
+* [Asynchronous Call](docs/9-Asynchronous-EN.md)
 
 
 ## Issues

+ 2 - 2
docs/6-Debug-CN.md

@@ -1,8 +1,8 @@
-[← 超时](5-Timeout-CN.md) | 调试[(English)](6-Debug-EN.md) | [并发 →](7-Concurrent-CN.md)
+[← 超时](5-Timeout-CN.md) | 调试[(English)](6-Debug-EN.md) | [日志 →](7-Logger-CN.md)
 ***
 
 # 调试
 如果环境变量 `DEBUG=sdk` 存在, 所有的请求都将启用调试模式。
 
 ***
-[← 超时](5-Timeout-CN.md) | 调试[(English)](6-Debug-EN.md) | [并发 →](7-Concurrent-CN.md)
+[← 超时](5-Timeout-CN.md) | 调试[(English)](6-Debug-EN.md) | [日志 →](7-Logger-CN.md)

+ 2 - 2
docs/6-Debug-EN.md

@@ -1,8 +1,8 @@
-[← Timeout](5-Timeout-EN.md) | Debug[(中文)](6-Debug-CN.md) | [Concurrent →](7-Concurrent-EN.md)
+[← Timeout](5-Timeout-EN.md) | Debug[(中文)](6-Debug-CN.md) | [Logger →](7-Logger-EN.md)
 ***
 
 # Debugging
 If there is an environment variable `DEBUG=sdk` , all requests will enable debug mode.
 
 ***
-[← Timeout](5-Timeout-EN.md) | Debug[(中文)](6-Debug-CN.md) | [Concurrent →](7-Concurrent-EN.md)
+[← Timeout](5-Timeout-EN.md) | Debug[(中文)](6-Debug-CN.md) | [Logger →](7-Logger-EN.md)

+ 55 - 0
docs/7-Logger-CN.md

@@ -0,0 +1,55 @@
+[← 调试](6-Debug-CN.md) | 日志[(English)](7-Logger-EN.md) | [并发 →](8-Concurrent-CN.md)
+***
+
+# 日志
+
+## 描述
+
+logger 主要用于提供支持审计的能力,用于记录每次的调用情况,类似服务端的 access log。
+
+## 使用
+
+### 初始化日志
+
+如果您想要使用日志功能,您需要先初始化一个日志对象,您可以在初始化日志对象的时候设置日志等级,日志模版, 日志的输出路径以及 channel。
+```go
+// level: 默认为 info
+// channel: 默认为 AlibabaCloud
+// file: 一个实现了 io.writer 接口的对象
+// templete: 日志的模板, 若不输入,则默认为 `{time} {channel}: "{method} {uri} HTTP/{version}" {code} {cost} {hostname}`
+client.SetLogger("level", "channel", file, templete)      // 设置客户端的日志, 当您调用该方法,默认为您开启日志功能
+```
+
+### 相关操作
+
+```go
+logger := client.GetLogger()    // 获取客户端的 logger 
+client.OpenLogger()            // 开启日志功能,若此时客户端的 logger 不存在, 则创建一个配置一个默认的 logger
+client.CloseLogger()           // 关闭日志功能
+client.GetLoggerMsg()          // 获取上一条日志信息,若此时客户端的 logger 不存在, 则创建一个配置一个默认的 logger 
+client.SetTemplate(templete)   // 设置日志模板,若此时客户端的 logger 不存在, 则创建一个配置一个默认的 logger
+client.GetTemplate()           // 获取当前的日志模板,若此时客户端的 logger 不存在, 则创建一个配置一个默认的 logger
+```
+
+### 变量
+
+|    变量    |   描述    |
+|----------|-------------|
+| {channel}     | 日志的对象 |
+| {host}     | 请求主机 |
+| {ts}     | GMT中的 ISO 8601日期 |
+| {method}     | 请求方法 |
+| {uri}     | 请求的URI |
+| {version}     | 协议版本 |
+| {target}     | 请求目标 (path + query) |
+| {hostname}     | 发送请求的计算机的主机名 |
+| {code}     | 响应的状态代码(如果可用) |
+| {error}     | 任何错误消息(如果有) |
+| {req_headers}     | 请求头 |
+| {res_headers}     | 响应头 |
+| {pid}     | PID |
+| {cost}     | 耗时 |
+| {start_time}  | 开始时间 |
+
+***
+[← 调试](6-Debug-CN.md) | 日志[(English)](7-Logger-EN.md) | [并发 →](8-Concurrent-CN.md)

+ 56 - 0
docs/7-Logger-EN.md

@@ -0,0 +1,56 @@
+[← Debug](6-Debug-EN.md) | Logger[(中文)](7-Logger-CN.md) | [Concurrent →](8-Concurrent-EN.md)
+***
+
+
+# Logger
+
+## Description
+
+The logger is mainly used to provide support for auditing, to record each call, similar to the server's access log.
+
+## Using
+
+### Init logger
+
+If you want to use the log function, you need to initialize a log object first. You can set the log level, log template, log output path and channel when initializing the log object.
+```go
+// level: default value is info
+// channel: default value is AlibabaCloud
+// file: should be an object that implements the io.writer interface
+// templete: logger template, If not entered, the default value is `{time} {channel}: "{method} {uri} HTTP/{version}" {code} {cost} {hostname}`
+client.SetLogger("level", "channel", file, templete)      // Set the client's log. When you call this method, the log function is enabled by default.
+```
+
+### Related operations
+
+```go
+logger := client.GetLogger()    // Get client logger 
+client.OpenLogger()            // Open logger, if clien logger is not exist, there will create a default logger for client
+client.CloseLogger()           // Close logger
+client.GetLoggerMsg()          // Get last logger message,if clien logger is not exist, there will create a default logger for client
+client.SetTemplate(templete)   // Set client logger template,if clien logger is not exist, there will create a default logger for client
+client.GetTemplate()           // Get client logger template,if clien logger is not exist, there will create a default logger for client
+```
+
+### Variables
+
+| Variables |  Description  |
+|----------|-------------|
+| {channel}     | name of the log |
+| {host}     | Host of the request |
+| {ts}     | GMT中的 ISO 8601日期 |
+| {method}     | Method of the request |
+| {uri}     | URI of the request |
+| {version}     | Protocol version |
+| {target}     | Request target of the request (path + query) |
+| {hostname}     | Hostname of the machine that sent the request |
+| {code}     | Status code of the response (if available) |
+| {error}     | Any error messages (if available) |
+| {req_headers}     | Request headers |
+| {res_headers}     | Response headers |
+| {pid}     | PID |
+| {cost}     | Cost Time |
+| {start_time}     | start Time |
+
+***
+[← Debug](6-Debug-EN.md) | Logger[(中文)](7-Logger-CN.md) | [Concurrent →](8-Concurrent-EN.md)

+ 2 - 2
docs/7-Concurrent-CN.md → docs/8-Concurrent-CN.md

@@ -1,4 +1,4 @@
-[← 调试](6-Debug-CN.md) | 并发[(English)](7-Concurrent-EN.md) | [异步调用 →](8-Asynchronous-CN.md)
+[← 日志](7-Logger-CN.md) | 并发[(English)](8-Concurrent-EN.md) | [异步调用 →](9-Asynchronous-CN.md)
 ***
 
 ## 并发请求
@@ -26,4 +26,4 @@ client.EnableAsync(poolSize, maxTaskQueueSize)
 ```
 
 ***
-[← 调试](6-Debug-CN.md) | 并发[(English)](7-Concurrent-EN.md) | [异步调用 →](8-Asynchronous-CN.md)
+[← 日志](7-Logger-CN.md) | 并发[(English)](8-Concurrent-EN.md) | [异步调用 →](9-Asynchronous-CN.md)

+ 2 - 2
docs/7-Concurrent-EN.md → docs/8-Concurrent-EN.md

@@ -1,4 +1,4 @@
-[← Debug](6-Debug-EN.md) | Concurrent[(中文)](7-Concurrent-CN.md) | [Asynchronous Call →](8-Asynchronous-EN.md)
+[← Logger](7-Logger-EN.md) | Concurrent[(中文)](8-Concurrent-CN.md) | [Asynchronous Call →](9-Asynchronous-EN.md)
 ***
 
 ## Concurrent Request
@@ -26,4 +26,4 @@ client.EnableAsync(poolSize, maxTaskQueueSize)
 ```
 
 ***
-[← Debug](6-Debug-EN.md) | Concurrent[(中文)](7-Concurrent-CN.md) | [Asynchronous Call →](8-Asynchronous-EN.md)
+[← Logger](7-Logger-EN.md) | Concurrent[(中文)](8-Concurrent-CN.md) | [Asynchronous Call →](9-Asynchronous-EN.md)

+ 2 - 2
docs/8-Asynchronous-CN.md → docs/9-Asynchronous-CN.md

@@ -1,4 +1,4 @@
-[← 并发](7-Concurrent-CN.md) | 异步调用[(English)](8-Asynchronous-EN.md) | [首页 →](../README-CN.md)
+[← 并发](8-Concurrent-CN.md) | 异步调用[(English)](9-Asynchronous-EN.md) | [首页 →](../README-CN.md)
 ***
 ## 异步调用
 
@@ -27,4 +27,4 @@ Alibaba Cloud SDK for Go 支持两种方式的异步调用:
     ```
 
 ***
-[← 并发](7-Concurrent-CN.md) | 异步调用[(English)](8-Asynchronous-EN.md) | [首页 →](../README-CN.md)
+[← 并发](8-Concurrent-CN.md) | 异步调用[(English)](9-Asynchronous-EN.md) | [首页 →](../README-CN.md)

+ 2 - 2
docs/8-Asynchronous-EN.md → docs/9-Asynchronous-EN.md

@@ -1,4 +1,4 @@
-[← Concurrent](7-Concurrent-EN.md) | Asynchronous Call[(中文)](8-Asynchronous-CN.md) | [Home →](../README.md)
+[← Concurrent](8-Concurrent-EN.md) | Asynchronous Call[(中文)](9-Asynchronous-CN.md) | [Home →](../README.md)
 ***
 ## Asynchronous Call
 
@@ -27,4 +27,4 @@ Alibaba Cloud Go SDK supports asynchronous calls in two ways:
     ```
 
 ***
-[← Concurrent](7-Concurrent-EN.md) | Asynchronous Call[(中文)](8-Asynchronous-CN.md) | [Home →](../README.md)
+[← Concurrent](8-Concurrent-EN.md) | Asynchronous Call[(中文)](9-Asynchronous-CN.md) | [Home →](../README.md)

+ 7 - 0
integration/core_test.go

@@ -158,8 +158,14 @@ func Test_DescribeClustersWithCommonRequestWithROAWithSTStoken(t *testing.T) {
 	request.PathPattern = "/clusters/[ClusterId]"
 	request.QueryParams["RegionId"] = os.Getenv("REGION_ID")
 	request.TransToAcsRequest()
+	f1, err := os.Create("test.txt")
+	defer os.Remove("test.txt")
+	assert.Nil(t, err)
+	templete := `{version}, {host}`
+	client.NewLogger("error", "Alibaba", f1, templete)
 	_, err = client.ProcessCommonRequest(request)
 	assert.NotNil(t, err)
+	assert.Contains(t, client.GetLoggerMsg(), `1.1, cs.aliyuncs.com`)
 	assert.Contains(t, err.Error(), "Request url is invalid")
 }
 
@@ -174,6 +180,7 @@ func Test_DescribeClusterDetailWithCommonRequestWithROAWithHTTPS(t *testing.T) {
 	request.PathPattern = "/clusters/[ClusterId]"
 	request.QueryParams["RegionId"] = os.Getenv("REGION_ID")
 	request.TransToAcsRequest()
+
 	_, err = client.ProcessCommonRequest(request)
 	assert.NotNil(t, err)
 	assert.Contains(t, err.Error(), "Request url is invalid")

+ 26 - 1
sdk/client.go

@@ -50,6 +50,7 @@ var Version = "0.0.1"
 var defaultConnectTimeout = 5 * time.Second
 var defaultReadTimeout = 10 * time.Second
 
+var fieldMap = make(map[string]string)
 var DefaultUserAgent = fmt.Sprintf("AlibabaCloud (%s; %s) Golang/%s Core/%s", runtime.GOOS, runtime.GOARCH, strings.Trim(runtime.Version(), "go"), Version)
 
 var hookDo = func(fn func(req *http.Request) (*http.Response, error)) func(req *http.Request) (*http.Response, error) {
@@ -64,6 +65,7 @@ type Client struct {
 	httpProxy      string
 	httpsProxy     string
 	noProxy        string
+	logger         *Logger
 	userAgent      map[string]string
 	signer         auth.Signer
 	httpClient     *http.Client
@@ -450,10 +452,16 @@ func (client *Client) getHTTPSInsecure(request requests.AcsRequest) (insecure bo
 }
 
 func (client *Client) DoActionWithSigner(request requests.AcsRequest, response responses.AcsResponse, signer auth.Signer) (err error) {
+
+	defer func() {
+		client.printLog(err)
+	}()
+	initLogMsg()
 	httpRequest, err := client.buildRequestWithSigner(request, signer)
 	if err != nil {
 		return
 	}
+
 	client.setTimeout(request)
 	proxy, err := client.getHttpProxy(httpRequest.URL.Scheme)
 	if err != nil {
@@ -484,19 +492,36 @@ func (client *Client) DoActionWithSigner(request requests.AcsRequest, response r
 
 	var httpResponse *http.Response
 	for retryTimes := 0; retryTimes <= client.config.MaxRetryTime; retryTimes++ {
-		if proxy != nil && proxy.User != nil{
+		if proxy != nil && proxy.User != nil {
 			if password, passwordSet := proxy.User.Password(); passwordSet {
 				httpRequest.SetBasicAuth(proxy.User.Username(), password)
 			}
 		}
+		if retryTimes > 0 {
+			client.printLog(err)
+		}
 		debug("> %s %s %s", httpRequest.Method, httpRequest.URL.RequestURI(), httpRequest.Proto)
 		debug("> Host: %s", httpRequest.Host)
+		fieldMap["{host}"] = httpRequest.Host
+		fieldMap["{method}"] = httpRequest.Method
+		fieldMap["{uri}"] = httpRequest.URL.RequestURI()
+		fieldMap["{pid}"] = strconv.Itoa(os.Getpid())
+		fieldMap["{version}"] = strings.Split(httpRequest.Proto, "/")[1]
+		hostname, _ := os.Hostname()
+		fieldMap["{hostname}"] = hostname
+		fieldMap["{req_headers}"] = TransToString(httpRequest.Header)
+		fieldMap["{target}"] = httpRequest.URL.Path + httpRequest.URL.RawQuery
 		for key, value := range httpRequest.Header {
 			debug("> %s: %v", key, strings.Join(value, ""))
 		}
 		debug(">")
+		startTime := time.Now()
+		fieldMap["{start_time}"] = startTime.Format("2006-01-02 15:04:05")
 		httpResponse, err = hookDo(client.httpClient.Do)(httpRequest)
+		fieldMap["{cost}"] = time.Now().Sub(startTime).String()
 		if err == nil {
+			fieldMap["{code}"] = strconv.Itoa(httpResponse.StatusCode)
+			fieldMap["{res_headers}"] = TransToString(httpResponse.Header)
 			debug("< %s %s", httpResponse.Proto, httpResponse.Status)
 			for key, value := range httpResponse.Header {
 				debug("< %s: %v", key, strings.Join(value, ""))

+ 53 - 0
sdk/client_test.go

@@ -429,6 +429,59 @@ func Test_DoAction_With500(t *testing.T) {
 	assert.Equal(t, "Server Internel Error", response.GetHttpContentString())
 }
 
+func Test_DoAction_WithLogger(t *testing.T) {
+	client, err := NewClientWithAccessKey("regionid", "acesskeyid", "accesskeysecret")
+	assert.Nil(t, err)
+	assert.NotNil(t, client)
+	assert.Equal(t, true, client.isRunning)
+	request := requests.NewCommonRequest()
+	request.Version = "2014-05-26"
+	request.ApiName = "DescribeInstanceStatus"
+	request.TransToAcsRequest()
+	response := responses.NewCommonResponse()
+	ts := mockServer(500, "Server Internel Error")
+	defer ts.Close()
+	domain := strings.Replace(ts.URL, "http://", "", 1)
+	request.Domain = domain
+	f1, err := os.Create("test.txt")
+	defer os.Remove("test.txt")
+	assert.Nil(t, err)
+
+	// Test when set logger, it will create a new client logger.
+	client.SetLogger("error", "Alibaba", f1, "")
+	err = client.DoAction(request, response)
+	assert.NotNil(t, err)
+	log := client.GetLogger()
+	assert.Contains(t, client.GetLoggerMsg(), "Alibaba: \"GET /?AccessKeyId=acesskeyid&Action=DescribeInstanceStatus&Format=JSON&RegionId=regionid")
+	assert.Equal(t, 500, response.GetHttpStatus())
+	assert.Equal(t, true, log.isOpen)
+	assert.Equal(t, "Server Internel Error", response.GetHttpContentString())
+
+	// Test when close logger, it will not print log.
+	client.CloseLogger()
+	err = client.DoAction(request, response)
+	assert.NotNil(t, err)
+	log = client.GetLogger()
+	assert.Equal(t, 500, response.GetHttpStatus())
+	assert.Equal(t, false, log.isOpen)
+	assert.Equal(t, "{time} {channel}: \"{method} {uri} HTTP/{version}\" {code} {cost} {hostname}", client.GetTemplate())
+	assert.Contains(t, client.GetLoggerMsg(), `GET /?AccessKeyId=acesskeyid&Action=DescribeInstanceStatus&Format=JSON&RegionId=regionid`)
+	assert.Equal(t, "Server Internel Error", response.GetHttpContentString())
+
+	// Test when open logger, it will print log.
+	client.OpenLogger()
+	template := "{channel}: \"{method} {code}"
+	client.SetTemplate(template)
+	err = client.DoAction(request, response)
+	assert.NotNil(t, err)
+	log = client.GetLogger()
+	assert.Equal(t, 500, response.GetHttpStatus())
+	assert.Equal(t, true, log.isOpen)
+	assert.Equal(t, "{channel}: \"{method} {code}", client.GetTemplate())
+	assert.Contains(t, client.GetLoggerMsg(), `Alibaba: "GET 500`)
+	assert.Equal(t, "Server Internel Error", response.GetHttpContentString())
+}
+
 func TestClient_BuildRequestWithSigner(t *testing.T) {
 	client, err := NewClientWithAccessKey("regionid", "acesskeyid", "accesskeysecret")
 	assert.Nil(t, err)

+ 116 - 0
sdk/logger.go

@@ -0,0 +1,116 @@
+package sdk
+
+import (
+	"encoding/json"
+	"github.com/aliyun/alibaba-cloud-sdk-go/sdk/utils"
+	"io"
+	"log"
+	"os"
+	"strings"
+	"time"
+)
+
+var logChannel string
+var defaultChannel = "AlibabaCloud"
+
+type Logger struct {
+	*log.Logger
+	formatTemplate string
+	isOpen         bool
+	lastLogMsg     string
+}
+
+var defaultLoggerTemplate = `{time} {channel}: "{method} {uri} HTTP/{version}" {code} {cost} {hostname}`
+var loggerParam = []string{"{time}", "{start_time}", "{ts}", "{channel}", "{pid}", "{host}", "{method}", "{uri}", "{version}", "{target}", "{hostname}", "{code}", "{error}", "{req_headers}", "{res_headers}", "{cost}"}
+
+func initLogMsg() {
+	for _, value := range loggerParam {
+		fieldMap[value] = ""
+	}
+}
+
+func (client *Client) GetLogger() *Logger {
+	return client.logger
+}
+
+func (client *Client) GetLoggerMsg() string {
+	if client.logger == nil {
+		client.SetLogger("", "", os.Stdout, "")
+	}
+	return client.logger.lastLogMsg
+}
+
+func (client *Client) SetLogger(level string, channel string, out io.Writer, template string) {
+	if level == "" {
+		level = "info"
+	}
+
+	logChannel = "AlibabaCloud"
+	if channel != "" {
+		logChannel = channel
+	}
+	log := log.New(out, "["+strings.ToUpper(level)+"]", log.Lshortfile)
+	if template == "" {
+		template = defaultLoggerTemplate
+	}
+
+	client.logger = &Logger{
+		Logger:         log,
+		formatTemplate: template,
+		isOpen:         true,
+	}
+}
+
+func (client *Client) OpenLogger() {
+	if client.logger == nil {
+		client.SetLogger("", "", os.Stdout, "")
+	}
+	client.logger.isOpen = true
+}
+
+func (client *Client) CloseLogger() {
+	if client.logger != nil {
+		client.logger.isOpen = false
+	}
+}
+
+func (client *Client) SetTemplate(template string) {
+	if client.logger == nil {
+		client.SetLogger("", "", os.Stdout, "")
+	}
+	client.logger.formatTemplate = template
+}
+
+func (client *Client) GetTemplate() string {
+	if client.logger == nil {
+		client.SetLogger("", "", os.Stdout, "")
+	}
+	return client.logger.formatTemplate
+}
+
+func TransToString(object interface{}) string {
+	byt, err := json.Marshal(object)
+	if err != nil {
+		return ""
+	}
+	return string(byt)
+}
+
+func (client *Client) printLog(err error) {
+	if err != nil {
+		fieldMap["{error}"] = err.Error()
+	}
+	fieldMap["{time}"] = time.Now().Format("2006-01-02 15:04:05")
+	fieldMap["{ts}"] = utils.GetTimeInFormatISO8601()
+	fieldMap["{channel}"] = logChannel
+	if client.logger != nil {
+		logMsg := client.logger.formatTemplate
+		for key, value := range fieldMap {
+			logMsg = strings.Replace(logMsg, key, value, -1)
+		}
+		client.logger.lastLogMsg = logMsg
+		if client.logger.isOpen == true {
+			client.logger.Output(2, logMsg)
+		}
+	}
+}

+ 45 - 0
sdk/logger_test.go

@@ -0,0 +1,45 @@
+package sdk
+
+import (
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+func Test_OpenLogger(t *testing.T) {
+	client, err := NewClientWithAccessKey("regionid", "acesskeyid", "accesskeysecret")
+	assert.Nil(t, err)
+	assert.NotNil(t, client)
+	assert.Equal(t, true, client.isRunning)
+
+	client.OpenLogger()
+	assert.Equal(t, true, client.logger.isOpen)
+}
+
+func Test_SetTemplate(t *testing.T) {
+	client, err := NewClientWithAccessKey("regionid", "acesskeyid", "accesskeysecret")
+	assert.Nil(t, err)
+	assert.NotNil(t, client)
+	assert.Equal(t, true, client.isRunning)
+
+	template := "{time}"
+	client.SetTemplate(template)
+	assert.Equal(t, "{time}", client.logger.formatTemplate)
+}
+
+func Test_GetTemplate(t *testing.T) {
+	client, err := NewClientWithAccessKey("regionid", "acesskeyid", "accesskeysecret")
+	assert.Nil(t, err)
+	assert.NotNil(t, client)
+	assert.Equal(t, true, client.isRunning)
+
+	assert.Equal(t, defaultLoggerTemplate, client.GetTemplate())
+}
+
+func Test_GetLoggerMsg(t *testing.T) {
+	client, err := NewClientWithAccessKey("regionid", "acesskeyid", "accesskeysecret")
+	assert.Nil(t, err)
+	assert.NotNil(t, client)
+	assert.Equal(t, true, client.isRunning)
+
+	assert.Equal(t, "", client.GetLoggerMsg())
+}