snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
This commit is contained in:
63
server/public/shared/mlog/default.go
Normal file
63
server/public/shared/mlog/default.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package mlog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// defaultLog manually encodes the log to STDERR, providing a basic, default logging implementation
|
||||
// before mlog is fully configured.
|
||||
func defaultLog(level Level, msg string, fields ...Field) {
|
||||
mFields := make(map[string]string)
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
for _, fld := range fields {
|
||||
buf.Reset()
|
||||
fld.ValueString(buf, shouldQuote)
|
||||
mFields[fld.Key] = buf.String()
|
||||
}
|
||||
|
||||
log := struct {
|
||||
Level string `json:"level"`
|
||||
Message string `json:"msg"`
|
||||
Fields map[string]string `json:"fields,omitempty"`
|
||||
}{
|
||||
level.Name,
|
||||
msg,
|
||||
mFields,
|
||||
}
|
||||
|
||||
if b, err := json.Marshal(log); err != nil {
|
||||
fmt.Fprintf(os.Stderr, `{"level":"error","msg":"failed to encode log message"}%s`, "\n")
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", b)
|
||||
}
|
||||
}
|
||||
|
||||
func defaultIsLevelEnabled(level Level) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func defaultCustomMultiLog(lvl []Level, msg string, fields ...Field) {
|
||||
for _, level := range lvl {
|
||||
defaultLog(level, msg, fields...)
|
||||
}
|
||||
}
|
||||
|
||||
// shouldQuote returns true if val contains any characters that require quotations.
|
||||
func shouldQuote(val string) bool {
|
||||
for _, c := range val {
|
||||
if !((c >= '0' && c <= '9') ||
|
||||
(c >= 'a' && c <= 'z') ||
|
||||
(c >= 'A' && c <= 'Z') ||
|
||||
c == '-' || c == '.' || c == '_' || c == '/' || c == '@' || c == '^' || c == '+') {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
129
server/public/shared/mlog/global.go
Normal file
129
server/public/shared/mlog/global.go
Normal file
@@ -0,0 +1,129 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package mlog
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
globalLogger *Logger
|
||||
muxGlobalLogger sync.RWMutex
|
||||
)
|
||||
|
||||
func InitGlobalLogger(logger *Logger) {
|
||||
muxGlobalLogger.Lock()
|
||||
defer muxGlobalLogger.Unlock()
|
||||
|
||||
globalLogger = logger
|
||||
}
|
||||
|
||||
func getGlobalLogger() *Logger {
|
||||
muxGlobalLogger.RLock()
|
||||
defer muxGlobalLogger.RUnlock()
|
||||
|
||||
return globalLogger
|
||||
}
|
||||
|
||||
// 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 IsLevelEnabled(level Level) bool {
|
||||
logger := getGlobalLogger()
|
||||
if logger == nil {
|
||||
return defaultIsLevelEnabled(level)
|
||||
}
|
||||
return logger.IsLevelEnabled(level)
|
||||
}
|
||||
|
||||
// Log emits the log record for any targets configured for the specified level.
|
||||
func Log(level Level, msg string, fields ...Field) {
|
||||
logger := getGlobalLogger()
|
||||
if logger == nil {
|
||||
defaultLog(level, msg, fields...)
|
||||
return
|
||||
}
|
||||
logger.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 LogM(levels []Level, msg string, fields ...Field) {
|
||||
logger := getGlobalLogger()
|
||||
if logger == nil {
|
||||
defaultCustomMultiLog(levels, msg, fields...)
|
||||
return
|
||||
}
|
||||
logger.LogM(levels, msg, fields...)
|
||||
}
|
||||
|
||||
// Convenience method equivalent to calling `Log` with the `Trace` level.
|
||||
func Trace(msg string, fields ...Field) {
|
||||
logger := getGlobalLogger()
|
||||
if logger == nil {
|
||||
defaultLog(LvlTrace, msg, fields...)
|
||||
return
|
||||
}
|
||||
logger.Trace(msg, fields...)
|
||||
}
|
||||
|
||||
// Convenience method equivalent to calling `Log` with the `Debug` level.
|
||||
func Debug(msg string, fields ...Field) {
|
||||
logger := getGlobalLogger()
|
||||
if logger == nil {
|
||||
defaultLog(LvlDebug, msg, fields...)
|
||||
return
|
||||
}
|
||||
logger.Debug(msg, fields...)
|
||||
}
|
||||
|
||||
// Convenience method equivalent to calling `Log` with the `Info` level.
|
||||
func Info(msg string, fields ...Field) {
|
||||
logger := getGlobalLogger()
|
||||
if logger == nil {
|
||||
defaultLog(LvlInfo, msg, fields...)
|
||||
return
|
||||
}
|
||||
logger.Info(msg, fields...)
|
||||
}
|
||||
|
||||
// Convenience method equivalent to calling `Log` with the `Warn` level.
|
||||
func Warn(msg string, fields ...Field) {
|
||||
logger := getGlobalLogger()
|
||||
if logger == nil {
|
||||
defaultLog(LvlWarn, msg, fields...)
|
||||
return
|
||||
}
|
||||
logger.Warn(msg, fields...)
|
||||
}
|
||||
|
||||
// Convenience method equivalent to calling `Log` with the `Error` level.
|
||||
func Error(msg string, fields ...Field) {
|
||||
logger := getGlobalLogger()
|
||||
if logger == nil {
|
||||
defaultLog(LvlError, msg, fields...)
|
||||
return
|
||||
}
|
||||
logger.Error(msg, fields...)
|
||||
}
|
||||
|
||||
// Convenience method equivalent to calling `Log` with the `Critical` level.
|
||||
// DEPRECATED: Either use Error or Fatal.
|
||||
// Critical level isn't added in mlog/levels.go:StdAll so calling this doesn't
|
||||
// really work. For now we just call Fatal to atleast print something.
|
||||
func Critical(msg string, fields ...Field) {
|
||||
Fatal(msg, fields...)
|
||||
}
|
||||
|
||||
func Fatal(msg string, fields ...Field) {
|
||||
logger := getGlobalLogger()
|
||||
if logger == nil {
|
||||
defaultLog(LvlFatal, msg, fields...)
|
||||
return
|
||||
}
|
||||
logger.Fatal(msg, fields...)
|
||||
}
|
||||
138
server/public/shared/mlog/global_test.go
Normal file
138
server/public/shared/mlog/global_test.go
Normal file
@@ -0,0 +1,138 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package mlog_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
func TestLoggingBeforeInitialized(t *testing.T) {
|
||||
require.NotPanics(t, func() {
|
||||
// None of these should segfault before mlog is globally configured
|
||||
mlog.Info("info log")
|
||||
mlog.Debug("debug log")
|
||||
mlog.Warn("warning log")
|
||||
mlog.Error("error log")
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoggingAfterInitialized(t *testing.T) {
|
||||
testCases := []struct {
|
||||
description string
|
||||
cfg mlog.TargetCfg
|
||||
expectedLogs []string
|
||||
}{
|
||||
{
|
||||
"file logging, json, debug",
|
||||
mlog.TargetCfg{
|
||||
Type: "file",
|
||||
Format: "json",
|
||||
FormatOptions: json.RawMessage(`{"enable_caller":true}`),
|
||||
Levels: []mlog.Level{mlog.LvlError, mlog.LvlWarn, mlog.LvlInfo, mlog.LvlDebug},
|
||||
},
|
||||
[]string{
|
||||
`{"timestamp":0,"level":"debug","msg":"real debug log","caller":"mlog/global_test.go:0"}`,
|
||||
`{"timestamp":0,"level":"info","msg":"real info log","caller":"mlog/global_test.go:0"}`,
|
||||
`{"timestamp":0,"level":"warn","msg":"real warning log","caller":"mlog/global_test.go:0"}`,
|
||||
`{"timestamp":0,"level":"error","msg":"real error log","caller":"mlog/global_test.go:0"}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
"file logging, json, error",
|
||||
mlog.TargetCfg{
|
||||
Type: "file",
|
||||
Format: "json",
|
||||
FormatOptions: json.RawMessage(`{"enable_caller":true}`),
|
||||
Levels: []mlog.Level{mlog.LvlError},
|
||||
},
|
||||
[]string{
|
||||
`{"timestamp":0,"level":"error","msg":"real error log","caller":"mlog/global_test.go:0"}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
"file logging, non-json, debug",
|
||||
mlog.TargetCfg{
|
||||
Type: "file",
|
||||
Format: "plain",
|
||||
FormatOptions: json.RawMessage(`{"delim":" | ", "enable_caller":true}`),
|
||||
Levels: []mlog.Level{mlog.LvlError, mlog.LvlWarn, mlog.LvlInfo, mlog.LvlDebug},
|
||||
},
|
||||
[]string{
|
||||
`debug | TIME | real debug log | caller="mlog/global_test.go:0"`,
|
||||
`info | TIME | real info log | caller="mlog/global_test.go:0"`,
|
||||
`warn | TIME | real warning log | caller="mlog/global_test.go:0"`,
|
||||
`error | TIME | real error log | caller="mlog/global_test.go:0"`,
|
||||
},
|
||||
},
|
||||
{
|
||||
"file logging, non-json, error",
|
||||
mlog.TargetCfg{
|
||||
Type: "file",
|
||||
Format: "plain",
|
||||
FormatOptions: json.RawMessage(`{"delim":" | ", "enable_caller":true}`),
|
||||
Levels: []mlog.Level{mlog.LvlError},
|
||||
},
|
||||
[]string{
|
||||
`error | TIME | real error log | caller="mlog/global_test.go:0"`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.description, func(t *testing.T) {
|
||||
var filePath string
|
||||
if testCase.cfg.Type == "file" {
|
||||
tempDir, err := os.MkdirTemp(os.TempDir(), "TestLoggingAfterInitialized")
|
||||
require.NoError(t, err)
|
||||
defer os.Remove(tempDir)
|
||||
|
||||
filePath = filepath.Join(tempDir, "file.log")
|
||||
testCase.cfg.Options = json.RawMessage(fmt.Sprintf(`{"filename": "%s"}`, filePath))
|
||||
}
|
||||
|
||||
logger, _ := mlog.NewLogger()
|
||||
err := logger.ConfigureTargets(map[string]mlog.TargetCfg{testCase.description: testCase.cfg}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
mlog.InitGlobalLogger(logger)
|
||||
|
||||
mlog.Debug("real debug log")
|
||||
mlog.Info("real info log")
|
||||
mlog.Warn("real warning log")
|
||||
mlog.Error("real error log")
|
||||
|
||||
logger.Shutdown()
|
||||
|
||||
if testCase.cfg.Type == "file" {
|
||||
logs, err := os.ReadFile(filePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
actual := strings.TrimSpace(string(logs))
|
||||
|
||||
if testCase.cfg.Format == "json" {
|
||||
reTs := regexp.MustCompile(`"timestamp":"[0-9\.\-\+\:\sZ]+"`)
|
||||
reCaller := regexp.MustCompile(`"caller":"([^"]+):[0-9\.]+"`)
|
||||
actual = reTs.ReplaceAllString(actual, `"timestamp":0`)
|
||||
actual = reCaller.ReplaceAllString(actual, `"caller":"$1:0"`)
|
||||
} else {
|
||||
reTs := regexp.MustCompile(`\[\d\d\d\d-\d\d-\d\d\s[0-9\:\.\s\-\+Z]+\]`)
|
||||
reCaller := regexp.MustCompile(`caller="([^"]+):[0-9\.]+"`)
|
||||
actual = reTs.ReplaceAllString(actual, "TIME")
|
||||
actual = reCaller.ReplaceAllString(actual, `caller="$1:0"`)
|
||||
}
|
||||
require.ElementsMatch(t, testCase.expectedLogs, strings.Split(actual, "\n"))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
158
server/public/shared/mlog/levels.go
Normal file
158
server/public/shared/mlog/levels.go
Normal file
@@ -0,0 +1,158 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package mlog
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/mattermost/logr/v2"
|
||||
)
|
||||
|
||||
// Standard levels.
|
||||
var (
|
||||
LvlPanic = logr.Panic // ID = 0
|
||||
LvlFatal = logr.Fatal // ID = 1
|
||||
LvlError = logr.Error // ID = 2
|
||||
LvlWarn = logr.Warn // ID = 3
|
||||
LvlInfo = logr.Info // ID = 4
|
||||
LvlDebug = logr.Debug // ID = 5
|
||||
LvlTrace = logr.Trace // ID = 6
|
||||
// non-standard "critical" level
|
||||
LvlCritical = Level{ID: 7, Name: "critical"}
|
||||
// used by redirected standard logger
|
||||
LvlStdLog = Level{ID: 10, Name: "stdlog"}
|
||||
// used only by the logger
|
||||
LvlLogError = Level{ID: 11, Name: "logerror", Stacktrace: true}
|
||||
|
||||
StdAll = []Level{LvlPanic, LvlFatal, LvlError, LvlWarn, LvlInfo, LvlDebug, LvlTrace, LvlStdLog}
|
||||
)
|
||||
|
||||
// Register custom (discrete) levels here.
|
||||
// !!!!! Custom ID's must be between 20 and 32,768 !!!!!!
|
||||
|
||||
var (
|
||||
/* Audit system */
|
||||
|
||||
// LvlAuditAPI is used for auditing REST API endpoint access. This is the most commonly used
|
||||
// audit level and should be applied whenever a REST API endpoint is accessed. It provides
|
||||
// a record of API usage patterns and access.
|
||||
//
|
||||
// Example usage: Logging when a user accesses the /api/v4/posts endpoint.
|
||||
LvlAuditAPI = Level{ID: 100, Name: "audit-api"}
|
||||
|
||||
// LvlAuditContent is used for auditing content-generating operations such as creating posts,
|
||||
// adding reactions, or other actions that create new content in the system. Note that this
|
||||
// level can generate a large volume of logs and is typically disabled by default in production
|
||||
// environments.
|
||||
//
|
||||
// Example usage: Logging when a user creates a new post or adds a reaction.
|
||||
LvlAuditContent = Level{ID: 101, Name: "audit-content"}
|
||||
|
||||
// LvlAuditPerms is used for auditing permissions checks. This level is automatically applied
|
||||
// when permission-related errors occur, helping to track authorization failures and access
|
||||
// control issues.
|
||||
//
|
||||
// Example usage: Logging when a user attempts to access a resource they don't have permission for.
|
||||
LvlAuditPerms = Level{ID: 102, Name: "audit-permissions"}
|
||||
|
||||
// LvlAuditCLI is intended for auditing command-line interface operations. This level was
|
||||
// originally designed for the legacy CLI. It's mostly unused now.
|
||||
LvlAuditCLI = Level{ID: 103, Name: "audit-cli"}
|
||||
|
||||
// LvlAuditDelivery is used for auditing the delivery of post content to users and
|
||||
// integrations. This level can generate an extremely large volume of records — one per
|
||||
// post per recipient — so it is deliberately not bound to the basic
|
||||
// ExperimentalAuditSettings file target. An admin opts in by configuring an advanced
|
||||
// audit logging target for it.
|
||||
//
|
||||
// Example usage: Logging that a post was included in a page of posts returned to a user.
|
||||
LvlAuditDelivery = Level{ID: 104, Name: "audit-delivery"}
|
||||
|
||||
MLvlAuditAll = []Level{
|
||||
LvlAuditAPI,
|
||||
LvlAuditContent,
|
||||
LvlAuditPerms,
|
||||
LvlAuditCLI,
|
||||
LvlAuditDelivery,
|
||||
}
|
||||
|
||||
LvlRemoteClusterServiceDebug = Level{ID: 130, Name: "RemoteClusterServiceDebug"}
|
||||
LvlRemoteClusterServiceError = Level{ID: 131, Name: "RemoteClusterServiceError"}
|
||||
LvlRemoteClusterServiceWarn = Level{ID: 132, Name: "RemoteClusterServiceWarn"}
|
||||
MLvlRemoteClusterServiceAll = []Level{
|
||||
LvlRemoteClusterServiceDebug,
|
||||
LvlRemoteClusterServiceError,
|
||||
LvlRemoteClusterServiceWarn,
|
||||
}
|
||||
|
||||
/* LDAP sync job */
|
||||
|
||||
LvlLDAPError = Level{ID: 140, Name: "LDAPError"}
|
||||
LvlLDAPWarn = Level{ID: 141, Name: "LDAPWarn"}
|
||||
LvlLDAPInfo = Level{ID: 142, Name: "LDAPInfo"}
|
||||
LvlLDAPDebug = Level{ID: 143, Name: "LDAPDebug"}
|
||||
LvlLDAPTrace = Level{ID: 144, Name: "LDAPTrace"}
|
||||
MLvlLDAPAll = []Level{LvlLDAPError, LvlLDAPWarn, LvlLDAPInfo, LvlLDAPDebug, LvlLDAPTrace}
|
||||
|
||||
/* Shared Channel Sync Service */
|
||||
|
||||
LvlSharedChannelServiceDebug = Level{ID: 200, Name: "SharedChannelServiceDebug"}
|
||||
LvlSharedChannelServiceError = Level{ID: 201, Name: "SharedChannelServiceError"}
|
||||
LvlSharedChannelServiceWarn = Level{ID: 202, Name: "SharedChannelServiceWarn"}
|
||||
LvlSharedChannelServiceMessagesInbound = Level{ID: 203, Name: "SharedChannelServiceMsgInbound"}
|
||||
LvlSharedChannelServiceMessagesOutbound = Level{ID: 204, Name: "SharedChannelServiceMsgOutbound"}
|
||||
MlvlSharedChannelServiceAll = []Level{
|
||||
LvlSharedChannelServiceDebug,
|
||||
LvlSharedChannelServiceError,
|
||||
LvlSharedChannelServiceWarn,
|
||||
LvlSharedChannelServiceMessagesInbound,
|
||||
LvlSharedChannelServiceMessagesOutbound,
|
||||
}
|
||||
|
||||
/* Notification Service */
|
||||
|
||||
LvlNotificationError = Level{ID: 300, Name: "NotificationError"}
|
||||
LvlNotificationWarn = Level{ID: 301, Name: "NotificationWarn"}
|
||||
LvlNotificationInfo = Level{ID: 302, Name: "NotificationInfo"}
|
||||
LvlNotificationDebug = Level{ID: 303, Name: "NotificationDebug"}
|
||||
LvlNotificationTrace = Level{ID: 304, Name: "NotificationTrace"}
|
||||
MlvlNotificationAll = []Level{
|
||||
LvlNotificationError,
|
||||
LvlNotificationWarn,
|
||||
LvlNotificationInfo,
|
||||
LvlNotificationDebug,
|
||||
LvlNotificationTrace,
|
||||
}
|
||||
)
|
||||
|
||||
// Combinations for LogM (log multi).
|
||||
var (
|
||||
MlvlRemoteClusterServiceError = []Level{LvlError, LvlRemoteClusterServiceError}
|
||||
MlvlRemoteClusterServiceWarn = []Level{LvlWarn, LvlRemoteClusterServiceWarn}
|
||||
|
||||
MlvlSharedChannelServiceError = []Level{LvlError, LvlSharedChannelServiceError}
|
||||
MlvlSharedChannelServiceWarn = []Level{LvlWarn, LvlSharedChannelServiceWarn}
|
||||
|
||||
MlvlLDAPError = []Level{LvlError, LvlLDAPError}
|
||||
MlvlLDAPWarn = []Level{LvlWarn, LvlLDAPWarn}
|
||||
MlvlLDAPInfo = []Level{LvlInfo, LvlLDAPInfo}
|
||||
MlvlLDAPDebug = []Level{LvlDebug, LvlLDAPDebug}
|
||||
|
||||
MlvlNotificationError = []Level{LvlError, LvlNotificationError}
|
||||
MlvlNotificationWarn = []Level{LvlWarn, LvlNotificationWarn}
|
||||
MlvlNotificationInfo = []Level{LvlInfo, LvlNotificationInfo}
|
||||
MlvlNotificationDebug = []Level{LvlDebug, LvlNotificationDebug}
|
||||
MlvlNotificationTrace = []Level{LvlTrace, LvlNotificationTrace}
|
||||
)
|
||||
|
||||
var (
|
||||
// MlvlAll contains all log levels except the audit ones
|
||||
MlvlAll = slices.Concat(
|
||||
StdAll,
|
||||
MLvlRemoteClusterServiceAll,
|
||||
MLvlLDAPAll,
|
||||
MlvlSharedChannelServiceAll,
|
||||
MlvlNotificationAll,
|
||||
)
|
||||
)
|
||||
11
server/public/shared/mlog/metrics.go
Normal file
11
server/public/shared/mlog/metrics.go
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package mlog
|
||||
|
||||
import (
|
||||
"github.com/mattermost/logr/v2"
|
||||
)
|
||||
|
||||
type Gauge = logr.Gauge
|
||||
type Counter = logr.Counter
|
||||
479
server/public/shared/mlog/mlog.go
Normal file
479
server/public/shared/mlog/mlog.go
Normal file
@@ -0,0 +1,479 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Package mlog provides a simple wrapper around Logr.
|
||||
package mlog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"maps"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/logr/v2"
|
||||
logrcfg "github.com/mattermost/logr/v2/config"
|
||||
)
|
||||
|
||||
const (
|
||||
ShutdownTimeout = time.Second * 15
|
||||
FlushTimeout = time.Second * 15
|
||||
DefaultMaxQueueSize = 1000
|
||||
DefaultMetricsUpdateFreqMillis = 15000
|
||||
)
|
||||
|
||||
// LoggerIFace should be abbreviated as `logger`.
|
||||
type LoggerIFace interface {
|
||||
IsLevelEnabled(Level) bool
|
||||
Trace(string, ...Field)
|
||||
Debug(string, ...Field)
|
||||
Info(string, ...Field)
|
||||
Warn(string, ...Field)
|
||||
Error(string, ...Field)
|
||||
Critical(string, ...Field)
|
||||
Fatal(string, ...Field)
|
||||
Log(Level, string, ...Field)
|
||||
LogM([]Level, string, ...Field)
|
||||
With(fields ...Field) *Logger
|
||||
Flush() error
|
||||
Sugar(fields ...Field) Sugar
|
||||
StdLogger(level Level) *log.Logger
|
||||
}
|
||||
|
||||
// Type and function aliases from Logr to limit the spread of dependencies.
|
||||
type Field = logr.Field
|
||||
type Level = logr.Level
|
||||
type Option = logr.Option
|
||||
type Target = logr.Target
|
||||
type TargetInfo = logr.TargetInfo
|
||||
type LogRec = logr.LogRec
|
||||
type LogCloner = logr.LogCloner
|
||||
type MetricsCollector = logr.MetricsCollector
|
||||
type TargetCfg = logrcfg.TargetCfg
|
||||
type TargetFactory = logrcfg.TargetFactory
|
||||
type FormatterFactory = logrcfg.FormatterFactory
|
||||
type Factories = logrcfg.Factories
|
||||
type Sugar = logr.Sugar
|
||||
|
||||
// LoggerConfiguration is a map of LogTarget configurations.
|
||||
type LoggerConfiguration map[string]TargetCfg
|
||||
|
||||
func (lc LoggerConfiguration) Append(cfg LoggerConfiguration) {
|
||||
maps.Copy(lc, cfg)
|
||||
}
|
||||
|
||||
func (lc LoggerConfiguration) IsValid(validLevels []Level) error {
|
||||
logger, err := logr.New()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create logger")
|
||||
}
|
||||
defer logger.Shutdown()
|
||||
|
||||
err = logrcfg.ConfigureTargets(logger, lc, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "logger configuration is invalid")
|
||||
}
|
||||
|
||||
validLevelIDs := make([]logr.LevelID, 0, len(validLevels))
|
||||
for _, l := range validLevels {
|
||||
validLevelIDs = append(validLevelIDs, l.ID)
|
||||
}
|
||||
|
||||
for _, c := range lc {
|
||||
for _, l := range c.Levels {
|
||||
if !slices.Contains(validLevelIDs, l.ID) {
|
||||
return errors.Errorf("invalid log level id %d", l.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lc LoggerConfiguration) toTargetCfg() map[string]logrcfg.TargetCfg {
|
||||
tcfg := make(map[string]logrcfg.TargetCfg)
|
||||
maps.Copy(tcfg, lc)
|
||||
return tcfg
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// Int constructs a field containing a key and int value.
|
||||
func Int[T ~int | ~int8 | ~int16 | ~int32 | ~int64](key string, val T) Field {
|
||||
return logr.Int[T](key, val)
|
||||
}
|
||||
|
||||
// Uint constructs a field containing a key and uint value.
|
||||
func Uint[T ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr](key string, val T) Field {
|
||||
return logr.Uint[T](key, val)
|
||||
}
|
||||
|
||||
// Float constructs a field containing a key and float value.
|
||||
func Float[T ~float32 | ~float64](key string, val T) Field {
|
||||
return logr.Float[T](key, val)
|
||||
}
|
||||
|
||||
// String constructs a field containing a key and string value.
|
||||
func String[T ~string | ~[]byte](key string, val T) Field {
|
||||
return logr.String[T](key, val)
|
||||
}
|
||||
|
||||
// Stringer constructs a field containing a key and a fmt.Stringer value.
|
||||
// The fmt.Stringer's `String` method is called lazily.
|
||||
var Stringer = func(key string, s fmt.Stringer) logr.Field {
|
||||
if s == nil {
|
||||
return Field{Key: key, Type: logr.StringType, String: ""}
|
||||
}
|
||||
return Field{Key: key, Type: logr.StringType, String: s.String()}
|
||||
}
|
||||
|
||||
// Err constructs a field containing a default key ("error") and error value.
|
||||
var Err = func(err error) logr.Field {
|
||||
return NamedErr("error", err)
|
||||
}
|
||||
|
||||
// NamedErr constructs a field containing a key and error value.
|
||||
var NamedErr = func(key string, err error) logr.Field {
|
||||
if err == nil {
|
||||
return Field{Key: key, Type: logr.StringType, String: ""}
|
||||
}
|
||||
return Field{Key: key, Type: logr.StringType, String: err.Error()}
|
||||
}
|
||||
|
||||
// Bool constructs a field containing a key and bool value.
|
||||
func Bool[T ~bool](key string, val T) Field {
|
||||
return logr.Bool[T](key, val)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func Array[S ~[]E, E any](key string, val S) Field {
|
||||
return logr.Array[S](key, val)
|
||||
}
|
||||
|
||||
// Map constructs a field containing a key and map value.
|
||||
func Map[M ~map[K]V, K comparable, V any](key string, val M) Field {
|
||||
return logr.Map[M](key, val)
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// Logger should be abbreviated as `logger`.
|
||||
type Logger struct {
|
||||
log *logr.Logger
|
||||
lockConfig *int32
|
||||
}
|
||||
|
||||
// NewLogger creates a new Logger instance which can be configured via `(*Logger).Configure`.
|
||||
// Some options with invalid values can cause an error to be returned, however `NewLogger()`
|
||||
// using just defaults never errors.
|
||||
func NewLogger(options ...Option) (*Logger, error) {
|
||||
options = append(options, logr.StackFilter(logr.GetPackageName("NewLogger")))
|
||||
|
||||
lgr, err := logr.New(options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log := lgr.NewLogger()
|
||||
var lockConfig int32
|
||||
|
||||
return &Logger{
|
||||
log: &log,
|
||||
lockConfig: &lockConfig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Configure provides a new configuration for this logger.
|
||||
// Zero or more sources of config can be provided:
|
||||
//
|
||||
// cfgFile - path to file containing JSON
|
||||
// cfgEscaped - JSON string probably from ENV var
|
||||
//
|
||||
// For each case JSON containing log targets is provided. Target name collisions are resolved
|
||||
// using the following precedence:
|
||||
//
|
||||
// cfgFile > cfgEscaped
|
||||
//
|
||||
// An optional set of factories can be provided which will be called to create any target
|
||||
// types or formatters not built-in.
|
||||
func (l *Logger) Configure(cfgFile string, cfgEscaped string, factories *Factories) error {
|
||||
if atomic.LoadInt32(l.lockConfig) != 0 {
|
||||
return ErrConfigurationLock
|
||||
}
|
||||
|
||||
cfgMap := make(LoggerConfiguration)
|
||||
|
||||
// Add config from file
|
||||
if cfgFile != "" {
|
||||
b, err := os.ReadFile(cfgFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading logger config file %s: %w", cfgFile, err)
|
||||
}
|
||||
|
||||
var mapCfgFile LoggerConfiguration
|
||||
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 != "" {
|
||||
var mapCfgEscaped LoggerConfiguration
|
||||
if err := json.Unmarshal([]byte(cfgEscaped), &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.toTargetCfg(), factories)
|
||||
}
|
||||
|
||||
// ConfigureTargets provides a new configuration for this logger via a `LoggerConfig` map.
|
||||
// `Logger.Configure` can be used instead which accepts JSON formatted configuration.
|
||||
// An optional set of factories can be provided which will be called to create any target
|
||||
// types or formatters not built-in.
|
||||
func (l *Logger) ConfigureTargets(cfg LoggerConfiguration, factories *Factories) error {
|
||||
if atomic.LoadInt32(l.lockConfig) != 0 {
|
||||
return ErrConfigurationLock
|
||||
}
|
||||
return logrcfg.ConfigureTargets(l.log.Logr(), cfg.toTargetCfg(), factories)
|
||||
}
|
||||
|
||||
// LockConfiguration disallows further configuration changes until `UnlockConfiguration`
|
||||
// is called. The previous locked stated is returned.
|
||||
func (l *Logger) LockConfiguration() bool {
|
||||
old := atomic.SwapInt32(l.lockConfig, 1)
|
||||
return old != 0
|
||||
}
|
||||
|
||||
// UnlockConfiguration allows configuration changes. The previous locked stated is returned.
|
||||
func (l *Logger) UnlockConfiguration() bool {
|
||||
old := atomic.SwapInt32(l.lockConfig, 0)
|
||||
return old != 0
|
||||
}
|
||||
|
||||
// IsConfigurationLocked returns the current state of the configuration lock.
|
||||
func (l *Logger) IsConfigurationLocked() bool {
|
||||
return atomic.LoadInt32(l.lockConfig) != 0
|
||||
}
|
||||
|
||||
// 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,
|
||||
lockConfig: l.lockConfig,
|
||||
}
|
||||
}
|
||||
|
||||
// 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.log.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 `Critical` level.
|
||||
func (l *Logger) Critical(msg string, fields ...Field) {
|
||||
l.log.Log(LvlCritical, 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)
|
||||
}
|
||||
|
||||
// StdLogWriter returns a writer that can be hooked up to the output of a golang standard logger
|
||||
// anything written will be interpreted as log entries and passed to this logger.
|
||||
func (l *Logger) StdLogWriter() io.Writer {
|
||||
return &logWriter{
|
||||
logger: l,
|
||||
}
|
||||
}
|
||||
|
||||
// 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...)
|
||||
}
|
||||
|
||||
// RemoveTargets safely removes one or more targets based on the filtering method.
|
||||
// `f` should return true to delete the target, false to keep it.
|
||||
// When removing a target, best effort is made to write any queued log records before
|
||||
// closing, with ctx determining how much time can be spent in total.
|
||||
// Note, keep the timeout short since this method blocks certain logging operations.
|
||||
func (l *Logger) RemoveTargets(ctx context.Context, f func(ti TargetInfo) bool) error {
|
||||
return l.log.Logr().RemoveTargets(ctx, f)
|
||||
}
|
||||
|
||||
// SetMetricsCollector sets (or resets) the metrics collector to be used for gathering
|
||||
// metrics for all targets. Only targets added after this call will use the collector.
|
||||
//
|
||||
// To ensure all targets use a collector, use the `SetMetricsCollector` option when
|
||||
// creating the Logger instead, or configure/reconfigure the Logger after calling this method.
|
||||
func (l *Logger) SetMetricsCollector(collector MetricsCollector, updateFrequencyMillis int64) {
|
||||
l.log.Logr().SetMetricsCollector(collector, updateFrequencyMillis)
|
||||
}
|
||||
|
||||
// Sugar creates a new `Logger` with a less structured API. Any fields are preserved.
|
||||
func (l *Logger) Sugar(fields ...Field) Sugar {
|
||||
return l.log.Sugar(fields...)
|
||||
}
|
||||
|
||||
// Flush forces all targets to write out any queued log records with a default timeout.
|
||||
func (l *Logger) Flush() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), FlushTimeout)
|
||||
defer cancel()
|
||||
return l.log.Logr().FlushWithTimeout(ctx)
|
||||
}
|
||||
|
||||
// Flush forces all targets to write out any queued log records with the specified timeout.
|
||||
func (l *Logger) FlushWithTimeout(ctx context.Context) error {
|
||||
return l.log.Logr().FlushWithTimeout(ctx)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Shutdown shuts down the logger after making best efforts to flush any
|
||||
// remaining records.
|
||||
func (l *Logger) ShutdownWithTimeout(ctx context.Context) error {
|
||||
return l.log.Logr().ShutdownWithTimeout(ctx)
|
||||
}
|
||||
|
||||
// GetPackageName reduces a fully qualified function name to the package name
|
||||
// By sirupsen: https://github.com/sirupsen/logrus/blob/master/entry.go
|
||||
func GetPackageName(f string) string {
|
||||
for {
|
||||
lastPeriod := strings.LastIndex(f, ".")
|
||||
lastSlash := strings.LastIndex(f, "/")
|
||||
if lastPeriod > lastSlash {
|
||||
f = f[:lastPeriod]
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// ShouldQuote returns true if val contains any characters that might be unsafe
|
||||
// when injecting log output into an aggregator, viewer or report.
|
||||
// Returning true means that val should be surrounded by quotation marks before being
|
||||
// output into logs.
|
||||
func ShouldQuote(val string) bool {
|
||||
for _, c := range val {
|
||||
if !((c >= '0' && c <= '9') ||
|
||||
(c >= 'a' && c <= 'z') ||
|
||||
(c >= 'A' && c <= 'Z') ||
|
||||
c == '-' || c == '.' || c == '_' || c == '/' || c == '@' || c == '^' || c == '+') {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type logWriter struct {
|
||||
logger *Logger
|
||||
}
|
||||
|
||||
func (lw *logWriter) Write(p []byte) (int, error) {
|
||||
lw.logger.Info(string(p))
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// ErrConfigurationLock is returned when one of a logger's configuration APIs is called
|
||||
// while the configuration is locked.
|
||||
var ErrConfigurationLock = errors.New("configuration is locked")
|
||||
62
server/public/shared/mlog/options.go
Normal file
62
server/public/shared/mlog/options.go
Normal file
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package mlog
|
||||
|
||||
import "github.com/mattermost/logr/v2"
|
||||
|
||||
// MaxQueueSize is the maximum number of log records that can be queued.
|
||||
// If exceeded, `OnQueueFull` is called which determines if the log
|
||||
// record will be dropped or block until add is successful.
|
||||
// Defaults to DefaultMaxQueueSize.
|
||||
func MaxQueueSize(size int) Option {
|
||||
return logr.MaxQueueSize(size)
|
||||
}
|
||||
|
||||
// OnLoggerError, when not nil, is called any time an internal
|
||||
// logging error occurs. For example, this can happen when a
|
||||
// target cannot connect to its data sink.
|
||||
func OnLoggerError(f func(error)) Option {
|
||||
return logr.OnLoggerError(f)
|
||||
}
|
||||
|
||||
// OnQueueFull, when not nil, is called on an attempt to add
|
||||
// a log record to a full Logr queue.
|
||||
// `MaxQueueSize` can be used to modify the maximum queue size.
|
||||
// This function should return quickly, with a bool indicating whether
|
||||
// the log record should be dropped (true) or block until the log record
|
||||
// is successfully added (false). If nil then blocking (false) is assumed.
|
||||
func OnQueueFull(f func(rec *LogRec, maxQueueSize int) bool) Option {
|
||||
return logr.OnQueueFull(f)
|
||||
}
|
||||
|
||||
// OnTargetQueueFull, when not nil, is called on an attempt to add
|
||||
// a log record to a full target queue provided the target supports reporting
|
||||
// this condition.
|
||||
// This function should return quickly, with a bool indicating whether
|
||||
// the log record should be dropped (true) or block until the log record
|
||||
// is successfully added (false). If nil then blocking (false) is assumed.
|
||||
func OnTargetQueueFull(f func(target Target, rec *LogRec, maxQueueSize int) bool) Option {
|
||||
return logr.OnTargetQueueFull(f)
|
||||
}
|
||||
|
||||
// SetMetricsCollector enables metrics collection by supplying a MetricsCollector.
|
||||
// The MetricsCollector provides counters and gauges that are updated by log targets.
|
||||
// `updateFreqMillis` determines how often polled metrics are updated. Defaults to 15000 (15 seconds)
|
||||
// and must be at least 250 so we don't peg the CPU.
|
||||
func SetMetricsCollector(collector MetricsCollector, updateFreqMillis int64) Option {
|
||||
return logr.SetMetricsCollector(collector, updateFreqMillis)
|
||||
}
|
||||
|
||||
// StackFilter provides a list of package names to exclude from the top of
|
||||
// stack traces. The Logr packages are automatically filtered.
|
||||
func StackFilter(pkg ...string) Option {
|
||||
return logr.StackFilter(pkg...)
|
||||
}
|
||||
|
||||
// MaxFieldLen is the maximum number of characters for a field.
|
||||
// If exceeded, remaining bytes will be discarded.
|
||||
// Defaults to DefaultMaxFieldLength.
|
||||
func MaxFieldLen(size int) Option {
|
||||
return logr.MaxFieldLen(size)
|
||||
}
|
||||
145
server/public/shared/mlog/tlog.go
Normal file
145
server/public/shared/mlog/tlog.go
Normal file
@@ -0,0 +1,145 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package mlog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/logr/v2"
|
||||
"github.com/mattermost/logr/v2/formatters"
|
||||
"github.com/mattermost/logr/v2/targets"
|
||||
)
|
||||
|
||||
// AddWriterTarget adds a simple io.Writer target to an existing Logger.
|
||||
// The `io.Writer` can be a buffer which is useful for testing.
|
||||
// When adding a buffer to collect logs make sure to use `mlog.Buffer` which is
|
||||
// a thread safe version of `bytes.Buffer`.
|
||||
func AddWriterTarget(logger *Logger, w io.Writer, useJSON bool, levels ...Level) error {
|
||||
filter := logr.NewCustomFilter(levels...)
|
||||
|
||||
var formatter logr.Formatter
|
||||
if useJSON {
|
||||
formatter = &formatters.JSON{EnableCaller: true}
|
||||
} else {
|
||||
formatter = &formatters.Plain{EnableCaller: true}
|
||||
}
|
||||
|
||||
target := targets.NewWriterTarget(w)
|
||||
return logger.log.Logr().AddTarget(target, "_testWriter", filter, formatter, 1000)
|
||||
}
|
||||
|
||||
// CreateConsole createa a logger that outputs to [os.Stdout].
|
||||
// It's useful in places where no log configuration is accessible.
|
||||
func CreateConsoleLogger() *Logger {
|
||||
logger, err := NewLogger()
|
||||
if err != nil {
|
||||
panic("failed create logger " + err.Error())
|
||||
}
|
||||
|
||||
filter := logr.StdFilter{
|
||||
Lvl: LvlTrace,
|
||||
Stacktrace: LvlPanic,
|
||||
}
|
||||
formatter := &formatters.Plain{
|
||||
EnableCaller: true,
|
||||
EnableColor: true,
|
||||
}
|
||||
|
||||
target := targets.NewWriterTarget(os.Stdout)
|
||||
if err := logger.log.Logr().AddTarget(target, "_testcon", filter, formatter, 1000); err != nil {
|
||||
panic("failed to add target " + err.Error())
|
||||
}
|
||||
|
||||
return logger
|
||||
}
|
||||
|
||||
// CreateConsoleTestLogger creates a logger for unit tests. Log records are output to `os.Stdout`.
|
||||
// All log messages with level trace or lower are logged.
|
||||
// The returned logger get Shutdown() when the tests completes. The caller should not shut it down.
|
||||
func CreateConsoleTestLogger(tb testing.TB) *Logger {
|
||||
tb.Helper()
|
||||
|
||||
logger, err := NewLogger()
|
||||
if err != nil {
|
||||
tb.Fatalf("failed create logger %v", err)
|
||||
}
|
||||
|
||||
filter := logr.StdFilter{
|
||||
Lvl: LvlTrace,
|
||||
Stacktrace: LvlPanic,
|
||||
}
|
||||
formatter := &formatters.Plain{EnableCaller: true}
|
||||
|
||||
target := targets.NewWriterTarget(os.Stdout)
|
||||
if err := logger.log.Logr().AddTarget(target, "_testcon", filter, formatter, 1000); err != nil {
|
||||
tb.Fatalf("failed to add target %v", err)
|
||||
}
|
||||
|
||||
tb.Cleanup(func() {
|
||||
err := logger.Shutdown()
|
||||
if err != nil {
|
||||
tb.Fatalf("failed to shut down test logger %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
return logger
|
||||
}
|
||||
|
||||
// CreateTestLogger creates a logger for unit tests. Log records are output via `t.Log`.
|
||||
// All log messages with level trace or lower are logged.
|
||||
// The returned logger get Shutdown() when the tests completes. The caller should not shut it down.
|
||||
func CreateTestLogger(t *testing.T) *Logger {
|
||||
t.Helper()
|
||||
|
||||
logger, err := NewLogger()
|
||||
if err != nil {
|
||||
t.Fatalf("failed create logger %v", err)
|
||||
}
|
||||
|
||||
filter := logr.StdFilter{
|
||||
Lvl: LvlTrace,
|
||||
Stacktrace: LvlPanic,
|
||||
}
|
||||
formatter := &formatters.Plain{EnableCaller: true}
|
||||
target := targets.NewTestingTarget(t)
|
||||
|
||||
if err := logger.log.Logr().AddTarget(target, "test", filter, formatter, 1000); err != nil {
|
||||
t.Fatalf("failed to add target %v", err)
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
err := logger.Shutdown()
|
||||
if err != nil {
|
||||
t.Errorf("failed to shut down test logger %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
return logger
|
||||
}
|
||||
|
||||
// Buffer provides a thread-safe buffer useful for logging to memory in unit tests.
|
||||
type Buffer struct {
|
||||
buf bytes.Buffer
|
||||
mux sync.Mutex
|
||||
}
|
||||
|
||||
func (b *Buffer) Read(p []byte) (n int, err error) {
|
||||
b.mux.Lock()
|
||||
defer b.mux.Unlock()
|
||||
return b.buf.Read(p)
|
||||
}
|
||||
func (b *Buffer) Write(p []byte) (n int, err error) {
|
||||
b.mux.Lock()
|
||||
defer b.mux.Unlock()
|
||||
return b.buf.Write(p)
|
||||
}
|
||||
func (b *Buffer) String() string {
|
||||
b.mux.Lock()
|
||||
defer b.mux.Unlock()
|
||||
return b.buf.String()
|
||||
}
|
||||
Reference in New Issue
Block a user