diff --git a/.github/workflows/cli_test.yml b/.github/workflows/cli_test.yml new file mode 100644 index 0000000000..01c56e402f --- /dev/null +++ b/.github/workflows/cli_test.yml @@ -0,0 +1,31 @@ +name: Test CLI + +on: + push: + branches: + - master + - v2.0 + pull_request: + branches: + - master + - v2.0 + +jobs: + run-test: + runs-on: ubuntu-latest + + services: + etcd: + image: bitnami/etcd:3.4.13 + ports: + - 2379:2379 + - 2380:2380 + env: + ALLOW_NONE_AUTHENTICATION: yes + + steps: + - uses: actions/checkout@v2 + + - name: run test + working-directory: ./api + run: sudo ./test/shell/cli_test.sh diff --git a/Dockerfile b/Dockerfile index 2baad1cf73..a1e219f4da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -77,6 +77,8 @@ COPY --from=api-builder /usr/local/apisix-dashboard/output/ ./ COPY --from=fe-builder /usr/local/apisix-dashboard/output/ ./ +RUN mkdir logs + EXPOSE 8080 CMD [ "/usr/local/apisix-dashboard/manager-api" ] diff --git a/api/Dockerfile b/api/Dockerfile index 426e7c6dee..eb56b8e862 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -54,6 +54,8 @@ WORKDIR /go/manager-api COPY --from=build-env /go/manager-api/ /go/manager-api/ COPY --from=build-env /usr/share/zoneinfo/Hongkong /etc/localtime +RUN mkdir logs + EXPOSE 8080 RUN chmod +x ./entry.sh diff --git a/api/conf/conf.go b/api/conf/conf.go index e8303b99f8..f7f5cfec2d 100644 --- a/api/conf/conf.go +++ b/api/conf/conf.go @@ -22,6 +22,7 @@ import ( "io/ioutil" "log" "os" + "path/filepath" "github.com/tidwall/gjson" "gopkg.in/yaml.v2" @@ -45,6 +46,8 @@ var ( ServerHost = "127.0.0.1" ServerPort = 80 ETCDEndpoints = []string{"127.0.0.1:2379"} + ErrorLogLevel = "warn" + ErrorLogPath = "logs/error.log" UserList = make(map[string]User, 2) AuthConf Authentication ) @@ -58,9 +61,19 @@ type Listen struct { Port int } +type ErrorLog struct { + Level string + FilePath string `yaml:"file_path"` +} + +type Log struct { + ErrorLog ErrorLog `yaml:"error_log"` +} + type Conf struct { Etcd Etcd Listen Listen + Log Log } type User struct { @@ -109,6 +122,7 @@ func setConf() { if config.Conf.Listen.Port != 0 { ServerPort = config.Conf.Listen.Port } + if config.Conf.Listen.Host != "" { ServerHost = config.Conf.Listen.Host } @@ -118,6 +132,20 @@ func setConf() { ETCDEndpoints = config.Conf.Etcd.Endpoints } + //error log + if config.Conf.Log.ErrorLog.Level != "" { + ErrorLogLevel = config.Conf.Log.ErrorLog.Level + } + if config.Conf.Log.ErrorLog.FilePath != "" { + ErrorLogPath = config.Conf.Log.ErrorLog.FilePath + } + if !filepath.IsAbs(ErrorLogPath) { + ErrorLogPath, err = filepath.Abs(WorkDir + "/" + ErrorLogPath) + if err != nil { + panic(err) + } + } + //auth initAuthentication(config.Authentication) } diff --git a/api/conf/conf.json b/api/conf/conf.json deleted file mode 100644 index c369afe93c..0000000000 --- a/api/conf/conf.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "conf": { - "syslog": { - "host": "127.0.0.1" - }, - "listen": { - "host": "127.0.0.1", - "port": 8080 - }, - "dag-lib-path": "", - "etcd": { - "endpoints": "127.0.0.1:2379" - } - }, - "authentication": { - "session": { - "secret": "secret", - "expireTime": 3600 - }, - "user": [ - { - "username": "admin", - "password": "admin" - }, - { - "username": "user", - "password": "user" - } - ] - } -} diff --git a/api/conf/conf.yaml b/api/conf/conf.yaml index c4e98b2c9f..49bf28fdeb 100644 --- a/api/conf/conf.yaml +++ b/api/conf/conf.yaml @@ -22,6 +22,11 @@ conf: etcd: endpoints: # supports defining multiple etcd host addresses for an etcd cluster - 127.0.0.1:2379 + log: + error_log: + level: warn # supports levels, lower to higher: debug, info, warn, error, panic, fatal + file_path: logs/error.log # supports relative path, absolute path, standard output + # such as: logs/error.log, /tmp/logs/error.log, /dev/stdout, /dev/stderr authentication: secret: secret # secret for jwt token generation. # *NOTE*: Highly recommended to modify this value to protect `manager api`. diff --git a/api/conf/conf_preview.json b/api/conf/conf_preview.json deleted file mode 100644 index eef8683d2a..0000000000 --- a/api/conf/conf_preview.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "conf": { - "syslog": { - "host": "#syslogAddress#" - } - }, - "authentication": { - "session": { - "secret": "secret", - "expireTime": 3600 - }, - "user": [ - { - "username": "admin", - "password": "admin" - }, - { - "username": "user", - "password": "user" - } - ] - } -} diff --git a/api/filter/logging.go b/api/filter/logging.go index a62aef5c7f..328ac4dc6d 100644 --- a/api/filter/logging.go +++ b/api/filter/logging.go @@ -16,74 +16,4 @@ */ package filter -import ( - "bytes" - "io/ioutil" - "time" - - "github.com/gin-gonic/gin" - "github.com/shiningrush/droplet/log" - "github.com/sirupsen/logrus" -) - -func RequestLogHandler() gin.HandlerFunc { - return func(c *gin.Context) { - start, host, remoteIP, path, method := time.Now(), c.Request.Host, c.ClientIP(), c.Request.URL.Path, c.Request.Method - var val interface{} - if method == "GET" { - val = c.Request.URL.Query() - } else { - val, _ = c.GetRawData() - - // set RequestBody back - c.Request.Body = ioutil.NopCloser(bytes.NewReader(val.([]byte))) - } - c.Set("requestBody", val) - uuid, _ := c.Get("X-Request-Id") - - param, _ := c.Get("requestBody") - - switch paramType := param.(type) { - case []byte: - param = string(param.([]byte)) - log.Infof("type of param: %#v", paramType) - default: - } - - blw := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer} - c.Writer = blw - c.Next() - latency := time.Since(start) / 1000000 - statusCode := c.Writer.Status() - respBody := blw.body.String() - if uuid == "" { - uuid = c.Writer.Header().Get("X-Request-Id") - } - var errs []string - for _, err := range c.Errors { - errs = append(errs, err.Error()) - } - logger.WithFields(logrus.Fields{ - "requestId": uuid, - "latency": latency, - "remoteIp": remoteIP, - "method": method, - "path": path, - "statusCode": statusCode, - "host": host, - "params": param, - "respBody": respBody, - "errMsg": errs, - }).Info("") - } -} - -type bodyLogWriter struct { - gin.ResponseWriter - body *bytes.Buffer -} - -func (w bodyLogWriter) Write(b []byte) (int, error) { - w.body.Write(b) - return w.ResponseWriter.Write(b) -} +//for logging access log, will refactor it in a new pr. diff --git a/api/filter/recover.go b/api/filter/recover.go index a47e270d88..77e44a3311 100644 --- a/api/filter/recover.go +++ b/api/filter/recover.go @@ -26,11 +26,9 @@ import ( "github.com/apisix/manager-api/log" "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" ) var ( - logger = log.GetLogger() dunno = []byte("???") centerDot = []byte("·") dot = []byte(".") @@ -41,12 +39,13 @@ func RecoverHandler() gin.HandlerFunc { return func(c *gin.Context) { defer func() { if err := recover(); err != nil { - uuid := c.Writer.Header().Get("X-Request-Id") - logger.WithFields(logrus.Fields{ - "uuid": uuid, - }) + fmt.Println("err;", err) + //uuid := c.Writer.Header().Get("X-Request-Id") stack := stack(3) - logger.Errorf("[Recovery] %s panic recovered:\n\n%s\n%s", timeFormat(time.Now()), err, stack) + fmt.Printf("[Recovery] %s panic recovered:\n\n%s\n%s", timeFormat(time.Now()), err, stack) + + //log.With(zap.String("uuid", uuid)) + log.Errorf("[Recovery] %s panic recovered:\n\n%s\n%s", timeFormat(time.Now()), err, stack) c.AbortWithStatus(http.StatusInternalServerError) } }() @@ -58,7 +57,7 @@ func WrapGo(f func(...interface{}), args ...interface{}) { defer func() { if err := recover(); err != nil { stack := stack(3) - logger.Errorf("[Recovery] %s panic recovered:\n\n%s\n%s", timeFormat(time.Now()), err, stack) + log.Errorf("[Recovery] %s panic recovered:\n\n%s\n%s", timeFormat(time.Now()), err, stack) } }() f(args...) diff --git a/api/go.mod b/api/go.mod index 2accb96cfd..d054357660 100644 --- a/api/go.mod +++ b/api/go.mod @@ -17,6 +17,9 @@ require ( github.com/gin-gonic/gin v1.6.3 github.com/gogo/protobuf v1.3.1 // indirect github.com/google/uuid v1.1.2 // indirect + github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible + github.com/lestrrat-go/strftime v1.0.3 // indirect + github.com/natefinch/lumberjack v2.0.0+incompatible github.com/satori/go.uuid v1.2.0 github.com/shiningrush/droplet v0.2.1 github.com/shiningrush/droplet/wrapper/gin v0.2.0 diff --git a/api/go.sum b/api/go.sum index 7dcfbc192d..c1e2010feb 100644 --- a/api/go.sum +++ b/api/go.sum @@ -89,6 +89,11 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is= +github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible h1:Y6sqxHMyB1D2YSzWkLibYKgg+SwmyFU9dF2hn6MdTj4= +github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA= +github.com/lestrrat-go/strftime v1.0.3 h1:qqOPU7y+TM8Y803I8fG9c/DyKG3xH/xkng6keC1015Q= +github.com/lestrrat-go/strftime v1.0.3/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= @@ -97,6 +102,9 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM= +github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= +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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/api/internal/core/store/query.go b/api/internal/core/store/query.go index 7f59594449..20036d06ab 100644 --- a/api/internal/core/store/query.go +++ b/api/internal/core/store/query.go @@ -16,6 +16,7 @@ package store import ( "github.com/apisix/manager-api/internal/core/entity" + "github.com/apisix/manager-api/log" ) type Query struct { @@ -88,7 +89,7 @@ func NewQuery(sort *Sort, filter *Filter, pagination *Pagination) *Query { func NewSort(sortRaw []string) *Sort { if sortRaw == nil || len(sortRaw)%2 == 1 { - // Empty sort list or invalid (odd) length + log.Info("empty sort for query") return NoSort } list := []SortBy{} @@ -117,6 +118,7 @@ func NewSort(sortRaw []string) *Sort { func NewFilter(filterRaw []string) *Filter { if filterRaw == nil || len(filterRaw)%2 == 1 { + log.Info("empty filter for query") return NoFilter } list := []FilterBy{} diff --git a/api/internal/core/store/store.go b/api/internal/core/store/store.go index b285183dfb..1627e2ee9c 100644 --- a/api/internal/core/store/store.go +++ b/api/internal/core/store/store.go @@ -21,7 +21,6 @@ import ( "context" "encoding/json" "fmt" - "log" "reflect" "sort" "sync" @@ -31,6 +30,7 @@ import ( "github.com/apisix/manager-api/internal/core/entity" "github.com/apisix/manager-api/internal/core/storage" + "github.com/apisix/manager-api/log" ) type Interface interface { @@ -60,12 +60,15 @@ type GenericStoreOption struct { func NewGenericStore(opt GenericStoreOption) (*GenericStore, error) { if opt.BasePath == "" { + log.Warn("base path empty") return nil, fmt.Errorf("base path can not be empty") } if opt.ObjType == nil { + log.Warn("object type is nil") return nil, fmt.Errorf("object type can not be nil") } if opt.KeyFunc == nil { + log.Warn("key func is nil") return nil, fmt.Errorf("key func can not be nil") } @@ -73,6 +76,7 @@ func NewGenericStore(opt GenericStoreOption) (*GenericStore, error) { opt.ObjType = opt.ObjType.Elem() } if opt.ObjType.Kind() != reflect.Struct { + log.Warn("obj type is invalid") return nil, fmt.Errorf("obj type is invalid") } s := &GenericStore{ @@ -106,7 +110,7 @@ func (s *GenericStore) Init() error { go func() { for event := range ch { if event.Canceled { - log.Println("watch failed", event.Error) + log.Warnf("watch failed: %w", event.Error) } for i := range event.Events { @@ -114,7 +118,7 @@ func (s *GenericStore) Init() error { case storage.EventTypePut: objPtr, err := s.StringToObjPtr(event.Events[i].Value) if err != nil { - log.Println("value convert to obj failed", err) + log.Warnf("value convert to obj failed: %w", err) continue } s.cache.Store(event.Events[i].Key[len(s.opt.BasePath)+1:], objPtr) @@ -131,6 +135,7 @@ func (s *GenericStore) Init() error { func (s *GenericStore) Get(key string) (interface{}, error) { ret, ok := s.cache.Load(key) if !ok { + log.Warnf("data not found by key: %s", key) return nil, data.ErrNotFound } return ret, nil @@ -213,6 +218,7 @@ func (s *GenericStore) List(input ListInput) (*ListOutput, error) { func (s *GenericStore) ingestValidate(obj interface{}) (err error) { if s.opt.Validator != nil { if err := s.opt.Validator.Validate(obj); err != nil { + log.Infof("data validate fail: %w", err) return err } } @@ -244,11 +250,13 @@ func (s *GenericStore) Create(ctx context.Context, obj interface{}) error { } _, ok := s.cache.Load(key) if ok { + log.Warnf("key: %s is conflicted", key) return fmt.Errorf("key: %s is conflicted", key) } bs, err := json.Marshal(obj) if err != nil { + log.Warnf("json marshal failed: %s", err) return fmt.Errorf("json marshal failed: %s", err) } if err := s.Stg.Create(ctx, s.GetObjStorageKey(obj), string(bs)); err != nil { @@ -272,6 +280,7 @@ func (s *GenericStore) Update(ctx context.Context, obj interface{}, createIfNotE if createIfNotExist { return s.Create(ctx, obj) } + log.Warnf("key: %s is not found", key) return fmt.Errorf("key: %s is not found", key) } @@ -284,6 +293,7 @@ func (s *GenericStore) Update(ctx context.Context, obj interface{}, createIfNotE bs, err := json.Marshal(obj) if err != nil { + log.Warnf("json marshal failed: %s", err) return fmt.Errorf("json marshal failed: %s", err) } if err := s.Stg.Update(ctx, s.GetObjStorageKey(obj), string(bs)); err != nil { @@ -311,6 +321,7 @@ func (s *GenericStore) StringToObjPtr(str string) (interface{}, error) { objPtr := reflect.New(s.opt.ObjType) err := json.Unmarshal([]byte(str), objPtr.Interface()) if err != nil { + log.Warnf("json marshal failed: %s", err) return nil, fmt.Errorf("json unmarshal failed: %w", err) } diff --git a/api/internal/core/store/storehub.go b/api/internal/core/store/storehub.go index b2685c9ad9..c38286eeb8 100644 --- a/api/internal/core/store/storehub.go +++ b/api/internal/core/store/storehub.go @@ -22,6 +22,7 @@ import ( "github.com/apisix/manager-api/internal/core/entity" "github.com/apisix/manager-api/internal/utils" + "github.com/apisix/manager-api/log" ) type HubKey string @@ -50,9 +51,11 @@ func InitStore(key HubKey, opt GenericStoreOption) error { } s, err := NewGenericStore(opt) if err != nil { + log.Warnf("NewGenericStore error: %w", err) return err } if err := s.Init(); err != nil { + log.Warnf("GenericStore init error: %w", err) return err } diff --git a/api/internal/core/store/validate.go b/api/internal/core/store/validate.go index 14470e04df..b4f3bd1c4e 100644 --- a/api/internal/core/store/validate.go +++ b/api/internal/core/store/validate.go @@ -77,11 +77,13 @@ type APISIXJsonSchemaValidator struct { func NewAPISIXJsonSchemaValidator(jsonPath string) (Validator, error) { schemaDef := conf.Schema.Get(jsonPath).String() if schemaDef == "" { + log.Warnf("scheme validate failed: schema not found, path: %s", jsonPath) return nil, fmt.Errorf("scheme validate failed: schema not found, path: %s", jsonPath) } s, err := gojsonschema.NewSchema(gojsonschema.NewStringLoader(schemaDef)) if err != nil { + log.Warnf("new schema failed: %w", err) return nil, fmt.Errorf("new schema failed: %w", err) } return &APISIXJsonSchemaValidator{ @@ -216,6 +218,7 @@ func checkConf(reqBody interface{}) error { func (v *APISIXJsonSchemaValidator) Validate(obj interface{}) error { ret, err := v.schema.Validate(gojsonschema.NewGoLoader(obj)) if err != nil { + log.Warnf("scheme validate failed: %w", err) return fmt.Errorf("scheme validate failed: %w", err) } @@ -245,12 +248,14 @@ func (v *APISIXJsonSchemaValidator) Validate(obj interface{}) error { schemaDef = conf.Schema.Get("plugins." + pluginName + ".schema").String() } if schemaDef == "" { + log.Warnf("scheme validate failed: schema not found, path: %s", "plugins."+pluginName) return fmt.Errorf("scheme validate failed: schema not found, path: %s", "plugins."+pluginName) } schemaDef = reg.ReplaceAllString(schemaDef, `"properties":{}`) s, err := gojsonschema.NewSchema(gojsonschema.NewStringLoader(schemaDef)) if err != nil { + log.Warnf("init scheme validate failed: %w", err) return fmt.Errorf("scheme validate failed: %w", err) } diff --git a/api/internal/handler/route/route.go b/api/internal/handler/route/route.go index 3b281ca1d6..bab3e9a5d4 100644 --- a/api/internal/handler/route/route.go +++ b/api/internal/handler/route/route.go @@ -28,7 +28,6 @@ import ( "github.com/gin-gonic/gin" "github.com/shiningrush/droplet" "github.com/shiningrush/droplet/data" - "github.com/shiningrush/droplet/log" "github.com/shiningrush/droplet/wrapper" wgin "github.com/shiningrush/droplet/wrapper/gin" @@ -38,6 +37,7 @@ import ( "github.com/apisix/manager-api/internal/handler" "github.com/apisix/manager-api/internal/utils" "github.com/apisix/manager-api/internal/utils/consts" + "github.com/apisix/manager-api/log" ) type Handler struct { @@ -336,7 +336,7 @@ func (h *Handler) BatchDelete(c droplet.Context) (interface{}, error) { //delete stored script if err := h.scriptStore.BatchDelete(c.Context(), strings.Split(input.IDs, ",")); err != nil { //try again - log.Warnf("try to delete script %s again", input.IDs) + log.Warn("try to delete script %s again", input.IDs) if err := h.scriptStore.BatchDelete(c.Context(), strings.Split(input.IDs, ",")); err != nil { return nil, nil } diff --git a/api/log/log.go b/api/log/log.go index b9e68603c8..3c53a96375 100644 --- a/api/log/log.go +++ b/api/log/log.go @@ -16,188 +16,93 @@ */ package log -import ( - "bufio" - "fmt" - "os" - "runtime" - "strings" - - "github.com/apisix/manager-api/conf" - "github.com/sirupsen/logrus" +var ( + DefLogger Interface = emptyLog{} ) -var logEntry *logrus.Entry - -type DefLogger struct { +type emptyLog struct { } -func getLogrusFields(entry *logrus.Entry, fs []interface{}) *logrus.Entry { - if len(fs) == 0 { - return entry - } - - fd := logrus.Fields{} - for i := 0; i < len(fs); i += 2 { - fd[fs[i].(string)] = fs[i+1] - } - return entry.WithFields(fd) +type Interface interface { + Debug(msg string, fields ...interface{}) + Debugf(msg string, args ...interface{}) + Info(msg string, fields ...interface{}) + Infof(msg string, args ...interface{}) + Warn(msg string, fields ...interface{}) + Warnf(msg string, args ...interface{}) + Error(msg string, fields ...interface{}) + Errorf(msg string, args ...interface{}) + Fatal(msg string, fields ...interface{}) + Fatalf(msg string, args ...interface{}) } -func (e DefLogger) Debug(msg string, fields ...interface{}) { - getLogrusFields(logEntry, fields).Debug(msg) +func (e emptyLog) Debug(msg string, fields ...interface{}) { + getZapFields(logger, fields).Debug(msg) } -func (e DefLogger) Debugf(msg string, args ...interface{}) { - logEntry.Debugf(msg, args...) +func (e emptyLog) Debugf(msg string, args ...interface{}) { + logger.Debugf(msg, args...) } -func (e DefLogger) Info(msg string, fields ...interface{}) { - getLogrusFields(logEntry, fields).Info(msg) +func (e emptyLog) Info(msg string, fields ...interface{}) { + getZapFields(logger, fields).Info(msg) } -func (e DefLogger) Infof(msg string, args ...interface{}) { - logEntry.Infof(msg, args...) +func (e emptyLog) Infof(msg string, args ...interface{}) { + logger.Infof(msg, args...) } -func (e DefLogger) Warn(msg string, fields ...interface{}) { - getLogrusFields(logEntry, fields).Warn(msg) +func (e emptyLog) Warn(msg string, fields ...interface{}) { + getZapFields(logger, fields).Warn(msg) } -func (e DefLogger) Warnf(msg string, args ...interface{}) { - logEntry.Warnf(msg, args...) +func (e emptyLog) Warnf(msg string, args ...interface{}) { + logger.Warnf(msg, args...) } -func (e DefLogger) Error(msg string, fields ...interface{}) { - getLogrusFields(logEntry, fields).Error(msg) +func (e emptyLog) Error(msg string, fields ...interface{}) { + getZapFields(logger, fields).Error(msg) } -func (e DefLogger) Errorf(msg string, args ...interface{}) { - logEntry.Errorf(msg, args...) +func (e emptyLog) Errorf(msg string, args ...interface{}) { + logger.Errorf(msg, args...) } -func (e DefLogger) Fatal(msg string, fields ...interface{}) { - getLogrusFields(logEntry, fields).Fatal(msg) +func (e emptyLog) Fatal(msg string, fields ...interface{}) { + getZapFields(logger, fields).Fatal(msg) } -func (e DefLogger) Fatalf(msg string, args ...interface{}) { - logEntry.Fatalf(msg, args...) +func (e emptyLog) Fatalf(msg string, args ...interface{}) { + logger.Fatalf(msg, args...) } -func GetLogger() *logrus.Entry { - if logEntry == nil { - var log = logrus.New() - setNull(log) - log.SetLevel(logrus.DebugLevel) - if conf.ENV != conf.EnvLOCAL { - log.SetLevel(logrus.ErrorLevel) - } - log.SetFormatter(&logrus.JSONFormatter{}) - logEntry = log.WithFields(logrus.Fields{ - "app": "manager-api", - }) - if hook, err := createHook(); err == nil { - log.AddHook(hook) - } - } - return logEntry +func Debug(msg string, fields ...interface{}) { + DefLogger.Debug(msg, fields...) } - -func setNull(log *logrus.Logger) { - src, err := os.OpenFile(os.DevNull, os.O_APPEND|os.O_WRONLY, os.ModeAppend) - if err != nil { - fmt.Println("err", err) - } - writer := bufio.NewWriter(src) - log.SetOutput(writer) +func Debugf(msg string, args ...interface{}) { + DefLogger.Debugf(msg, args...) } - -type Hook struct { - Formatter func(file, function string, line int) string +func Info(msg string, fields ...interface{}) { + DefLogger.Info(msg, fields...) } - -func createHook() (*Hook, error) { - return &Hook{ - func(file, function string, line int) string { - return fmt.Sprintf("%s:%d", file, line) - }, - }, nil +func Infof(msg string, args ...interface{}) { + DefLogger.Infof(msg, args...) } - -func (hook *Hook) Fire(entry *logrus.Entry) error { - str := hook.Formatter(findCaller(5)) - en := entry.WithField("line", str) - en.Level = entry.Level - en.Message = entry.Message - en.Time = entry.Time - line, err := en.String() - if err != nil { - fmt.Fprintf(os.Stderr, "Unable to read entry, %v", err) - return err - } - switch en.Level { - case logrus.PanicLevel: - fmt.Print(line) - return nil - case logrus.FatalLevel: - fmt.Print(line) - return nil - case logrus.ErrorLevel: - fmt.Print(line) - return nil - case logrus.WarnLevel: - fmt.Print(line) - return nil - case logrus.InfoLevel: - fmt.Print(line) - return nil - case logrus.DebugLevel: - fmt.Print(line) - return nil - default: - return nil - } +func Warn(msg string, fields ...interface{}) { + DefLogger.Warn(msg, fields...) } - -func (hook *Hook) Levels() []logrus.Level { - return logrus.AllLevels +func Warnf(msg string, args ...interface{}) { + DefLogger.Warnf(msg, args...) } - -func findCaller(skip int) (string, string, int) { - var ( - pc uintptr - file string - function string - line int - ) - for i := 0; i < 10; i++ { - pc, file, line = getCaller(skip + i) - if !strings.HasPrefix(file, "logrus") { - break - } - } - if pc != 0 { - frames := runtime.CallersFrames([]uintptr{pc}) - frame, _ := frames.Next() - function = frame.Function - } - return file, function, line +func Error(msg string, fields ...interface{}) { + DefLogger.Error(msg, fields...) } - -func getCaller(skip int) (uintptr, string, int) { - pc, file, line, ok := runtime.Caller(skip) - if !ok { - return 0, "", 0 - } - n := 0 - for i := len(file) - 1; i > 0; i-- { - if file[i] == '/' { - n += 1 - if n >= 2 { - file = file[i+1:] - break - } - } - } - return pc, file, line +func Errorf(msg string, args ...interface{}) { + DefLogger.Errorf(msg, args...) +} +func Fatal(msg string, fields ...interface{}) { + DefLogger.Fatal(msg, fields...) +} +func Fatalf(msg string, args ...interface{}) { + DefLogger.Fatalf(msg, args...) } diff --git a/api/log/zap.go b/api/log/zap.go new file mode 100644 index 0000000000..223a71fb64 --- /dev/null +++ b/api/log/zap.go @@ -0,0 +1,89 @@ +/* + * 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 log + +import ( + "os" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + + "github.com/apisix/manager-api/conf" +) + +var logger *zap.SugaredLogger + +func init() { + writeSyncer := fileWriter() + encoder := getEncoder() + logLevel := getLogLevel() + core := zapcore.NewCore(encoder, writeSyncer, logLevel) + + zapLogger := zap.New(core, zap.AddCaller()) + + logger = zapLogger.Sugar() +} + +func getLogLevel() zapcore.LevelEnabler { + level := zapcore.WarnLevel + switch conf.ErrorLogLevel { + case "debug": + level = zapcore.DebugLevel + case "info": + level = zapcore.InfoLevel + case "warn": + level = zapcore.WarnLevel + case "error": + level = zapcore.ErrorLevel + case "panic": + level = zapcore.PanicLevel + case "fatal": + level = zapcore.FatalLevel + } + return level +} + +func getEncoder() zapcore.Encoder { + encoderConfig := zap.NewProductionEncoderConfig() + encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder + encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder + return zapcore.NewConsoleEncoder(encoderConfig) +} + +func fileWriter() zapcore.WriteSyncer { + //standard output + if conf.ErrorLogPath == "/dev/stdout" { + return zapcore.Lock(os.Stdout) + } + if conf.ErrorLogPath == "/dev/stderr" { + return zapcore.Lock(os.Stderr) + } + + writer, _, err := zap.Open(conf.ErrorLogPath) + if err != nil { + panic(err) + } + return writer +} + +func getZapFields(logger *zap.SugaredLogger, fields []interface{}) *zap.SugaredLogger { + if len(fields) == 0 { + return logger + } + + return logger.With(fields) +} diff --git a/api/logs/placeholder.txt b/api/logs/placeholder.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/main.go b/api/main.go index b001d07575..03b2e35e54 100644 --- a/api/main.go +++ b/api/main.go @@ -18,28 +18,25 @@ package main import ( "fmt" - "log" "net/http" "time" - dlog "github.com/shiningrush/droplet/log" - "github.com/apisix/manager-api/conf" "github.com/apisix/manager-api/internal" "github.com/apisix/manager-api/internal/core/storage" "github.com/apisix/manager-api/internal/core/store" "github.com/apisix/manager-api/internal/utils" - alog "github.com/apisix/manager-api/log" + "github.com/apisix/manager-api/log" ) -var logger = alog.GetLogger() - func main() { - dlog.DefLogger = alog.DefLogger{} + if err := storage.InitETCDClient(conf.ETCDEndpoints); err != nil { + log.Error("init etcd client fail: %w", err) panic(err) } if err := store.InitStores(); err != nil { + log.Error("init stores fail: %w", err) panic(err) } // routes @@ -52,10 +49,10 @@ func main() { WriteTimeout: time.Duration(5000) * time.Millisecond, } - log.Printf("The Manager API is listening on %s ", addr) + log.Infof("The Manager API is listening on %s ", addr) if err := s.ListenAndServe(); err != nil { - logger.WithError(err) + log.Errorf("listen and serv fail: %w", err) } utils.CloseAll() diff --git a/api/test/docker/manager-api-conf.yaml b/api/test/docker/manager-api-conf.yaml index 0ab4cc7724..f3b3a05af7 100644 --- a/api/test/docker/manager-api-conf.yaml +++ b/api/test/docker/manager-api-conf.yaml @@ -25,6 +25,11 @@ conf: - 172.16.238.10:2379 # ips here are defined in docker compose. - 172.16.238.11:2379 - 172.16.238.12:2379 + log: + error_log: + level: warn # supports levels, lower to higher: debug, info, warn, error, panic, fatal + file_path: logs/error.log # supports relative path, absolute path, standard output + # such as: logs/error.log, /tmp/logs/error.log, /dev/stdout, /dev/stderr authentication: secret: secret # secret for jwt token generation. # *NOTE*: Highly recommended to modify this value to protect `manager api`. diff --git a/api/test/shell/cli_test.sh b/api/test/shell/cli_test.sh new file mode 100755 index 0000000000..5023df3f94 --- /dev/null +++ b/api/test/shell/cli_test.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash + +# +# 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. +# + +set -ex + +clean_up() { + git checkout conf/conf.yaml +} + +trap clean_up EXIT + +export GO111MODULE=on +go build -o ./manager-api . + +#default level: warn, path: logs/error.log + +./manager-api & +sleep 3 +pkill -f manager-api + +if [[ ! -f "./logs/error.log" ]]; then + echo "failed: failed to write log" + exit 1 +fi + +if [[ `grep -c "INFO" ./logs/error.log` -ne '0' ]]; then + echo "failed: should not write info log when level is warn" + exit 1 +fi + +#change level and path + +sed -i 's/file_path: logs\/error.log/file_path: .\/error.log/' conf/conf.yaml +sed -i 's/warn/info/' conf/conf.yaml + +./manager-api & +sleep 3 +pkill -f manager-api + +if [[ ! -f "./error.log" ]]; then + echo "failed: failed to write log" + exit 1 +fi + +if [[ `grep -c "INFO" ./error.log` -eq '0' ]]; then + echo "failed: failed to write log on right level" + exit 1 +fi