Skip to content

Commit 3a3241d

Browse files
authored
Merge pull request #287 from lukaszbudnik/enhancements-to-logging
Enhancements to logging
2 parents 8ef8cd2 + 5cc5597 commit 3a3241d

File tree

11 files changed

+311
-32
lines changed

11 files changed

+311
-32
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,7 @@ singleScripts:
461461
# optional, directories of tenant SQL script which are applied always for all tenants, these are subdirectories of baseLocation
462462
tenantScripts:
463463
- tenants-scripts
464-
# optional, default is:
464+
# optional, default is 8080
465465
port: 8080
466466
# path prefix is optional and defaults to '/'
467467
# path prefix is used for application HTTP request routing by Application Load Balancers/Application Gateways
@@ -479,6 +479,9 @@ webHookHeaders:
479479
- "Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l"
480480
- "Content-Type: application/json"
481481
- "X-Custom-Header: value1,value2"
482+
# optional, allows to filter logs produced by migrator, valid values are: DEBUG, INFO, ERROR, PANIC
483+
# defaults to INFO
484+
logLevel: INFO
482485
```
483486
484487
## Env variables substitution

common/common.go

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,41 @@ import (
44
"context"
55
"fmt"
66
"log"
7+
"path/filepath"
78
"runtime"
89
)
910

11+
const (
12+
panicLevel = "PANIC"
13+
errorLevel = "ERROR"
14+
infoLevel = "INFO"
15+
debugLevel = "DEBUG"
16+
)
17+
1018
// RequestIDKey is used together with context for setting/getting X-Request-ID
1119
type RequestIDKey struct{}
1220

21+
// LogLevel
22+
type LogLevelKey struct{}
23+
1324
// LogError logs error message
1425
func LogError(ctx context.Context, format string, a ...interface{}) string {
15-
return logLevel(ctx, "ERROR", format, a...)
26+
return logLevel(ctx, errorLevel, format, a...)
1627
}
1728

1829
// LogInfo logs info message
1930
func LogInfo(ctx context.Context, format string, a ...interface{}) string {
20-
return logLevel(ctx, "INFO", format, a...)
31+
return logLevel(ctx, infoLevel, format, a...)
32+
}
33+
34+
// LogDebug logs debug message
35+
func LogDebug(ctx context.Context, format string, a ...interface{}) string {
36+
return logLevel(ctx, debugLevel, format, a...)
2137
}
2238

2339
// LogPanic logs error message
2440
func LogPanic(ctx context.Context, format string, a ...interface{}) string {
25-
return logLevel(ctx, "PANIC", format, a...)
41+
return logLevel(ctx, panicLevel, format, a...)
2642
}
2743

2844
// Log logs message with a given level with no request context
@@ -37,14 +53,22 @@ func Log(level string, format string, a ...interface{}) string {
3753
}
3854

3955
func logLevel(ctx context.Context, level string, format string, a ...interface{}) string {
40-
_, file, line, _ := runtime.Caller(2)
4156

42-
requestID := ctx.Value(RequestIDKey{})
43-
message := fmt.Sprintf(format, a...)
57+
logLevel := fmt.Sprintf("%v", ctx.Value(LogLevelKey{}))
4458

45-
log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds | log.LUTC)
46-
log.Printf("[%v:%v] %v requestId=%v %v", file, line, level, requestID, message)
47-
return message
59+
if shouldLogMessage(logLevel, level) {
60+
requestID := ctx.Value(RequestIDKey{})
61+
message := fmt.Sprintf(format, a...)
62+
_, file, line, _ := runtime.Caller(2)
63+
filename := filepath.Base(file)
64+
65+
log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds | log.LUTC)
66+
log.Printf("[%v:%v] %v requestId=%v %v", filename, line, level, requestID, message)
67+
68+
return message
69+
}
70+
71+
return ""
4872
}
4973

5074
// FindNthIndex finds index of nth occurance of a character c in string str
@@ -60,3 +84,27 @@ func FindNthIndex(str string, c byte, n int) int {
6084
}
6185
return -1
6286
}
87+
88+
func shouldLogMessage(configLogLevel, targetLevel string) bool {
89+
// if configLogLevel and targetLevel match then log
90+
if configLogLevel == targetLevel {
91+
return true
92+
}
93+
// if configLogLevel is debug then all messages are logged no need to check targetLevel
94+
if configLogLevel == debugLevel {
95+
return true
96+
}
97+
// if configLogLevel not set then INFO is assumed
98+
// if INFO then all levels should log except of debug
99+
if (len(configLogLevel) == 0 || configLogLevel == infoLevel) && targetLevel != debugLevel {
100+
return true
101+
}
102+
103+
// if logLevel is ERROR then only ERROR and PANIC are logged
104+
// ERROR is covered in the beginning of method so need to check only Panic level
105+
if configLogLevel == errorLevel && targetLevel == panicLevel {
106+
return true
107+
}
108+
109+
return false
110+
}

common/common_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,29 @@ import (
1010
func newTestContext() context.Context {
1111
ctx := context.TODO()
1212
ctx = context.WithValue(ctx, RequestIDKey{}, "123")
13+
// log level empty = default log level = INFO
14+
ctx = context.WithValue(ctx, LogLevelKey{}, "")
1315
return ctx
1416
}
1517

18+
func newTestContextWithDebugLogLevel() context.Context {
19+
ctx := newTestContext()
20+
ctx = context.WithValue(ctx, LogLevelKey{}, debugLevel)
21+
return ctx
22+
}
23+
24+
func TestLogDebugSkip(t *testing.T) {
25+
// DEBUG message will be skipped, as the default log level is INFO
26+
message := LogDebug(newTestContext(), "success")
27+
assert.Empty(t, message)
28+
}
29+
30+
func TestLogDebug(t *testing.T) {
31+
// DEBUG message will be returned, as the log level is set to DEBUG
32+
message := LogDebug(newTestContextWithDebugLogLevel(), "success")
33+
assert.Equal(t, "success", message)
34+
}
35+
1636
func TestLogInfo(t *testing.T) {
1737
message := LogInfo(newTestContext(), "success")
1838
assert.Equal(t, "success", message)
@@ -42,3 +62,35 @@ func TestFindNthIndexNotFound(t *testing.T) {
4262
indx := FindNthIndex("https://lukaszbudniktest.blob.core.windows.net/mycontainer", '/', 4)
4363
assert.Equal(t, -1, indx)
4464
}
65+
66+
func TestShouldLogMessage(t *testing.T) {
67+
// default logLevel is info, should log all except of debug
68+
assert.False(t, shouldLogMessage("", debugLevel))
69+
assert.True(t, shouldLogMessage("", infoLevel))
70+
assert.True(t, shouldLogMessage("", errorLevel))
71+
assert.True(t, shouldLogMessage("", panicLevel))
72+
73+
// debug logLevel logs all
74+
assert.True(t, shouldLogMessage(debugLevel, debugLevel))
75+
assert.True(t, shouldLogMessage(debugLevel, infoLevel))
76+
assert.True(t, shouldLogMessage(debugLevel, errorLevel))
77+
assert.True(t, shouldLogMessage(debugLevel, panicLevel))
78+
79+
// info logLevel logs all except of debug
80+
assert.False(t, shouldLogMessage(infoLevel, debugLevel))
81+
assert.True(t, shouldLogMessage(infoLevel, infoLevel))
82+
assert.True(t, shouldLogMessage(infoLevel, errorLevel))
83+
assert.True(t, shouldLogMessage(infoLevel, panicLevel))
84+
85+
// error logLevel logs only error or panic
86+
assert.False(t, shouldLogMessage(errorLevel, debugLevel))
87+
assert.False(t, shouldLogMessage(errorLevel, infoLevel))
88+
assert.True(t, shouldLogMessage(errorLevel, errorLevel))
89+
assert.True(t, shouldLogMessage(errorLevel, panicLevel))
90+
91+
// panic logLevel logs only panic
92+
assert.False(t, shouldLogMessage(panicLevel, debugLevel))
93+
assert.False(t, shouldLogMessage(panicLevel, infoLevel))
94+
assert.False(t, shouldLogMessage(panicLevel, errorLevel))
95+
assert.True(t, shouldLogMessage(panicLevel, panicLevel))
96+
}

config/config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ type Config struct {
2727
WebHookURL string `yaml:"webHookURL,omitempty"`
2828
WebHookHeaders []string `yaml:"webHookHeaders,omitempty"`
2929
WebHookTemplate string `yaml:"webHookTemplate,omitempty"`
30+
LogLevel string `yaml:"logLevel,omitempty" validate:"logLevel"`
3031
}
3132

3233
func (config Config) String() string {
@@ -54,6 +55,7 @@ func FromBytes(contents []byte) (*Config, error) {
5455
}
5556

5657
validate := validator.New()
58+
validate.RegisterValidation("logLevel", validateLogLevel)
5759
if err := validate.Struct(config); err != nil {
5860
return nil, err
5961
}
@@ -94,3 +96,8 @@ func substituteEnvVariable(s string) string {
9496
}
9597
return s
9698
}
99+
100+
func validateLogLevel(fl validator.FieldLevel) bool {
101+
value := fl.Field().String()
102+
return value == "" || value == "DEBUG" || value == "INFO" || value == "ERROR" || value == "PANIC"
103+
}

config/config_test.go

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,23 @@ func TestWithEnvFromFile(t *testing.T) {
4343
}
4444

4545
func TestConfigString(t *testing.T) {
46-
config := &Config{"/opt/app/migrations", "postgres", "user=p dbname=db host=localhost", "select abc", "insert into table", ":tenant", []string{"ref"}, []string{"tenants"}, []string{"procedures"}, []string{}, "8181", "", "https://hooks.slack.com/services/TTT/BBB/XXX", []string{}, `{"text": "Results are: ${summary}"}`}
46+
config := &Config{
47+
BaseLocation: "/opt/app/migrations",
48+
Driver: "postgres",
49+
DataSource: "user=p dbname=db host=localhost",
50+
TenantSelectSQL: "select abc",
51+
TenantInsertSQL: "insert into table",
52+
SchemaPlaceHolder: ":tenant",
53+
SingleMigrations: []string{"ref"},
54+
TenantMigrations: []string{"tenants"},
55+
SingleScripts: []string{"procedures"},
56+
TenantScripts: []string{},
57+
Port: "8181",
58+
PathPrefix: "",
59+
WebHookURL: "https://hooks.slack.com/services/TTT/BBB/XXX",
60+
WebHookHeaders: []string{},
61+
WebHookTemplate: `{"text": "Results are: ${summary}"}`,
62+
}
4763
// check if go naming convention applies
4864
expected := `baseLocation: /opt/app/migrations
4965
driver: postgres
@@ -81,3 +97,27 @@ func TestConfigFromWrongSyntaxFile(t *testing.T) {
8197
assert.Nil(t, config)
8298
assert.IsType(t, (*yaml.TypeError)(nil), err, "Should error because of wrong yaml syntax")
8399
}
100+
101+
func TestCustomValidatorLogLevelError(t *testing.T) {
102+
config := `name: invoicesdb1
103+
baseLocation: /opt/app/migrations
104+
driver: postgres
105+
dataSource: user=p dbname=db host=localhost
106+
tenantSelectSQL: select abc
107+
tenantInsertSQL: insert into table
108+
schemaPlaceHolder: :tenant
109+
singleMigrations:
110+
- ref
111+
tenantMigrations:
112+
- tenants
113+
singleScripts:
114+
- procedures
115+
port: "8181"
116+
webHookURL: https://hooks.slack.com/services/TTT/BBB/XXX
117+
webHookTemplate: '{"text": "Results are: ${summary}"}'
118+
logLevel: ABC`
119+
120+
_, err := FromBytes([]byte(config))
121+
assert.NotNil(t, err)
122+
assert.Contains(t, err.Error(), `Error:Field validation for 'LogLevel' failed on the 'logLevel' tag`)
123+
}

db/db.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,7 @@ func (bc *baseConnector) applyMigrationsInTx(tx *sql.Tx, versionName string, act
515515
}
516516

517517
for _, s := range schemas {
518-
common.LogInfo(bc.ctx, "Applying migration type: %d, schema: %s, file: %s ", m.MigrationType, s, m.File)
518+
common.LogDebug(bc.ctx, "Applying migration type: %d, schema: %s, file: %s ", m.MigrationType, s, m.File)
519519

520520
if action == types.ActionApply {
521521
contents := strings.Replace(m.Contents, schemaPlaceHolder, s, -1)

loader/s3_loader.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,22 @@ type s3Loader struct {
1919
baseLoader
2020
}
2121

22-
// GetSourceMigrations returns all migrations from AWS S3 location
23-
func (s3l *s3Loader) GetSourceMigrations() []types.Migration {
22+
func (s3l *s3Loader) newClient() *s3.S3 {
2423
sess, err := session.NewSession()
2524
if err != nil {
2625
panic(err.Error())
2726
}
28-
client := s3.New(sess)
27+
return s3.New(sess)
28+
}
29+
30+
// GetSourceMigrations returns all migrations from AWS S3 location
31+
func (s3l *s3Loader) GetSourceMigrations() []types.Migration {
32+
client := s3l.newClient()
2933
return s3l.doGetSourceMigrations(client)
3034
}
3135

3236
func (s3l *s3Loader) HealthCheck() error {
33-
sess, err := session.NewSession()
34-
if err != nil {
35-
return err
36-
}
37-
client := s3.New(sess)
37+
client := s3l.newClient()
3838
return s3l.doHealthCheck(client)
3939
}
4040

notifications/notifications_test.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,21 @@ func TestWebHookNotifierCustomHeaders(t *testing.T) {
106106

107107
func TestWebHookURLError(t *testing.T) {
108108
config := config.Config{}
109-
config.WebHookURL = "xczxcvv"
109+
config.WebHookURL = "://xczxcvv/path"
110110
notifier := New(context.TODO(), &config)
111-
result, err := notifier.Notify(&types.Summary{})
111+
_, err := notifier.Notify(&types.Summary{})
112+
113+
assert.NotNil(t, err)
114+
assert.Contains(t, err.Error(), "missing protocol scheme")
115+
}
116+
117+
func TestWebHookClientError(t *testing.T) {
118+
config := config.Config{}
119+
// passes URL parsing but HTTP client returns error
120+
config.WebHookURL = "non-existing-server"
121+
notifier := New(context.TODO(), &config)
122+
_, err := notifier.Notify(&types.Summary{})
112123

113124
assert.NotNil(t, err)
114-
assert.Equal(t, "", result)
125+
assert.Contains(t, err.Error(), "unsupported protocol scheme")
115126
}

0 commit comments

Comments
 (0)