I've got a rather large Golang program with rather many dependencies. Somewhere, someone writes rogue data to stderr.
I want to find where this write is occurring so I can remove it.
I know I can intercept stderr using Dup2
and Pipes, so I can detect the write as it occurs; but this does not let me get a stack from the goroutine doing the write.
In other languages, I'd set os.Stderr
to a custom File with a hooked Write
function. Is there a way to do this in Go that I'm not aware of? If not, what would be a good way to find where this method call is happening?
You can intercept logging by providing io.Writer
to log.SetOutput. There you can simply check if string that is being loged is what you need to trace and write stacktrace to the delegated wrapped writer.
Here is an example playground: https://play.golang.org/p/2bClt2JBuFs
Please note that getting current stacktrace is not free, it causes stop-of-the-world and can drastically slow your application down. In case you have an application that produces a high volume of logs I would recommend limit amount of data being traced (e.g. trace only first match or N times per second/minute)
package main
import (
"fmt"
"log"
"os"
"regexp"
"runtime/debug"
)
func main() {
log.SetOutput(NewLogInterceptor(LogInterceptionCheck{Pattern: ".*Some.*", Description: "with 'Some' substring"}))
f1()
log.Println("message that is not traced")
}
func f1() {
log.Println("Some message")
}
type LogInterceptor struct {
target *os.File
checks []LogInterceptionCheck
}
type LogInterceptionCheck struct {
regexp *regexp.Regexp
Pattern string
Description string
}
func NewLogInterceptor(checks ...LogInterceptionCheck) *LogInterceptor {
for i := 0; i < len(checks); i++ {
each := checks[i]
compiled, e := regexp.Compile(each.Pattern)
if e != nil {
log.Fatalf("cannot compile regexpt [%s]: %s", each, e)
}
checks[i].regexp = compiled
}
return &LogInterceptor{os.Stderr, checks}
}
func (interceptor *LogInterceptor) Write(p []byte) (n int, err error) {
i, err := interceptor.target.Write(p)
// use loop because it is faster and generates less garbage compared to for-range loop
for i := 0; i < len(interceptor.checks); i++ {
check := interceptor.checks[i]
if check.regexp.Match(p) {
_, e := fmt.Fprintf(interceptor.target, ">>>> Printing stacktrace [%s]
", check.Description)
if e != nil {
log.Fatalf("cannot write: %s", e)
}
_, e = interceptor.target.Write(debug.Stack())
if e != nil {
log.Fatalf("cannot write stacktrace: %s", e)
}
break
}
}
return i, err
}