#79 record polling history  (#94)

* record polling history

* fix bugs and use chan get body

* add unit test

* rename by comment

* use context instead of chan

* use const instead string

* modify as comment
diff --git a/deployments/db.js b/deployments/db.js
index 1fa2520..0c8acb2 100644
--- a/deployments/db.js
+++ b/deployments/db.js
@@ -96,11 +96,53 @@
             }
         } }
 } );
+
+db.createCollection( "polling_detail", {
+    validator: { $jsonSchema: {
+            bsonType: "object",
+            required: [ "id","params","session_id","url_path" ],
+            properties: {
+                id: {
+                    bsonType: "string",
+                },
+                session_id: {
+                    bsonType: "string",
+                },
+                domain: {
+                    bsonType: "string",
+                },
+                params: {
+                    bsonType: "string"
+                },
+                ip: {
+                    bsonType: "string"
+                },
+                user_agent: {
+                    bsonType: "string"
+                },
+                url_path: {
+                    bsonType: "string"
+                },
+                response_body: {
+                    bsonType: "object"
+                },
+                response_header: {
+                    bsonType: "object"
+                },
+                response_code: {
+                    bsonType: "string"
+                }
+            }
+        } }
+} );
+
 //index
 db.kv.createIndex({"id": 1}, { unique: true } );
 db.kv.createIndex({key: 1, label_id: 1,domain:1,project:1},{ unique: true });
 db.label.createIndex({"id": 1}, { unique: true } );
 db.label.createIndex({format: 1,domain:1,project:1},{ unique: true });
+db.polling_detail.createIndex({"id": 1}, { unique: true } );
+db.polling_detail.createIndex({session:1,domain:1}, { unique: true } );
 db.view.createIndex({"id": 1}, { unique: true } );
 db.view.createIndex({display:1,domain:1,project:1},{ unique: true });
 //db config
diff --git a/pkg/common/common.go b/pkg/common/common.go
index a6bd6bb..4f2f34a 100644
--- a/pkg/common/common.go
+++ b/pkg/common/common.go
@@ -58,6 +58,7 @@
 	MsgInvalidWait      = "wait param should be formed with number and time unit like 5s,100ms, and less than 5m"
 	MsgInvalidRev       = "revision param should be formed with number greater than 0"
 	ErrKvIDMustNotEmpty = "must supply kv id if you want to remove key"
+	RespBodyContextKey  = "responseBody"
 
 	MaxWait = 5 * time.Minute
 )
diff --git a/pkg/iputil/ip_util.go b/pkg/iputil/ip_util.go
new file mode 100644
index 0000000..7e21854
--- /dev/null
+++ b/pkg/iputil/ip_util.go
@@ -0,0 +1,24 @@
+package iputil
+
+import (
+	"net"
+	"net/http"
+	"strings"
+)
+
+//ClientIP try to get ip from http header
+func ClientIP(r *http.Request) string {
+	xForwardedFor := r.Header.Get("X-Forwarded-For")
+	ip := strings.TrimSpace(strings.Split(xForwardedFor, ",")[0])
+	if ip != "" {
+		return ip
+	}
+	ip = strings.TrimSpace(r.Header.Get("X-Real-Ip"))
+	if ip != "" {
+		return ip
+	}
+	if ip, _, err := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr)); err == nil {
+		return ip
+	}
+	return ""
+}
diff --git a/pkg/model/kv.go b/pkg/model/kv.go
index ac92854..d898329 100644
--- a/pkg/model/kv.go
+++ b/pkg/model/kv.go
@@ -52,3 +52,17 @@
 	Total int        `json:"total,omitempty"`
 	Data  []*ViewDoc `json:"data,omitempty"`
 }
+
+//PollingDetail record operation history
+type PollingDetail struct {
+	ID             string                 `json:"id,omitempty" yaml:"id,omitempty"`
+	SessionID      string                 `json:"session_id,omitempty" yaml:"session_id,omitempty"`
+	Domain         string                 `json:"domain,omitempty" yaml:"domain,omitempty"`
+	PollingData    map[string]interface{} `json:"params,omitempty" yaml:"params,omitempty"`
+	IP             string                 `json:"ip,omitempty" yaml:"ip,omitempty"`
+	UserAgent      string                 `json:"user_agent,omitempty" yaml:"user_agent,omitempty"`
+	URLPath        string                 `json:"url_path,omitempty" yaml:"url_path,omitempty"`
+	ResponseBody   interface{}            `json:"response_body,omitempty" yaml:"response_body,omitempty"`
+	ResponseHeader map[string][]string    `json:"response_header,omitempty" yaml:"response_header,omitempty"`
+	ResponseCode   int                    `json:"response_code,omitempty" yaml:"response_code,omitempty"`
+}
diff --git a/server/resource/v1/common.go b/server/resource/v1/common.go
index b99dc80..e121d9b 100644
--- a/server/resource/v1/common.go
+++ b/server/resource/v1/common.go
@@ -243,6 +243,7 @@
 	}
 	rctx.ReadResponseWriter().Header().Set(common.HeaderRevision, strconv.FormatInt(rev, 10))
 	err = writeResponse(rctx, kv)
+	rctx.Ctx = context.WithValue(rctx.Ctx, common.RespBodyContextKey, kv)
 	if err != nil {
 		openlogging.Error(err.Error())
 	}
diff --git a/server/resource/v1/kv_resource.go b/server/resource/v1/kv_resource.go
index 0054025..5db4912 100644
--- a/server/resource/v1/kv_resource.go
+++ b/server/resource/v1/kv_resource.go
@@ -21,12 +21,15 @@
 import (
 	"fmt"
 	"github.com/apache/servicecomb-kie/pkg/common"
+	"github.com/apache/servicecomb-kie/pkg/iputil"
 	"github.com/apache/servicecomb-kie/pkg/model"
 	"github.com/apache/servicecomb-kie/server/pubsub"
 	"github.com/apache/servicecomb-kie/server/service"
+	"github.com/apache/servicecomb-kie/server/service/mongo/record"
 	goRestful "github.com/emicklei/go-restful"
 	"github.com/go-chassis/go-chassis/server/restful"
 	"github.com/go-mesh/openlogging"
+	uuid "github.com/satori/go.uuid"
 	"net/http"
 )
 
@@ -108,13 +111,14 @@
 		WriteErrResponse(rctx, http.StatusBadRequest, err.Error(), common.ContentTypeText)
 		return
 	}
+	insID := rctx.ReadHeader("sessionID")
 	statusStr := rctx.ReadQueryParameter("status")
 	status, err := checkStatus(statusStr)
 	if err != nil {
 		WriteErrResponse(rctx, http.StatusBadRequest, err.Error(), common.ContentTypeText)
 		return
 	}
-	returnData(rctx, domain, project, labels, pageNum, pageSize, status)
+	returnData(rctx, domain, project, labels, pageNum, pageSize, status, insID)
 }
 
 //List response kv list
@@ -138,18 +142,22 @@
 		WriteErrResponse(rctx, http.StatusBadRequest, err.Error(), common.ContentTypeText)
 		return
 	}
+	sessionID := rctx.ReadHeader("sessionID")
 	statusStr := rctx.ReadQueryParameter("status")
 	status, err := checkStatus(statusStr)
 	if err != nil {
 		WriteErrResponse(rctx, http.StatusBadRequest, err.Error(), common.ContentTypeText)
 		return
 	}
-	returnData(rctx, domain, project, labels, pageNum, pageSize, status)
+	returnData(rctx, domain, project, labels, pageNum, pageSize, status, sessionID)
 }
 
-func returnData(rctx *restful.Context, domain interface{}, project string, labels map[string]string, pageNum, pageSize int64, status string) {
+func returnData(rctx *restful.Context, domain interface{}, project string, labels map[string]string, pageNum, pageSize int64, status, sessionID string) {
 	revStr := rctx.ReadQueryParameter(common.QueryParamRev)
 	wait := rctx.ReadQueryParameter(common.QueryParamWait)
+	if sessionID != "" {
+		defer RecordPollingDetail(rctx, revStr, wait, domain.(string), project, labels, pageNum, pageSize, sessionID)
+	}
 	if revStr == "" {
 		if wait == "" {
 			queryAndResponse(rctx, domain, project, "", labels, pageNum, pageSize, status)
@@ -206,6 +214,35 @@
 	}
 }
 
+//RecordPollingDetail to record data after get or list
+func RecordPollingDetail(context *restful.Context, revStr, wait, domain, project string, labels map[string]string, limit, offset int64, sessionID string) {
+	data := &model.PollingDetail{}
+	data.ID = uuid.NewV4().String()
+	data.SessionID = sessionID
+	data.Domain = domain
+	data.IP = iputil.ClientIP(context.Req.Request)
+	dataMap := map[string]interface{}{
+		"revStr":  revStr,
+		"wait":    wait,
+		"domain":  domain,
+		"project": project,
+		"labels":  labels,
+		"limit":   limit,
+		"offset":  offset,
+	}
+	data.PollingData = dataMap
+	data.UserAgent = context.Req.HeaderParameter("User-Agent")
+	data.URLPath = context.ReadRequest().Method + " " + context.ReadRequest().URL.Path
+	data.ResponseHeader = context.Resp.Header()
+	data.ResponseCode = context.Resp.StatusCode()
+	data.ResponseBody = context.Ctx.Value(common.RespBodyContextKey)
+	_, err := record.CreateOrUpdate(context.Ctx, data)
+	if err != nil {
+		openlogging.Warn("record polling detail failed" + err.Error())
+		return
+	}
+}
+
 //Search search key only by label
 func (r *KVResource) Search(context *restful.Context) {
 	var err error
diff --git a/server/resource/v1/kv_resource_test.go b/server/resource/v1/kv_resource_test.go
index b0370b2..93dea44 100644
--- a/server/resource/v1/kv_resource_test.go
+++ b/server/resource/v1/kv_resource_test.go
@@ -76,6 +76,7 @@
 		noopH := &handler2.NoopAuthHandler{}
 		chain, _ := handler.CreateChain(common.Provider, "testchain1", noopH.Name())
 		r.Header.Set("Content-Type", "application/json")
+		r.Header.Set("sessionID", "test")
 		kvr := &v1.KVResource{}
 		c, _ := restfultest.New(kvr, chain)
 		resp := httptest.NewRecorder()
@@ -101,6 +102,7 @@
 		noopH := &handler2.NoopAuthHandler{}
 		chain, _ := handler.CreateChain(common.Provider, "testchain1", noopH.Name())
 		r.Header.Set("Content-Type", "application/json")
+		r.Header.Set("sessionID", "test")
 		kvr := &v1.KVResource{}
 		c, _ := restfultest.New(kvr, chain)
 		resp := httptest.NewRecorder()
diff --git a/server/service/mongo/kv/kv_test.go b/server/service/mongo/kv/kv_test.go
index 69afb3a..b08c1c0 100644
--- a/server/service/mongo/kv/kv_test.go
+++ b/server/service/mongo/kv/kv_test.go
@@ -28,6 +28,7 @@
 	"testing"
 )
 
+//
 func TestService_CreateOrUpdate(t *testing.T) {
 	var err error
 	config.Configurations = &config.Config{DB: config.DB{URI: "mongodb://kie:123@127.0.0.1:27017/kie"}}
@@ -43,7 +44,7 @@
 				"service": "cart",
 			},
 			Domain:  "default",
-			Project: "test",
+			Project: "kv-test",
 		})
 		assert.NoError(t, err)
 		assert.NotEmpty(t, kv.ID)
@@ -58,9 +59,9 @@
 				"version": "1.0.0",
 			},
 			Domain:  "default",
-			Project: "test",
+			Project: "kv-test",
 		})
-		oid, err := kvsvc.Exist(context.TODO(), "default", "timeout", "test", service.WithLabels(map[string]string{
+		oid, err := kvsvc.Exist(context.TODO(), "default", "timeout", "kv-test", service.WithLabels(map[string]string{
 			"app":     "mall",
 			"service": "cart",
 			"version": "1.0.0",
@@ -78,10 +79,10 @@
 				"app": "mall",
 			},
 			Domain:  "default",
-			Project: "test",
+			Project: "kv-test",
 		})
 		assert.NoError(t, err)
-		kvs1, err := kvsvc.FindKV(context.Background(), "default", "test",
+		kvs1, err := kvsvc.FindKV(context.Background(), "default", "kv-test",
 			service.WithKey("timeout"),
 			service.WithLabels(map[string]string{
 				"app": "mall",
@@ -95,15 +96,15 @@
 				"app": "mall",
 			},
 			Domain:  "default",
-			Project: "test",
+			Project: "kv-test",
 		})
 		assert.Equal(t, "3s", afterKV.Value)
-		savedKV, err := kvsvc.Exist(context.Background(), "default", "timeout", "test", service.WithLabels(map[string]string{
+		savedKV, err := kvsvc.Exist(context.Background(), "default", "timeout", "kv-test", service.WithLabels(map[string]string{
 			"app": "mall",
 		}))
 		assert.NoError(t, err)
 		assert.Equal(t, afterKV.Value, savedKV.Value)
-		kvs, err := kvsvc.FindKV(context.Background(), "default", "test",
+		kvs, err := kvsvc.FindKV(context.Background(), "default", "kv-test",
 			service.WithKey("timeout"),
 			service.WithLabels(map[string]string{
 				"app": "mall",
@@ -117,7 +118,7 @@
 func TestService_FindKV(t *testing.T) {
 	kvsvc := &kv.Service{}
 	t.Run("exact find by kv and labels with label app", func(t *testing.T) {
-		kvs, err := kvsvc.FindKV(context.Background(), "default", "test",
+		kvs, err := kvsvc.FindKV(context.Background(), "default", "kv-test",
 			service.WithKey("timeout"),
 			service.WithLabels(map[string]string{
 				"app": "mall",
@@ -127,7 +128,7 @@
 		assert.Equal(t, 1, len(kvs))
 	})
 	t.Run("greedy find by labels,with labels app ans service ", func(t *testing.T) {
-		kvs, err := kvsvc.FindKV(context.Background(), "default", "test",
+		kvs, err := kvsvc.FindKV(context.Background(), "default", "kv-test",
 			service.WithLabels(map[string]string{
 				"app":     "mall",
 				"service": "cart",
@@ -146,20 +147,20 @@
 				"env": "test",
 			},
 			Domain:  "default",
-			Project: "test",
+			Project: "kv-test",
 		})
 		assert.NoError(t, err)
 
-		err = kvsvc.Delete(context.TODO(), kv1.ID, "default", "test")
+		err = kvsvc.Delete(context.TODO(), kv1.ID, "default", "kv-test")
 		assert.NoError(t, err)
 
 	})
 	t.Run("miss id", func(t *testing.T) {
-		err := kvsvc.Delete(context.TODO(), "", "default", "test")
+		err := kvsvc.Delete(context.TODO(), "", "default", "kv-test")
 		assert.Error(t, err)
 	})
 	t.Run("miss domain", func(t *testing.T) {
-		err := kvsvc.Delete(context.TODO(), "2", "", "test")
+		err := kvsvc.Delete(context.TODO(), "2", "", "kv-test")
 		assert.Equal(t, session.ErrMissingDomain, err)
 	})
 }
diff --git a/server/service/mongo/record/polling_detail_dao.go b/server/service/mongo/record/polling_detail_dao.go
new file mode 100644
index 0000000..3e76bcb
--- /dev/null
+++ b/server/service/mongo/record/polling_detail_dao.go
@@ -0,0 +1,48 @@
+/*
+ * 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 record
+
+import (
+	"context"
+	"github.com/apache/servicecomb-kie/pkg/model"
+	"github.com/apache/servicecomb-kie/server/service/mongo/session"
+	"go.mongodb.org/mongo-driver/bson"
+	"go.mongodb.org/mongo-driver/mongo"
+)
+
+//CreateOrUpdate create a record or update exist record
+func CreateOrUpdate(ctx context.Context, detail *model.PollingDetail) (*model.PollingDetail, error) {
+	collection := session.GetDB().Collection(session.CollectionPollingDetail)
+	queryFilter := bson.M{"domain": detail.Domain, "session_id": detail.SessionID}
+	res := collection.FindOne(ctx, queryFilter)
+	if res.Err() != nil {
+		if res.Err() == mongo.ErrNoDocuments {
+			_, err := collection.InsertOne(ctx, detail)
+			if err != nil {
+				return nil, err
+			}
+			return detail, nil
+		}
+		return nil, res.Err()
+	}
+	_, err := collection.UpdateOne(ctx, queryFilter, detail)
+	if err != nil {
+		return nil, err
+	}
+	return detail, nil
+}
diff --git a/server/service/mongo/session/session.go b/server/service/mongo/session/session.go
index 246e164..40af57d 100644
--- a/server/service/mongo/session/session.go
+++ b/server/service/mongo/session/session.go
@@ -42,13 +42,14 @@
 const (
 	DBName = "kie"
 
-	CollectionLabel      = "label"
-	CollectionKV         = "kv"
-	CollectionKVRevision = "kv_revision"
-	CollectionCounter    = "counter"
-	CollectionView       = "view"
-	DefaultTimeout       = 5 * time.Second
-	DefaultValueType     = "text"
+	CollectionLabel         = "label"
+	CollectionKV            = "kv"
+	CollectionKVRevision    = "kv_revision"
+	CollectionPollingDetail = "polling_detail"
+	CollectionCounter       = "counter"
+	CollectionView          = "view"
+	DefaultTimeout          = 5 * time.Second
+	DefaultValueType        = "text"
 )
 
 //db errors