diff --git a/config.json b/config.json index 2fe0993b1..353cc38a0 100644 --- a/config.json +++ b/config.json @@ -18,5 +18,6 @@ "localOnly": false, "enableLocalMode": true, "localModeSocketLocation": "/var/tmp/focalboard_local.socket", - "authMode": "native" + "authMode": "native", + "logging_file": "logging.json" } diff --git a/logging.json b/logging.json new file mode 100644 index 000000000..41823bb16 --- /dev/null +++ b/logging.json @@ -0,0 +1,45 @@ +{ + "console": { + "type": "console", + "options": { + "out": "stdout" + }, + "format": "plain", + "format_options": { + "min_level_len": 5, + "min_msg_len": 40, + "enable_color": true + }, + "levels": [ + {"id": 5, "name": "debug"}, + {"id": 4, "name": "info", "color": 36}, + {"id": 3, "name": "warn"}, + {"id": 2, "name": "error", "color": 31}, + {"id": 1, "name": "fatal", "stacktrace": true}, + {"id": 0, "name": "panic", "stacktrace": true}, + {"id": 500, "name": "telemetry", "color":34} + ], + "maxqueuesize": 1000 + }, + "file": { + "type": "file", + "options": { + "filename": "focalboard-server.log", + "max_size": 1000000, + "max_age": 1, + "max_backups": 10, + "compress": true + }, + "format": "json", + "levels": [ + {"id": 5, "name": "debug"}, + {"id": 4, "name": "info"}, + {"id": 3, "name": "warn"}, + {"id": 2, "name": "error"}, + {"id": 1, "name": "fatal", "stacktrace": true}, + {"id": 0, "name": "panic", "stacktrace": true}, + {"id": 500, "name": "telemetry"} + ], + "maxqueuesize": 1000 + } +} diff --git a/server/api/admin.go b/server/api/admin.go index 70fa692fc..e49f39fb3 100644 --- a/server/api/admin.go +++ b/server/api/admin.go @@ -3,11 +3,11 @@ package api import ( "encoding/json" "io/ioutil" - "log" "net/http" "strings" "github.com/gorilla/mux" + "github.com/mattermost/focalboard/server/services/mlog" ) type AdminSetPasswordData struct { @@ -20,29 +20,29 @@ func (a *API) handleAdminSetPassword(w http.ResponseWriter, r *http.Request) { requestBody, err := ioutil.ReadAll(r.Body) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } var requestData AdminSetPasswordData err = json.Unmarshal(requestBody, &requestData) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } if !strings.Contains(requestData.Password, "") { - errorResponse(w, http.StatusBadRequest, "password is required", err) + a.errorResponse(w, http.StatusBadRequest, "password is required", err) return } err = a.app().UpdateUserPassword(username, requestData.Password) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } - log.Printf("AdminSetPassword, username: %s", username) + a.logger.Debug("AdminSetPassword, username: %s", mlog.String("username", username)) jsonStringResponse(w, http.StatusOK, "{}") } diff --git a/server/api/api.go b/server/api/api.go index b78852be7..f4c8eda3e 100644 --- a/server/api/api.go +++ b/server/api/api.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "io/ioutil" - "log" "net/http" "path/filepath" "strconv" @@ -16,6 +15,7 @@ import ( "github.com/gorilla/mux" "github.com/mattermost/focalboard/server/app" "github.com/mattermost/focalboard/server/model" + "github.com/mattermost/focalboard/server/services/mlog" "github.com/mattermost/focalboard/server/services/store" "github.com/mattermost/focalboard/server/utils" ) @@ -38,13 +38,15 @@ type API struct { authService string singleUserToken string MattermostAuth bool + logger *mlog.Logger } -func NewAPI(appBuilder func() *app.App, singleUserToken string, authService string) *API { +func NewAPI(appBuilder func() *app.App, singleUserToken string, authService string, logger *mlog.Logger) *API { return &API{ appBuilder: appBuilder, singleUserToken: singleUserToken, authService: authService, + logger: logger, } } @@ -93,8 +95,8 @@ func (a *API) RegisterAdminRoutes(r *mux.Router) { func (a *API) requireCSRFToken(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !a.checkCSRFToken(r) { - log.Println("checkCSRFToken FAILED") - errorResponse(w, http.StatusBadRequest, "", nil) + a.logger.Error("checkCSRFToken FAILED") + a.errorResponse(w, http.StatusBadRequest, "", nil) return } @@ -122,7 +124,7 @@ func (a *API) hasValidReadTokenForBlock(r *http.Request, container store.Contain isValid, err := a.app().IsValidReadToken(container, blockID, readToken) if err != nil { - log.Printf("IsValidReadToken ERROR: %v", err) + a.logger.Error("IsValidReadToken ERROR", mlog.Err(err)) return false } @@ -224,21 +226,25 @@ func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) { blockType := query.Get("type") container, err := a.getContainer(r) if err != nil { - noContainerErrorResponse(w, err) + a.noContainerErrorResponse(w, err) return } blocks, err := a.app().GetBlocks(*container, parentID, blockType) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } - // log.Printf("GetBlocks parentID: %s, type: %s, %d result(s)", parentID, blockType, len(blocks)) + a.logger.Debug("GetBlocks", + mlog.String("parentID", parentID), + mlog.String("blockType", blockType), + mlog.Int("block_count", len(blocks)), + ) json, err := json.Marshal(blocks) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } @@ -292,13 +298,13 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) { container, err := a.getContainer(r) if err != nil { - noContainerErrorResponse(w, err) + a.noContainerErrorResponse(w, err) return } requestBody, err := ioutil.ReadAll(r.Body) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } @@ -306,7 +312,7 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) { err = json.Unmarshal(requestBody, &blocks) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } @@ -314,19 +320,19 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) { // Error checking if len(block.Type) < 1 { message := fmt.Sprintf("missing type for block id %s", block.ID) - errorResponse(w, http.StatusBadRequest, message, nil) + a.errorResponse(w, http.StatusBadRequest, message, nil) return } if block.CreateAt < 1 { message := fmt.Sprintf("invalid createAt for block id %s", block.ID) - errorResponse(w, http.StatusBadRequest, message, nil) + a.errorResponse(w, http.StatusBadRequest, message, nil) return } if block.UpdateAt < 1 { message := fmt.Sprintf("invalid UpdateAt for block id %s", block.ID) - errorResponse(w, http.StatusBadRequest, message, nil) + a.errorResponse(w, http.StatusBadRequest, message, nil) return } } @@ -335,11 +341,11 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) { err = a.app().InsertBlocks(*container, blocks) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } - log.Printf("POST Blocks %d block(s)", len(blocks)) + a.logger.Debug("POST Blocks", mlog.Int("block_count", len(blocks))) jsonStringResponse(w, http.StatusOK, "{}") } @@ -374,13 +380,13 @@ func (a *API) handleGetUser(w http.ResponseWriter, r *http.Request) { user, err := a.app().GetUser(userID) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } userData, err := json.Marshal(user) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } @@ -424,14 +430,14 @@ func (a *API) handleGetMe(w http.ResponseWriter, r *http.Request) { } else { user, err = a.app().GetUser(session.UserID) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } } userData, err := json.Marshal(user) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } @@ -476,18 +482,18 @@ func (a *API) handleDeleteBlock(w http.ResponseWriter, r *http.Request) { container, err := a.getContainer(r) if err != nil { - noContainerErrorResponse(w, err) + a.noContainerErrorResponse(w, err) return } err = a.app().DeleteBlock(*container, blockID, userID) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } - log.Printf("DELETE Block %s", blockID) + a.logger.Debug("DELETE Block", mlog.String("blockID", blockID)) jsonStringResponse(w, http.StatusOK, "{}") } @@ -536,7 +542,7 @@ func (a *API) handleGetSubTree(w http.ResponseWriter, r *http.Request) { container, err := a.getContainerAllowingReadTokenForBlock(r, blockID) if err != nil { - noContainerErrorResponse(w, err) + a.noContainerErrorResponse(w, err) return } @@ -547,21 +553,25 @@ func (a *API) handleGetSubTree(w http.ResponseWriter, r *http.Request) { } if levels != 2 && levels != 3 { - log.Printf(`ERROR Invalid levels: %d`, levels) - errorResponse(w, http.StatusBadRequest, "invalid levels", nil) + a.logger.Error("Invalid levels", mlog.Int64("levels", levels)) + a.errorResponse(w, http.StatusBadRequest, "invalid levels", nil) return } blocks, err := a.app().GetSubTree(*container, blockID, int(levels)) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } - log.Printf("GetSubTree (%v) blockID: %s, %d result(s)", levels, blockID, len(blocks)) + a.logger.Debug("GetSubTree", + mlog.Int64("levels", levels), + mlog.String("blockID", blockID), + mlog.Int("block_count", len(blocks)), + ) json, err := json.Marshal(blocks) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } @@ -600,7 +610,7 @@ func (a *API) handleExport(w http.ResponseWriter, r *http.Request) { rootID := query.Get("root_id") container, err := a.getContainer(r) if err != nil { - noContainerErrorResponse(w, err) + a.noContainerErrorResponse(w, err) return } @@ -611,13 +621,13 @@ func (a *API) handleExport(w http.ResponseWriter, r *http.Request) { blocks, err = a.app().GetBlocksWithRootID(*container, rootID) } - log.Printf("%d raw block(s)", len(blocks)) + a.logger.Debug("raw blocks", mlog.Int("block_count", len(blocks))) blocks = filterOrphanBlocks(blocks) - log.Printf("EXPORT %d filtered block(s)", len(blocks)) + a.logger.Debug("EXPORT filtered blocks", mlog.Int("block_count", len(blocks))) json, err := json.Marshal(blocks) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } @@ -702,13 +712,13 @@ func (a *API) handleImport(w http.ResponseWriter, r *http.Request) { container, err := a.getContainer(r) if err != nil { - noContainerErrorResponse(w, err) + a.noContainerErrorResponse(w, err) return } requestBody, err := ioutil.ReadAll(r.Body) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } @@ -716,7 +726,7 @@ func (a *API) handleImport(w http.ResponseWriter, r *http.Request) { err = json.Unmarshal(requestBody, &blocks) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } @@ -724,11 +734,11 @@ func (a *API) handleImport(w http.ResponseWriter, r *http.Request) { err = a.app().InsertBlocks(*container, blocks) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } - log.Printf("IMPORT Blocks %d block(s)", len(blocks)) + a.logger.Debug("IMPORT Blocks", mlog.Int("block_count", len(blocks))) jsonStringResponse(w, http.StatusOK, "{}") } @@ -770,23 +780,23 @@ func (a *API) handleGetSharing(w http.ResponseWriter, r *http.Request) { container, err := a.getContainer(r) if err != nil { - noContainerErrorResponse(w, err) + a.noContainerErrorResponse(w, err) return } sharing, err := a.app().GetSharing(*container, rootID) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } sharingData, err := json.Marshal(sharing) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } - log.Printf("GET sharing %s", rootID) + a.logger.Debug("GET sharing", mlog.String("rootID", rootID)) jsonBytesResponse(w, http.StatusOK, sharingData) } @@ -827,13 +837,13 @@ func (a *API) handlePostSharing(w http.ResponseWriter, r *http.Request) { container, err := a.getContainer(r) if err != nil { - noContainerErrorResponse(w, err) + a.noContainerErrorResponse(w, err) return } requestBody, err := ioutil.ReadAll(r.Body) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } @@ -841,7 +851,7 @@ func (a *API) handlePostSharing(w http.ResponseWriter, r *http.Request) { err = json.Unmarshal(requestBody, &sharing) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } @@ -856,11 +866,11 @@ func (a *API) handlePostSharing(w http.ResponseWriter, r *http.Request) { err = a.app().UpsertSharing(*container, sharing) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } - log.Printf("POST sharing %s", sharing.ID) + a.logger.Debug("POST sharing", mlog.String("sharingID", sharing.ID)) jsonStringResponse(w, http.StatusOK, "{}") } @@ -902,29 +912,29 @@ func (a *API) handleGetWorkspace(w http.ResponseWriter, r *http.Request) { ctx := r.Context() session := ctx.Value("session").(*model.Session) if !a.app().DoesUserHaveWorkspaceAccess(session.UserID, workspaceID) { - errorResponse(w, http.StatusUnauthorized, "", nil) + a.errorResponse(w, http.StatusUnauthorized, "", nil) return } workspace, err = a.app().GetWorkspace(workspaceID) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) } if workspace == nil { - errorResponse(w, http.StatusUnauthorized, "", nil) + a.errorResponse(w, http.StatusUnauthorized, "", nil) return } } else { workspace, err = a.app().GetRootWorkspace() if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } } workspaceData, err := json.Marshal(workspace) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } @@ -957,7 +967,7 @@ func (a *API) handlePostWorkspaceRegenerateSignupToken(w http.ResponseWriter, r workspace, err := a.app().GetRootWorkspace() if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } @@ -965,7 +975,7 @@ func (a *API) handlePostWorkspaceRegenerateSignupToken(w http.ResponseWriter, r err = a.app().UpsertWorkspaceSignupToken(*workspace) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } @@ -1018,7 +1028,7 @@ func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) { // Caller must have access to the root block's container _, err := a.getContainerAllowingReadTokenForBlock(r, rootID) if err != nil { - noContainerErrorResponse(w, err) + a.noContainerErrorResponse(w, err) return } @@ -1033,7 +1043,7 @@ func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) { fileReader, err := a.app().GetFileReader(workspaceID, rootID, filename) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } defer fileReader.Close() @@ -1092,7 +1102,7 @@ func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) { // Caller must have access to the root block's container _, err := a.getContainerAllowingReadTokenForBlock(r, rootID) if err != nil { - noContainerErrorResponse(w, err) + a.noContainerErrorResponse(w, err) return } @@ -1106,14 +1116,17 @@ func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) { fileId, err := a.app().SaveFile(file, workspaceID, rootID, handle.Filename) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } - log.Printf("uploadFile, filename: %s, fileId: %s", handle.Filename, fileId) + a.logger.Debug("uploadFile", + mlog.String("filename", handle.Filename), + mlog.String("fileID", fileId), + ) data, err := json.Marshal(FileUploadResponse{FileID: fileId}) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } @@ -1122,6 +1135,39 @@ func (a *API) handleUploadFile(w http.ResponseWriter, r *http.Request) { // Response helpers +func (a *API) errorResponse(w http.ResponseWriter, code int, message string, sourceError error) { + a.logger.Error("API ERROR", + mlog.Int("code", code), + mlog.Err(sourceError), + ) + w.Header().Set("Content-Type", "application/json") + data, err := json.Marshal(model.ErrorResponse{Error: message, ErrorCode: code}) + if err != nil { + data = []byte("{}") + } + w.WriteHeader(code) + w.Write(data) +} + +func (a *API) errorResponseWithCode(w http.ResponseWriter, statusCode int, errorCode int, message string, sourceError error) { + a.logger.Error("API ERROR", + mlog.Int("status", statusCode), + mlog.Int("code", errorCode), + mlog.Err(sourceError), + ) + w.Header().Set("Content-Type", "application/json") + data, err := json.Marshal(model.ErrorResponse{Error: message, ErrorCode: errorCode}) + if err != nil { + data = []byte("{}") + } + w.WriteHeader(statusCode) + w.Write(data) +} + +func (a *API) noContainerErrorResponse(w http.ResponseWriter, sourceError error) { + a.errorResponseWithCode(w, http.StatusBadRequest, ERROR_NO_WORKSPACE_CODE, ERROR_NO_WORKSPACE_MESSAGE, sourceError) +} + func jsonStringResponse(w http.ResponseWriter, code int, message string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(code) @@ -1134,32 +1180,6 @@ func jsonBytesResponse(w http.ResponseWriter, code int, json []byte) { w.Write(json) } -func errorResponse(w http.ResponseWriter, code int, message string, sourceError error) { - log.Printf("API ERROR %d, err: %v\n", code, sourceError) - w.Header().Set("Content-Type", "application/json") - data, err := json.Marshal(model.ErrorResponse{Error: message, ErrorCode: code}) - if err != nil { - data = []byte("{}") - } - w.WriteHeader(code) - w.Write(data) -} - -func errorResponseWithCode(w http.ResponseWriter, statusCode int, errorCode int, message string, sourceError error) { - log.Printf("API ERROR status %d, errorCode: %d, err: %v\n", statusCode, errorCode, sourceError) - w.Header().Set("Content-Type", "application/json") - data, err := json.Marshal(model.ErrorResponse{Error: message, ErrorCode: errorCode}) - if err != nil { - data = []byte("{}") - } - w.WriteHeader(statusCode) - w.Write(data) -} - -func noContainerErrorResponse(w http.ResponseWriter, sourceError error) { - errorResponseWithCode(w, http.StatusBadRequest, ERROR_NO_WORKSPACE_CODE, ERROR_NO_WORKSPACE_MESSAGE, sourceError) -} - func addUserID(rw http.ResponseWriter, req *http.Request, next http.Handler) { ctx := context.WithValue(req.Context(), "userid", req.Header.Get("userid")) req = req.WithContext(ctx) diff --git a/server/api/auth.go b/server/api/auth.go index 95d7a098e..844e224bf 100644 --- a/server/api/auth.go +++ b/server/api/auth.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "io/ioutil" - "log" "net" "net/http" "strings" @@ -15,6 +14,7 @@ import ( serverContext "github.com/mattermost/focalboard/server/context" "github.com/mattermost/focalboard/server/model" "github.com/mattermost/focalboard/server/services/auth" + "github.com/mattermost/focalboard/server/services/mlog" ) // LoginRequest is a login request @@ -154,32 +154,32 @@ func (a *API) handleLogin(w http.ResponseWriter, r *http.Request) { if len(a.singleUserToken) > 0 { // Not permitted in single-user mode - errorResponse(w, http.StatusUnauthorized, "", nil) + a.errorResponse(w, http.StatusUnauthorized, "", nil) return } requestBody, err := ioutil.ReadAll(r.Body) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } var loginData LoginRequest err = json.Unmarshal(requestBody, &loginData) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } if loginData.Type == "normal" { token, err := a.app().Login(loginData.Username, loginData.Email, loginData.Password, loginData.MfaToken) if err != nil { - errorResponse(w, http.StatusUnauthorized, "incorrect login", err) + a.errorResponse(w, http.StatusUnauthorized, "incorrect login", err) return } json, err := json.Marshal(LoginResponse{Token: token}) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } @@ -187,7 +187,7 @@ func (a *API) handleLogin(w http.ResponseWriter, r *http.Request) { return } - errorResponse(w, http.StatusBadRequest, "invalid login type", nil) + a.errorResponse(w, http.StatusBadRequest, "invalid login type", nil) } func (a *API) handleRegister(w http.ResponseWriter, r *http.Request) { @@ -217,20 +217,20 @@ func (a *API) handleRegister(w http.ResponseWriter, r *http.Request) { if len(a.singleUserToken) > 0 { // Not permitted in single-user mode - errorResponse(w, http.StatusUnauthorized, "", nil) + a.errorResponse(w, http.StatusUnauthorized, "", nil) return } requestBody, err := ioutil.ReadAll(r.Body) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } var registerData RegisterRequest err = json.Unmarshal(requestBody, ®isterData) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } @@ -238,35 +238,35 @@ func (a *API) handleRegister(w http.ResponseWriter, r *http.Request) { if len(registerData.Token) > 0 { workspace, err := a.app().GetRootWorkspace() if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } if registerData.Token != workspace.SignupToken { - errorResponse(w, http.StatusUnauthorized, "", nil) + a.errorResponse(w, http.StatusUnauthorized, "", nil) return } } else { // No signup token, check if no active users userCount, err := a.app().GetRegisteredUserCount() if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } if userCount > 0 { - errorResponse(w, http.StatusUnauthorized, "", nil) + a.errorResponse(w, http.StatusUnauthorized, "", nil) return } } if err = registerData.IsValid(); err != nil { - errorResponse(w, http.StatusBadRequest, err.Error(), err) + a.errorResponse(w, http.StatusBadRequest, err.Error(), err) return } err = a.app().RegisterUser(registerData.Username, registerData.Email, registerData.Password) if err != nil { - errorResponse(w, http.StatusBadRequest, err.Error(), err) + a.errorResponse(w, http.StatusBadRequest, err.Error(), err) return } @@ -309,7 +309,7 @@ func (a *API) handleChangePassword(w http.ResponseWriter, r *http.Request) { if len(a.singleUserToken) > 0 { // Not permitted in single-user mode - errorResponse(w, http.StatusUnauthorized, "", nil) + a.errorResponse(w, http.StatusUnauthorized, "", nil) return } @@ -318,23 +318,23 @@ func (a *API) handleChangePassword(w http.ResponseWriter, r *http.Request) { requestBody, err := ioutil.ReadAll(r.Body) if err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } var requestData ChangePasswordRequest if err := json.Unmarshal(requestBody, &requestData); err != nil { - errorResponse(w, http.StatusInternalServerError, "", err) + a.errorResponse(w, http.StatusInternalServerError, "", err) return } if err = requestData.IsValid(); err != nil { - errorResponse(w, http.StatusBadRequest, err.Error(), err) + a.errorResponse(w, http.StatusBadRequest, err.Error(), err) return } if err = a.app().ChangePassword(userID, requestData.OldPassword, requestData.NewPassword); err != nil { - errorResponse(w, http.StatusBadRequest, err.Error(), err) + a.errorResponse(w, http.StatusBadRequest, err.Error(), err) return } @@ -349,10 +349,10 @@ func (a *API) attachSession(handler func(w http.ResponseWriter, r *http.Request) return func(w http.ResponseWriter, r *http.Request) { token, _ := auth.ParseAuthTokenFromRequest(r) - log.Printf(`Single User: %v`, len(a.singleUserToken) > 0) + a.logger.Debug(`attachSession`, mlog.Bool("single_user", len(a.singleUserToken) > 0)) if len(a.singleUserToken) > 0 { if required && (token != a.singleUserToken) { - errorResponse(w, http.StatusUnauthorized, "", nil) + a.errorResponse(w, http.StatusUnauthorized, "", nil) return } @@ -391,7 +391,7 @@ func (a *API) attachSession(handler func(w http.ResponseWriter, r *http.Request) session, err := a.app().GetSession(token) if err != nil { if required { - errorResponse(w, http.StatusUnauthorized, "", err) + a.errorResponse(w, http.StatusUnauthorized, "", err) return } @@ -401,8 +401,12 @@ func (a *API) attachSession(handler func(w http.ResponseWriter, r *http.Request) authService := session.AuthService if authService != a.authService { - log.Printf(`Session '%s' authService mismatch '%s' instead of '%s'`, session.ID, authService, a.authService) - errorResponse(w, http.StatusUnauthorized, "", err) + a.logger.Error(`Session authService mismatch`, + mlog.String("sessionID", session.ID), + mlog.String("want", a.authService), + mlog.String("got", authService), + ) + a.errorResponse(w, http.StatusUnauthorized, "", err) return } @@ -416,7 +420,7 @@ func (a *API) adminRequired(handler func(w http.ResponseWriter, r *http.Request) // Currently, admin APIs require local unix connections conn := serverContext.GetContextConn(r) if _, isUnix := conn.(*net.UnixConn); !isUnix { - errorResponse(w, http.StatusUnauthorized, "", nil) + a.errorResponse(w, http.StatusUnauthorized, "", nil) return } diff --git a/server/app/app.go b/server/app/app.go index 8f4870cf6..bc44a4949 100644 --- a/server/app/app.go +++ b/server/app/app.go @@ -3,9 +3,11 @@ package app import ( "github.com/mattermost/focalboard/server/auth" "github.com/mattermost/focalboard/server/services/config" + "github.com/mattermost/focalboard/server/services/mlog" "github.com/mattermost/focalboard/server/services/store" "github.com/mattermost/focalboard/server/services/webhook" "github.com/mattermost/focalboard/server/ws" + "github.com/mattermost/mattermost-server/v5/shared/filestore" ) @@ -16,6 +18,7 @@ type App struct { wsServer *ws.Server filesBackend filestore.FileBackend webhook *webhook.Client + logger *mlog.Logger } func New( @@ -25,6 +28,7 @@ func New( wsServer *ws.Server, filesBackend filestore.FileBackend, webhook *webhook.Client, + logger *mlog.Logger, ) *App { return &App{ config: config, @@ -33,5 +37,6 @@ func New( wsServer: wsServer, filesBackend: filesBackend, webhook: webhook, + logger: logger, } } diff --git a/server/app/auth.go b/server/app/auth.go index e027dd66e..ab7979770 100644 --- a/server/app/auth.go +++ b/server/app/auth.go @@ -1,11 +1,10 @@ package app import ( - "log" - "github.com/google/uuid" "github.com/mattermost/focalboard/server/model" "github.com/mattermost/focalboard/server/services/auth" + "github.com/mattermost/focalboard/server/services/mlog" "github.com/mattermost/focalboard/server/services/store" "github.com/pkg/errors" @@ -80,7 +79,7 @@ func (a *App) Login(username, email, password, mfaToken string) (string, error) } if !auth.ComparePassword(user.Password, password) { - log.Printf("Invalid password for userID: %s\n", user.ID) + a.logger.Debug("Invalid password for user", mlog.String("userID", user.ID)) return "", errors.New("invalid username or password") } @@ -175,7 +174,7 @@ func (a *App) ChangePassword(userID, oldPassword, newPassword string) error { } if !auth.ComparePassword(user.Password, oldPassword) { - log.Printf("Invalid password for userID: %s\n", user.ID) + a.logger.Debug("Invalid password for user", mlog.String("userID", user.ID)) return errors.New("invalid username or password") } diff --git a/server/app/files.go b/server/app/files.go index 6a7f1065c..65ed17d62 100644 --- a/server/app/files.go +++ b/server/app/files.go @@ -4,12 +4,13 @@ import ( "errors" "fmt" "io" - "log" "os" "path/filepath" "strings" + "github.com/mattermost/focalboard/server/services/mlog" "github.com/mattermost/focalboard/server/utils" + "github.com/mattermost/mattermost-server/v5/shared/filestore" ) @@ -46,9 +47,9 @@ func (a *App) GetFileReader(workspaceID, rootID, filename string) (filestore.Rea if oldExists { err := a.filesBackend.MoveFile(filename, filePath) if err != nil { - log.Printf("ERROR moving old file from '%s' to '%s'", filename, filePath) + a.logger.Error("ERROR moving file", mlog.String("old", filename), mlog.String("new", filePath)) } else { - log.Printf("Moved old file from '%s' to '%s'", filename, filePath) + a.logger.Debug("Moved file", mlog.String("old", filename), mlog.String("new", filePath)) } } } diff --git a/server/app/helper_test.go b/server/app/helper_test.go index 3d887fd49..13d6c9f38 100644 --- a/server/app/helper_test.go +++ b/server/app/helper_test.go @@ -9,9 +9,11 @@ import ( "github.com/mattermost/focalboard/server/auth" "github.com/mattermost/focalboard/server/services/config" + "github.com/mattermost/focalboard/server/services/mlog" "github.com/mattermost/focalboard/server/services/store/mockstore" "github.com/mattermost/focalboard/server/services/webhook" "github.com/mattermost/focalboard/server/ws" + "github.com/mattermost/mattermost-server/v5/shared/filestore/mocks" ) @@ -27,10 +29,12 @@ func SetupTestHelper(t *testing.T) *TestHelper { cfg := config.Configuration{} store := mockstore.NewMockStore(ctrl) auth := auth.New(&cfg, store) + logger := mlog.NewLogger() + logger.Configure("", cfg.LoggingEscapedJson) sessionToken := "TESTTOKEN" - wsserver := ws.NewServer(auth, sessionToken, false) - webhook := webhook.NewClient(&cfg) - app2 := New(&cfg, store, auth, wsserver, &mocks.FileBackend{}, webhook) + wsserver := ws.NewServer(auth, sessionToken, false, logger) + webhook := webhook.NewClient(&cfg, logger) + app2 := New(&cfg, store, auth, wsserver, &mocks.FileBackend{}, webhook, logger) return &TestHelper{ App: app2, diff --git a/server/app/workspaces.go b/server/app/workspaces.go index 2eb047e0c..9404b8985 100644 --- a/server/app/workspaces.go +++ b/server/app/workspaces.go @@ -2,9 +2,9 @@ package app import ( "database/sql" - "log" "github.com/mattermost/focalboard/server/model" + "github.com/mattermost/focalboard/server/services/mlog" "github.com/mattermost/focalboard/server/utils" ) @@ -18,16 +18,16 @@ func (a *App) GetRootWorkspace() (*model.Workspace, error) { } err := a.store.UpsertWorkspaceSignupToken(*workspace) if err != nil { - log.Fatal("Unable to initialize workspace", err) + a.logger.Fatal("Unable to initialize workspace", mlog.Err(err)) return nil, err } workspace, err = a.store.GetWorkspace(workspaceID) if err != nil { - log.Fatal("Unable to get initialized workspace", err) + a.logger.Fatal("Unable to get initialized workspace", mlog.Err(err)) return nil, err } - log.Println("initialized workspace") + a.logger.Info("initialized workspace") } return workspace, nil diff --git a/server/go.mod b/server/go.mod index ab404ed27..9c9c393bb 100644 --- a/server/go.mod +++ b/server/go.mod @@ -12,6 +12,7 @@ require ( github.com/gorilla/websocket v1.4.2 github.com/lib/pq v1.10.0 github.com/magiconair/properties v1.8.5 // indirect + github.com/mattermost/logr/v2 v2.0.0-20210525034931-179e4b3c986d github.com/mattermost/mattermost-server/v5 v5.3.2-0.20210524045451-a4f7df6f6e3c github.com/mattn/go-sqlite3 v2.0.3+incompatible github.com/mitchellh/mapstructure v1.4.1 // indirect @@ -25,7 +26,7 @@ require ( github.com/spf13/viper v1.7.1 github.com/stretchr/testify v1.7.0 github.com/tidwall/gjson v1.7.3 // indirect - go.uber.org/zap v1.16.0 + go.uber.org/zap v1.16.0 // indirect golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d golang.org/x/sys v0.0.0-20210324051608-47abb6519492 // indirect diff --git a/server/go.sum b/server/go.sum index 31c1896d7..ff3e0ca1b 100644 --- a/server/go.sum +++ b/server/go.sum @@ -606,6 +606,8 @@ github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d h1:/RJ/UV7M5c7L2TQ github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d/go.mod h1:HLbgMEI5K131jpxGazJ97AxfPDt31osq36YS1oxFQPQ= github.com/mattermost/logr v1.0.13 h1:6F/fM3csvH6Oy5sUpJuW7YyZSzZZAhJm5VcgKMxA2P8= github.com/mattermost/logr v1.0.13/go.mod h1:Mt4DPu1NXMe6JxPdwCC0XBoxXmN9eXOIRPoZarU2PXs= +github.com/mattermost/logr/v2 v2.0.0-20210525034931-179e4b3c986d h1:MfG19tMusEOb0k/UBrLQFYhHnLOWt8vcuGDzDQ1fvJw= +github.com/mattermost/logr/v2 v2.0.0-20210525034931-179e4b3c986d/go.mod h1:mpPp935r5dIkFDo2y9Q87cQWhFR/4xXpNh0k/y8Hmwg= github.com/mattermost/mattermost-server/v5 v5.3.2-0.20210524045451-a4f7df6f6e3c h1:p0C9yt6UYyTExEeHjBPBUCwCMAyTWvwAEc2/plNuZL4= github.com/mattermost/mattermost-server/v5 v5.3.2-0.20210524045451-a4f7df6f6e3c/go.mod h1:6CqGEG0Vnhrl23h8LB+lcOIT8KIUhzbJ7qhXlV7Ek9U= github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0/go.mod h1:nV5bfVpT//+B1RPD2JvRnxbkLmJEYXmRaaVl15fsXjs= diff --git a/server/integrationtests/clienttestlib.go b/server/integrationtests/clienttestlib.go index a158a1e91..cc38cb799 100644 --- a/server/integrationtests/clienttestlib.go +++ b/server/integrationtests/clienttestlib.go @@ -1,7 +1,6 @@ package integrationtests import ( - "log" "net/http" "os" "time" @@ -9,6 +8,7 @@ import ( "github.com/mattermost/focalboard/server/client" "github.com/mattermost/focalboard/server/server" "github.com/mattermost/focalboard/server/services/config" + "github.com/mattermost/focalboard/server/services/mlog" ) type TestHelper struct { @@ -27,22 +27,47 @@ func getTestConfig() *config.Configuration { connectionString = ":memory:" } + logging := []byte(` + { + "testing": { + "type": "console", + "options": { + "out": "stdout" + }, + "format": "plain", + "format_options": { + "delim": " " + }, + "levels": [ + {"id": 5, "name": "debug"}, + {"id": 4, "name": "info"}, + {"id": 3, "name": "warn"}, + {"id": 2, "name": "error", "stacktrace": true}, + {"id": 1, "name": "fatal", "stacktrace": true}, + {"id": 0, "name": "panic", "stacktrace": true} + ] + } + }`) + return &config.Configuration{ - ServerRoot: "http://localhost:8888", - Port: 8888, - DBType: dbType, - DBConfigString: connectionString, - DBTablePrefix: "test_", - WebPath: "./pack", - FilesDriver: "local", - FilesPath: "./files", + ServerRoot: "http://localhost:8888", + Port: 8888, + DBType: dbType, + DBConfigString: connectionString, + DBTablePrefix: "test_", + WebPath: "./pack", + FilesDriver: "local", + FilesPath: "./files", + LoggingEscapedJson: string(logging), } } func SetupTestHelper() *TestHelper { sessionToken := "TESTTOKEN" th := &TestHelper{} - srv, err := server.New(getTestConfig(), sessionToken) + logger := mlog.NewLogger() + logger.Configure("", getTestConfig().LoggingEscapedJson) + srv, err := server.New(getTestConfig(), sessionToken, logger) if err != nil { panic(err) } @@ -61,10 +86,10 @@ func (th *TestHelper) InitBasic() *TestHelper { for { URL := th.Server.Config().ServerRoot - log.Printf("Polling server at %v", URL) + th.Server.Logger().Info("Polling server", mlog.String("url", URL)) resp, err := http.Get(URL) if err != nil { - log.Println("Polling failed:", err) + th.Server.Logger().Error("Polling failed", mlog.Err(err)) time.Sleep(100 * time.Millisecond) continue } @@ -72,12 +97,12 @@ func (th *TestHelper) InitBasic() *TestHelper { // Currently returns 404 // if resp.StatusCode != http.StatusOK { - // log.Println("Not OK:", resp.StatusCode) + // th.Server.Logger().Error("Not OK", mlog.Int("statusCode", resp.StatusCode)) // continue // } // Reached this point: server is up and running! - log.Println("Server ping OK, statusCode:", resp.StatusCode) + th.Server.Logger().Info("Server ping OK", mlog.Int("statusCode", resp.StatusCode)) break } @@ -86,6 +111,8 @@ func (th *TestHelper) InitBasic() *TestHelper { } func (th *TestHelper) TearDown() { + defer th.Server.Logger().Shutdown() + err := th.Server.Shutdown() if err != nil { panic(err) diff --git a/server/main/main.go b/server/main/main.go index 05485b544..040483ce7 100644 --- a/server/main/main.go +++ b/server/main/main.go @@ -38,6 +38,9 @@ import ( "github.com/mattermost/focalboard/server/server" "github.com/mattermost/focalboard/server/services/config" ) +import ( + "github.com/mattermost/focalboard/server/services/mlog" +) // Active server used with shared code (dll) var pServer *server.Server @@ -58,13 +61,13 @@ func isProcessRunning(pid int) bool { } // monitorPid is used to keep the server lifetime in sync with another (client app) process -func monitorPid(pid int) { - log.Printf("Monitoring PID: %d", pid) +func monitorPid(pid int, logger *mlog.Logger) { + logger.Info("Monitoring PID", mlog.Int("pid", pid)) go func() { for { if !isProcessRunning(pid) { - log.Printf("Monitored process not found, exiting.") + logger.Info("Monitored process not found, exiting.") os.Exit(1) } @@ -73,18 +76,17 @@ func monitorPid(pid int) { }() } -func logInfo() { - log.Println("Focalboard Server") - log.Println("Version: " + model.CurrentVersion) - log.Println("Edition: " + model.Edition) - log.Println("Build Number: " + model.BuildNumber) - log.Println("Build Date: " + model.BuildDate) - log.Println("Build Hash: " + model.BuildHash) +func logInfo(logger *mlog.Logger) { + logger.Info("FocalBoard Server", + mlog.String("version", model.CurrentVersion), + mlog.String("edition", model.Edition), + mlog.String("build_number", model.BuildNumber), + mlog.String("build_date", model.BuildDate), + mlog.String("build_hash", model.BuildHash), + ) } func main() { - logInfo() - // config.json file config, err := config.ReadConfigFile() if err != nil { @@ -92,6 +94,21 @@ func main() { return } + logger := mlog.NewLogger() + err = logger.Configure(config.LoggingFile, config.LoggingEscapedJson) + if err != nil { + log.Fatal("Error in config file for logger: ", err) + return + } + defer logger.Shutdown() + + if logger.HasTargets() { + restore := logger.RedirectStdLog(mlog.Info, mlog.String("src", "stdlog")) + defer restore() + } + + logInfo(logger) + // Command line args pMonitorPid := flag.Int("monitorpid", -1, "a process ID") pPort := flag.Int("port", config.Port, "the port number") @@ -109,42 +126,42 @@ func main() { if singleUser { singleUserToken = os.Getenv("FOCALBOARD_SINGLE_USER_TOKEN") if len(singleUserToken) < 1 { - log.Fatal("The FOCALBOARD_SINGLE_USER_TOKEN environment variable must be set for single user mode ") + logger.Fatal("The FOCALBOARD_SINGLE_USER_TOKEN environment variable must be set for single user mode ") return } - log.Printf("Single user mode") + logger.Info("Single user mode") } if pMonitorPid != nil && *pMonitorPid > 0 { - monitorPid(*pMonitorPid) + monitorPid(*pMonitorPid, logger) } // Override config from commandline if pDBType != nil && len(*pDBType) > 0 { config.DBType = *pDBType - log.Printf("DBType from commandline: %s", *pDBType) + logger.Info("DBType from commandline", mlog.String("DBType", *pDBType)) } if pDBConfig != nil && len(*pDBConfig) > 0 { config.DBConfigString = *pDBConfig // Don't echo, as the confix string may contain passwords - log.Printf("DBConfigString overriden from commandline") + logger.Info("DBConfigString overriden from commandline") } if pPort != nil && *pPort > 0 && *pPort != config.Port { // Override port - log.Printf("Port from commandline: %d", *pPort) + logger.Info("Port from commandline", mlog.Int("port", *pPort)) config.Port = *pPort } - server, err := server.New(config, singleUserToken) + server, err := server.New(config, singleUserToken, logger) if err != nil { - log.Fatal("server.New ERROR: ", err) + logger.Fatal("server.New ERROR", mlog.Err(err)) } if err := server.Start(); err != nil { - log.Fatal("server.Start ERROR: ", err) + logger.Fatal("server.Start ERROR", mlog.Err(err)) } // Setting up signal capturing @@ -176,8 +193,6 @@ func StopServer() { } func startServer(webPath string, filesPath string, port int, singleUserToken, dbConfigString string) { - logInfo() - if pServer != nil { stopServer() pServer = nil @@ -190,6 +205,15 @@ func startServer(webPath string, filesPath string, port int, singleUserToken, db return } + logger := mlog.NewLogger() + err = logger.Configure(config.LoggingFile, config.LoggingEscapedJson) + if err != nil { + log.Fatal("Error in config file for logger: ", err) + return + } + + logInfo(logger) + if len(filesPath) > 0 { config.FilesPath = filesPath } @@ -206,13 +230,13 @@ func startServer(webPath string, filesPath string, port int, singleUserToken, db config.DBConfigString = dbConfigString } - pServer, err = server.New(config, singleUserToken) + pServer, err = server.New(config, singleUserToken, logger) if err != nil { - log.Fatal("server.New ERROR: ", err) + logger.Fatal("server.New ERROR", mlog.Err(err)) } if err := pServer.Start(); err != nil { - log.Fatal("server.Start ERROR: ", err) + logger.Fatal("server.Start ERROR", mlog.Err(err)) } } @@ -223,6 +247,8 @@ func stopServer() { err := pServer.Shutdown() if err != nil { - log.Fatal("server.Shutdown ERROR: ", err) + pServer.Logger().Error("server.Shutdown ERROR", mlog.Err(err)) } + pServer.Logger().Shutdown() + pServer = nil } diff --git a/server/server/server.go b/server/server/server.go index 76dc2408f..350b39ca7 100644 --- a/server/server/server.go +++ b/server/server/server.go @@ -9,8 +9,6 @@ import ( "syscall" "time" - "go.uber.org/zap" - "github.com/google/uuid" "github.com/gorilla/mux" "github.com/pkg/errors" @@ -21,6 +19,7 @@ import ( "github.com/mattermost/focalboard/server/context" appModel "github.com/mattermost/focalboard/server/model" "github.com/mattermost/focalboard/server/services/config" + "github.com/mattermost/focalboard/server/services/mlog" "github.com/mattermost/focalboard/server/services/prometheus" "github.com/mattermost/focalboard/server/services/scheduler" "github.com/mattermost/focalboard/server/services/store" @@ -30,9 +29,10 @@ import ( "github.com/mattermost/focalboard/server/services/webhook" "github.com/mattermost/focalboard/server/web" "github.com/mattermost/focalboard/server/ws" + "github.com/oklog/run" + "github.com/mattermost/mattermost-server/v5/shared/filestore" "github.com/mattermost/mattermost-server/v5/utils" - "github.com/oklog/run" ) const ( @@ -49,7 +49,7 @@ type Server struct { store store.Store filesBackend filestore.FileBackend telemetry *telemetry.Service - logger *zap.Logger + logger *mlog.Logger cleanUpSessionsTask *scheduler.ScheduledTask promServer *prometheus.Service promInstrumentor *prometheus.Instrumentor @@ -60,16 +60,11 @@ type Server struct { appBuilder func() *app.App } -func New(cfg *config.Configuration, singleUserToken string) (*Server, error) { - logger, err := zap.NewProduction() - if err != nil { - return nil, err - } - +func New(cfg *config.Configuration, singleUserToken string, logger *mlog.Logger) (*Server, error) { var db store.Store - db, err = sqlstore.New(cfg.DBType, cfg.DBConfigString, cfg.DBTablePrefix) + db, err := sqlstore.New(cfg.DBType, cfg.DBConfigString, cfg.DBTablePrefix, logger) if err != nil { - log.Print("Unable to start the database", err) + logger.Error("Unable to start the database", mlog.Err(err)) return nil, err } if cfg.AuthMode == "mattermost" { @@ -83,7 +78,7 @@ func New(cfg *config.Configuration, singleUserToken string) (*Server, error) { authenticator := auth.New(cfg, db) - wsServer := ws.NewServer(authenticator, singleUserToken, cfg.AuthMode == "mattermost") + wsServer := ws.NewServer(authenticator, singleUserToken, cfg.AuthMode == "mattermost", logger) filesBackendSettings := filestore.FileBackendSettings{} filesBackendSettings.DriverName = cfg.FilesDriver @@ -101,15 +96,15 @@ func New(cfg *config.Configuration, singleUserToken string) (*Server, error) { filesBackend, appErr := filestore.NewFileBackend(filesBackendSettings) if appErr != nil { - log.Print("Unable to initialize the files storage") + logger.Error("Unable to initialize the files storage", mlog.Err(appErr)) return nil, errors.New("unable to initialize the files storage") } - webhookClient := webhook.NewClient(cfg) + webhookClient := webhook.NewClient(cfg, logger) - appBuilder := func() *app.App { return app.New(cfg, db, authenticator, wsServer, filesBackend, webhookClient) } - focalboardAPI := api.NewAPI(appBuilder, singleUserToken, cfg.AuthMode) + appBuilder := func() *app.App { return app.New(cfg, db, authenticator, wsServer, filesBackend, webhookClient, logger) } + focalboardAPI := api.NewAPI(appBuilder, singleUserToken, cfg.AuthMode, logger) // Local router for admin APIs localRouter := mux.NewRouter() @@ -117,11 +112,11 @@ func New(cfg *config.Configuration, singleUserToken string) (*Server, error) { // Init workspace if _, err = appBuilder().GetRootWorkspace(); err != nil { - log.Print("Unable to get root workspace", err) + logger.Error("Unable to get root workspace", mlog.Err(err)) return nil, err } - webServer := web.NewServer(cfg.WebPath, cfg.ServerRoot, cfg.Port, cfg.UseSSL, cfg.LocalOnly) + webServer := web.NewServer(cfg.WebPath, cfg.ServerRoot, cfg.Port, cfg.UseSSL, cfg.LocalOnly, logger) webServer.AddRoutes(wsServer) webServer.AddRoutes(focalboardAPI) @@ -160,7 +155,7 @@ func New(cfg *config.Configuration, singleUserToken string) (*Server, error) { return nil, err } - telemetryService := telemetry.New(telemetryID, zap.NewStdLog(logger)) + telemetryService := telemetry.New(telemetryID, logger.StdLogger(mlog.Telemetry)) telemetryService.RegisterTracker("server", func() map[string]interface{} { return map[string]interface{}{ "version": appModel.CurrentVersion, @@ -226,7 +221,7 @@ func (s *Server) Start() error { } if err := s.store.CleanUpSessions(secondsAgo); err != nil { - s.logger.Error("Unable to clean up the sessions", zap.Error(err)) + s.logger.Error("Unable to clean up the sessions", mlog.Err(err)) } }, cleanupSessionTaskFrequency) @@ -267,7 +262,7 @@ func (s *Server) Shutdown() error { } if err := s.telemetry.Shutdown(); err != nil { - s.logger.Warn("Error occurred when shutting down telemetry", zap.Error(err)) + s.logger.Warn("Error occurred when shutting down telemetry", mlog.Err(err)) } defer s.logger.Info("Server.Shutdown") @@ -279,6 +274,10 @@ func (s *Server) Config() *config.Configuration { return s.config } +func (s *Server) Logger() *mlog.Logger { + return s.logger +} + // Local server func (s *Server) startLocalModeServer() error { @@ -289,7 +288,7 @@ func (s *Server) startLocalModeServer() error { // TODO: Close and delete socket file on shutdown if err := syscall.Unlink(s.config.LocalModeSocketLocation); err != nil { - log.Print("Unable to unlink socket.", err) + s.logger.Error("Unable to unlink socket.", mlog.Err(err)) } socket := s.config.LocalModeSocketLocation @@ -302,10 +301,10 @@ func (s *Server) startLocalModeServer() error { } go func() { - log.Println("Starting unix socket server") + s.logger.Info("Starting unix socket server") err = s.localModeServer.Serve(unixListener) if err != nil && !errors.Is(err, http.ErrServerClosed) { - log.Printf("Error starting unix socket server: %v", err) + s.logger.Error("Error starting unix socket server", mlog.Err(err)) } }() diff --git a/server/services/config/config.go b/server/services/config/config.go index bc194573c..84e31a29f 100644 --- a/server/services/config/config.go +++ b/server/services/config/config.go @@ -48,6 +48,9 @@ type Configuration struct { LocalModeSocketLocation string `json:"localModeSocketLocation" mapstructure:"localModeSocketLocation"` AuthMode string `json:"authMode" mapstructure:"authMode"` + + LoggingFile string `json:"logging_file" mapstructure:"logging_file"` + LoggingEscapedJson string `json:"logging_escaped_json" mapstructure:"logging_escaped_json"` } // ReadConfigFile read the configuration from the filesystem. diff --git a/server/services/mlog/levels.go b/server/services/mlog/levels.go new file mode 100644 index 000000000..4507f9243 --- /dev/null +++ b/server/services/mlog/levels.go @@ -0,0 +1,40 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package mlog + +import "github.com/mattermost/logr/v2" + +// Standard levels +var ( + Panic = logr.Panic // ID = 0 + Fatal = logr.Fatal // ID = 1 + Error = logr.Error // ID = 2 + Warn = logr.Warn // ID = 3 + Info = logr.Info // ID = 4 + Debug = logr.Debug // ID = 5 + Trace = logr.Trace // ID = 6 + StdAll = []Level{Panic, Fatal, Error, Warn, Info, Debug, Trace} +) + +// Register custom (discrete) levels here. +// !!!!! Custom ID's must be between 20 and 32,768 !!!!!! +var ( + /* Example + // used by the audit system + AuditAPI = Level{ID: 100, Name: "audit-api"} + AuditContent = Level{ID: 101, Name: "audit-content"} + AuditPerms = Level{ID: 102, Name: "audit-permissions"} + AuditCLI = Level{ID: 103, Name: "audit-cli"} + */ + + // add more here ... + Telemetry = Level{ID: 500, Name: "telemetry"} +) + +// Combinations for LogM (log multi) +var ( +/* Example +MAuditAll = []Level{AuditAPI, AuditContent, AuditPerms, AuditCLI} +*/ +) diff --git a/server/services/mlog/mlog.go b/server/services/mlog/mlog.go new file mode 100644 index 000000000..26ae6149b --- /dev/null +++ b/server/services/mlog/mlog.go @@ -0,0 +1,256 @@ +// Package mlog provides a simple wrapper around Logr. +package mlog + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "os" + "time" + + "github.com/mattermost/logr/v2" + logrcfg "github.com/mattermost/logr/v2/config" +) + +const ( + ShutdownTimeout = time.Second * 15 +) + +// Type and function aliases from Logr to limit the spread of dependencies throughout Focalboard. +type Field = logr.Field +type Level = logr.Level + +// Any picks the best supported field type based on type of val. +// For best performance when passing a struct (or struct pointer), +// implement `logr.LogWriter` on the struct, otherwise reflection +// will be used to generate a string representation. +var Any = logr.Any + +// Int64 constructs a field containing a key and Int64 value. +var Int64 = logr.Int64 + +// Int32 constructs a field containing a key and Int32 value. +var Int32 = logr.Int32 + +// Int constructs a field containing a key and Int value. +var Int = logr.Int + +// Uint64 constructs a field containing a key and Uint64 value. +var Uint64 = logr.Uint64 + +// Uint32 constructs a field containing a key and Uint32 value. +var Uint32 = logr.Uint32 + +// Uint constructs a field containing a key and Uint value. +var Uint = logr.Uint + +// Float64 constructs a field containing a key and Float64 value. +var Float64 = logr.Float64 + +// Float32 constructs a field containing a key and Float32 value. +var Float32 = logr.Float32 + +// String constructs a field containing a key and String value. +var String = logr.String + +// Stringer constructs a field containing a key and a fmt.Stringer value. +// The fmt.Stringer's `String` method is called lazily. +var Stringer = logr.Stringer + +// Err constructs a field containing a default key ("error") and error value. +var Err = logr.Err + +// NamedErr constructs a field containing a key and error value. +var NamedErr = logr.NamedErr + +// Bool constructs a field containing a key and bool value. +var Bool = logr.Bool + +// Time constructs a field containing a key and time.Time value. +var Time = logr.Time + +// Duration constructs a field containing a key and time.Duration value. +var Duration = logr.Duration + +// Millis constructs a field containing a key and timestamp value. +// The timestamp is expected to be milliseconds since Jan 1, 1970 UTC. +var Millis = logr.Millis + +// Array constructs a field containing a key and array value. +var Array = logr.Array + +// Map constructs a field containing a key and map value. +var Map = logr.Map + +// LoggerConfig is a map of LogTarget configurations. +type LoggerConfig map[string]logrcfg.TargetCfg + +func (lc LoggerConfig) append(cfg LoggerConfig) { + for k, v := range cfg { + lc[k] = v + } +} + +// Logger provides a thin wrapper around a Logr instance. This is a struct instead of an interface +// so that there are no allocations on the heap each interface method invocation. Normally not +// something to be concerned about, but logging calls for disabled levels should have as little CPU +// and memory impact as possible. Most of these wrapper calls will be inlined as well. +type Logger struct { + log *logr.Logger +} + +// NewLogger creates a new Logger instance which can be configured via `(*Logger).Configure` +func NewLogger() *Logger { + lgr, _ := logr.New() + log := lgr.NewLogger() + + return &Logger{ + log: &log, + } +} + +// Configure provides a new configuration for this logger. +// Zero or more sources of config can be provided, with target name collisions resolved using the +// following precedence: +// cfgFile > cfgJson +func (l *Logger) Configure(cfgFile string, cfgEscaped string) error { + cfgMap := make(LoggerConfig) + + // Add config from file + if cfgFile != "" { + if b, err := ioutil.ReadFile(string(cfgFile)); err != nil { + return fmt.Errorf("error reading logger config file %s: %w", cfgFile, err) + } else { + var mapCfgFile LoggerConfig + if err := json.Unmarshal(b, &mapCfgFile); err != nil { + return fmt.Errorf("error decoding logger config file %s: %w", cfgFile, err) + } + cfgMap.append(mapCfgFile) + } + } + + // Add config from escaped json string + if cfgEscaped != "" { + if b, err := decodeEscapedJSONString(string(cfgEscaped)); err != nil { + return fmt.Errorf("error unescaping logger config as escaped json: %w", err) + } else { + var mapCfgEscaped LoggerConfig + if err := json.Unmarshal(b, &mapCfgEscaped); err != nil { + return fmt.Errorf("error decoding logger config as escaped json: %w", err) + } + cfgMap.append(mapCfgEscaped) + } + } + + if len(cfgMap) == 0 { + return nil + } + + return logrcfg.ConfigureTargets(l.log.Logr(), cfgMap, nil) +} + +func decodeEscapedJSONString(s string) ([]byte, error) { + type wrapper struct { + wrap string + } + var wrapped wrapper + ss := fmt.Sprintf("{\"wrap\":%s}", s) + if err := json.Unmarshal([]byte(ss), &wrapped); err != nil { + return nil, err + } + return []byte(wrapped.wrap), nil +} + +// With creates a new Logger with the specified fields. This is a light-weight +// operation and can be called on demand. +func (l *Logger) With(fields ...Field) *Logger { + logWith := l.log.With(fields...) + return &Logger{ + log: &logWith, + } +} + +// IsLevelEnabled returns true only if at least one log target is +// configured to emit the specified log level. Use this check when +// gathering the log info may be expensive. +// +// Note, transformations and serializations done via fields are already +// lazily evaluated and don't require this check beforehand. +func (l *Logger) IsLevelEnabled(level Level) bool { + return l.IsLevelEnabled(level) +} + +// Log emits the log record for any targets configured for the specified level. +func (l *Logger) Log(level Level, msg string, fields ...Field) { + l.log.Log(level, msg, fields...) +} + +// LogM emits the log record for any targets configured for the specified levels. +// Equivalent to calling `Log` once for each level. +func (l *Logger) LogM(levels []Level, msg string, fields ...Field) { + l.log.LogM(levels, msg, fields...) +} + +// Convenience method equivalent to calling `Log` with the `Trace` level. +func (l *Logger) Trace(msg string, fields ...Field) { + l.log.Trace(msg, fields...) +} + +// Convenience method equivalent to calling `Log` with the `Debug` level. +func (l *Logger) Debug(msg string, fields ...Field) { + l.log.Debug(msg, fields...) +} + +// Convenience method equivalent to calling `Log` with the `Info` level. +func (l *Logger) Info(msg string, fields ...Field) { + l.log.Info(msg, fields...) +} + +// Convenience method equivalent to calling `Log` with the `Warn` level. +func (l *Logger) Warn(msg string, fields ...Field) { + l.log.Warn(msg, fields...) +} + +// Convenience method equivalent to calling `Log` with the `Error` level. +func (l *Logger) Error(msg string, fields ...Field) { + l.log.Error(msg, fields...) +} + +// Convenience method equivalent to calling `Log` with the `Fatal` level, +// followed by `os.Exit(1)`. +func (l *Logger) Fatal(msg string, fields ...Field) { + l.log.Log(logr.Fatal, msg, fields...) + l.Shutdown() + os.Exit(1) +} + +// HasTargets returns true if at least one log target has been added. +func (l *Logger) HasTargets() bool { + return l.log.Logr().HasTargets() +} + +// StdLogger creates a standard logger backed by this logger. +// All log records are output with the specified level. +func (l *Logger) StdLogger(level Level) *log.Logger { + return l.log.StdLogger(level) +} + +// RedirectStdLog redirects output from the standard library's package-global logger +// to this logger at the specified level and with zero or more Field's. Since this logger already +// handles caller annotations, timestamps, etc., it automatically disables the standard +// library's annotations and prefixing. +// A function is returned that restores the original prefix and flags and resets the standard +// library's output to os.Stdout. +func (l *Logger) RedirectStdLog(level Level, fields ...Field) func() { + return l.log.Logr().RedirectStdLog(level, fields...) +} + +// Shutdown shuts down the logger after making best efforts to flush any +// remaining records. +func (l *Logger) Shutdown() error { + ctx, cancel := context.WithTimeout(context.Background(), ShutdownTimeout) + defer cancel() + return l.log.Logr().ShutdownWithTimeout(ctx) +} diff --git a/server/services/mlog/tlog.go b/server/services/mlog/tlog.go new file mode 100644 index 000000000..c2815f37a --- /dev/null +++ b/server/services/mlog/tlog.go @@ -0,0 +1,59 @@ +package mlog + +import ( + "sync" + "testing" + + "github.com/mattermost/logr/v2" + "github.com/mattermost/logr/v2/formatters" +) + +// CreateTestLogger creates a logger for unit tests. Log records are output to `(*testing.T)Log` +func CreateTestLogger(t *testing.T, levels ...Field) (logger *Logger) { + logger = NewLogger() + + filter := logr.NewCustomFilter(StdAll...) + formatter := &formatters.Plain{} + target := newTestingTarget(t) + + logger.log.Logr().AddTarget(target, "test", filter, formatter, 1000) + return logger +} + +// testingTarget is a simple log target that writes to the testing log. +type testingTarget struct { + mux sync.Mutex + t *testing.T +} + +func newTestingTarget(t *testing.T) *testingTarget { + return &testingTarget{ + t: t, + } +} + +// Init is called once to initialize the target. +func (tt *testingTarget) Init() error { + return nil +} + +// Write outputs bytes to this file target. +func (tt *testingTarget) Write(p []byte, rec *logr.LogRec) (int, error) { + tt.mux.Lock() + defer tt.mux.Unlock() + + if tt.t != nil { + tt.t.Log(string(p)) + } + return len(p), nil +} + +// Shutdown is called once to free/close any resources. +// Target queue is already drained when this is called. +func (tt *testingTarget) Shutdown() error { + tt.mux.Lock() + defer tt.mux.Unlock() + + tt.t = nil + return nil +} diff --git a/server/services/store/sqlstore/blocks.go b/server/services/store/sqlstore/blocks.go index 14021fc0e..493299dcf 100644 --- a/server/services/store/sqlstore/blocks.go +++ b/server/services/store/sqlstore/blocks.go @@ -5,12 +5,12 @@ import ( "database/sql" "encoding/json" "errors" - "log" "time" sq "github.com/Masterminds/squirrel" _ "github.com/lib/pq" "github.com/mattermost/focalboard/server/model" + "github.com/mattermost/focalboard/server/services/mlog" "github.com/mattermost/focalboard/server/services/store" _ "github.com/mattn/go-sqlite3" ) @@ -37,12 +37,12 @@ func (s *SQLStore) GetBlocksWithParentAndType(c store.Container, parentID string rows, err := query.Query() if err != nil { - log.Printf(`getBlocksWithParentAndType ERROR: %v`, err) + s.logger.Error(`getBlocksWithParentAndType ERROR`, mlog.Err(err)) return nil, err } - return blocksFromRows(rows) + return s.blocksFromRows(rows) } func (s *SQLStore) GetBlocksWithParent(c store.Container, parentID string) ([]model.Block, error) { @@ -66,12 +66,12 @@ func (s *SQLStore) GetBlocksWithParent(c store.Container, parentID string) ([]mo rows, err := query.Query() if err != nil { - log.Printf(`getBlocksWithParent ERROR: %v`, err) + s.logger.Error(`getBlocksWithParent ERROR`, mlog.Err(err)) return nil, err } - return blocksFromRows(rows) + return s.blocksFromRows(rows) } func (s *SQLStore) GetBlocksWithRootID(c store.Container, rootID string) ([]model.Block, error) { @@ -95,12 +95,12 @@ func (s *SQLStore) GetBlocksWithRootID(c store.Container, rootID string) ([]mode rows, err := query.Query() if err != nil { - log.Printf(`GetBlocksWithRootID ERROR: %v`, err) + s.logger.Error(`GetBlocksWithRootID ERROR`, mlog.Err(err)) return nil, err } - return blocksFromRows(rows) + return s.blocksFromRows(rows) } func (s *SQLStore) GetBlocksWithType(c store.Container, blockType string) ([]model.Block, error) { @@ -124,12 +124,12 @@ func (s *SQLStore) GetBlocksWithType(c store.Container, blockType string) ([]mod rows, err := query.Query() if err != nil { - log.Printf(`getBlocksWithParentAndType ERROR: %v`, err) + s.logger.Error(`getBlocksWithParentAndType ERROR`, mlog.Err(err)) return nil, err } - return blocksFromRows(rows) + return s.blocksFromRows(rows) } // GetSubTree2 returns blocks within 2 levels of the given blockID @@ -154,12 +154,12 @@ func (s *SQLStore) GetSubTree2(c store.Container, blockID string) ([]model.Block rows, err := query.Query() if err != nil { - log.Printf(`getSubTree ERROR: %v`, err) + s.logger.Error(`getSubTree ERROR`, mlog.Err(err)) return nil, err } - return blocksFromRows(rows) + return s.blocksFromRows(rows) } // GetSubTree3 returns blocks within 3 levels of the given blockID @@ -192,12 +192,12 @@ func (s *SQLStore) GetSubTree3(c store.Container, blockID string) ([]model.Block rows, err := query.Query() if err != nil { - log.Printf(`getSubTree3 ERROR: %v`, err) + s.logger.Error(`getSubTree3 ERROR`, mlog.Err(err)) return nil, err } - return blocksFromRows(rows) + return s.blocksFromRows(rows) } func (s *SQLStore) GetAllBlocks(c store.Container) ([]model.Block, error) { @@ -220,15 +220,15 @@ func (s *SQLStore) GetAllBlocks(c store.Container) ([]model.Block, error) { rows, err := query.Query() if err != nil { - log.Printf(`getAllBlocks ERROR: %v`, err) + s.logger.Error(`getAllBlocks ERROR`, mlog.Err(err)) return nil, err } - return blocksFromRows(rows) + return s.blocksFromRows(rows) } -func blocksFromRows(rows *sql.Rows) ([]model.Block, error) { +func (s *SQLStore) blocksFromRows(rows *sql.Rows) ([]model.Block, error) { defer rows.Close() results := []model.Block{} @@ -252,7 +252,7 @@ func blocksFromRows(rows *sql.Rows) ([]model.Block, error) { &block.DeleteAt) if err != nil { // handle this error - log.Printf(`ERROR blocksFromRows: %v`, err) + s.logger.Error(`ERROR blocksFromRows`, mlog.Err(err)) return nil, err } @@ -264,7 +264,7 @@ func blocksFromRows(rows *sql.Rows) ([]model.Block, error) { err = json.Unmarshal([]byte(fieldsJSON), &block.Fields) if err != nil { // handle this error - log.Printf(`ERROR blocksFromRows fields: %v`, err) + s.logger.Error(`ERROR blocksFromRows fields`, mlog.Err(err)) return nil, err } diff --git a/server/services/store/sqlstore/initialize.go b/server/services/store/sqlstore/initialize.go index 745ac39d4..1987b57f8 100644 --- a/server/services/store/sqlstore/initialize.go +++ b/server/services/store/sqlstore/initialize.go @@ -2,10 +2,10 @@ package sqlstore import ( "encoding/json" - "log" sq "github.com/Masterminds/squirrel" "github.com/mattermost/focalboard/server/model" + "github.com/mattermost/focalboard/server/services/mlog" "github.com/mattermost/focalboard/server/services/store" "github.com/mattermost/focalboard/server/services/store/sqlstore/initializations" ) @@ -25,7 +25,7 @@ func (s *SQLStore) InitializeTemplates() error { } func (s *SQLStore) importInitialTemplates() error { - log.Printf("importInitialTemplates") + s.logger.Debug("importInitialTemplates") blocksJSON := initializations.MustAsset("templates.json") var archive model.Archive @@ -38,9 +38,13 @@ func (s *SQLStore) importInitialTemplates() error { WorkspaceID: "0", } - log.Printf("Inserting %d blocks", len(archive.Blocks)) + s.logger.Debug("Inserting blocks", mlog.Int("block_count", len(archive.Blocks))) for _, block := range archive.Blocks { - // log.Printf("\t%v %v %v", block.ID, block.Type, block.Title) + s.logger.Trace("insert block", + mlog.String("blockID", block.ID), + mlog.String("block_type", block.Type), + mlog.String("block_title", block.Title), + ) err := s.InsertBlock(globalContainer, block) if err != nil { return err @@ -62,7 +66,7 @@ func (s *SQLStore) isInitializationNeeded() (bool, error) { var count int err := row.Scan(&count) if err != nil { - log.Fatal(err) + s.logger.Fatal("isInitializationNeeded", mlog.Err(err)) return false, err } diff --git a/server/services/store/sqlstore/sqlstore.go b/server/services/store/sqlstore/sqlstore.go index d0fa8f0d0..78f645d2f 100644 --- a/server/services/store/sqlstore/sqlstore.go +++ b/server/services/store/sqlstore/sqlstore.go @@ -2,9 +2,9 @@ package sqlstore import ( "database/sql" - "log" sq "github.com/Masterminds/squirrel" + "github.com/mattermost/focalboard/server/services/mlog" ) const ( @@ -15,27 +15,28 @@ const ( // SQLStore is a SQL database. type SQLStore struct { - db *sql.DB - dbType string - tablePrefix string + db *sql.DB + dbType string + tablePrefix string connectionString string + logger *mlog.Logger } // New creates a new SQL implementation of the store. -func New(dbType, connectionString string, tablePrefix string) (*SQLStore, error) { - log.Println("connectDatabase", dbType, connectionString) +func New(dbType, connectionString string, tablePrefix string, logger *mlog.Logger) (*SQLStore, error) { + logger.Info("connectDatabase", mlog.String("dbType", dbType), mlog.String("connStr", connectionString)) var err error db, err := sql.Open(dbType, connectionString) if err != nil { - log.Print("connectDatabase: ", err) + logger.Error("connectDatabase failed", mlog.Err(err)) return nil, err } err = db.Ping() if err != nil { - log.Printf(`Database Ping failed: %v`, err) + logger.Error(`Database Ping failed`, mlog.Err(err)) return nil, err } @@ -45,18 +46,19 @@ func New(dbType, connectionString string, tablePrefix string) (*SQLStore, error) dbType: dbType, tablePrefix: tablePrefix, connectionString: connectionString, + logger: logger, } err = store.Migrate() if err != nil { - log.Printf(`Table creation / migration failed: %v`, err) + logger.Error(`Table creation / migration failed`, mlog.Err(err)) return nil, err } err = store.InitializeTemplates() if err != nil { - log.Printf(`InitializeTemplates failed: %v`, err) + logger.Error(`InitializeTemplates failed`, mlog.Err(err)) return nil, err } diff --git a/server/services/store/sqlstore/sqlstore_test.go b/server/services/store/sqlstore/sqlstore_test.go index 589066502..75dc6759d 100644 --- a/server/services/store/sqlstore/sqlstore_test.go +++ b/server/services/store/sqlstore/sqlstore_test.go @@ -4,6 +4,7 @@ import ( "os" "testing" + "github.com/mattermost/focalboard/server/services/mlog" "github.com/mattermost/focalboard/server/services/store" "github.com/mattermost/focalboard/server/services/store/storetests" "github.com/stretchr/testify/require" @@ -20,10 +21,13 @@ func SetupTests(t *testing.T) (store.Store, func()) { connectionString = ":memory:" } - store, err := New(dbType, connectionString, "test_") + logger := mlog.CreateTestLogger(t) + + store, err := New(dbType, connectionString, "test_", logger) require.Nil(t, err) tearDown := func() { + defer logger.Shutdown() err = store.Shutdown() require.Nil(t, err) } diff --git a/server/services/store/sqlstore/workspaces.go b/server/services/store/sqlstore/workspaces.go index 652101ef5..00897018f 100644 --- a/server/services/store/sqlstore/workspaces.go +++ b/server/services/store/sqlstore/workspaces.go @@ -2,10 +2,10 @@ package sqlstore import ( "encoding/json" - "log" "time" "github.com/mattermost/focalboard/server/model" + "github.com/mattermost/focalboard/server/services/mlog" sq "github.com/Masterminds/squirrel" ) @@ -98,7 +98,7 @@ func (s *SQLStore) GetWorkspace(ID string) (*model.Workspace, error) { err = json.Unmarshal([]byte(settingsJSON), &workspace.Settings) if err != nil { - log.Printf(`ERROR GetWorkspace settings json.Unmarshal: %v`, err) + s.logger.Error(`ERROR GetWorkspace settings json.Unmarshal`, mlog.Err(err)) return nil, err } diff --git a/server/services/webhook/webhook.go b/server/services/webhook/webhook.go index 07301549c..ab183fd53 100644 --- a/server/services/webhook/webhook.go +++ b/server/services/webhook/webhook.go @@ -3,11 +3,11 @@ package webhook import ( "bytes" "encoding/json" - "log" "net/http" "github.com/mattermost/focalboard/server/model" "github.com/mattermost/focalboard/server/services/config" + "github.com/mattermost/focalboard/server/services/mlog" ) // NotifyUpdate calls webhooks @@ -18,22 +18,24 @@ func (wh *Client) NotifyUpdate(block model.Block) { json, err := json.Marshal(block) if err != nil { - log.Fatal("NotifyUpdate: json.Marshal", err) + wh.logger.Fatal("NotifyUpdate: json.Marshal", mlog.Err(err)) } for _, url := range wh.config.WebhookUpdate { http.Post(url, "application/json", bytes.NewBuffer(json)) - log.Printf("webhook.NotifyUpdate: %s", url) + wh.logger.Debug("webhook.NotifyUpdate", mlog.String("url", url)) } } // Client is a webhook client type Client struct { config *config.Configuration + logger *mlog.Logger } // NewClient creates a new Client -func NewClient(config *config.Configuration) *Client { +func NewClient(config *config.Configuration, logger *mlog.Logger) *Client { return &Client{ config: config, + logger: logger, } } diff --git a/server/web/webserver.go b/server/web/webserver.go index ccbd9976d..bd0d863be 100644 --- a/server/web/webserver.go +++ b/server/web/webserver.go @@ -2,7 +2,6 @@ package web import ( "fmt" - "log" "net/http" "net/url" "os" @@ -11,6 +10,7 @@ import ( "text/template" "github.com/gorilla/mux" + "github.com/mattermost/focalboard/server/services/mlog" ) // RoutedService defines the interface that is needed for any service to @@ -29,10 +29,11 @@ type Server struct { port int ssl bool localOnly bool + logger *mlog.Logger } // NewServer creates a new instance of the webserver. -func NewServer(rootPath string, serverRoot string, port int, ssl, localOnly bool) *Server { +func NewServer(rootPath string, serverRoot string, port int, ssl, localOnly bool, logger *mlog.Logger) *Server { r := mux.NewRouter() var addr string @@ -45,7 +46,7 @@ func NewServer(rootPath string, serverRoot string, port int, ssl, localOnly bool baseURL := "" url, err := url.Parse(serverRoot) if err != nil { - log.Printf("Invalid ServerRoot setting: %v\n", err) + logger.Error("Invalid ServerRoot setting", mlog.Err(err)) } baseURL = url.Path @@ -58,6 +59,7 @@ func NewServer(rootPath string, serverRoot string, port int, ssl, localOnly bool rootPath: rootPath, port: port, ssl: ssl, + logger: logger, } return ws @@ -78,13 +80,13 @@ func (ws *Server) registerRoutes() { w.Header().Set("Content-Type", "text/html; charset=utf-8") indexTemplate, err := template.New("index").ParseFiles(path.Join(ws.rootPath, "index.html")) if err != nil { - log.Printf("Unable to serve the index.html fil, err: %v\n", err) + ws.logger.Error("Unable to serve the index.html file", mlog.Err(err)) w.WriteHeader(500) return } err = indexTemplate.ExecuteTemplate(w, "index.html", map[string]string{"BaseURL": ws.baseURL}) if err != nil { - log.Printf("Unable to serve the index.html fil, err: %v\n", err) + ws.logger.Error("Unable to serve the index.html file", mlog.Err(err)) w.WriteHeader(500) return } @@ -95,28 +97,28 @@ func (ws *Server) registerRoutes() { func (ws *Server) Start() { ws.registerRoutes() if ws.port == -1 { - log.Print("server not bind to any port\n") + ws.logger.Error("server not bind to any port") return } isSSL := ws.ssl && fileExists("./cert/cert.pem") && fileExists("./cert/key.pem") if isSSL { - log.Printf("https server started on :%d\n", ws.port) + ws.logger.Info("https server started", mlog.Int("port", ws.port)) go func() { if err := ws.ListenAndServeTLS("./cert/cert.pem", "./cert/key.pem"); err != nil { - log.Fatalf("ListenAndServeTLS: %v", err) + ws.logger.Fatal("ListenAndServeTLS", mlog.Err(err)) } }() return } - log.Printf("http server started on :%d\n", ws.port) + ws.logger.Info("http server started", mlog.Int("port", ws.port)) go func() { if err := ws.ListenAndServe(); err != http.ErrServerClosed { - log.Fatalf("ListenAndServe: %v", err) + ws.logger.Fatal("ListenAndServeTLS", mlog.Err(err)) } - log.Println("http server stopped") + ws.logger.Info("http server stopped") }() } diff --git a/server/ws/websockets.go b/server/ws/websockets.go index 558968641..d5909da31 100644 --- a/server/ws/websockets.go +++ b/server/ws/websockets.go @@ -12,6 +12,7 @@ import ( "github.com/gorilla/websocket" "github.com/mattermost/focalboard/server/auth" "github.com/mattermost/focalboard/server/model" + "github.com/mattermost/focalboard/server/services/mlog" "github.com/mattermost/focalboard/server/services/store" ) @@ -32,6 +33,7 @@ type Server struct { hub Hub singleUserToken string isMattermostAuth bool + logger *mlog.Logger } // UpdateMsg is sent on block updates @@ -68,7 +70,7 @@ type websocketSession struct { } // NewServer creates a new Server. -func NewServer(auth *auth.Auth, singleUserToken string, isMattermostAuth bool) *Server { +func NewServer(auth *auth.Auth, singleUserToken string, isMattermostAuth bool, logger *mlog.Logger) *Server { return &Server{ listeners: make(map[string][]*websocket.Conn), upgrader: websocket.Upgrader{ @@ -79,6 +81,7 @@ func NewServer(auth *auth.Auth, singleUserToken string, isMattermostAuth bool) * auth: auth, singleUserToken: singleUserToken, isMattermostAuth: isMattermostAuth, + logger: logger, } } @@ -91,13 +94,13 @@ func (ws *Server) handleWebSocketOnChange(w http.ResponseWriter, r *http.Request // Upgrade initial GET request to a websocket client, err := ws.upgrader.Upgrade(w, r, nil) if err != nil { - log.Printf("ERROR upgrading to websocket: %v", err) + ws.logger.Error("ERROR upgrading to websocket", mlog.Err(err)) return } // Make sure we close the connection when the function returns defer func() { - log.Printf("DISCONNECT WebSocket onChange, client: %s", client.RemoteAddr()) + ws.logger.Debug("DISCONNECT WebSocket onChange", mlog.Stringer("client", client.RemoteAddr())) // Remove client from listeners ws.removeListener(client) @@ -119,7 +122,10 @@ func (ws *Server) handleWebSocketOnChange(w http.ResponseWriter, r *http.Request for { _, p, err := client.ReadMessage() if err != nil { - log.Printf("ERROR WebSocket onChange, client: %s, err: %v", client.RemoteAddr(), err) + ws.logger.Error("ERROR WebSocket onChange", + mlog.Stringer("client", client.RemoteAddr()), + mlog.Err(err), + ) ws.removeListener(client) break @@ -130,7 +136,7 @@ func (ws *Server) handleWebSocketOnChange(w http.ResponseWriter, r *http.Request err = json.Unmarshal(p, &command) if err != nil { // handle this error - log.Printf(`ERROR webSocket parsing command JSON: %v`, string(p)) + ws.logger.Error(`ERROR webSocket parsing command`, mlog.String("json", string(p))) continue } @@ -139,23 +145,32 @@ func (ws *Server) handleWebSocketOnChange(w http.ResponseWriter, r *http.Request if ws.auth.DoesUserHaveWorkspaceAccess(userID, command.WorkspaceID) { wsSession.workspaceID = command.WorkspaceID } else { - log.Printf(`ERROR User doesn't have permissions to read the workspace: %s`, command.WorkspaceID) + ws.logger.Error(`ERROR User doesn't have permissions to read the workspace`, mlog.String("workspaceID", command.WorkspaceID)) continue } } switch command.Action { case "AUTH": - log.Printf(`Command: AUTH, client: %s`, client.RemoteAddr()) + ws.logger.Debug(`Command: AUTH`, mlog.Stringer("client", client.RemoteAddr())) ws.authenticateListener(&wsSession, command.WorkspaceID, command.Token) case "ADD": - log.Printf(`Command: Add workspaceID: %s, blockIDs: %v, client: %s`, wsSession.workspaceID, command.BlockIDs, client.RemoteAddr()) + ws.logger.Debug(`Command: ADD`, + mlog.String("workspaceID", wsSession.workspaceID), + mlog.Array("blockIDs", command.BlockIDs), + mlog.Stringer("client", client.RemoteAddr()), + ) ws.addListener(&wsSession, &command) case "REMOVE": - log.Printf(`Command: Remove workspaceID: %s, blockID: %v, client: %s`, wsSession.workspaceID, command.BlockIDs, client.RemoteAddr()) + ws.logger.Debug(`Command: REMOVE`, + mlog.String("workspaceID", wsSession.workspaceID), + mlog.Array("blockIDs", command.BlockIDs), + mlog.Stringer("client", client.RemoteAddr()), + ) + ws.removeListenerFromBlocks(&wsSession, &command) default: - log.Printf(`ERROR webSocket command, invalid action: %v`, command.Action) + ws.logger.Error(`ERROR webSocket command, invalid action`, mlog.String("action", command.Action)) } } } @@ -177,7 +192,7 @@ func (ws *Server) isValidSessionToken(token, workspaceID string) bool { func (ws *Server) authenticateListener(wsSession *websocketSession, workspaceID, token string) { if wsSession.isAuthenticated { // Do not allow multiple auth calls (for security) - log.Printf("authenticateListener: Ignoring already authenticated session") + ws.logger.Debug("authenticateListener: Ignoring already authenticated session", mlog.String("workspaceID", workspaceID)) return } @@ -192,7 +207,7 @@ func (ws *Server) authenticateListener(wsSession *websocketSession, workspaceID, wsSession.workspaceID = workspaceID wsSession.isAuthenticated = true - log.Printf("authenticateListener: Authenticated, workspaceID: %s", workspaceID) + ws.logger.Debug("authenticateListener: Authenticated", mlog.String("workspaceID", workspaceID)) } func (ws *Server) getAuthenticatedWorkspaceID(wsSession *websocketSession, command *WebsocketCommand) (string, error) { @@ -203,7 +218,7 @@ func (ws *Server) getAuthenticatedWorkspaceID(wsSession *websocketSession, comma // If not authenticated, try to authenticate the read token against the supplied workspaceID workspaceID := command.WorkspaceID if len(workspaceID) == 0 { - log.Printf("getAuthenticatedWorkspaceID: No workspace") + ws.logger.Error("getAuthenticatedWorkspaceID: No workspace") return "", errors.New("No workspace") } @@ -234,8 +249,8 @@ func makeItemID(workspaceID, blockID string) string { func (ws *Server) addListener(wsSession *websocketSession, command *WebsocketCommand) { workspaceID, err := ws.getAuthenticatedWorkspaceID(wsSession, command) if err != nil { - log.Printf("addListener: NOT AUTHENTICATED, ERROR: %v", err) - sendError(wsSession.client, "not authenticated") + ws.logger.Error("addListener: NOT AUTHENTICATED", mlog.Err(err)) + ws.sendError(wsSession.client, "not authenticated") return } @@ -272,8 +287,8 @@ func (ws *Server) removeListener(client *websocket.Conn) { func (ws *Server) removeListenerFromBlocks(wsSession *websocketSession, command *WebsocketCommand) { workspaceID, err := ws.getAuthenticatedWorkspaceID(wsSession, command) if err != nil { - log.Printf("addListener: NOT AUTHENTICATED, ERROR: %v", err) - sendError(wsSession.client, "not authenticated") + ws.logger.Error("addListener: NOT AUTHENTICATED", mlog.Err(err)) + ws.sendError(wsSession.client, "not authenticated") return } @@ -300,14 +315,14 @@ func (ws *Server) removeListenerFromBlocks(wsSession *websocketSession, command ws.mu.Unlock() } -func sendError(conn *websocket.Conn, message string) { +func (ws *Server) sendError(conn *websocket.Conn, message string) { errorMsg := ErrorMsg{ Error: message, } err := conn.WriteJSON(errorMsg) if err != nil { - log.Printf("sendError error: %v", err) + ws.logger.Error("sendError error", mlog.Err(err)) conn.Close() } } @@ -372,7 +387,10 @@ func (ws *Server) BroadcastBlockChange(workspaceID string, block model.Block) { for _, blockID := range blockIDsToNotify { listeners := ws.getListeners(workspaceID, blockID) - log.Printf("%d listener(s) for blockID: %s", len(listeners), blockID) + ws.logger.Debug("listener(s) for blockID", + mlog.Int("listener_count", len(listeners)), + mlog.String("blockID", blockID), + ) message := UpdateMsg{ Action: "UPDATE_BLOCK", @@ -388,11 +406,15 @@ func (ws *Server) BroadcastBlockChange(workspaceID string, block model.Block) { if listeners != nil { for _, listener := range listeners { - log.Printf("Broadcast change, workspaceID: %s, blockID: %s, remoteAddr: %s", workspaceID, blockID, listener.RemoteAddr()) + ws.logger.Debug("Broadcast change", + mlog.String("workspaceID", workspaceID), + mlog.String("blockID", blockID), + mlog.Stringer("remoteAddr", listener.RemoteAddr()), + ) err := listener.WriteJSON(message) if err != nil { - log.Printf("broadcast error: %v", err) + ws.logger.Error("broadcast error", mlog.Err(err)) listener.Close() } } diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 5de78029b..7b6d18164 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -8,6 +8,7 @@ "name": "focalboard", "version": "0.6.7", "dependencies": { + "cypress": "^6.8.0", "emoji-mart": "^3.0.1", "imagemin-gifsicle": "^7.0.0", "imagemin-mozjpeg": "^9.0.0", @@ -1312,6 +1313,7 @@ "jest-resolve": "^26.6.2", "jest-util": "^26.6.2", "jest-worker": "^26.6.2", + "node-notifier": "^8.0.0", "slash": "^3.0.0", "source-map": "^0.6.0", "string-length": "^4.0.1", @@ -3788,6 +3790,7 @@ "dependencies": { "anymatch": "~3.1.1", "braces": "~3.0.2", + "fsevents": "~2.3.1", "glob-parent": "~5.1.0", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", @@ -9511,7 +9514,350 @@ "node": ">= 10.14.2" } }, +<<<<<<< HEAD + "node_modules/jest-each/node_modules/pretty-format": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "dev": true, + "dependencies": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/jest-each/node_modules/react-is": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz", + "integrity": "sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==", + "dev": true + }, + "node_modules/jest-environment-jsdom": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-26.6.2.tgz", + "integrity": "sha512-jgPqCruTlt3Kwqg5/WVFyHIOJHsiAvhcp2qiR2QQstuG9yWox5+iHpU3ZrcBxW14T4fe5Z68jAfLRh7joCSP2Q==", + "dev": true, + "dependencies": { + "@jest/environment": "^26.6.2", + "@jest/fake-timers": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "jest-mock": "^26.6.2", + "jest-util": "^26.6.2", + "jsdom": "^16.4.0" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/jest-environment-jsdom/node_modules/chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jest-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", + "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", + "dev": true, + "dependencies": { + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/jest-environment-node": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-26.6.2.tgz", + "integrity": "sha512-zhtMio3Exty18dy8ee8eJ9kjnRyZC1N4C1Nt/VShN1apyXc8rWGtJ9lI7vqiWcyyXS4BVSEn9lxAM2D+07/Tag==", + "dev": true, + "dependencies": { + "@jest/environment": "^26.6.2", + "@jest/fake-timers": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "jest-mock": "^26.6.2", + "jest-util": "^26.6.2" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/jest-environment-node/node_modules/@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/jest-environment-node/node_modules/@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/jest-environment-node/node_modules/chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-environment-node/node_modules/jest-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", + "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", + "dev": true, + "dependencies": { + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/jest-get-type": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", + "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", + "dev": true, + "engines": { + "node": ">= 10.14.2" + } + }, "node_modules/jest-haste-map": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-26.6.2.tgz", + "integrity": "sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w==", + "dev": true, + "dependencies": { + "@jest/types": "^26.6.2", + "@types/graceful-fs": "^4.1.2", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.1.2", + "graceful-fs": "^4.2.4", + "jest-regex-util": "^26.0.0", + "jest-serializer": "^26.6.2", + "jest-util": "^26.6.2", + "jest-worker": "^26.6.2", + "micromatch": "^4.0.2", + "sane": "^4.0.3", + "walker": "^1.0.7" + }, + "engines": { + "node": ">= 10.14.2" + }, + "optionalDependencies": { + "fsevents": "^2.1.2" + } + }, + "node_modules/jest-haste-map/node_modules/@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/jest-haste-map/node_modules/@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/jest-haste-map/node_modules/chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-haste-map/node_modules/jest-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", + "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", + "dev": true, + "dependencies": { + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/jest-haste-map/node_modules/jest-worker": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-jasmine2": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-26.6.3.tgz", + "integrity": "sha512-kPKUrQtc8aYwBV7CqBg5pu+tmYXlvFlSFYn18ev4gPFtrRzB15N2gW/Roew3187q2w2eHuu0MU9TJz6w0/nPEg==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.1.0", + "@jest/environment": "^26.6.2", + "@jest/source-map": "^26.6.2", + "@jest/test-result": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "expect": "^26.6.2", + "is-generator-fn": "^2.0.0", + "jest-each": "^26.6.2", + "jest-matcher-utils": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-runtime": "^26.6.3", + "jest-snapshot": "^26.6.2", + "jest-util": "^26.6.2", + "pretty-format": "^26.6.2", + "throat": "^5.0.0" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/jest-jasmine2/node_modules/@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/jest-jasmine2/node_modules/@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/jest-jasmine2/node_modules/chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-jasmine2/node_modules/jest-util": { +======= + "node_modules/jest-haste-map": { +>>>>>>> upstream/main "version": "26.6.2", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-26.6.2.tgz", "integrity": "sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w==", diff --git a/webapp/package.json b/webapp/package.json index 2608cd0f7..dcd4cc06a 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -23,6 +23,7 @@ "cypress:open": "cypress open" }, "dependencies": { + "cypress": "^6.8.0", "emoji-mart": "^3.0.1", "imagemin-gifsicle": "^7.0.0", "imagemin-mozjpeg": "^9.0.0",