This is an automated email from the ASF dual-hosted git repository.

littlecui pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/servicecomb-service-center.git


The following commit(s) were added to refs/heads/master by this push:
     new 848b740  [SCB-1719] Support access log (#622)
848b740 is described below

commit 848b7407dfc6fb8e4b110a304a28f96543c73bb2
Author: humingcheng <[email protected]>
AuthorDate: Sun Jan 19 15:14:57 2020 +0800

    [SCB-1719] Support access log (#622)
    
    * [SCB-1719] Support access log
    
    * [SCB-1719] Support access log
    
    * Ensure thread safety and timely sync in log
---
 etc/conf/app.conf                        |   7 ++
 pkg/log/log_test.go                      |   5 ++
 pkg/log/zap.go                           |  46 ++++++++++--
 server/bootstrap/bootstrap.go            |   2 +
 server/core/config.go                    |  16 ++--
 server/core/proto/types.go               |  14 ++--
 server/handler/accesslog/handler.go      | 121 +++++++++++++++++++++++++++++++
 server/handler/accesslog/handler_test.go |  35 +++++++++
 8 files changed, 227 insertions(+), 19 deletions(-)

diff --git a/etc/conf/app.conf b/etc/conf/app.conf
index 41eb652..d40cc50 100644
--- a/etc/conf/app.conf
+++ b/etc/conf/app.conf
@@ -152,6 +152,13 @@ log_backup_count = 50
 log_format = text
 # whether enable record syslog
 log_sys = false
+# access log format: remoteIp requestReceiveTime "method requestUri proto" 
statusCode requestBodySize delay(ms)
+# example: 127.0.0.1 2006-01-02T15:04:05.000Z07:00 "GET 
/v4/default/registry/microservices HTTP/1.1" 200 0 0
+# access log inherits log's rotate and backup configuration
+# whether enable access log
+# enable_access_log = true
+# access log file
+access_log_file = "./access.log"
 
 ###################################################################
 # above is the global configurations
diff --git a/pkg/log/log_test.go b/pkg/log/log_test.go
index be79e9c..72eaa9c 100644
--- a/pkg/log/log_test.go
+++ b/pkg/log/log_test.go
@@ -84,6 +84,11 @@ func TestNewLogger(t *testing.T) {
        l.Fatal("a", nil)
 }
 
+func TestNewLoggerWithEmptyConfig(t *testing.T) {
+       l := NewLogger(Config{})
+       l.Info("a")
+}
+
 func TestLogPanic(t *testing.T) {
        defer func() {
                defer func() {
diff --git a/pkg/log/zap.go b/pkg/log/zap.go
index 3040c6e..49922a2 100644
--- a/pkg/log/zap.go
+++ b/pkg/log/zap.go
@@ -37,8 +37,8 @@ const (
 )
 
 var (
-       StdoutSyncer = zapcore.AddSync(os.Stdout)
-       StderrSyncer = zapcore.AddSync(os.Stderr)
+       StdoutSyncer = zapcore.Lock(os.Stdout)
+       StderrSyncer = zapcore.Lock(os.Stderr)
 
        zapLevelMap = map[string]zapcore.Level{
                "DEBUG": zap.DebugLevel,
@@ -61,6 +61,9 @@ type Config struct {
        // days
        LogBackupAge int
        CallerSkip   int
+       NoTime       bool // if true, not record time
+       NoLevel      bool // if true, not record level
+       NoCaller     bool // if true, not record caller
 }
 
 func (cfg Config) WithCallerSkip(s int) Config {
@@ -73,6 +76,21 @@ func (cfg Config) WithFile(path string) Config {
        return cfg
 }
 
+func (cfg Config) WithNoTime(b bool) Config {
+       cfg.NoTime = b
+       return cfg
+}
+
+func (cfg Config) WithNoLevel(b bool) Config {
+       cfg.NoLevel = b
+       return cfg
+}
+
+func (cfg Config) WithNoCaller(b bool) Config {
+       cfg.NoCaller = b
+       return cfg
+}
+
 func Configure() Config {
        return Config{
                LoggerLevel:   defaultLogLevel,
@@ -106,6 +124,16 @@ func toZapConfig(c Config) zapcore.Core {
                EncodeCaller:   zapcore.ShortCallerEncoder,
                EncodeName:     zapcore.FullNameEncoder,
        }
+       if c.NoCaller {
+               format.CallerKey = ""
+       }
+       if c.NoLevel {
+               format.LevelKey = ""
+               levelEnabler = func(_ zapcore.Level) bool { return true }
+       }
+       if c.NoTime {
+               format.TimeKey = ""
+       }
        var enc zapcore.Encoder
        if c.LogFormatText {
                enc = zapcore.NewConsoleEncoder(format)
@@ -209,6 +237,7 @@ func (l *Logger) Recover(r interface{}, callerSkip int) {
                e.Caller.TrimmedPath(),
                r,
                e.Stack)
+       StderrSyncer.Sync() // sync immediately, for server may exit abnormally
        if err := l.zapLogger.Core().With([]zap.Field{zap.Reflect("recover", 
r)}).Write(e, nil); err != nil {
                fmt.Fprintf(StderrSyncer, "%s\tERROR\t%v\n", 
time.Now().Format("2006-01-02T15:04:05.000Z0700"), err)
                fmt.Fprintln(StderrSyncer, 
util.BytesToStringWithNoCopy(debug.Stack()))
@@ -219,14 +248,17 @@ func (l *Logger) Recover(r interface{}, callerSkip int) {
 
 func (l *Logger) Sync() {
        l.zapLogger.Sync()
+       StderrSyncer.Sync()
+       StdoutSyncer.Sync()
 }
 
 func NewLogger(cfg Config) *Logger {
-       l := zap.New(toZapConfig(cfg),
-               zap.ErrorOutput(StderrSyncer),
-               zap.AddCaller(),
-               zap.AddCallerSkip(cfg.CallerSkip),
-       )
+       opts := make([]zap.Option, 1)
+       opts[0] = zap.ErrorOutput(StderrSyncer)
+       if !cfg.NoCaller {
+               opts = append(opts, zap.AddCaller(), 
zap.AddCallerSkip(cfg.CallerSkip))
+       }
+       l := zap.New(toZapConfig(cfg), opts...)
        return &Logger{
                Config:    cfg,
                zapLogger: l,
diff --git a/server/bootstrap/bootstrap.go b/server/bootstrap/bootstrap.go
index dc53ccf..985bcd3 100644
--- a/server/bootstrap/bootstrap.go
+++ b/server/bootstrap/bootstrap.go
@@ -65,6 +65,7 @@ import _ 
"github.com/apache/servicecomb-service-center/server/metric"
 
 import (
        "github.com/apache/servicecomb-service-center/pkg/log"
+       "github.com/apache/servicecomb-service-center/server/handler/accesslog"
        "github.com/apache/servicecomb-service-center/server/handler/auth"
        "github.com/apache/servicecomb-service-center/server/handler/cache"
        "github.com/apache/servicecomb-service-center/server/handler/context"
@@ -84,6 +85,7 @@ func init() {
        interceptor.RegisterInterceptFunc(cors.Intercept)
 
        // handle requests after routing.
+       accesslog.RegisterHandlers()
        maxbody.RegisterHandlers()
        metric.RegisterHandlers()
        tracing.RegisterHandlers()
diff --git a/server/core/config.go b/server/core/config.go
index 00948fe..fa720a1 100644
--- a/server/core/config.go
+++ b/server/core/config.go
@@ -82,6 +82,8 @@ func newInfo() pb.ServerInformation {
        if err == nil && cacheTTL < minCacheTTL {
                cacheTTL = minCacheTTL
        }
+       accessLogFileFromConfig := 
beego.AppConfig.DefaultString("access_log_file", "./access.log")
+       accessLogFile := util.GetEnvString("SC_ACCESS_LOG_FILE", 
accessLogFileFromConfig)
 
        return pb.ServerInformation{
                Version: InitVersion,
@@ -108,12 +110,14 @@ func newInfo() pb.ServerInformation {
                        CompactIndexDelta: 
beego.AppConfig.DefaultInt64("compact_index_delta", 100),
                        CompactInterval:   
beego.AppConfig.String("compact_interval"),
 
-                       LogRotateSize:  maxLogFileSize,
-                       LogBackupCount: maxLogBackupCount,
-                       LogFilePath:    beego.AppConfig.String("logfile"),
-                       LogLevel:       beego.AppConfig.String("loglevel"),
-                       LogFormat:      
beego.AppConfig.DefaultString("log_format", "text"),
-                       LogSys:         beego.AppConfig.DefaultBool("log_sys", 
false),
+                       LogRotateSize:   maxLogFileSize,
+                       LogBackupCount:  maxLogBackupCount,
+                       LogFilePath:     beego.AppConfig.String("logfile"),
+                       LogLevel:        beego.AppConfig.String("loglevel"),
+                       LogFormat:       
beego.AppConfig.DefaultString("log_format", "text"),
+                       LogSys:          beego.AppConfig.DefaultBool("log_sys", 
false),
+                       EnableAccessLog: 
beego.AppConfig.String("enable_access_log") == "true",
+                       AccessLogFile:   accessLogFile,
 
                        PluginsDir: 
beego.AppConfig.DefaultString("plugins_dir", "./plugins"),
                        Plugins:    util.NewJSONObject(),
diff --git a/server/core/proto/types.go b/server/core/proto/types.go
index 3acde78..429cd57 100644
--- a/server/core/proto/types.go
+++ b/server/core/proto/types.go
@@ -52,12 +52,14 @@ type ServerConfig struct {
        EnablePProf bool `json:"enablePProf"`
        EnableCache bool `json:"enableCache"`
 
-       LogRotateSize  int64  `json:"-"`
-       LogBackupCount int64  `json:"-"`
-       LogFilePath    string `json:"-"`
-       LogLevel       string `json:"-"`
-       LogFormat      string `json:"-"`
-       LogSys         bool   `json:"-"`
+       LogRotateSize   int64  `json:"-"`
+       LogBackupCount  int64  `json:"-"`
+       LogFilePath     string `json:"-"`
+       LogLevel        string `json:"-"`
+       LogFormat       string `json:"-"`
+       LogSys          bool   `json:"-"`
+       EnableAccessLog bool   `json:"-"`
+       AccessLogFile   string `json:"-"`
 
        PluginsDir string          `json:"-"`
        Plugins    util.JSONObject `json:"plugins"`
diff --git a/server/handler/accesslog/handler.go 
b/server/handler/accesslog/handler.go
new file mode 100644
index 0000000..dab3d5f
--- /dev/null
+++ b/server/handler/accesslog/handler.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.
+
+// +build go1.9
+
+package accesslog
+
+import (
+       "fmt"
+       "net/http"
+       "os"
+       "time"
+
+       "github.com/apache/servicecomb-service-center/pkg/chain"
+       "github.com/apache/servicecomb-service-center/pkg/log"
+       "github.com/apache/servicecomb-service-center/pkg/rest"
+       "github.com/apache/servicecomb-service-center/pkg/util"
+       "github.com/apache/servicecomb-service-center/server/core"
+       svr "github.com/apache/servicecomb-service-center/server/rest"
+)
+
+// Handler implements chain.Handler
+// Handler records access log.
+// Make sure to complete the initialization before handling the request.
+type Handler struct {
+       logger        *log.Logger
+       whiteListAPIs map[string]struct{} // not record access log
+}
+
+// AddWhiteListAPIs adds APIs to white list, where the APIs will be ignored
+// in access log.
+// Not safe for concurrent use.
+func (h *Handler) AddWhiteListAPIs(apis ...string) {
+       for _, api := range apis {
+               h.whiteListAPIs[api] = struct{}{}
+       }
+}
+
+// ShouldIgnoreAPI judges whether the API should be ignored in access log.
+func (h *Handler) ShouldIgnoreAPI(api string) bool {
+       _, ok := h.whiteListAPIs[api]
+       return ok
+}
+
+// Handle handles the request
+func (h *Handler) Handle(i *chain.Invocation) {
+       matchPattern := i.Context().Value(rest.CTX_MATCH_PATTERN).(string)
+       if h.ShouldIgnoreAPI(matchPattern) {
+               i.Next()
+               return
+       }
+       startTimeStr := "unknown"
+       start, ok := i.Context().Value(svr.CTX_START_TIMESTAMP).(time.Time)
+       if ok {
+               startTimeStr = start.Format("2006-01-02T15:04:05.000Z07:00")
+       }
+       r := i.Context().Value(rest.CTX_REQUEST).(*http.Request)
+       w := i.Context().Value(rest.CTX_RESPONSE).(http.ResponseWriter)
+       i.Next(chain.WithAsyncFunc(func(_ chain.Result) {
+               delayByMillisecond := "unknown"
+               if ok {
+                       delayByMillisecond = fmt.Sprintf("%d", 
time.Since(start)/time.Millisecond)
+               }
+               statusCode := w.Header().Get(rest.HEADER_RESPONSE_STATUS)
+               // format:  remoteIp requestReceiveTime "method requestUri 
proto" statusCode requestBodySize delay(ms)
+               // example: 127.0.0.1 2006-01-02T15:04:05.000Z07:00 "GET 
/v4/default/registry/microservices HTTP/1.1" 200 0 0
+               h.logger.Infof("%s %s \"%s %s %s\" %s %d %s",
+                       util.GetIPFromContext(i.Context()),
+                       startTimeStr,
+                       r.Method,
+                       r.RequestURI,
+                       r.Proto,
+                       statusCode,
+                       r.ContentLength,
+                       delayByMillisecond)
+       }))
+}
+
+// NewAccessLogHandler creates a Handler
+func NewAccessLogHandler(l *log.Logger) *Handler {
+       return &Handler{
+               logger:        l,
+               whiteListAPIs: make(map[string]struct{}, 0)}
+}
+
+// RegisterHandlers registers an access log handler to the handler chain
+func RegisterHandlers() {
+       if !core.ServerInfo.Config.EnableAccessLog {
+               return
+       }
+       logger := log.NewLogger(log.Config{
+               LoggerFile:     
os.ExpandEnv(core.ServerInfo.Config.AccessLogFile),
+               LogFormatText:  true,
+               LogRotateSize:  int(core.ServerInfo.Config.LogRotateSize),
+               LogBackupCount: int(core.ServerInfo.Config.LogBackupCount),
+               NoCaller:       true,
+               NoTime:         true,
+               NoLevel:        true,
+       })
+       h := NewAccessLogHandler(logger)
+       // no access log for heartbeat
+       h.AddWhiteListAPIs(
+               
"/v4/:project/registry/microservices/:serviceId/instances/:instanceId/heartbeat",
+               "/v4/:project/registry/heartbeats",
+               
"/registry/v3/microservices/:serviceId/instances/:instanceId/heartbeat",
+               "/registry/v3/heartbeats",
+               "")
+       chain.RegisterHandler(rest.ServerChainName, h)
+}
diff --git a/server/handler/accesslog/handler_test.go 
b/server/handler/accesslog/handler_test.go
new file mode 100644
index 0000000..c6edcd3
--- /dev/null
+++ b/server/handler/accesslog/handler_test.go
@@ -0,0 +1,35 @@
+package accesslog_test
+
+import (
+       "context"
+       "net/http"
+       "net/http/httptest"
+       "testing"
+
+       "github.com/apache/servicecomb-service-center/pkg/chain"
+       "github.com/apache/servicecomb-service-center/pkg/log"
+       "github.com/apache/servicecomb-service-center/pkg/rest"
+       "github.com/apache/servicecomb-service-center/server/handler/accesslog"
+)
+
+func TestHandler(t *testing.T) {
+       // add white list apis, ignore
+       l := log.NewLogger(log.Config{})
+       h := accesslog.NewAccessLogHandler(l)
+       testAPI := "testAPI"
+       h.AddWhiteListAPIs(testAPI)
+       if !h.ShouldIgnoreAPI(testAPI) {
+               t.Fatalf("Should ignore API: %s", testAPI)
+       }
+
+       // handle
+       inv := &chain.Invocation{}
+       ctx := context.Background()
+       inv.Init(ctx, chain.NewChain("c", []chain.Handler{}))
+       inv.WithContext(rest.CTX_MATCH_PATTERN, "/a")
+       r, _ := http.NewRequest(http.MethodGet, "http://127.0.0.1:80/a";, nil)
+       w := httptest.NewRecorder()
+       inv.WithContext(rest.CTX_REQUEST, r)
+       inv.WithContext(rest.CTX_RESPONSE, w)
+       h.Handle(inv)
+}

Reply via email to