Skip to content

Commit

Permalink
logger_cb: remove default callbacks (#294)
Browse files Browse the repository at this point in the history
The logic related to filtering by callbacks must be a decision of the
library consumers, as it is not possible to know which is the best
strategy for each case.

This also adds to the related selftest other example that shows how to
filter using callbacks.

Be warned that if filters are not defined via callbacks, all output is
passed to logFallback() which outputs to stderr.
  • Loading branch information
geyslan authored Feb 27, 2023
1 parent 8f83f25 commit fde1f6e
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 116 deletions.
93 changes: 10 additions & 83 deletions logger_cb.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import "C"
import (
"fmt"
"os"
"regexp"
"strings"
)

Expand Down Expand Up @@ -47,102 +46,30 @@ const (

// Callbacks stores the callbacks to be used by libbpfgo
type Callbacks struct {
Log func(level int, msg string, keyValues ...interface{})
Log func(level int, msg string)
LogFilters []func(libLevel int, msg string) bool
}

// callbacks is initialized with default callbacks, but can be changed by SetLoggerCbs
var callbacks = Callbacks{
Log: logFallback,
LogFilters: []func(libLevel int, msg string) bool{
LogFilterLevel,
LogFilterOutput,
},
Log: logFallback,
LogFilters: []func(libLevel int, msg string) bool{},
}

// SetLoggerCbs receives Callbacks type to be used to log libbpf outputs and to filter out those outputs
func SetLoggerCbs(cbs Callbacks) {
if cbs.Log == nil {
if cbs.Log == nil { // guarantee that there is always an outputter
cbs.Log = logFallback
}

callbacks = cbs
}

// logFallback:
// - level is ignored in this stage
// - type coercion only takes care of string types
// - keyValues is not required to contain pairs
// - outputs all to stderr
func logFallback(level int, msg string, keyValues ...interface{}) {
var (
args = make([]string, 0)
outMsg = msg
)

for _, v := range keyValues {
if s, ok := v.(string); ok {
outMsg += " [%s]"
args = append(args, s)
}
}

outMsg += "\n"
if len(keyValues) > 0 {
fmt.Fprintf(os.Stderr, outMsg, args)
} else {
fmt.Fprint(os.Stderr, outMsg)
}
}

// LogFilterLevel filters by checking its print level
// In case the consumer defines its own filters functions via SetLoggerCbs, this can also be passed
func LogFilterLevel(libbpfPrintLevel int, output string) bool {
return libbpfPrintLevel != LibbpfWarnLevel
}

var (
// triggered by: libbpf/src/nlattr.c->libbpf_nla_dump_errormsg()
// "libbpf: Kernel error message: %s\n"
// 1. %s = "Exclusivity flag on"
regexKernelExclusivityFlagOn = regexp.MustCompile(`libbpf:.*Kernel error message:.*Exclusivity flag on`)

// triggered by: libbpf/src/libbpf.c->bpf_program__attach_kprobe_opts()
// "libbpf: prog '%s': failed to create %s '%s+0x%zx' perf event: %s\n"
// 1. %s = trace_check_map_func_compatibility
// 2. %s = kretprobe or kprobe
// 3. %s = check_map_func_compatibility (function name)
// 4. %x = offset (ignored in this check)
// 5. %s = No such file or directory
regexKprobePerfEvent = regexp.MustCompile(`libbpf:.*prog 'trace_check_map_func_compatibility'.*failed to create kprobe.*perf event: No such file or directory`)

// triggered by: libbpf/src/libbpf.c->bpf_program__attach_fd()
// "libbpf: prog '%s': failed to attach to %s: %s\n"
// 1. %s = cgroup_skb_ingress or cgroup_skb_egress
// 2. %s = cgroup
// 3. %s = Invalid argument
regexAttachCgroup = regexp.MustCompile(`libbpf:.*prog 'cgroup_skb_ingress|cgroup_skb_egress'.*failed to attach to cgroup.*Invalid argument`)
)

// LogFilterOutput filters out some errors by using regex
// In case the consumer defines its own filters functions via SetLoggerCbs, this can also be passed
func LogFilterOutput(libbpfPrintLevel int, output string) bool {
// BUG: https:/github.com/aquasecurity/tracee/issues/1676
if regexKernelExclusivityFlagOn.MatchString(output) {
return true
}

// BUG: https://github.com/aquasecurity/tracee/issues/2446
if regexKprobePerfEvent.MatchString(output) {
return true
}

// AttachCgroupLegacy() will first try AttachCgroup() and it might fail. This
// is not an error and is the best way of probing for eBPF cgroup attachment
// link existence.
if regexAttachCgroup.MatchString(output) {
return true
}
// logFallback is the default logger callback
// - level is ignored
// - output, suffixed with a newline, is printed to stderr
func logFallback(level int, msg string) {
var outMsg = msg + "\n"

return false
fmt.Fprint(os.Stderr, outMsg)
}
67 changes: 38 additions & 29 deletions logger_cb_test.go
Original file line number Diff line number Diff line change
@@ -1,43 +1,52 @@
package libbpfgo

import "testing"
import (
"bytes"
"io"
"os"
"testing"

func TestLogFilterOutput(t *testing.T) {
tests := []struct {
libbpfPrintLevel int
output string
expectedResult bool
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestLogFallback(t *testing.T) {
tt := []struct {
message string
}{
{
output: "libbpf: prog 'trace_check_map_func_compatibility': failed to create kprobe 'check_map_func_compatibility+0x0' perf event: No such file or directory\n",
expectedResult: true,
},
{
output: "libbpf: Kernel error message: Exclusivity flag on\n",
expectedResult: true,
},
{
output: "libbpf: prog 'cgroup_skb_ingress': failed to attach to cgroup 'cgroup': Invalid argument\n",
expectedResult: true,
},
{
output: "libbpf: prog 'cgroup_skb_egress': failed to attach to cgroup 'cgroup': Invalid argument\n",
expectedResult: true,
message: "This is a warning message",
},
{
output: "This is not a log message that should be filtered\n",
expectedResult: false,
message: "This is a information message",
},
{
output: "libbpf: This is not a log message that should be filtered\n",
expectedResult: false,
message: "This is a debug message",
},
}

for _, test := range tests {
result := LogFilterOutput(test.libbpfPrintLevel, test.output)
if result != test.expectedResult {
t.Errorf("For input '%s', expected %v but got %v", test.output, test.expectedResult, result)
}
for _, tc := range tt {
var buf bytes.Buffer

r, w, err := os.Pipe()
require.NoError(t, err, "failed to create pipe")

writeEnd := os.NewFile(uintptr(w.Fd()), "pipe")

oldStderr := os.Stderr
os.Stderr = writeEnd

// level is ignored
logFallback(LibbpfInfoLevel, tc.message)

os.Stderr = oldStderr

err = writeEnd.Close()
require.NoError(t, err, "failed to close writeEnd")
_, err = io.Copy(&buf, r)
require.NoError(t, err, "failed to copy from read end to buffer")

// The message should be printed to stderr with a newline
assert.Equal(t, tc.message+"\n", buf.String())
}
}
39 changes: 35 additions & 4 deletions selftest/log-callbacks/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,20 @@ import (

var logOutput []string

func log(level int, msg string, keyValues ...interface{}) {
// log is a handler to save the log output
func log(level int, msg string) {
logOutput = append(logOutput, msg)
}

func main() {
//
// Filter example 1: filter out all outputs but containing "found program 'kprobe__sys_mmap'"
//
filterMatch := "found program 'kprobe__sys_mmap'"
bpf.SetLoggerCbs(bpf.Callbacks{
Log: log, // use log() as a handler for libbpf outputs that are not excluded by LogFilters
Log: log,
LogFilters: []func(libLevel int, msg string) bool{
func(libLevel int, msg string) bool {
// filter all output but containing "found program 'kprobe__sys_mmap'"
return !strings.Contains(msg, filterMatch)
},
},
Expand All @@ -34,11 +37,39 @@ func main() {
fmt.Fprintln(os.Stderr, err)
os.Exit(-1)
}
defer bpfModule.Close()
bpfModule.Close()

if len(logOutput) != 1 {
fmt.Fprintln(os.Stderr, fmt.Sprintf("Log output should contain only one output matching the string: %s", filterMatch))
fmt.Fprintln(os.Stderr, fmt.Sprintf("Log output: %v", logOutput))
os.Exit(-1)
}

// clean logOutput
logOutput = []string{}

//
// Filter example 2: filter out all outputs which level is LibbpfDebugLevel
//
bpf.SetLoggerCbs(bpf.Callbacks{
Log: log,
LogFilters: []func(libLevel int, msg string) bool{
func(libLevel int, msg string) bool {
return libLevel == bpf.LibbpfDebugLevel
},
},
})

bpfModule, err = bpf.NewModuleFromFile("main.bpf.o")
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(-1)
}
bpfModule.Close()

if len(logOutput) != 0 {
fmt.Fprintln(os.Stderr, "Log output should be empty")
fmt.Fprintln(os.Stderr, fmt.Sprintf("Log output: %v", logOutput))
os.Exit(-1)
}
}

0 comments on commit fde1f6e

Please sign in to comment.