1
0
mirror of https://github.com/mattermost/focalboard.git synced 2025-03-23 20:52:42 +02:00

Logger for FocalBoard server ()

- structured, asynchronous logging
- supports discreet log levels, including custom levels
- supports output to console, files, and all common log aggregators.
- supports JSON, plain text and GELF formats
- lazy formatting and writing
This commit is contained in:
Doug Lauder 2021-05-29 02:23:10 -04:00 committed by GitHub
parent fe6b0d04b3
commit 417de9f837
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1150 additions and 275 deletions

@ -18,5 +18,6 @@
"localOnly": false,
"enableLocalMode": true,
"localModeSocketLocation": "/var/tmp/focalboard_local.socket",
"authMode": "native"
"authMode": "native",
"logging_file": "logging.json"
}

45
logging.json Normal file

@ -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
}
}

@ -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, "{}")
}

@ -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)

@ -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, &registerData)
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
}

@ -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,
}
}

@ -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")
}

@ -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))
}
}
}

@ -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,

@ -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

@ -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

@ -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=

@ -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)

@ -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
}

@ -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))
}
}()

@ -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.

@ -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}
*/
)

@ -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)
}

@ -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
}

@ -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
}

@ -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
}

@ -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
}

@ -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)
}

@ -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
}

@ -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,
}
}

@ -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")
}()
}

@ -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()
}
}

346
webapp/package-lock.json generated

@ -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==",

@ -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",