Skip to content

Commit

Permalink
[easy/ezdbg] support filter rule (#69)
Browse files Browse the repository at this point in the history
Change-Id: I670060c21056f1b1c59eb24378da49f4eaed541f
  • Loading branch information
jxskiss authored Jan 13, 2024
1 parent 671af77 commit c82bf01
Show file tree
Hide file tree
Showing 9 changed files with 395 additions and 34 deletions.
75 changes: 70 additions & 5 deletions easy/ezdbg/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,72 @@ import (
"context"
"fmt"
"log"

"github.com/jxskiss/gopkg/v2/zlog"
"os"
)

// Config configures the behavior of functions in this package.
func Config(cfg Cfg) {
envRule := os.Getenv(FilterRuleEnvName)
if envRule != "" {
stdLogger{}.Infof("ezdbg: using filter rule from env: %q", envRule)
cfg.FilterRule = envRule
}
if cfg.FilterRule != "" {
cfg.filter = newLogFilter(cfg.FilterRule)
}
_logcfg = cfg
}

var _logcfg Cfg

// Cfg provides optional config to configure this package.
type Cfg struct {

// EnableDebug determines whether debug log is enabled, it may use
// the given context.Context to enable or disable request-level debug log.
// If EnableDebug returns false, the log message is discarded.
//
// User must configure this to enable debug log from this package.
// By default, functions in this package discard all messages.
EnableDebug func(context.Context) bool
LoggerFunc func(context.Context) DebugLogger

// LoggerFunc optionally retrieves DebugLogger from a context.Context.
LoggerFunc func(context.Context) DebugLogger

// FilterRule optionally configures filter rule to allow or deny log messages
// in some packages or files.
//
// It uses glob to match filename, the syntax is "allow=glob1,glob2;deny=glob3,glob4".
// For example:
//
// - "", empty rule means allow all messages
// - "allow=all", allow all messages
// - "deny=all", deny all messages
// - "allow=pkg1/*,pkg2/*.go",
// allow messages from files in `pkg1` and `pkg2`,
// deny messages from all other packages
// - "allow=pkg1/sub1/abc.go,pkg1/sub2/def.go",
// allow messages from file `pkg1/sub1/abc.go` and `pkg1/sub2/def.go`,
// deny messages from all other files
// - "allow=pkg1/**",
// allow messages from files and sub-packages in `pkg1`,
// deny messages from all other packages
// - "deny=pkg1/**.go,pkg2/**.go",
// deny messages from files and sub-packages in `pkg1` and `pkg2`,
// allow messages from all other packages
// - "allow=all;deny=pkg/**", same as "deny=pkg/**"
//
// If both "allow" and "deny" directives are configured, the "allow" directive
// takes effect, the "deny" directive is ignored.
//
// The default value is empty, which means all messages are allowed.
//
// User can also set the environment variable "EZDBG_FILTER_RULE"
// to configure it in runtime, when the environment variable is available,
// this value is ignored.
FilterRule string

filter *logFilter
}

func (p Cfg) getLogger(ctxp *context.Context) DebugLogger {
Expand Down Expand Up @@ -48,8 +100,21 @@ func (f PrintFunc) Debugf(format string, args ...any) { f(format, args...) }

type stdLogger struct{}

const _stdLogDepth = 2
const (
stdLogDepth = 2
stdDebugPrefix = "[DEBUG] "
stdInfoPrefix = "[INFO] "
stdWarnPrefix = "[WARN] "
)

func (stdLogger) Debugf(format string, args ...any) {
log.Default().Output(_stdLogDepth, fmt.Sprintf(zlog.DebugPrefix+format, args...))
log.Default().Output(stdLogDepth, fmt.Sprintf(stdDebugPrefix+format, args...))
}

func (stdLogger) Infof(format string, args ...any) {
log.Default().Output(stdLogDepth, fmt.Sprintf(stdInfoPrefix+format, args...))
}

func (stdLogger) Warnf(format string, args ...any) {
log.Default().Output(stdLogDepth, fmt.Sprintf(stdWarnPrefix+format, args...))
}
39 changes: 32 additions & 7 deletions easy/ezdbg/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package ezdbg
import (
"context"
"reflect"
"runtime"
"strings"
"unicode/utf8"

Expand All @@ -15,7 +16,7 @@ type stringerFunc func(v any) string

/*
DEBUG is debug message logger which do nothing if debug level is not enabled (the default).
It gives best performance for production deployment by eliminating unnecessary
It has good performance for production deployment by eliminating unnecessary
parameter evaluation and control flows.
DEBUG accepts very flexible arguments to help development, see the following examples:
Expand Down Expand Up @@ -125,6 +126,12 @@ func logdebug(skip int, stringer stringerFunc, args ...any) {
return
}

// Check filter rules.
caller, fullFileName, simpleFileName, line := getCaller(skip + 1)
if _logcfg.filter != nil && !_logcfg.filter.Allow(fullFileName) {
return
}

var logger DebugLogger
if len(args) > 0 {
if arg0, ok := args[0].(func()); ok {
Expand All @@ -136,11 +143,6 @@ func logdebug(skip int, stringer stringerFunc, args ...any) {
if logger == nil {
logger = _logcfg.getLogger(nil)
}
outputDebugLog(skip+1, logger, stringer, args)
}

func outputDebugLog(skip int, logger DebugLogger, stringer stringerFunc, args []any) {
caller, file, line := easy.Caller(skip + 1)
callerPrefix := "[" + caller + "] "
if len(args) > 0 {
if format, ok := args[0].(string); ok && strings.IndexByte(format, '%') >= 0 {
Expand All @@ -150,7 +152,7 @@ func outputDebugLog(skip int, logger DebugLogger, stringer stringerFunc, args []
format := callerPrefix + "%v" + strings.Repeat(" %v", len(args)-1)
logger.Debugf(format, formatArgs(stringer, args)...)
} else {
logger.Debugf("======== %s#L%d - %s ========", file, line, caller)
logger.Debugf("======== %s#L%d - %s ========", simpleFileName, line, caller)
}
}

Expand Down Expand Up @@ -218,3 +220,26 @@ func isBasicType(typ reflect.Type) bool {
}
return false
}

func getCaller(skip int) (funcName, fullFileName, simpleFileName string, line int) {
pc, fullFileName, line, _ := runtime.Caller(skip + 1)
funcName = runtime.FuncForPC(pc).Name()
for i := len(funcName) - 1; i >= 0; i-- {
if funcName[i] == '/' {
funcName = funcName[i+1:]
break
}
}
simpleFileName = fullFileName
pathSepCnt := 0
for i := len(simpleFileName) - 1; i >= 0; i-- {
if simpleFileName[i] == '/' {
pathSepCnt++
if pathSepCnt == 2 {
simpleFileName = simpleFileName[i+1:]
break
}
}
}
return
}
42 changes: 42 additions & 0 deletions easy/ezdbg/debug_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,39 @@ func TestConfigLog(t *testing.T) {
return ctx.Value("TEST_LOGGER").(*bufLogger)
}
configTestLog(true, getCtxLogger)
assert.NotNil(t, _logcfg.LoggerFunc)
}

func TestFilterRule(t *testing.T) {
msg := "test FilterRule"

testCases := []struct {
name string
rule string
contains bool
}{
{name: "default", rule: "", contains: true},
{name: "allow all", rule: "allow=all", contains: true},
{name: "allow explicitly 1", rule: "allow=ezdbg/*.go", contains: true},
{name: "allow explicitly 2", rule: "allow=easy/**", contains: true},
{name: "not allowed explicitly", rule: "allow=confr/*,easy/*.go,easy/ezhttp/**", contains: false},
{name: "deny all", rule: "deny=all", contains: false},
{name: "deny explicitly", rule: "deny=ezdbg/*", contains: false},
{name: "not denied explicitly", rule: "deny=confr/*,easy/ezhttp/**", contains: true},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
configTestLogWithFilterRule(true, nil, tc.rule)
got := copyStdLog(func() {
DEBUG(msg)
})
if tc.contains {
assert.Contains(t, string(got), msg)
} else {
assert.NotContains(t, string(got), msg)
}
})
}
}

// -------- utilities -------- //
Expand Down Expand Up @@ -224,10 +257,19 @@ func DEBUGWrapSkip2(args ...any) {
func configTestLog(
enableDebug bool,
ctxLogger func(context.Context) DebugLogger,
) {
configTestLogWithFilterRule(enableDebug, ctxLogger, "")
}

func configTestLogWithFilterRule(
enableDebug bool,
ctxLogger func(context.Context) DebugLogger,
filterRule string,
) {
Config(Cfg{
EnableDebug: func(_ context.Context) bool { return enableDebug },
LoggerFunc: ctxLogger,
FilterRule: filterRule,
})
}

Expand Down
110 changes: 110 additions & 0 deletions easy/ezdbg/filter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package ezdbg

import (
"strings"

"github.com/gobwas/glob"
)

const FilterRuleEnvName = "EZDBG_FILTER_RULE"

type logFilter struct {
rule string
allowRule string
denyRule string

allowAll bool
allowGlobs []glob.Glob

denyAll bool
denyGlobs []glob.Glob
}

func newLogFilter(rule string) *logFilter {
lf := &logFilter{rule: rule}
if rule == "" {
lf.allowAll = true
return lf
}
directives := strings.Split(rule, ";")
for _, r := range directives {
if r == "" {
continue
}
if strings.HasPrefix(r, "allow=") {
lf.parseAllowRule(r)
}
if strings.HasPrefix(r, "deny=") {
lf.parseDenyRule(r)
}
}
if len(lf.allowGlobs) == 0 {
lf.allowAll = true
}
return lf
}

func (f *logFilter) parseAllowRule(rule string) {
f.allowRule = rule
// Remove the prefix "allow=".
globStrs := strings.Split(rule[6:], ",")
for _, s := range globStrs {
if s == "" {
continue
}
if s == "all" {
f.allowGlobs = nil
break
}
g, err := glob.Compile("**"+s, '/')
if err != nil {
stdLogger{}.Warnf("ezdbg: failed to compile filter pattern %q: %v", s, err)
continue
}
f.allowGlobs = append(f.allowGlobs, g)
}
}

func (f *logFilter) parseDenyRule(rule string) {
f.denyRule = rule
// Remove the prefix "deny=".
globStrs := strings.Split(rule[5:], ",")
for _, s := range globStrs {
if s == "" {
continue
}
if s == "all" {
f.denyAll = true
f.denyGlobs = nil
break
}
g, err := glob.Compile("**"+s, '/')
if err != nil {
stdLogger{}.Warnf("ezdbg: failed to compile filter pattern %q: %v", s, err)
continue
}
f.denyGlobs = append(f.denyGlobs, g)
}
}

func (f *logFilter) Allow(fileName string) bool {
// not allow-all, check allow rules
if !f.allowAll {
for _, g := range f.allowGlobs {
if g.Match(fileName) {
return true
}
}
return false
}

// allow-all, check deny rules
if !f.denyAll {
for _, g := range f.denyGlobs {
if g.Match(fileName) {
return false
}
}
}
return !f.denyAll
}
Loading

0 comments on commit c82bf01

Please sign in to comment.