From 023e1967bfbd875c6b96341ddd17f73da84e69d8 Mon Sep 17 00:00:00 2001 From: Muir Manders Date: Mon, 28 Jan 2019 09:36:54 -0800 Subject: [PATCH 1/3] Replace seelog logger with internal logger. The seelog logger had various drawbacks: - not easily user configurable (e.g. can't disable, can't write to a file, can't write to stderr, can't write to json, etc) - uses global seelog instance which can conflict w/ other packages or user's logger The replacement logger uses a simple Logger interface: type Logger interface { Log(level LogLevel, msg fmt.Stringer) } Rather than a separate method for each log level, the level is passed as an argument. This makes it easy/possible to add more log levels in the future. The lowest log level is now "debug" instead of seelog's "trace" (seemed mildly confusing to have a log method named "trace" in a cloud tracing package). We use a fmt.Stringer so any log message serialization can be deferred until the message is actually written. There is a default Logger implementation the user can use via NewDefaultLogger(io.Writer, LogLevel) to create a logger that writes log messages of at least a certain log level to a specified io.Writer. The logger defaults to a stdout writer of min level "info". The "xraylog" package is the user visible logging API. It contains "xray" in the package name so it doesn't annoyingly look like every other "logger" package (e.g. to goimports). The user can set a new logger using xray.SetLogger(). The actual logging instance and logging wrapper methods are in an internal "logger" package hidden from the user. There are Info(...interface{}) and Infof(string, ...interface{}) varieties for each log level. In addition there is a DebugDeferred(func() string) takes an arbitrary function (which won't get called unless the log message is actually logged). --- awsplugins/beanstalk/beanstalk.go | 6 +- awsplugins/ec2/ec2.go | 6 +- awsplugins/ecs/ecs.go | 4 +- daemoncfg/daemon_config.go | 8 +- internal/logger/logger.go | 78 +++++++++++++++++ internal/logger/logger_test.go | 83 +++++++++++++++++++ strategy/ctxmissing/ctxmissing_test.go | 35 +++----- .../ctxmissing/default_context_missing.go | 6 +- strategy/sampling/centralized.go | 50 +++++------ strategy/sampling/localized.go | 8 +- strategy/sampling/proxy.go | 4 +- strategy/sampling/sampling_rule.go | 8 +- xray/aws.go | 12 +-- xray/client.go | 5 +- xray/config.go | 74 ++++------------- xray/config_test.go | 6 -- xray/default_emitter.go | 24 ++---- xray/default_streaming_strategy.go | 8 +- xray/handler.go | 4 +- xray/lambda.go | 6 +- xray/segment.go | 20 ++--- xraylog/xray_log.go | 77 +++++++++++++++++ 22 files changed, 348 insertions(+), 184 deletions(-) create mode 100644 internal/logger/logger.go create mode 100644 internal/logger/logger_test.go create mode 100644 xraylog/xray_log.go diff --git a/awsplugins/beanstalk/beanstalk.go b/awsplugins/beanstalk/beanstalk.go index f10f11f0..e153c065 100644 --- a/awsplugins/beanstalk/beanstalk.go +++ b/awsplugins/beanstalk/beanstalk.go @@ -12,8 +12,8 @@ import ( "encoding/json" "io/ioutil" + "github.com/aws/aws-xray-sdk-go/internal/logger" "github.com/aws/aws-xray-sdk-go/internal/plugins" - log "github.com/cihub/seelog" ) const Origin = "AWS::ElasticBeanstalk::Environment" @@ -29,14 +29,14 @@ func addPluginMetadata(pluginmd *plugins.PluginMetadata) { rawConfig, err := ioutil.ReadFile(ebConfigPath) if err != nil { - log.Errorf("Unable to read Elastic Beanstalk configuration file %s: %v", ebConfigPath, err) + logger.Errorf("Unable to read Elastic Beanstalk configuration file %s: %v", ebConfigPath, err) return } config := &plugins.BeanstalkMetadata{} err = json.Unmarshal(rawConfig, config) if err != nil { - log.Errorf("Unable to unmarshal Elastic Beanstalk configuration file %s: %v", ebConfigPath, err) + logger.Errorf("Unable to unmarshal Elastic Beanstalk configuration file %s: %v", ebConfigPath, err) return } diff --git a/awsplugins/ec2/ec2.go b/awsplugins/ec2/ec2.go index de36aa4d..6dbbe3e0 100644 --- a/awsplugins/ec2/ec2.go +++ b/awsplugins/ec2/ec2.go @@ -11,8 +11,8 @@ package ec2 import ( "github.com/aws/aws-sdk-go/aws/ec2metadata" "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-xray-sdk-go/internal/logger" "github.com/aws/aws-xray-sdk-go/internal/plugins" - log "github.com/cihub/seelog" ) const Origin = "AWS::EC2::Instance" @@ -26,13 +26,13 @@ func Init() { func addPluginMetadata(pluginmd *plugins.PluginMetadata) { session, e := session.NewSession() if e != nil { - log.Errorf("Unable to create a new ec2 session: %v", e) + logger.Errorf("Unable to create a new ec2 session: %v", e) return } client := ec2metadata.New(session) doc, err := client.GetInstanceIdentityDocument() if err != nil { - log.Errorf("Unable to read EC2 instance metadata: %v", err) + logger.Errorf("Unable to read EC2 instance metadata: %v", err) return } diff --git a/awsplugins/ecs/ecs.go b/awsplugins/ecs/ecs.go index 248f53da..954ee26a 100644 --- a/awsplugins/ecs/ecs.go +++ b/awsplugins/ecs/ecs.go @@ -11,8 +11,8 @@ package ecs import ( "os" + "github.com/aws/aws-xray-sdk-go/internal/logger" "github.com/aws/aws-xray-sdk-go/internal/plugins" - log "github.com/cihub/seelog" ) const Origin = "AWS::ECS::Container" @@ -27,7 +27,7 @@ func addPluginMetadata(pluginmd *plugins.PluginMetadata) { hostname, err := os.Hostname() if err != nil { - log.Errorf("Unable to retrieve hostname from OS. %v", err) + logger.Errorf("Unable to retrieve hostname from OS. %v", err) return } diff --git a/daemoncfg/daemon_config.go b/daemoncfg/daemon_config.go index 26f00eee..ec74658b 100644 --- a/daemoncfg/daemon_config.go +++ b/daemoncfg/daemon_config.go @@ -13,8 +13,7 @@ import ( "strconv" "strings" - log "github.com/cihub/seelog" - + "github.com/aws/aws-xray-sdk-go/internal/logger" "github.com/pkg/errors" ) @@ -69,7 +68,7 @@ func GetDaemonEndpointsFromString(dAddr string) (*DaemonEndpoints, error) { // Try to get the X-Ray daemon address from an environment variable if envDaemonAddr := os.Getenv("AWS_XRAY_DAEMON_ADDRESS"); envDaemonAddr != "" { daemonAddr = envDaemonAddr - log.Infof("using daemon endpoints from environment variable AWS_XRAY_DAEMON_ADDRESS: %v", envDaemonAddr) + logger.Infof("using daemon endpoints from environment variable AWS_XRAY_DAEMON_ADDRESS: %v", envDaemonAddr) } else if dAddr != "" { daemonAddr = dAddr } @@ -85,12 +84,9 @@ func resolveAddress(dAddr string) (*DaemonEndpoints, error) { return parseSingleForm(addr[0]) } else if len(addr) == 2 { return parseDoubleForm(addr) - } else { return nil, errors.New("invalid daemon address: " + dAddr) } - - return nil, nil } func parseDoubleForm(addr []string) (*DaemonEndpoints, error) { diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 00000000..b3c99a90 --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,78 @@ +// Copyright 2017-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + +package logger + +import ( + "fmt" + "os" + + "github.com/aws/aws-xray-sdk-go/xraylog" +) + +// This internal pacakge hides the actual logging functions from the user. + +// The Logger instance used by xray to log. Set via xray.SetLogger(). +var Logger xraylog.Logger = xraylog.NewDefaultLogger(os.Stdout, xraylog.LogLevelInfo) + +func Debugf(format string, args ...interface{}) { + Logger.Log(xraylog.LogLevelDebug, printfArgs{format, args}) +} + +func Debug(args ...interface{}) { + Logger.Log(xraylog.LogLevelDebug, printArgs(args)) +} + +func DebugDeferred(fn func() string) { + Logger.Log(xraylog.LogLevelDebug, stringerFunc(fn)) +} + +func Infof(format string, args ...interface{}) { + Logger.Log(xraylog.LogLevelInfo, printfArgs{format, args}) +} + +func Info(args ...interface{}) { + Logger.Log(xraylog.LogLevelInfo, printArgs(args)) +} + +func Warnf(format string, args ...interface{}) { + Logger.Log(xraylog.LogLevelWarn, printfArgs{format, args}) +} + +func Warn(args ...interface{}) { + Logger.Log(xraylog.LogLevelWarn, printArgs(args)) +} + +func Errorf(format string, args ...interface{}) { + Logger.Log(xraylog.LogLevelError, printfArgs{format, args}) +} + +func Error(args ...interface{}) { + Logger.Log(xraylog.LogLevelError, printArgs(args)) +} + +type printfArgs struct { + format string + args []interface{} +} + +func (p printfArgs) String() string { + return fmt.Sprintf(p.format, p.args...) +} + +type printArgs []interface{} + +func (p printArgs) String() string { + return fmt.Sprint([]interface{}(p)...) +} + +type stringerFunc func() string + +func (sf stringerFunc) String() string { + return sf() +} diff --git a/internal/logger/logger_test.go b/internal/logger/logger_test.go new file mode 100644 index 00000000..fa1d58cc --- /dev/null +++ b/internal/logger/logger_test.go @@ -0,0 +1,83 @@ +// Copyright 2017-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + +package logger + +import ( + "bytes" + "strings" + "testing" + + "github.com/aws/aws-xray-sdk-go/xraylog" +) + +func TestLogger(t *testing.T) { + oldLogger := Logger + defer func() { Logger = oldLogger }() + + var buf bytes.Buffer + + // filter properly by level + Logger = xraylog.NewDefaultLogger(&buf, xraylog.LogLevelWarn) + + Debug("debug") + Info("info") + Warn("warn") + Error("error") + + gotLines := strings.Split(strings.TrimSpace(buf.String()), "\n") + if len(gotLines) != 2 { + t.Fatalf("got %d lines", len(gotLines)) + } + + if !strings.Contains(gotLines[0], "[WARN] warn") { + t.Error("expected first line to be warn") + } + + if !strings.Contains(gotLines[1], "[ERROR] error") { + t.Error("expected second line to be warn") + } +} + +func TestDeferredDebug(t *testing.T) { + oldLogger := Logger + defer func() { Logger = oldLogger }() + + var buf bytes.Buffer + + Logger = xraylog.NewDefaultLogger(&buf, xraylog.LogLevelInfo) + + var called bool + DebugDeferred(func() string { + called = true + return "deferred" + }) + + if called { + t.Error("deferred should not have been called") + } + + if buf.String() != "" { + t.Errorf("unexpected log contents: %s", buf.String()) + } + + Logger = xraylog.NewDefaultLogger(&buf, xraylog.LogLevelDebug) + + DebugDeferred(func() string { + called = true + return "deferred" + }) + + if !called { + t.Error("deferred should have been called") + } + + if !strings.Contains(buf.String(), "[DEBUG] deferred") { + t.Errorf("expected deferred message, got %s", buf.String()) + } +} diff --git a/strategy/ctxmissing/ctxmissing_test.go b/strategy/ctxmissing/ctxmissing_test.go index daab2790..57f44033 100644 --- a/strategy/ctxmissing/ctxmissing_test.go +++ b/strategy/ctxmissing/ctxmissing_test.go @@ -9,30 +9,14 @@ package ctxmissing import ( - log "github.com/cihub/seelog" - "github.com/stretchr/testify/assert" + "bytes" "strings" "testing" -) - -type LogWriter struct { - Logs []string -} - -func (sw *LogWriter) Write(p []byte) (n int, err error) { - sw.Logs = append(sw.Logs, string(p)) - return len(p), nil -} -func LogSetup() *LogWriter { - writer := &LogWriter{} - logger, err := log.LoggerFromWriterWithMinLevelAndFormat(writer, log.TraceLvl, "%Ns [%Level] %Msg") - if err != nil { - panic(err) - } - log.ReplaceLogger(logger) - return writer -} + "github.com/aws/aws-xray-sdk-go/internal/logger" + "github.com/aws/aws-xray-sdk-go/xraylog" + "github.com/stretchr/testify/assert" +) func TestDefaultRuntimeErrorStrategy(t *testing.T) { defer func() { @@ -45,8 +29,13 @@ func TestDefaultRuntimeErrorStrategy(t *testing.T) { } func TestDefaultLogErrorStrategy(t *testing.T) { - logger := LogSetup() + oldLogger := logger.Logger + defer func() { logger.Logger = oldLogger }() + + var buf bytes.Buffer + logger.Logger = xraylog.NewDefaultLogger(&buf, xraylog.LogLevelDebug) + l := NewDefaultLogErrorStrategy() l.ContextMissing("TestLogError") - assert.True(t, strings.Contains(logger.Logs[0], "Suppressing AWS X-Ray context missing panic: TestLogError")) + assert.True(t, strings.Contains(buf.String(), "Suppressing AWS X-Ray context missing panic: TestLogError")) } diff --git a/strategy/ctxmissing/default_context_missing.go b/strategy/ctxmissing/default_context_missing.go index 6983497c..c9d61039 100644 --- a/strategy/ctxmissing/default_context_missing.go +++ b/strategy/ctxmissing/default_context_missing.go @@ -8,9 +8,7 @@ package ctxmissing -import ( - log "github.com/cihub/seelog" -) +import "github.com/aws/aws-xray-sdk-go/internal/logger" // RuntimeErrorStrategy provides the AWS_XRAY_CONTEXT_MISSING // environment variable value for enabling the runtime error @@ -50,5 +48,5 @@ func (dr *DefaultRuntimeErrorStrategy) ContextMissing(v interface{}) { // ContextMissing logs an error message when the // segment context is missing. func (dl *DefaultLogErrorStrategy) ContextMissing(v interface{}) { - log.Errorf("Suppressing AWS X-Ray context missing panic: %v", v) + logger.Errorf("Suppressing AWS X-Ray context missing panic: %v", v) } diff --git a/strategy/sampling/centralized.go b/strategy/sampling/centralized.go index 175c4835..8f678d5c 100644 --- a/strategy/sampling/centralized.go +++ b/strategy/sampling/centralized.go @@ -17,12 +17,12 @@ import ( "time" "github.com/aws/aws-xray-sdk-go/daemoncfg" + "github.com/aws/aws-xray-sdk-go/internal/logger" "github.com/aws/aws-xray-sdk-go/internal/plugins" "github.com/aws/aws-xray-sdk-go/utils" xraySvc "github.com/aws/aws-sdk-go/service/xray" - log "github.com/cihub/seelog" ) // CentralizedStrategy is an implementation of SamplingStrategy. It @@ -135,7 +135,7 @@ func (ss *CentralizedStrategy) ShouldTrace(request *Request) *Decision { if request.ServiceType == "" { request.ServiceType = plugins.InstancePluginMetadata.Origin } - log.Tracef( + logger.Debugf( "Determining ShouldTrace decision for:\n\thost: %s\n\tpath: %s\n\tmethod: %s\n\tservicename: %s\n\tservicetype: %s", request.Host, request.Url, @@ -146,7 +146,7 @@ func (ss *CentralizedStrategy) ShouldTrace(request *Request) *Decision { // Use fallback if manifest is expired if ss.manifest.expired() { - log.Trace("Centralized sampling data expired. Using fallback sampling strategy") + logger.Debug("Centralized sampling data expired. Using fallback sampling strategy") return ss.fallback.ShouldTrace(request) } @@ -165,20 +165,20 @@ func (ss *CentralizedStrategy) ShouldTrace(request *Request) *Decision { continue } - log.Tracef("Applicable rule: %s", r.ruleName) + logger.Debugf("Applicable rule: %s", r.ruleName) return r.Sample() } // Match against default rule if r := ss.manifest.Default; r != nil { - log.Tracef("Applicable rule: %s", r.ruleName) + logger.Debugf("Applicable rule: %s", r.ruleName) return r.Sample() } // Use fallback if default rule is unavailable - log.Trace("Centralized default sampling rule unavailable. Using fallback sampling strategy") + logger.Debug("Centralized default sampling rule unavailable. Using fallback sampling strategy") return ss.fallback.ShouldTrace(request) } @@ -207,9 +207,9 @@ func (ss *CentralizedStrategy) startRulePoller() { // Initial refresh go func() { if err := ss.refreshManifest(); err != nil { - log.Tracef("Error occurred during initial refresh of sampling rules. %v", err) + logger.Debugf("Error occurred during initial refresh of sampling rules. %v", err) } else { - log.Info("Successfully fetched sampling rules") + logger.Info("Successfully fetched sampling rules") } }() @@ -221,9 +221,9 @@ func (ss *CentralizedStrategy) startRulePoller() { for range t.C() { t.Reset() if err := ss.refreshManifest(); err != nil { - log.Tracef("Error occurred while refreshing sampling rules. %v", err) + logger.Debugf("Error occurred while refreshing sampling rules. %v", err) } else { - log.Info("Successfully fetched sampling rules") + logger.Info("Successfully fetched sampling rules") } } }() @@ -239,7 +239,7 @@ func (ss *CentralizedStrategy) startTargetPoller() { for range t.C() { t.Reset() if err := ss.refreshTargets(); err != nil { - log.Tracef("Error occurred while refreshing targets for sampling rules. %v", err) + logger.Debugf("Error occurred while refreshing targets for sampling rules. %v", err) } } }() @@ -277,43 +277,43 @@ func (ss *CentralizedStrategy) refreshManifest() (err error) { // Extract rule from record svcRule := record.SamplingRule if svcRule == nil { - log.Trace("Sampling rule missing from sampling rule record.") + logger.Debug("Sampling rule missing from sampling rule record.") failed = true continue } if svcRule.RuleName == nil { - log.Trace("Sampling rule without rule name is not supported") + logger.Debug("Sampling rule without rule name is not supported") failed = true continue } // Only sampling rule with version 1 is valid if svcRule.Version == nil { - log.Trace("Sampling rule without version number is not supported: ", *svcRule.RuleName) + logger.Debug("Sampling rule without version number is not supported: ", *svcRule.RuleName) failed = true continue } version := *svcRule.Version if version != int64(1) { - log.Trace("Sampling rule without version 1 is not supported: ", *svcRule.RuleName) + logger.Debug("Sampling rule without version 1 is not supported: ", *svcRule.RuleName) failed = true continue } if len(svcRule.Attributes) != 0 { - log.Trace("Sampling rule with non nil Attributes is not applicable: ", *svcRule.RuleName) + logger.Debug("Sampling rule with non nil Attributes is not applicable: ", *svcRule.RuleName) continue } if svcRule.ResourceARN == nil { - log.Trace("Sampling rule without ResourceARN is not applicable: ", *svcRule.RuleName) + logger.Debug("Sampling rule without ResourceARN is not applicable: ", *svcRule.RuleName) continue } resourceARN := *svcRule.ResourceARN if resourceARN != "*" { - log.Trace("Sampling rule with ResourceARN not equal to * is not applicable: ", *svcRule.RuleName) + logger.Debug("Sampling rule with ResourceARN not equal to * is not applicable: ", *svcRule.RuleName) continue } @@ -321,7 +321,7 @@ func (ss *CentralizedStrategy) refreshManifest() (err error) { r, putErr := ss.manifest.putRule(svcRule) if putErr != nil { failed = true - log.Tracef("Error occurred creating/updating rule. %v", putErr) + logger.Debugf("Error occurred creating/updating rule. %v", putErr) } else if r != nil { actives[r] = true } @@ -368,7 +368,7 @@ func (ss *CentralizedStrategy) refreshTargets() (err error) { // Do not refresh targets if no statistics to report if len(statistics) == 0 { - log.Tracef("No statistics to report. Not refreshing sampling targets.") + logger.Debugf("No statistics to report. Not refreshing sampling targets.") return nil } @@ -382,13 +382,13 @@ func (ss *CentralizedStrategy) refreshTargets() (err error) { for _, t := range output.SamplingTargetDocuments { if err = ss.updateTarget(t); err != nil { failed = true - log.Tracef("Error occurred updating target for rule. %v", err) + logger.Debugf("Error occurred updating target for rule. %v", err) } } // Consume unprocessed statistics messages for _, s := range output.UnprocessedStatistics { - log.Tracef( + logger.Debugf( "Error occurred updating sampling target for rule: %s, code: %s, message: %s", s.RuleName, s.ErrorCode, @@ -415,7 +415,7 @@ func (ss *CentralizedStrategy) refreshTargets() (err error) { if failed { err = errors.New("error occurred updating sampling targets") } else { - log.Info("Successfully refreshed sampling targets") + logger.Info("Successfully refreshed sampling targets") } // Set refresh flag if modifiedAt timestamp from remote is greater than ours. @@ -430,11 +430,11 @@ func (ss *CentralizedStrategy) refreshTargets() (err error) { } // Perform out-of-band async manifest refresh if flag is set if refresh { - log.Info("Refreshing sampling rules out-of-band.") + logger.Infof("Refreshing sampling rules out-of-band.") go func() { if err = ss.refreshManifest(); err != nil { - log.Tracef("Error occurred refreshing sampling rules out-of-band. %v", err) + logger.Debugf("Error occurred refreshing sampling rules out-of-band. %v", err) } }() } diff --git a/strategy/sampling/localized.go b/strategy/sampling/localized.go index c27799f5..df05cb66 100644 --- a/strategy/sampling/localized.go +++ b/strategy/sampling/localized.go @@ -9,8 +9,8 @@ package sampling import ( + "github.com/aws/aws-xray-sdk-go/internal/logger" "github.com/aws/aws-xray-sdk-go/resources" - log "github.com/cihub/seelog" ) // LocalizedStrategy makes trace sampling decisions based on @@ -60,15 +60,15 @@ func NewLocalizedStrategyFromJSONBytes(b []byte) (*LocalizedStrategy, error) { // ShouldTrace consults the LocalizedStrategy's rule set to determine // if the given request should be traced or not. func (lss *LocalizedStrategy) ShouldTrace(rq *Request) *Decision { - log.Tracef("Determining ShouldTrace decision for:\n\thost: %s\n\tpath: %s\n\tmethod: %s", rq.Host, rq.Url, rq.Method) + logger.Debugf("Determining ShouldTrace decision for:\n\thost: %s\n\tpath: %s\n\tmethod: %s", rq.Host, rq.Url, rq.Method) if nil != lss.manifest.Rules { for _, r := range lss.manifest.Rules { if r.AppliesTo(rq.Host, rq.Url, rq.Method) { - log.Tracef("Applicable rule:\n\tfixed_target: %d\n\trate: %f\n\thost: %s\n\turl_path: %s\n\thttp_method: %s", r.FixedTarget, r.Rate, r.Host, r.URLPath, r.HTTPMethod) + logger.Debugf("Applicable rule:\n\tfixed_target: %d\n\trate: %f\n\thost: %s\n\turl_path: %s\n\thttp_method: %s", r.FixedTarget, r.Rate, r.Host, r.URLPath, r.HTTPMethod) return r.Sample() } } } - log.Tracef("Default rule applies:\n\tfixed_target: %d\n\trate: %f", lss.manifest.Default.FixedTarget, lss.manifest.Default.Rate) + logger.Debugf("Default rule applies:\n\tfixed_target: %d\n\trate: %f", lss.manifest.Default.FixedTarget, lss.manifest.Default.Rate) return lss.manifest.Default.Sample() } diff --git a/strategy/sampling/proxy.go b/strategy/sampling/proxy.go index 0165cdd2..958e3a49 100644 --- a/strategy/sampling/proxy.go +++ b/strategy/sampling/proxy.go @@ -16,7 +16,7 @@ import ( "github.com/aws/aws-sdk-go/aws/session" xraySvc "github.com/aws/aws-sdk-go/service/xray" "github.com/aws/aws-xray-sdk-go/daemoncfg" - log "github.com/cihub/seelog" + "github.com/aws/aws-xray-sdk-go/internal/logger" ) // proxy is an implementation of svcProxy that forwards requests to the XRay daemon @@ -31,7 +31,7 @@ func NewProxy(d *daemoncfg.DaemonEndpoints) (svcProxy, error) { if d == nil { d = daemoncfg.GetDaemonEndpoints() } - log.Infof("X-Ray proxy using address : %v", d.TCPAddr.String()) + logger.Infof("X-Ray proxy using address : %v", d.TCPAddr.String()) url := "http://" + d.TCPAddr.String() // Endpoint resolver for proxying requests through the daemon diff --git a/strategy/sampling/sampling_rule.go b/strategy/sampling/sampling_rule.go index bc7776dc..59d4edcd 100644 --- a/strategy/sampling/sampling_rule.go +++ b/strategy/sampling/sampling_rule.go @@ -11,11 +11,11 @@ package sampling import ( "sync" + "github.com/aws/aws-xray-sdk-go/internal/logger" "github.com/aws/aws-xray-sdk-go/pattern" "github.com/aws/aws-xray-sdk-go/utils" xraySvc "github.com/aws/aws-sdk-go/service/xray" - log "github.com/cihub/seelog" ) // Properties is the base set of properties that define a sampling rule. @@ -113,7 +113,7 @@ func (r *CentralizedRule) Sample() *Decision { // Fallback to bernoulli sampling if quota has expired if r.reservoir.expired(now) { if r.reservoir.borrow(now) { - log.Tracef( + logger.Debugf( "Sampling target has expired for rule %s. Borrowing a request.", r.ruleName, ) @@ -123,7 +123,7 @@ func (r *CentralizedRule) Sample() *Decision { return sd } - log.Tracef( + logger.Debugf( "Sampling target has expired for rule %s. Using fixed rate.", r.ruleName, ) @@ -140,7 +140,7 @@ func (r *CentralizedRule) Sample() *Decision { return sd } - log.Tracef( + logger.Debugf( "Sampling target has been exhausted for rule %s. Using fixed rate.", r.ruleName, ) diff --git a/xray/aws.go b/xray/aws.go index a5f99852..e3f0d059 100644 --- a/xray/aws.go +++ b/xray/aws.go @@ -21,8 +21,8 @@ import ( "github.com/aws/aws-sdk-go/aws/client" "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-xray-sdk-go/internal/logger" "github.com/aws/aws-xray-sdk-go/resources" - log "github.com/cihub/seelog" ) const RequestIDKey string = "request_id" @@ -225,7 +225,7 @@ func parseWhitelistJSON(filename string) []byte { if filename != "" { readBytes, err := ioutil.ReadFile(filename) if err != nil { - log.Errorf("Error occurred while reading customized AWS whitelist JSON file. %v \nReverting to default AWS whitelist JSON file.", err) + logger.Errorf("Error occurred while reading customized AWS whitelist JSON file. %v \nReverting to default AWS whitelist JSON file.", err) } else { return readBytes } @@ -244,7 +244,7 @@ func keyValue(r interface{}, tag string) interface{} { v = v.Elem() } if v.Kind() != reflect.Struct { - log.Errorf("keyValue only accepts structs; got %T", v) + logger.Errorf("keyValue only accepts structs; got %T", v) } typ := v.Type() for i := 1; i < v.NumField(); i++ { @@ -327,7 +327,7 @@ func extractParameters(whitelistKey string, rType int, r *request.Request, white if params != nil { children, err := params.children() if err != nil { - log.Errorf("failed to get values for aws attribute: %v", err) + logger.Errorf("failed to get values for aws attribute: %v", err) return } for _, child := range children { @@ -351,7 +351,7 @@ func extractDescriptors(whitelistKey string, rType int, r *request.Request, whit if responseDtr != nil { items, err := responseDtr.childrenMap() if err != nil { - log.Errorf("failed to get values for aws attribute: %v", err) + logger.Errorf("failed to get values for aws attribute: %v", err) return } for k := range items { @@ -374,7 +374,7 @@ func descriptorType(descriptorMap map[string]interface{}) string { } else if descriptorMap["value"] != nil { typeValue = "value" } else { - log.Error("Missing keys in request / response descriptors in AWS whitelist JSON file.") + logger.Error("Missing keys in request / response descriptors in AWS whitelist JSON file.") } return typeValue } diff --git a/xray/client.go b/xray/client.go index a8b35ee8..cf22386a 100644 --- a/xray/client.go +++ b/xray/client.go @@ -10,10 +10,11 @@ package xray import ( "context" - log "github.com/cihub/seelog" "net/http" "net/http/httptrace" "strconv" + + "github.com/aws/aws-xray-sdk-go/internal/logger" ) const emptyHostRename = "empty_host_error" @@ -66,7 +67,7 @@ func (rt *roundtripper) RoundTrip(r *http.Request) (*http.Response, error) { seg := GetSegment(ctx) if seg == nil { resp, err = rt.Base.RoundTrip(r) - log.Warnf("failed to record HTTP transaction: segment cannot be found.") + logger.Warnf("failed to record HTTP transaction: segment cannot be found.") return err } diff --git a/xray/config.go b/xray/config.go index 2bce4a85..3eb90bbe 100644 --- a/xray/config.go +++ b/xray/config.go @@ -10,17 +10,17 @@ package xray import ( "context" - "fmt" "net" "os" "sync" "github.com/aws/aws-xray-sdk-go/daemoncfg" + "github.com/aws/aws-xray-sdk-go/internal/logger" + "github.com/aws/aws-xray-sdk-go/xraylog" "github.com/aws/aws-xray-sdk-go/strategy/ctxmissing" "github.com/aws/aws-xray-sdk-go/strategy/exception" "github.com/aws/aws-xray-sdk-go/strategy/sampling" - log "github.com/cihub/seelog" ) // SDKVersion records the current X-Ray Go SDK version. @@ -36,14 +36,17 @@ type SDK struct { RuleName string `json:"sampling_rule_name,omitempty"` } +// The logger instance used by xray. Only set from init() functions as +// SetLogger is not goroutine safe. +func SetLogger(l xraylog.Logger) { + logger.Logger = l +} + var globalCfg = newGlobalConfig() func newGlobalConfig() *globalConfig { ret := &globalConfig{} - // Set the logging configuration to the defaults - ret.logLevel, ret.logFormat = loadLogConfig("", "") - ret.daemonAddr = daemoncfg.GetDaemonEndpoints().UDPAddr ss, err := sampling.NewCentralizedStrategy() @@ -87,8 +90,6 @@ type globalConfig struct { streamingStrategy StreamingStrategy exceptionFormattingStrategy exception.FormattingStrategy contextMissingStrategy ctxmissing.Strategy - logLevel log.LogLevel - logFormat string } // Config is a set of X-Ray configurations. @@ -100,8 +101,12 @@ type Config struct { StreamingStrategy StreamingStrategy ExceptionFormattingStrategy exception.FormattingStrategy ContextMissingStrategy ctxmissing.Strategy - LogLevel string - LogFormat string + + // LogLevel and LogFormat are deprecated and no longer have any effect. + // See SetLogger() and the associated xraylog.Logger interface to control + // logging. + LogLevel string + LogFormat string } // ContextWithConfig returns context with given configuration settings. @@ -132,8 +137,6 @@ func ContextWithConfig(ctx context.Context, c Config) (context.Context, error) { } } - loadLogConfig(c.LogLevel, c.LogFormat) - var err error switch len(errors) { case 0: @@ -206,8 +209,6 @@ func Configure(c Config) error { globalCfg.serviceVersion = c.ServiceVersion } - globalCfg.logLevel, globalCfg.logFormat = loadLogConfig(c.LogLevel, c.LogFormat) - switch len(errors) { case 0: return nil @@ -218,41 +219,6 @@ func Configure(c Config) error { } } -func loadLogConfig(logLevel string, logFormat string) (log.LogLevel, string) { - var level log.LogLevel - var format string - - switch logLevel { - case "trace": - level = log.TraceLvl - case "debug": - level = log.DebugLvl - case "info": - level = log.InfoLvl - case "warn": - level = log.WarnLvl - case "error": - level = log.ErrorLvl - default: - level = log.InfoLvl - logLevel = "info" - } - - if logFormat != "" { - format = logFormat - } else { - format = "%Date(2006-01-02T15:04:05Z07:00) [%Level] %Msg%n" - } - - writer, _ := log.NewConsoleWriter() - logger, err := log.LoggerFromWriterWithMinLevelAndFormat(writer, level, format) - if err != nil { - panic(fmt.Errorf("failed to create logs as StdOut: %v", err)) - } - log.ReplaceLogger(logger) - return level, format -} - func (c *globalConfig) DaemonAddr() *net.UDPAddr { c.RLock() defer c.RUnlock() @@ -288,15 +254,3 @@ func (c *globalConfig) ServiceVersion() string { defer c.RUnlock() return c.serviceVersion } - -func (c *globalConfig) LogLevel() log.LogLevel { - c.RLock() - defer c.RUnlock() - return c.logLevel -} - -func (c *globalConfig) LogFormat() string { - c.RLock() - defer c.RUnlock() - return c.logFormat -} diff --git a/xray/config_test.go b/xray/config_test.go index a3a924d6..0376dde1 100644 --- a/xray/config_test.go +++ b/xray/config_test.go @@ -132,15 +132,11 @@ func TestInvalidEnvironmentDaemonAddress(t *testing.T) { func TestDefaultConfigureParameters(t *testing.T) { daemonAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 2000} - logLevel := "info" - logFormat := "%Date(2006-01-02T15:04:05Z07:00) [%Level] %Msg%n" efs, _ := exception.NewDefaultFormattingStrategy() sms, _ := NewDefaultStreamingStrategy() cms := ctxmissing.NewDefaultRuntimeErrorStrategy() assert.Equal(t, daemonAddr, globalCfg.daemonAddr) - assert.Equal(t, logLevel, globalCfg.logLevel.String()) - assert.Equal(t, logFormat, globalCfg.logFormat) assert.Equal(t, efs, globalCfg.exceptionFormattingStrategy) assert.Equal(t, "", globalCfg.serviceVersion) assert.Equal(t, sms, globalCfg.streamingStrategy) @@ -170,8 +166,6 @@ func TestSetConfigureParameters(t *testing.T) { }) assert.Equal(t, &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 3000}, globalCfg.daemonAddr) - assert.Equal(t, logLevel, globalCfg.logLevel.String()) - assert.Equal(t, logFormat, globalCfg.logFormat) assert.Equal(t, ss, globalCfg.samplingStrategy) assert.Equal(t, efs, globalCfg.exceptionFormattingStrategy) assert.Equal(t, sms, globalCfg.streamingStrategy) diff --git a/xray/default_emitter.go b/xray/default_emitter.go index 4211efc2..befa63a7 100644 --- a/xray/default_emitter.go +++ b/xray/default_emitter.go @@ -14,7 +14,7 @@ import ( "net" "sync" - log "github.com/cihub/seelog" + "github.com/aws/aws-xray-sdk-go/internal/logger" ) // Header is added before sending segments to daemon. @@ -39,7 +39,7 @@ func NewDefaultEmitter(raddr *net.UDPAddr) (*DefaultEmitter, error) { func (de *DefaultEmitter) RefreshEmitterWithAddress(raddr *net.UDPAddr) { de.Lock() de.conn, _ = net.DialUDP("udp", nil, raddr) - log.Infof("Emitter using address: %v", raddr) + logger.Infof("Emitter using address: %v", raddr) de.Unlock() } @@ -49,23 +49,17 @@ func (de *DefaultEmitter) Emit(seg *Segment) { return } - var logLevel string - if seg.Configuration != nil && seg.Configuration.LogLevel == "trace" { - logLevel = "trace" - } else if globalCfg.logLevel <= log.TraceLvl { - logLevel = "trace" - } - for _, p := range packSegments(seg, nil) { - if logLevel == "trace" { - b := &bytes.Buffer{} - json.Indent(b, p, "", " ") - log.Trace(b.String()) - } + // defer expensive marshal until log message is actually logged + logger.DebugDeferred(func() string { + var b bytes.Buffer + json.Indent(&b, p, "", " ") + return b.String() + }) de.Lock() _, err := de.conn.Write(append(Header, p...)) if err != nil { - log.Error(err) + logger.Error(err) } de.Unlock() } diff --git a/xray/default_streaming_strategy.go b/xray/default_streaming_strategy.go index d763d313..38ad7d9b 100644 --- a/xray/default_streaming_strategy.go +++ b/xray/default_streaming_strategy.go @@ -12,7 +12,7 @@ import ( "encoding/json" "errors" - log "github.com/cihub/seelog" + "github.com/aws/aws-xray-sdk-go/internal/logger" ) var defaultMaxSubsegmentCount = 20 @@ -52,7 +52,7 @@ func (dSS *DefaultStreamingStrategy) RequiresStreaming(seg *Segment) bool { // StreamCompletedSubsegments separates subsegments from the provided // segment tree and sends them to daemon as streamed subsegment UDP packets. func (dSS *DefaultStreamingStrategy) StreamCompletedSubsegments(seg *Segment) [][]byte { - log.Trace("Beginning to stream subsegments.") + logger.Debug("Beginning to stream subsegments.") var outSegments [][]byte for i := 0; i < len(seg.rawSubsegments); i++ { child := seg.rawSubsegments[i] @@ -71,11 +71,11 @@ func (dSS *DefaultStreamingStrategy) StreamCompletedSubsegments(seg *Segment) [] child.beforeEmitSubsegment(seg) cb, _ := json.Marshal(child) outSegments = append(outSegments, cb) - log.Tracef("Streaming subsegment named '%s' from segment tree.", child.Name) + logger.Debugf("Streaming subsegment named '%s' from segment tree.", child.Name) child.Unlock() break } - log.Trace("Finished streaming subsegments.") + logger.Debug("Finished streaming subsegments.") return outSegments } diff --git a/xray/handler.go b/xray/handler.go index 2b99c14b..20ebc083 100644 --- a/xray/handler.go +++ b/xray/handler.go @@ -17,12 +17,12 @@ import ( "strconv" "strings" + "github.com/aws/aws-xray-sdk-go/internal/logger" "github.com/aws/aws-xray-sdk-go/strategy/sampling" "github.com/aws/aws-xray-sdk-go/header" "github.com/aws/aws-xray-sdk-go/internal/plugins" "github.com/aws/aws-xray-sdk-go/pattern" - log "github.com/cihub/seelog" ) // SegmentNamer is the interface for naming service node. @@ -147,7 +147,7 @@ func httpTrace(seg *Segment, h http.Handler, w http.ResponseWriter, r *http.Requ } sd := seg.ParentSegment.GetConfiguration().SamplingStrategy.ShouldTrace(samplingRequest) seg.Sampled = sd.Sample - log.Tracef("SamplingStrategy decided: %t", seg.Sampled) + logger.Debugf("SamplingStrategy decided: %t", seg.Sampled) seg.AddRuleName(sd) } if traceHeader.SamplingDecision == header.Requested { diff --git a/xray/lambda.go b/xray/lambda.go index 4a66cebc..ba26be8a 100644 --- a/xray/lambda.go +++ b/xray/lambda.go @@ -15,7 +15,7 @@ import ( "time" "github.com/aws/aws-xray-sdk-go/header" - log "github.com/cihub/seelog" + "github.com/aws/aws-xray-sdk-go/internal/logger" ) // LambdaTraceHeaderKey is key to get trace header from context. @@ -54,11 +54,11 @@ func initLambda() { now := time.Now() filePath, err := createFile(SDKInitializedFileFolder, SDKInitializedFileName) if err != nil { - log.Tracef("unable to create file at %s. failed to signal SDK initialization with error: %v", filePath, err) + logger.Debugf("unable to create file at %s. failed to signal SDK initialization with error: %v", filePath, err) } else { e := os.Chtimes(filePath, now, now) if e != nil { - log.Tracef("unable to write to %s. failed to signal SDK initialization with error: %v", filePath, e) + logger.Debugf("unable to write to %s. failed to signal SDK initialization with error: %v", filePath, e) } } } diff --git a/xray/segment.go b/xray/segment.go index c1ac2c2a..e2a30f6e 100644 --- a/xray/segment.go +++ b/xray/segment.go @@ -17,8 +17,8 @@ import ( "time" "github.com/aws/aws-xray-sdk-go/header" + "github.com/aws/aws-xray-sdk-go/internal/logger" "github.com/aws/aws-xray-sdk-go/internal/plugins" - log "github.com/cihub/seelog" ) // NewTraceID generates a string format of random trace ID. @@ -82,7 +82,7 @@ func basicSegment(name string, h *header.Header) *Segment { name = name[:200] } seg := &Segment{parent: nil} - log.Tracef("Beginning segment named %s", name) + logger.Debugf("Beginning segment named %s", name) seg.ParentSegment = seg seg.Lock() @@ -181,7 +181,7 @@ func BeginSubsegment(ctx context.Context, name string) (context.Context, *Segmen } seg := &Segment{parent: parent} - log.Tracef("Beginning subsegment named %s", name) + logger.Debugf("Beginning subsegment named %s", name) seg.Lock() defer seg.Unlock() @@ -220,9 +220,9 @@ func NewSegmentFromHeader(ctx context.Context, name string, h *header.Header) (c seg.Sampled = h.SamplingDecision == header.Sampled switch h.SamplingDecision { case header.Sampled: - log.Trace("Incoming header decided: Sampled=true") + logger.Debug("Incoming header decided: Sampled=true") case header.NotSampled: - log.Trace("Incoming header decided: Sampled=false") + logger.Debug("Incoming header decided: Sampled=false") } seg.IncomingHeader = h @@ -236,9 +236,9 @@ func (seg *Segment) Close(err error) { seg.Lock() defer seg.Unlock() if seg.parent != nil { - log.Tracef("Closing subsegment named %s", seg.Name) + logger.Debugf("Closing subsegment named %s", seg.Name) } else { - log.Tracef("Closing segment named %s", seg.Name) + logger.Debugf("Closing segment named %s", seg.Name) } seg.EndTime = float64(time.Now().UnixNano()) / float64(time.Second) seg.InProgress = false @@ -255,12 +255,12 @@ func (subseg *Segment) CloseAndStream(err error) { subseg.Lock() if subseg.parent != nil { - log.Tracef("Ending subsegment named: %s", subseg.Name) + logger.Debugf("Ending subsegment named: %s", subseg.Name) subseg.EndTime = float64(time.Now().UnixNano()) / float64(time.Second) subseg.InProgress = false subseg.Emitted = true if subseg.parent.RemoveSubsegment(subseg) { - log.Tracef("Removing subsegment named: %s", subseg.Name) + logger.Debugf("Removing subsegment named: %s", subseg.Name) } } @@ -315,7 +315,7 @@ func (seg *Segment) flush() { } else if seg.parent != nil && seg.parent.Facade { seg.Emitted = true seg.beforeEmitSubsegment(seg.parent) - log.Tracef("emit lambda subsegment named: %v", seg.Name) + logger.Debugf("emit lambda subsegment named: %v", seg.Name) seg.emit() } else { seg.parent.safeFlush() diff --git a/xraylog/xray_log.go b/xraylog/xray_log.go new file mode 100644 index 00000000..b4d80c86 --- /dev/null +++ b/xraylog/xray_log.go @@ -0,0 +1,77 @@ +// Copyright 2017-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + +package xraylog + +import ( + "fmt" + "io" + "time" +) + +// Logger is the logging interface used by xray. fmt.Stringer is used to +// defer expensive serialization operations until the message is actually +// logged (i.e. don't bother serializing debug messages if they aren't going +// to show up). +type Logger interface { + Log(level LogLevel, msg fmt.Stringer) +} + +// LogLevel represents the severity of a log message, where a higher value +// means more severe. The integer value should not be serialized as it is +// subject to change. +type LogLevel int + +const ( + LogLevelDebug LogLevel = iota + 1 + LogLevelInfo + LogLevelWarn + LogLevelError +) + +func (ll LogLevel) String() string { + switch ll { + case LogLevelDebug: + return "DEBUG" + case LogLevelInfo: + return "INFO" + case LogLevelWarn: + return "WARN" + case LogLevelError: + return "ERROR" + default: + return fmt.Sprintf("UNKNOWN<%d>", ll) + } +} + +// NewDefaultLogger makes a Logger object that writes newline separated +// messages to w, if the level of the message is at least minLogLevel. +func NewDefaultLogger(w io.Writer, minLogLevel LogLevel) Logger { + return &defaultLogger{w, minLogLevel} +} + +type defaultLogger struct { + w io.Writer + minLevel LogLevel +} + +func (l *defaultLogger) Log(ll LogLevel, msg fmt.Stringer) { + if ll < l.minLevel { + return + } + + fmt.Fprintf(l.w, "%s [%s] %s\n", time.Now().Format(time.RFC3339), ll, msg) +} + +// NullLogger can be used to disable logging (pass to xray.SetLogger()). +var NullLogger = nullLogger{} + +type nullLogger struct{} + +func (nl nullLogger) Log(ll LogLevel, msg fmt.Stringer) { +} From 470e95d1d285422ce1304545031b4ce37a8cb454 Mon Sep 17 00:00:00 2001 From: Muir Manders Date: Thu, 31 Jan 2019 13:59:16 -0800 Subject: [PATCH 2/3] Add README.md note about logger interface --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index f14c9add..9adc30c7 100644 --- a/README.md +++ b/README.md @@ -204,6 +204,31 @@ func init() { } ``` +**Logger** + +xray uses an interface for its logger: + +```go +type Logger interface { + Log(level LogLevel, msg fmt.Stringer) +} + +const ( + LogLevelDebug LogLevel = iota + 1 + LogLevelInfo + LogLevelWarn + LogLevelError +) +``` + +The default logger logs to stdout at "info" and above. To change the logger, call `xray.SetLogger(myLogger)`. There is a default logger implementation that writes to an `io.Writer` from a specified minimum log level. For example, to log to stderr at "error" and above: + +```go +xray.SetLogger(xraylog.NewDefaultLogger(os.Stderr, xraylog.LogLevelError)) +``` + +Note that the `xray.Config{}` fields `LogLevel` and `LogFormat` are deprecated and no longer have any effect. + ## License The AWS X-Ray SDK for Go is licensed under the Apache 2.0 License. See LICENSE and NOTICE.txt for more information. From e547f3934529909ea662310513f1adc395b51bd9 Mon Sep 17 00:00:00 2001 From: Muir Manders Date: Fri, 8 Feb 2019 15:06:45 -0800 Subject: [PATCH 3/3] Synchronize log messages in default logger. Use a mutex to synchronize calls to underling io.Writer. This avoids log message interleaving and potentially worse race conditions, depending on what the writer is. Also tweak the invalid LogLevel String() to be UNKNOWNLOGLEVEL instead of just UNKNOWN so it is easier to understand what is unknown if it ever shows up. --- xraylog/xray_log.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/xraylog/xray_log.go b/xraylog/xray_log.go index b4d80c86..5883bf91 100644 --- a/xraylog/xray_log.go +++ b/xraylog/xray_log.go @@ -11,6 +11,7 @@ package xraylog import ( "fmt" "io" + "sync" "time" ) @@ -19,6 +20,8 @@ import ( // logged (i.e. don't bother serializing debug messages if they aren't going // to show up). type Logger interface { + // Log can be called concurrently from multiple goroutines so make sure + // your implemenation is goroutine safe. Log(level LogLevel, msg fmt.Stringer) } @@ -45,17 +48,20 @@ func (ll LogLevel) String() string { case LogLevelError: return "ERROR" default: - return fmt.Sprintf("UNKNOWN<%d>", ll) + return fmt.Sprintf("UNKNOWNLOGLEVEL<%d>", ll) } } // NewDefaultLogger makes a Logger object that writes newline separated // messages to w, if the level of the message is at least minLogLevel. +// The default logger synchronizes around Write() calls to the underlying +// io.Writer. func NewDefaultLogger(w io.Writer, minLogLevel LogLevel) Logger { - return &defaultLogger{w, minLogLevel} + return &defaultLogger{w: w, minLevel: minLogLevel} } type defaultLogger struct { + sync.Mutex w io.Writer minLevel LogLevel } @@ -65,6 +71,8 @@ func (l *defaultLogger) Log(ll LogLevel, msg fmt.Stringer) { return } + l.Lock() + defer l.Unlock() fmt.Fprintf(l.w, "%s [%s] %s\n", time.Now().Format(time.RFC3339), ll, msg) }