diff --git a/ReadMe.Md b/ReadMe.Md index 0a1c095..aeb6012 100644 --- a/ReadMe.Md +++ b/ReadMe.Md @@ -110,14 +110,6 @@ Loglevel for stdout, one of TRACE, DEBUG, INFO or WARN LOGLEVEL="DEBUG" ``` -#### TZ - -TimeZone - -```bash -TZ="Australia/Sydney" -``` - ## Contributions The j8a team welcomes all [contributors](https://github.com/simonmittag/j8a/blob/master/CONTRIBUTING.md). Everyone diff --git a/config.go b/config.go index 55ab718..c91a713 100644 --- a/config.go +++ b/config.go @@ -19,7 +19,7 @@ import ( "github.com/rs/zerolog/log" ) -//Config is the system wide configuration for j8a +// Config is the system wide configuration for j8a type Config struct { Policies map[string]Policy Routes Routes @@ -27,6 +27,8 @@ type Config struct { Resources map[string][]ResourceMapping Connection Connection DisableXRequestInfo bool + TimeZone string + LogLevel string } const HTTP = "HTTP" @@ -110,6 +112,53 @@ func (config Config) parse(yml []byte) *Config { return &config } +func (config Config) validateLogLevel() *Config { + logLevel := strings.ToUpper(config.LogLevel) + old := strings.ToUpper(zerolog.GlobalLevel().String()) + + if len(logLevel) > 0 && logLevel != old { + switch logLevel { + case "TRACE": + log.Info().Msgf("resetting global log level to %v", logLevel) + zerolog.SetGlobalLevel(zerolog.TraceLevel) + case "DEBUG": + log.Info().Msgf("resetting global log level to %v", logLevel) + zerolog.SetGlobalLevel(zerolog.DebugLevel) + case "INFO": + log.Info().Msgf("resetting global log level to %v", logLevel) + zerolog.SetGlobalLevel(zerolog.InfoLevel) + case "WARN": + log.Info().Msgf("resetting global log level to %v", logLevel) + zerolog.SetGlobalLevel(zerolog.WarnLevel) + default: + config.panic(fmt.Sprintf("invalid log level %v must be one of TRACE | DEBUG | INFO | WARN ", logLevel)) + } + } + + return &config +} + +func (config Config) validateTimeZone() *Config { + var tz *time.Location + var e error + if len(config.TimeZone) > 0 { + tz, e = time.LoadLocation(config.TimeZone) + if e != nil { + config.panic(fmt.Sprintf("Not a valid TimeZone identifier %s, see: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones", config.TimeZone)) + } + } else { + //we default to UTC if time wasn't specified + tz = time.UTC + } + + zerolog.TimestampFunc = func() time.Time { + return time.Now().In(tz) + } + log.Info().Msgf("timeZone for this log and all system events set to %s", tz.String()) + + return &config +} + func (config Config) reApplyResourceNames() *Config { for name := range config.Resources { resourceMappings := config.Resources[name] diff --git a/config_test.go b/config_test.go index 261a35e..9cdbdaf 100644 --- a/config_test.go +++ b/config_test.go @@ -16,12 +16,35 @@ func TestMain(m *testing.M) { } func testSetup() { - os.Setenv("TZ", "Australia/Sydney") os.Setenv("LOGLEVEL", "TRACE") os.Setenv("LOGCOLOR", "true") } -//TestDefaultDownstreamReadTimeout +func TestTimeZones(t *testing.T) { + tzs := []struct { + tz string + valid bool + }{ + { + "Australia/Sydney", true, + }, + { + "Africa/Johannesburg", true, + }, + { + "UTC", true, + }, + } + + config := new(Config) + for _, tz := range tzs { + config.TimeZone = tz.tz + config.validateTimeZone() + } + +} + +// TestDefaultDownstreamReadTimeout func TestDefaultDownstreamReadTimeout(t *testing.T) { config := new(Config).setDefaultDownstreamParams() got := config.Connection.Downstream.ReadTimeoutSeconds @@ -31,7 +54,7 @@ func TestDefaultDownstreamReadTimeout(t *testing.T) { } } -//TestDefaultDownstreamIdleTimeout +// TestDefaultDownstreamIdleTimeout func TestDefaultDownstreamIdleTimeout(t *testing.T) { config := new(Config).setDefaultDownstreamParams() got := config.Connection.Downstream.IdleTimeoutSeconds @@ -41,7 +64,7 @@ func TestDefaultDownstreamIdleTimeout(t *testing.T) { } } -//TestDefaultDownstreamRoundtripTimeout +// TestDefaultDownstreamRoundtripTimeout func TestDefaultDownstreamRoundtripTimeout(t *testing.T) { config := new(Config).setDefaultDownstreamParams() got := config.Connection.Downstream.RoundTripTimeoutSeconds @@ -60,7 +83,7 @@ func TestDefaultDownstreamMaxBodyBytes(t *testing.T) { } } -//TestDefaultUpstreamSocketTimeout +// TestDefaultUpstreamSocketTimeout func TestDefaultUpstreamSocketTimeout(t *testing.T) { config := new(Config).setDefaultUpstreamParams() got := config.Connection.Upstream.SocketTimeoutSeconds @@ -70,7 +93,7 @@ func TestDefaultUpstreamSocketTimeout(t *testing.T) { } } -//TestDefaultUpstreamSocketTimeout +// TestDefaultUpstreamSocketTimeout func TestDefaultUpstreamReadTimeout(t *testing.T) { config := new(Config).setDefaultUpstreamParams() got := config.Connection.Upstream.ReadTimeoutSeconds @@ -80,7 +103,7 @@ func TestDefaultUpstreamReadTimeout(t *testing.T) { } } -//TestDefaultUpstreamIdleTimeout +// TestDefaultUpstreamIdleTimeout func TestDefaultUpstreamIdleTimeout(t *testing.T) { config := new(Config).setDefaultUpstreamParams() got := config.Connection.Upstream.IdleTimeoutSeconds @@ -90,7 +113,7 @@ func TestDefaultUpstreamIdleTimeout(t *testing.T) { } } -//TestDefaultUpstreamConnectionPoolSize +// TestDefaultUpstreamConnectionPoolSize func TestDefaultUpstreamConnectionPoolSize(t *testing.T) { config := new(Config).setDefaultUpstreamParams() got := config.Connection.Upstream.PoolSize @@ -100,7 +123,7 @@ func TestDefaultUpstreamConnectionPoolSize(t *testing.T) { } } -//TestDefaultUpstreamConnectionMaxAttempts +// TestDefaultUpstreamConnectionMaxAttempts func TestDefaultUpstreamConnectionMaxAttempts(t *testing.T) { config := new(Config).setDefaultUpstreamParams() got := config.Connection.Upstream.MaxAttempts @@ -110,7 +133,7 @@ func TestDefaultUpstreamConnectionMaxAttempts(t *testing.T) { } } -//TestDefaultPolicy +// TestDefaultPolicy func TestDefaultPolicy(t *testing.T) { wantLabel := "default" @@ -159,7 +182,7 @@ func TestParsePolicy(t *testing.T) { } } -//TestParseResource +// TestParseResource func TestParseResource(t *testing.T) { configJson := []byte("{\n\t\"resources\": {\n\t\t\"customer\": [{\n\t\t\t\"labels\": [\n\t\t\t\t\"blue\"\n\t\t\t],\n\t\t\t\"url\": {\n\t\t\t\t\"scheme\": \"http\",\n\t\t\t\t\"host\": \"localhost\",\n\t\t\t\t\"port\": 8081\n\t\t\t}\n\t\t}]\n\t}\n}") config := new(Config).parse(configJson) @@ -181,7 +204,7 @@ func TestParseResource(t *testing.T) { } } -//TestParseConnection +// TestParseConnection func TestParseConnection(t *testing.T) { configJson := []byte("{\n\t\"connection\": {\n\t\t\"downstream\": {\n\t\t\t\"readTimeoutSeconds\": 120,\n\t\t\t\"roundTripTimeoutSeconds\": 240,\n\t\t\t\"idleTimeoutSeconds\": 30,\n\t\t\t\"port\": 8080,\n\t\t\t\"mode\": \"TLS\"\n\t\t},\n\t\t\"upstream\": {\n\t\t\t\"socketTimeoutSeconds\": 3,\n\t\t\t\"readTimeoutSeconds\": 120,\n\t\t\t\"idleTimeoutSeconds\": 120,\n\t\t\t\"maxAttempts\": 4,\n\t\t\t\"poolSize\": 1024\n\t\t}\n\t}\n}") config := new(Config).parse(configJson) @@ -245,7 +268,7 @@ func TestParseConnection(t *testing.T) { } } -//TestParseRoute +// TestParseRoute func TestParseRoute(t *testing.T) { configJson := []byte("{\"routes\": [{\n\t\t\t\"path\": \"/about\",\n\t\t\t\"resource\": \"aboutj8a\"\n\t\t},\n\t\t{\n\t\t\t\"path\": \"/customer\",\n\t\t\t\"resource\": \"customer\",\n\t\t\t\"policy\": \"ab\"\n\t\t}\n\t]}") config := new(Config).parse(configJson) @@ -264,7 +287,7 @@ func TestParseRoute(t *testing.T) { } } -//TestValidateAcmeEmail +// TestValidateAcmeEmail func TestValidateAcmeEmail(t *testing.T) { config := &Config{ Connection: Connection{ @@ -284,7 +307,7 @@ func TestValidateAcmeEmail(t *testing.T) { config = config.validateAcmeConfig() } -//TestValidateAcmeEmail +// TestValidateAcmeEmail func TestValidateAcmeGracePeriod30(t *testing.T) { config := &Config{ Connection: Connection{ @@ -390,7 +413,7 @@ func TestValidateAcmeGracePeriodFailsGreater30(t *testing.T) { config = config.validateAcmeConfig() } -//TestValidateAcmeDomainInvalidLeadingDotFails +// TestValidateAcmeDomainInvalidLeadingDotFails func TestValidateValidateAcmeDomainInvalidLeadingDotFails(t *testing.T) { defer func() { if r := recover(); r == nil { @@ -402,7 +425,7 @@ func TestValidateValidateAcmeDomainInvalidLeadingDotFails(t *testing.T) { t.Errorf("config did not panic for supported Acme provider but with missing domain") } -//TestValidateAcmeDomainInvalidTrailingDotFails +// TestValidateAcmeDomainInvalidTrailingDotFails func TestValidateAcmeDomainInvalidTrailingDotFails(t *testing.T) { defer func() { if r := recover(); r == nil { @@ -414,8 +437,8 @@ func TestValidateAcmeDomainInvalidTrailingDotFails(t *testing.T) { t.Errorf("config did not panic for supported Acme provider but with missing domain") } -//TestValidateAcmeDomainInvalidTrailingDotFails -//NOTE WE DO NOT SUPPORT WILDCART CERTS BECAUSE THEY CANNOT BE VERIFIED USING HTTP01 CHALLENGE ON LETSENCRYPT, SEE: https://letsencrypt.org/docs/faq/ +// TestValidateAcmeDomainInvalidTrailingDotFails +// NOTE WE DO NOT SUPPORT WILDCART CERTS BECAUSE THEY CANNOT BE VERIFIED USING HTTP01 CHALLENGE ON LETSENCRYPT, SEE: https://letsencrypt.org/docs/faq/ func TestValidateAcmeDomainInvalidWildcartCertFails(t *testing.T) { defer func() { if r := recover(); r == nil { @@ -427,7 +450,7 @@ func TestValidateAcmeDomainInvalidWildcartCertFails(t *testing.T) { t.Errorf("config did not panic for supported Acme provider but with missing domain") } -//TestValidateAcmeDomainValidSubdomainPasses +// TestValidateAcmeDomainValidSubdomainPasses func TestValidateAcmeDomainValidSubdomainPasses(t *testing.T) { defer func() { if r := recover(); r != nil { @@ -439,7 +462,7 @@ func TestValidateAcmeDomainValidSubdomainPasses(t *testing.T) { t.Logf("normal. config did not panic for valid subdomain and supported Acme provider") } -//TestValidateAcmeDomainMissingFails +// TestValidateAcmeDomainMissingFails func TestValidateAcmeDomainMissingFails(t *testing.T) { defer func() { if r := recover(); r == nil { @@ -465,7 +488,7 @@ func TestValidateAcmeDomainMissingFails(t *testing.T) { t.Errorf("config did not panic for supported Acme provider but with missing domain") } -//TestValidateAcmeProviderLetsencrypt +// TestValidateAcmeProviderLetsencrypt func TestValidateAcmeProviderLetsencrypt(t *testing.T) { config := &Config{ Connection: Connection{ @@ -486,7 +509,7 @@ func TestValidateAcmeProviderLetsencrypt(t *testing.T) { t.Logf("normal. no config panic for Acme provider letsencrypt") } -//TestValidateAcmeProviderLetsencryptWithMultipleSubDomains +// TestValidateAcmeProviderLetsencryptWithMultipleSubDomains func TestValidateAcmeProviderLetsencryptWithMultipleSubdomains(t *testing.T) { config := &Config{ Connection: Connection{ @@ -507,7 +530,7 @@ func TestValidateAcmeProviderLetsencryptWithMultipleSubdomains(t *testing.T) { t.Logf("normal. no config panic for Acme provider letsencrypt") } -//TestValidateAcmeProviderLetsencryptFailsWithOneInvalidSubDomain +// TestValidateAcmeProviderLetsencryptFailsWithOneInvalidSubDomain func TestValidateAcmeProviderLetsencryptFailsWithOneInvalidSubDomain(t *testing.T) { defer func() { if r := recover(); r != nil { @@ -533,7 +556,7 @@ func TestValidateAcmeProviderLetsencryptFailsWithOneInvalidSubDomain(t *testing. t.Errorf("config did not panic for invalid subdomain") } -//TestValidateAcmeProviderLetsencrypt +// TestValidateAcmeProviderLetsencrypt func TestValidateAcmeProviderPort80(t *testing.T) { defer func() { if r := recover(); r != nil { @@ -562,7 +585,7 @@ func TestValidateAcmeProviderPort80(t *testing.T) { t.Logf("normal. no config panic for Acme provider letsencrypt with port 80") } -//TestValidateAcmeProviderFailsWithMissingPort80 +// TestValidateAcmeProviderFailsWithMissingPort80 func TestValidateAcmeProviderFailsWithMissingPort80(t *testing.T) { defer func() { if r := recover(); r != nil { @@ -588,7 +611,7 @@ func TestValidateAcmeProviderFailsWithMissingPort80(t *testing.T) { t.Error("no config panic for Acme provider without port 80 specified. should have panicked") } -//TestValidateAcmeProviderFailsWithCertSpecified +// TestValidateAcmeProviderFailsWithCertSpecified func TestValidateAcmeProviderFailsWithCertSpecified(t *testing.T) { defer func() { if r := recover(); r != nil { @@ -616,7 +639,7 @@ func TestValidateAcmeProviderFailsWithCertSpecified(t *testing.T) { t.Error("no config panic happened after superfluous cert specified but it should have") } -//TestValidateAcmeProviderFailsWithKeySpecified +// TestValidateAcmeProviderFailsWithKeySpecified func TestValidateAcmeProviderFailsWithKeySpecified(t *testing.T) { defer func() { if r := recover(); r != nil { @@ -644,7 +667,7 @@ func TestValidateAcmeProviderFailsWithKeySpecified(t *testing.T) { t.Error("no config panic occurred after extra private key specified next to acme") } -//TestValidateAcmeMissingProviderFails +// TestValidateAcmeMissingProviderFails func TestValidateAcmeMissingProviderFails(t *testing.T) { defer func() { if r := recover(); r != nil { @@ -670,7 +693,7 @@ func TestValidateAcmeMissingProviderFails(t *testing.T) { t.Errorf("config did not panic for missing provider") } -//TestValidateAcmeMissingEmailFails +// TestValidateAcmeMissingEmailFails func TestValidateAcmeEmailFails(t *testing.T) { defer func() { if r := recover(); r != nil { @@ -696,7 +719,7 @@ func TestValidateAcmeEmailFails(t *testing.T) { t.Errorf("config did not panic for missing email") } -//TestValidateAcmeInvalidEmailFails +// TestValidateAcmeInvalidEmailFails func TestValidateAcmeInvalidEmailFails(t *testing.T) { defer func() { if r := recover(); r != nil { @@ -771,7 +794,7 @@ func TestTlsInsecureSkipVerify_Optional(t *testing.T) { } } -//TestReadConfigFile +// TestReadConfigFile func TestReadConfigFile(t *testing.T) { config := new(Config).readYmlFile("./j8acfg.yml") if config.Routes == nil { diff --git a/integration/j8a1.yml b/integration/j8a1.yml index c9a64ab..6bbe879 100644 --- a/integration/j8a1.yml +++ b/integration/j8a1.yml @@ -1,4 +1,6 @@ --- +timeZone: Australia/Sydney +logLevel: info connection: downstream: readTimeoutSeconds: 3 diff --git a/integration/j8a2.yml b/integration/j8a2.yml index 734d8c3..3599ee7 100644 --- a/integration/j8a2.yml +++ b/integration/j8a2.yml @@ -1,4 +1,6 @@ --- +timeZone: Africa/Casablanca +logLevel: warn connection: downstream: readTimeoutSeconds: 3 diff --git a/integration/j8a3.yml b/integration/j8a3.yml index 323fae6..68df9ab 100644 --- a/integration/j8a3.yml +++ b/integration/j8a3.yml @@ -1,4 +1,5 @@ --- +timeZone: UTC connection: downstream: readTimeoutSeconds: 3 diff --git a/logger.go b/logger.go index 4e414fb..9252d56 100644 --- a/logger.go +++ b/logger.go @@ -3,11 +3,10 @@ package j8a import ( "crypto/md5" "encoding/hex" - "os" - "strings" - "github.com/rs/zerolog" "github.com/rs/zerolog/log" + "os" + "strings" ) const dwnReqRemoteAddr = "dwnReqRemoteAddr" @@ -39,8 +38,8 @@ const upAtmptResBodyBytes = "upAtmptResBodyBytes" const upAtmptElpsdMicros = "upAtmptElpsdMicros" const upAtmptAbort = "upAtmptAbort" -//ServerID is a unique identifier made up as md5 of hostname and version. -//initServerId creates a unique ID for the server log. +// ServerID is a unique identifier made up as md5 of hostname and version. +// initServerId creates a unique ID for the server log. func initServerID() { hasher := md5.New() hasher.Write([]byte(getHost() + getVersion())) @@ -49,8 +48,8 @@ func initServerID() { log.Logger = log.With().Str("srvId", ID).Logger() } -//ServerID is a unique identifier made up as md5 of hostname and version. -//initServerId creates a unique ID for the server log. +// ServerID is a unique identifier made up as md5 of hostname and version. +// initServerId creates a unique ID for the server log. func initPID() { pid := os.Getpid() log.Info().Int("pid", pid).Msg("pid determined") @@ -73,19 +72,8 @@ func getVersion() string { // Init sets up a global logger instance func initLogger() { - logLevel := strings.ToUpper(os.Getenv("LOGLEVEL")) - switch logLevel { - case "TRACE": - zerolog.SetGlobalLevel(zerolog.TraceLevel) - case "DEBUG": - zerolog.SetGlobalLevel(zerolog.DebugLevel) - case "INFO": - zerolog.SetGlobalLevel(zerolog.InfoLevel) - case "WARN": - zerolog.SetGlobalLevel(zerolog.WarnLevel) - default: - zerolog.SetGlobalLevel(zerolog.InfoLevel) - } + defaultLevel := zerolog.InfoLevel + zerolog.SetGlobalLevel(defaultLevel) logColor := strings.ToUpper(os.Getenv("LOGCOLOR")) switch logColor { @@ -100,7 +88,6 @@ func initLogger() { } initServerID() - initTime() initPID() - log.Info().Msgf("setting global log level to %s", logLevel) + log.Info().Msgf("setting global log level to %s", strings.ToUpper(defaultLevel.String())) } diff --git a/logger_test.go b/logger_test.go index e959897..decfa31 100644 --- a/logger_test.go +++ b/logger_test.go @@ -17,7 +17,6 @@ func TestServerID(t *testing.T) { } func TestDefaultLogLevelInit(t *testing.T) { - os.Setenv("LOGLEVEL", "not set") initLogger() got := zerolog.GlobalLevel().String() want := "info" @@ -27,18 +26,24 @@ func TestDefaultLogLevelInit(t *testing.T) { } func TestTraceLogLevelInit(t *testing.T) { - os.Setenv("LOGLEVEL", "TRACE") + c := Config{ + LogLevel: "trace", + } initLogger() + c.validateLogLevel() got := zerolog.GlobalLevel().String() want := "trace" if got != want { - t.Errorf("default log level not properly initialised, got %v, want %v", got, want) + t.Errorf("log level not properly initialised, got %v, want %v", got, want) } } func TestDebugLogLevelInit(t *testing.T) { - os.Setenv("LOGLEVEL", "DEBUG") + c := Config{ + LogLevel: "debug", + } initLogger() + c.validateLogLevel() got := zerolog.GlobalLevel().String() want := "debug" if got != want { @@ -47,8 +52,11 @@ func TestDebugLogLevelInit(t *testing.T) { } func TestInfoLogLevelInit(t *testing.T) { - os.Setenv("LOGLEVEL", "INFO") + c := Config{ + LogLevel: "INFO", + } initLogger() + c.validateLogLevel() got := zerolog.GlobalLevel().String() want := "info" if got != want { @@ -57,11 +65,23 @@ func TestInfoLogLevelInit(t *testing.T) { } func TestWarnLogLevelInit(t *testing.T) { - os.Setenv("LOGLEVEL", "WARN") + c := Config{ + LogLevel: "warn", + } initLogger() + c.validateLogLevel() got := zerolog.GlobalLevel().String() want := "warn" if got != want { t.Errorf("log level not properly initialised, got %v, want %v", got, want) } } + +func TestBadLogLevelPanic(t *testing.T) { + c := Config{ + LogLevel: "blah", + } + initLogger() + + shouldPanic(t, c.validateLogLevel) +} diff --git a/server.go b/server.go index f7a0591..3c733aa 100644 --- a/server.go +++ b/server.go @@ -175,6 +175,8 @@ func BootStrap() { config := new(Config). load(). + validateTimeZone(). + validateLogLevel(). reApplyResourceURLDefaults(). reApplyResourceNames(). validateJwt(). diff --git a/time.go b/time.go deleted file mode 100644 index a595e1c..0000000 --- a/time.go +++ /dev/null @@ -1,19 +0,0 @@ -package j8a - -import ( - "os" - - "github.com/rs/zerolog/log" -) - -var TZ = "UTC" - -func initTime() string { - tz := os.Getenv("TZ") - if len(tz) == 0 { - tz = "UTC" - } - log.Info().Str("timeZone", tz).Msg("timeZone determined") - TZ = tz - return tz -} diff --git a/time_test.go b/time_test.go deleted file mode 100644 index 2058b8a..0000000 --- a/time_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package j8a - -import ( - "os" - "testing" -) - -func TestTimezoneSet(t *testing.T) { - want := "Australia/Sydney" - os.Setenv("TZ", want) - initTime() - got := TZ - if want != got { - t.Errorf("timezone want %v, got %v", want, got) - } -} - -func TestTimezoneNotSet(t *testing.T) { - want := "UTC" - os.Setenv("TZ", "") - initTime() - got := TZ - if want != got { - t.Errorf("timezone want %v, got %v", want, got) - } -}