Skip to content

Commit a4d2b46

Browse files
authored
Merge pull request #262 from bobheadxi/master
Add pluggable Logger for pipeline
2 parents 4a50675 + 6d09c8d commit a4d2b46

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+554
-102
lines changed

Gopkg.lock

Lines changed: 6 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/hercules/plugin.template

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ type {{.name}} struct {
3434
hercules.NoopMerger
3535
// Process each merge commit only once
3636
hercules.OneShotMergeProcessor
37+
// Logger for consistent output
38+
l hercules.Logger
3739
}
3840

3941
// {{.name}}Result is returned by Finalize() and represents the analysis result.
@@ -81,11 +83,15 @@ func ({{.varname}} *{{.name}}) Description() string {
8183

8284
// Configure applies the parameters specified in the command line. Map keys correspond to "Name".
8385
func ({{.varname}} *{{.name}}) Configure(facts map[string]interface{}) error {
86+
if l, exists := facts[hercules.ConfigLogger].(hercules.Logger); exists {
87+
{{.varname}}.l = l
88+
}
8489
return nil
8590
}
8691

8792
// Initialize resets the internal temporary data structures and prepares the object for Consume().
8893
func ({{.varname}} *{{.name}}) Initialize(repository *git.Repository) error {
94+
{{.varname}}.l = hercules.NewLogger()
8995
{{.varname}}.OneShotMergeProcessor.Initialize()
9096
return nil
9197
}

contrib/_plugin_example/churn_analysis.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ type ChurnAnalysis struct {
3030

3131
// references IdentityDetector.ReversedPeopleDict
3232
reversedPeopleDict []string
33+
34+
l hercules.Logger
3335
}
3436

3537
type editInfo struct {
@@ -105,6 +107,9 @@ func (churn *ChurnAnalysis) Description() string {
105107

106108
// Configure applies the parameters specified in the command line. Map keys correspond to "Name".
107109
func (churn *ChurnAnalysis) Configure(facts map[string]interface{}) error {
110+
if l, exists := facts[hercules.ConfigLogger].(hercules.Logger); exists {
111+
churn.l = l
112+
}
108113
if val, exists := facts[ConfigChurnTrackPeople].(bool); exists {
109114
churn.TrackPeople = val
110115
}
@@ -116,6 +121,7 @@ func (churn *ChurnAnalysis) Configure(facts map[string]interface{}) error {
116121

117122
// Initialize resets the internal temporary data structures and prepares the object for Consume().
118123
func (churn *ChurnAnalysis) Initialize(repository *git.Repository) error {
124+
churn.l = hercules.NewLogger()
119125
churn.global = []editInfo{}
120126
churn.people = map[int][]editInfo{}
121127
churn.OneShotMergeProcessor.Initialize()

core.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ const (
8181
ConfigPipelineCommits = core.ConfigPipelineCommits
8282
// ConfigTickSize is the number of hours per 'tick'
8383
ConfigTickSize = plumbing.ConfigTicksSinceStartTickSize
84+
// ConfigLogger is used to set the logger in all pipeline items.
85+
ConfigLogger = core.ConfigLogger
8486
)
8587

8688
// NewPipeline initializes a new instance of Pipeline struct.
@@ -174,3 +176,9 @@ func PathifyFlagValue(flag *pflag.Flag) {
174176
func EnablePathFlagTypeMasquerade() {
175177
core.EnablePathFlagTypeMasquerade()
176178
}
179+
180+
// Logger is the Hercules logging interface
181+
type Logger core.Logger
182+
183+
// NewLogger returns an instance of the default Hercules logger
184+
func NewLogger() core.Logger { return core.NewLogger() }

doc.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ Finally extract the result:
3737
3838
The actual usage example is cmd/hercules/root.go - the command line tool's code.
3939
40+
You can provide additional options via `facts` on initialization. For example,
41+
to provide your own logger, enable people-tracking, and set a custom tick size:
42+
43+
pipe.Initialize(map[string]interface{}{
44+
hercules.ConfigLogger: zap.NewExample().Sugar(),
45+
hercules.ConfigTickSize: 12,
46+
leaves.ConfigBurndownTrackPeople: true,
47+
})
48+
4049
Hercules depends heavily on https://github.com/src-d/go-git and leverages the
4150
diff algorithm through https://github.com/sergi/go-diff.
4251

internal/core/logger.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package core
2+
3+
import (
4+
"log"
5+
"os"
6+
"runtime/debug"
7+
"strings"
8+
)
9+
10+
// ConfigLogger is the key for the pipeline's logger
11+
const ConfigLogger = "Core.Logger"
12+
13+
// Logger defines the output interface used by Hercules components.
14+
type Logger interface {
15+
Info(...interface{})
16+
Infof(string, ...interface{})
17+
Warn(...interface{})
18+
Warnf(string, ...interface{})
19+
Error(...interface{})
20+
Errorf(string, ...interface{})
21+
Critical(...interface{})
22+
Criticalf(string, ...interface{})
23+
}
24+
25+
// DefaultLogger is the default logger used by a pipeline, and wraps the standard
26+
// log library.
27+
type DefaultLogger struct {
28+
I *log.Logger
29+
W *log.Logger
30+
E *log.Logger
31+
}
32+
33+
// NewLogger returns a configured default logger.
34+
func NewLogger() *DefaultLogger {
35+
return &DefaultLogger{
36+
I: log.New(os.Stderr, "[INFO] ", log.LstdFlags),
37+
W: log.New(os.Stderr, "[WARN] ", log.LstdFlags),
38+
E: log.New(os.Stderr, "[ERROR] ", log.LstdFlags),
39+
}
40+
}
41+
42+
// Info writes to "info" logger.
43+
func (d *DefaultLogger) Info(v ...interface{}) { d.I.Println(v...) }
44+
45+
// Infof writes to "info" logger with printf-style formatting.
46+
func (d *DefaultLogger) Infof(f string, v ...interface{}) { d.I.Printf(f, v...) }
47+
48+
// Warn writes to the "warning" logger.
49+
func (d *DefaultLogger) Warn(v ...interface{}) { d.W.Println(v...) }
50+
51+
// Warnf writes to the "warning" logger with printf-style formatting.
52+
func (d *DefaultLogger) Warnf(f string, v ...interface{}) { d.W.Printf(f, v...) }
53+
54+
// Error writes to the "error" logger.
55+
func (d *DefaultLogger) Error(v ...interface{}) { d.E.Println(v...) }
56+
57+
// Errorf writes to the "error" logger with printf-style formatting.
58+
func (d *DefaultLogger) Errorf(f string, v ...interface{}) { d.E.Printf(f, v...) }
59+
60+
// Critical writes to the "error" logger and logs the current stacktrace.
61+
func (d *DefaultLogger) Critical(v ...interface{}) {
62+
d.E.Println(v...)
63+
d.logStacktraceToErr()
64+
}
65+
66+
// Criticalf writes to the "error" logger with printf-style formatting and logs the
67+
// current stacktrace.
68+
func (d *DefaultLogger) Criticalf(f string, v ...interface{}) {
69+
d.E.Printf(f, v...)
70+
d.logStacktraceToErr()
71+
}
72+
73+
// logStacktraceToErr prints a stacktrace to the logger's error output.
74+
// It skips 4 levels that aren't meaningful to a logged stacktrace:
75+
// * debug.Stack()
76+
// * core.captureStacktrace()
77+
// * DefaultLogger::logStacktraceToErr()
78+
// * DefaultLogger::Error() or DefaultLogger::Errorf()
79+
func (d *DefaultLogger) logStacktraceToErr() {
80+
d.E.Println("stacktrace:\n" + strings.Join(captureStacktrace(4), "\n"))
81+
}
82+
83+
func captureStacktrace(skip int) []string {
84+
stack := string(debug.Stack())
85+
lines := strings.Split(stack, "\n")
86+
linesToSkip := 2*skip + 1
87+
if linesToSkip > len(lines) {
88+
return lines
89+
}
90+
return lines[linesToSkip:]
91+
}

internal/core/logger_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package core
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestLogger(t *testing.T) {
11+
var (
12+
f = "%s-%s"
13+
v = []interface{}{"hello", "world"}
14+
l = NewLogger()
15+
16+
iBuf bytes.Buffer
17+
wBuf bytes.Buffer
18+
eBuf bytes.Buffer
19+
)
20+
21+
// capture output
22+
l.I.SetOutput(&iBuf)
23+
l.W.SetOutput(&wBuf)
24+
l.E.SetOutput(&eBuf)
25+
26+
l.Info(v...)
27+
assert.Contains(t, iBuf.String(), "[INFO]")
28+
iBuf.Reset()
29+
30+
l.Infof(f, v...)
31+
assert.Contains(t, iBuf.String(), "[INFO]")
32+
assert.Contains(t, iBuf.String(), "-")
33+
iBuf.Reset()
34+
35+
l.Warn(v...)
36+
assert.Contains(t, wBuf.String(), "[WARN]")
37+
wBuf.Reset()
38+
39+
l.Warnf(f, v...)
40+
assert.Contains(t, wBuf.String(), "[WARN]")
41+
assert.Contains(t, wBuf.String(), "-")
42+
wBuf.Reset()
43+
44+
l.Error(v...)
45+
assert.Contains(t, eBuf.String(), "[ERROR]")
46+
eBuf.Reset()
47+
48+
l.Errorf(f, v...)
49+
assert.Contains(t, eBuf.String(), "[ERROR]")
50+
assert.Contains(t, eBuf.String(), "-")
51+
eBuf.Reset()
52+
53+
l.Critical(v...)
54+
assert.Contains(t, eBuf.String(), "[ERROR]")
55+
assert.Contains(t, eBuf.String(), "internal/core.TestLogger")
56+
assert.Contains(t, eBuf.String(), "internal/core/logger_test.go:53")
57+
eBuf.Reset()
58+
59+
l.Criticalf(f, v...)
60+
assert.Contains(t, eBuf.String(), "[ERROR]")
61+
assert.Contains(t, eBuf.String(), "-")
62+
assert.Contains(t, eBuf.String(), "internal/core.TestLogger")
63+
assert.Contains(t, eBuf.String(), "internal/core/logger_test.go:59")
64+
println(eBuf.String())
65+
eBuf.Reset()
66+
}

0 commit comments

Comments
 (0)