Skip to content

Commit f40b3c8

Browse files
author
Andrew Stiegmann (stieg)
committed
Add log de-duplication support to glog
Adds deduplication support to glog so that we can eliminate noisy log sources in code. Will only deduplicate consecutive lines that are noisy.
1 parent 5101168 commit f40b3c8

File tree

2 files changed

+178
-32
lines changed

2 files changed

+178
-32
lines changed

glog.go

Lines changed: 125 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,7 @@ func init() {
407407
flag.Var(&logging.stderrThreshold, "stderrthreshold", "logs at or above this threshold go to stderr")
408408
flag.Var(&logging.vmodule, "vmodule", "comma-separated list of pattern=N settings for file-filtered logging")
409409
flag.Var(&logging.traceLocation, "log_backtrace_at", "when logging hits line file:N, emit a stack trace")
410+
flag.UintVar(&logging.dedupeAfter, "logdedupeafter", 0, "Will de-duplicate the same log statement after N consecutive repititions")
410411

411412
// Default stderrThreshold is ERROR.
412413
logging.stderrThreshold = errorLog
@@ -435,6 +436,10 @@ type loggingT struct {
435436
// Level flag. Handled atomically.
436437
stderrThreshold severity // The -stderrthreshold flag.
437438

439+
// Determines when the log deduplication code will kick in. Disabled
440+
// when value is 0.
441+
dedupeAfter uint
442+
438443
// Used to signal the flushDaemon to flush a particular log
439444
flushCh [numSeverity]chan struct{}
440445

@@ -477,6 +482,13 @@ type loggingT struct {
477482
currStdErrTmpBuffer *bytes.Buffer
478483
// stdErrWriterCh used to notify outputStdErrLoop which buffer to write to stderr
479484
stdErrWriterCh chan *bytes.Buffer
485+
486+
// lastStatement* tracks who our last log message came from and how many we
487+
// have since consecutively received from this caller.
488+
lastStatementMu sync.Mutex
489+
lastStatementCaller caller
490+
lastStatementSeverity severity
491+
lastStatementCount uint
480492
}
481493

482494
// buffer holds a byte Buffer for reuse. The zero value is ready for use.
@@ -537,6 +549,87 @@ func (l *loggingT) putBuffer(b *buffer) {
537549
l.freeListMu.Unlock()
538550
}
539551

552+
// caller is a simple structure that tracks the output of runtime.Caller. It is
553+
// done this way to make function calls easier.
554+
type caller struct {
555+
pc uintptr
556+
file string
557+
line int
558+
}
559+
560+
// getCaller returns runtime information about the caller of this function. This
561+
// data is used while printing log lines along with de-duplicating noisy log
562+
// statements. It is the same logic as before, just liberated into its own
563+
// function with the caller struct used to track the info.
564+
func getCaller(depth int) caller {
565+
pc, file, line, ok := runtime.Caller(3 + depth)
566+
567+
if !ok {
568+
pc = 0
569+
file = "???"
570+
line = 0
571+
} else {
572+
slash := strings.LastIndex(file, "/")
573+
if slash >= 0 {
574+
file = file[slash+1:]
575+
}
576+
// Unsure if needed, but since this was in code previously I am carrying it
577+
// forward here to avoid any surprise regressions.
578+
if line < 0 {
579+
line = 0
580+
}
581+
}
582+
583+
return caller{
584+
pc: pc,
585+
file: file,
586+
line: line,
587+
}
588+
}
589+
590+
// dedupe determines whether or not we need to de-duplicate a log
591+
// message. Method will return true if we do, false otherwise. Method is also
592+
// responsible for noticing a different logging statement is being called and
593+
// adding any relevant de-duplication log information about our previously
594+
// de-dupled log statements. As such IT MUST BE CALLED AS PART OF EVERY LOG
595+
// STATEMENT in order to be effective.
596+
func (l *loggingT) dedupe(s severity, c caller) bool {
597+
// Deduplication of logs is disabled if 0
598+
dedupeAfter := l.dedupeAfter
599+
if dedupeAfter == 0 {
600+
return false
601+
}
602+
603+
// Ensure lock is not held during any potential logging events to keep
604+
// function performant. Thus we must not use a defer unlock here.
605+
l.lastStatementMu.Lock()
606+
607+
if l.lastStatementCaller.pc == c.pc {
608+
l.lastStatementCount++
609+
skip := l.lastStatementCount > dedupeAfter
610+
l.lastStatementMu.Unlock()
611+
return skip
612+
}
613+
614+
prevCount := l.lastStatementCount
615+
prevCaller := l.lastStatementCaller
616+
prevSeverity := l.lastStatementSeverity
617+
l.lastStatementCaller = c
618+
l.lastStatementSeverity = s
619+
l.lastStatementCount = 1
620+
l.lastStatementMu.Unlock()
621+
622+
// Check if we deduplicated the previous message, and include de-dupe info
623+
// before allowing the next log message to proceed.
624+
if prevCount > dedupeAfter {
625+
buf := l.header(prevSeverity, prevCaller)
626+
fmt.Fprintf(buf, "...statement repeated %d time(s)...\n", prevCount-dedupeAfter)
627+
l.output(prevSeverity, buf, prevCaller, false)
628+
}
629+
630+
return false
631+
}
632+
540633
var timeNow = time.Now // Stubbed out for testing.
541634

542635
/*
@@ -556,26 +649,8 @@ where the fields are defined as follows:
556649
line The line number
557650
msg The user-supplied message
558651
*/
559-
func (l *loggingT) header(s severity, depth int) (*buffer, string, int) {
560-
_, file, line, ok := runtime.Caller(3 + depth)
561-
if !ok {
562-
file = "???"
563-
line = 1
564-
} else {
565-
slash := strings.LastIndex(file, "/")
566-
if slash >= 0 {
567-
file = file[slash+1:]
568-
}
569-
}
570-
return l.formatHeader(s, file, line), file, line
571-
}
572-
573-
// formatHeader formats a log header using the provided file name and line number.
574-
func (l *loggingT) formatHeader(s severity, file string, line int) *buffer {
652+
func (l *loggingT) header(s severity, c caller) *buffer {
575653
now := timeNow()
576-
if line < 0 {
577-
line = 0 // not a real line number, but acceptable to someDigits
578-
}
579654
if s > fatalLog {
580655
s = infoLog // for safety.
581656
}
@@ -601,9 +676,9 @@ func (l *loggingT) formatHeader(s severity, file string, line int) *buffer {
601676
buf.nDigits(7, 22, pid, ' ') // TODO: should be TID
602677
buf.tmp[29] = ' '
603678
buf.Write(buf.tmp[:30])
604-
buf.WriteString(file)
679+
buf.WriteString(c.file)
605680
buf.tmp[0] = ':'
606-
n := buf.someDigits(1, line)
681+
n := buf.someDigits(1, c.line)
607682
buf.tmp[n+1] = ']'
608683
buf.tmp[n+2] = ' '
609684
buf.Write(buf.tmp[:n+3])
@@ -652,43 +727,62 @@ func (buf *buffer) someDigits(i, d int) int {
652727
}
653728

654729
func (l *loggingT) println(s severity, args ...interface{}) {
655-
buf, file, line := l.header(s, 0)
730+
c := getCaller(0)
731+
skip := l.dedupe(s, c)
732+
if skip {
733+
return
734+
}
735+
buf := l.header(s, c)
656736
fmt.Fprintln(buf, args...)
657-
l.output(s, buf, file, line, false)
737+
l.output(s, buf, c, false)
658738
}
659739

660740
func (l *loggingT) print(s severity, args ...interface{}) {
661741
l.printDepth(s, 1, args...)
662742
}
663743

664744
func (l *loggingT) printDepth(s severity, depth int, args ...interface{}) {
665-
buf, file, line := l.header(s, depth)
745+
c := getCaller(depth)
746+
skip := l.dedupe(s, c)
747+
if skip {
748+
return
749+
}
750+
buf := l.header(s, c)
666751
fmt.Fprint(buf, args...)
667752
if buf.Bytes()[buf.Len()-1] != '\n' {
668753
buf.WriteByte('\n')
669754
}
670-
l.output(s, buf, file, line, false)
755+
l.output(s, buf, c, false)
671756
}
672757

673758
func (l *loggingT) printf(s severity, format string, args ...interface{}) {
674-
buf, file, line := l.header(s, 0)
759+
c := getCaller(0)
760+
skip := l.dedupe(s, c)
761+
if skip {
762+
return
763+
}
764+
buf := l.header(s, c)
675765
fmt.Fprintf(buf, format, args...)
676766
if buf.Bytes()[buf.Len()-1] != '\n' {
677767
buf.WriteByte('\n')
678768
}
679-
l.output(s, buf, file, line, false)
769+
l.output(s, buf, c, false)
680770
}
681771

682772
// printWithFileLine behaves like print but uses the provided file and line number. If
683773
// alsoLogToStderr is true, the log message always appears on standard error; it
684774
// will also appear in the log file unless --logtostderr is set.
685775
func (l *loggingT) printWithFileLine(s severity, file string, line int, alsoToStderr bool, args ...interface{}) {
686-
buf := l.formatHeader(s, file, line)
776+
c := caller{
777+
file: file,
778+
line: line,
779+
}
780+
buf := l.header(s, c)
687781
fmt.Fprint(buf, args...)
688782
if buf.Bytes()[buf.Len()-1] != '\n' {
689783
buf.WriteByte('\n')
690784
}
691-
l.output(s, buf, file, line, alsoToStderr)
785+
l.output(s, buf, c, alsoToStderr)
692786
}
693787

694788
// initializeStdErrFixtures sets up the stdErrWriterCh and currStdErrTmpBuffer
@@ -724,10 +818,10 @@ func (l *loggingT) outputStdErrLoop() {
724818
}
725819

726820
// output writes the data to the log files and releases the buffer.
727-
func (l *loggingT) output(s severity, buf *buffer, file string, line int, alsoToStderr bool) {
821+
func (l *loggingT) output(s severity, buf *buffer, c caller, alsoToStderr bool) {
728822
l.mu.Lock()
729823
if l.traceLocation.isSet() {
730-
if l.traceLocation.match(file, line) {
824+
if l.traceLocation.match(c.file, c.line) {
731825
buf.Write(stacks(false))
732826
}
733827
}

glog_test.go

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,8 +409,60 @@ func TestLogBacktraceAt(t *testing.T) {
409409
}
410410

411411
func BenchmarkHeader(b *testing.B) {
412+
c := caller{}
412413
for i := 0; i < b.N; i++ {
413-
buf, _, _ := logging.header(infoLog, 0)
414+
buf := logging.header(infoLog, c)
414415
logging.putBuffer(buf)
415416
}
416417
}
418+
419+
// Test that De-duplication of log lines works as intended
420+
func TestDedupe(t *testing.T) {
421+
dupeLogStatements := 4
422+
423+
for i := 0; i < 6; i++ {
424+
var tName string
425+
var expLines int
426+
var expStatement string
427+
428+
switch {
429+
case i <= 0:
430+
tName = "Dedupe disabled (0)"
431+
expLines = dupeLogStatements + 1
432+
case i < dupeLogStatements:
433+
tName = fmt.Sprintf("Dedupe after %d", i)
434+
expLines = i + 2
435+
expStatement = fmt.Sprintf("statement repeated %d time(s)", dupeLogStatements-i)
436+
case i >= dupeLogStatements:
437+
tName = fmt.Sprintf("Dedupe after %d (none expected)", i)
438+
expLines = dupeLogStatements + 1
439+
}
440+
441+
t.Run(tName, func(t *testing.T) {
442+
setFlags()
443+
defer logging.swap(logging.newBuffers())
444+
445+
logging.dedupeAfter = uint(i)
446+
447+
// Info log must be exact same spot in PC for test to work.
448+
for j := 0; j < dupeLogStatements; j++ {
449+
Info("test")
450+
}
451+
Info("done")
452+
453+
// Expect dedupeAfter lines + 2. One for the dedupe line, one for the second
454+
// Info line.
455+
cont := contents(infoLog)
456+
t.Log(cont)
457+
msgs := strings.Split(strings.TrimSuffix(cont, "\n"), "\n")
458+
if len(msgs) != expLines {
459+
t.Fatalf("Got %d lines, expected %d", len(msgs), expLines)
460+
}
461+
462+
// If we expected deduplication to happen, ensure our statement is there.
463+
if expStatement != "" && !contains(infoLog, expStatement, t) {
464+
t.Errorf("Expected statement %q not found", expStatement)
465+
}
466+
})
467+
}
468+
}

0 commit comments

Comments
 (0)