parent
34d2dafbd2
commit
ae121486ff
|
@ -0,0 +1,141 @@
|
|||
package logutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
var namespacesRegex = regexp.MustCompile(`^([a-zA-Z0-9_\.]+:(debug|info|warn|error))(,[a-zA-Z0-9_\.]+:(debug|info|warn|error))*$`)
|
||||
var errInvalidNamespacesFormat = fmt.Errorf("invalid namespaces format")
|
||||
|
||||
type namespacesTree struct {
|
||||
namespaces map[string]*namespaceNode
|
||||
minLevel zapcore.Level
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
type namespaceNode struct {
|
||||
level zapcore.Level
|
||||
children map[string]*namespaceNode
|
||||
explicit bool
|
||||
}
|
||||
|
||||
func newNamespacesTree() *namespacesTree {
|
||||
return &namespacesTree{
|
||||
namespaces: make(map[string]*namespaceNode),
|
||||
minLevel: zapcore.InvalidLevel,
|
||||
}
|
||||
}
|
||||
|
||||
// buildNamespacesTree creates a new tree instance from a string of namespaces.
|
||||
// The namespaces string should be a comma-separated list of namespace:level pairs,
|
||||
// where level is one of: "debug", "info", "warn", "error".
|
||||
//
|
||||
// Example: "namespace1:debug,namespace2.namespace3:error"
|
||||
func buildNamespacesTree(namespaces string) (*namespacesTree, error) {
|
||||
tree := newNamespacesTree()
|
||||
|
||||
err := tree.Rebuild(namespaces)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tree, nil
|
||||
}
|
||||
|
||||
// LevelFor finds the log level for the given namespace by searching for the closest match in the tree.
|
||||
// In case of no match, it returns zapcore.InvalidLevel.
|
||||
func (t *namespacesTree) LevelFor(namespace string) zapcore.Level {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
|
||||
currentNode := t.namespaces
|
||||
var lastFound *namespaceNode
|
||||
|
||||
for _, name := range strings.Split(namespace, ".") {
|
||||
if node, exists := currentNode[name]; exists {
|
||||
if node.explicit {
|
||||
lastFound = node
|
||||
}
|
||||
currentNode = node.children
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if lastFound != nil {
|
||||
return lastFound.level
|
||||
}
|
||||
|
||||
// Default to zapcore.InvalidLevel if no match found.
|
||||
return zapcore.InvalidLevel
|
||||
}
|
||||
|
||||
// Rebuild updates the tree with a new set of namespaces.
|
||||
func (t *namespacesTree) Rebuild(namespaces string) error {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
if namespaces == "" {
|
||||
t.namespaces = make(map[string]*namespaceNode)
|
||||
t.minLevel = zapcore.InvalidLevel
|
||||
return nil
|
||||
}
|
||||
|
||||
if !namespacesRegex.MatchString(namespaces) {
|
||||
return errInvalidNamespacesFormat
|
||||
}
|
||||
|
||||
for _, ns := range strings.Split(namespaces, ",") {
|
||||
parts := strings.Split(ns, ":")
|
||||
if len(parts) != 2 {
|
||||
return errInvalidNamespacesFormat
|
||||
}
|
||||
|
||||
lvl, err := lvlFromString(parts[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.addNamespace(parts[0], lvl)
|
||||
|
||||
if t.minLevel == zapcore.InvalidLevel || lvl < t.minLevel {
|
||||
t.minLevel = lvl
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *namespacesTree) MinLevel() zapcore.Level {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
|
||||
return t.minLevel
|
||||
}
|
||||
|
||||
// addNamespace adds a namespace with a specific logging level to the tree.
|
||||
func (t *namespacesTree) addNamespace(namespace string, level zapcore.Level) {
|
||||
currentParent := t.namespaces
|
||||
var currentNode *namespaceNode
|
||||
|
||||
for _, name := range strings.Split(namespace, ".") {
|
||||
if node, exists := currentParent[name]; exists {
|
||||
currentNode = node
|
||||
} else {
|
||||
newNode := &namespaceNode{
|
||||
children: make(map[string]*namespaceNode),
|
||||
}
|
||||
currentParent[name] = newNode
|
||||
currentNode = newNode
|
||||
}
|
||||
currentParent = currentNode.children
|
||||
}
|
||||
|
||||
currentNode.level = level
|
||||
currentNode.explicit = true
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
package logutils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
func TestBuildNamespacesTree(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
namespaces string
|
||||
minLevel zapcore.Level
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "valid namespaces",
|
||||
namespaces: "namespace1:debug,namespace2.namespace3:error",
|
||||
minLevel: zapcore.DebugLevel,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "invalid format",
|
||||
namespaces: "namespace1:debug,namespace2.namespace3",
|
||||
minLevel: zapcore.DebugLevel,
|
||||
err: errInvalidNamespacesFormat,
|
||||
},
|
||||
{
|
||||
name: "empty namespaces",
|
||||
namespaces: "",
|
||||
minLevel: zapcore.InvalidLevel,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "invalid level",
|
||||
namespaces: "namespace1:invalid",
|
||||
minLevel: zapcore.InvalidLevel,
|
||||
err: errInvalidNamespacesFormat,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tree, err := buildNamespacesTree(tt.namespaces)
|
||||
require.ErrorIs(t, err, tt.err)
|
||||
if tt.err == nil {
|
||||
require.NotNil(t, tree)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLevelFor(t *testing.T) {
|
||||
tree, err := buildNamespacesTree("namespace1:error,namespace1.namespace2:debug,namespace1.namespace2.namespace3:info,namespace3.namespace4:warn")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, tree)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected zapcore.Level
|
||||
}{
|
||||
{
|
||||
name: "exact match 1",
|
||||
input: "namespace1",
|
||||
expected: zapcore.ErrorLevel,
|
||||
},
|
||||
{
|
||||
name: "exact match 2",
|
||||
input: "namespace1.namespace2",
|
||||
expected: zapcore.DebugLevel,
|
||||
},
|
||||
{
|
||||
name: "exact match 3",
|
||||
input: "namespace3.namespace4",
|
||||
expected: zapcore.WarnLevel,
|
||||
},
|
||||
{
|
||||
name: "exact match 3",
|
||||
input: "namespace1.namespace2.namespace3",
|
||||
expected: zapcore.InfoLevel,
|
||||
},
|
||||
{
|
||||
name: "partial match 1",
|
||||
input: "namespace1.unregistered",
|
||||
expected: zapcore.ErrorLevel,
|
||||
},
|
||||
{
|
||||
name: "partial match 2",
|
||||
input: "namespace1.namespace2.unregistered",
|
||||
expected: zapcore.DebugLevel,
|
||||
},
|
||||
{
|
||||
name: "no match 1",
|
||||
input: "namespace2",
|
||||
expected: zapcore.InvalidLevel,
|
||||
},
|
||||
{
|
||||
name: "no match 2",
|
||||
input: "namespace3",
|
||||
expected: zapcore.InvalidLevel,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
level := tree.LevelFor(tt.input)
|
||||
require.Equal(t, tt.expected, level)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue