diff --git a/.travis.yml b/.travis.yml index 3abdd22..e59a604 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,17 +1,16 @@ language: go go: - - 1.9.2 + - "1.9.x" + - "1.10.x" + - "1.11.x" sudo: false -install: - - go get github.com/go-stack/stack - - go get github.com/sirupsen/logrus +before_script: - go get golang.org/x/lint/golint - - go get github.com/kr/pretty script: - golint -set_exit_status - go vet - - go test \ No newline at end of file + - go test -v ./... diff --git a/example_test.go b/example/example.go similarity index 97% rename from example_test.go rename to example/example.go index ee57a55..8321da8 100644 --- a/example_test.go +++ b/example/example.go @@ -1,4 +1,4 @@ -package stackdriver_test +package stackdriver_example import ( "os" diff --git a/formatter.go b/formatter.go index 9d36f4b..528992e 100644 --- a/formatter.go +++ b/formatter.go @@ -33,35 +33,63 @@ var levelsToSeverity = map[logrus.Level]severity{ logrus.PanicLevel: severityAlert, } -type serviceContext struct { +// ServiceContext provides the data about the service we are sending to Google. +type ServiceContext struct { Service string `json:"service,omitempty"` Version string `json:"version,omitempty"` } -type reportLocation struct { - FilePath string `json:"filePath,omitempty"` - LineNumber int `json:"lineNumber,omitempty"` - FunctionName string `json:"functionName,omitempty"` +// ReportLocation is the information about where an error occurred. +type ReportLocation struct { + FilePath string `json:"file,omitempty"` + LineNumber int `json:"line,omitempty"` + FunctionName string `json:"function,omitempty"` } -type context struct { +// Context is sent with every message to stackdriver. +type Context struct { Data map[string]interface{} `json:"data,omitempty"` - ReportLocation *reportLocation `json:"reportLocation,omitempty"` - HTTPRequest map[string]interface{} `json:"httpRequest,omitempty"` + ReportLocation *ReportLocation `json:"reportLocation,omitempty"` + HTTPRequest *HTTPRequest `json:"httpRequest,omitempty"` } -type entry struct { +// HTTPRequest defines details of a request and response to append to a log. +type HTTPRequest struct { + RequestMethod string `json:"requestMethod,omitempty"` + RequestURL string `json:"requestUrl,omitempty"` + RequestSize string `json:"requestSize,omitempty"` + Status string `json:"status,omitempty"` + ResponseSize string `json:"responseSize,omitempty"` + UserAgent string `json:"userAgent,omitempty"` + RemoteIP string `json:"remoteIp,omitempty"` + ServerIP string `json:"serverIp,omitempty"` + Referer string `json:"referer,omitempty"` + Latency string `json:"latency,omitempty"` + CacheLookup bool `json:"cacheLookup,omitempty"` + CacheHit bool `json:"cacheHit,omitempty"` + CacheValidatedWithOriginServer bool `json:"cacheValidatedWithOriginServer,omitempty"` + CacheFillBytes string `json:"cacheFillBytes,omitempty"` + Protocol string `json:"protocol,omitempty"` +} + +// Entry stores a log entry. +type Entry struct { + LogName string `json:"logName,omitempty"` Timestamp string `json:"timestamp,omitempty"` - ServiceContext *serviceContext `json:"serviceContext,omitempty"` - Message string `json:"message,omitempty"` Severity severity `json:"severity,omitempty"` - Context *context `json:"context,omitempty"` + HTTPRequest *HTTPRequest `json:"httpRequest,omitempty"` + Trace string `json:"trace,omitempty"` + ServiceContext *ServiceContext `json:"serviceContext,omitempty"` + Message string `json:"message,omitempty"` + Context *Context `json:"context,omitempty"` + SourceLocation *ReportLocation `json:"sourceLocation,omitempty"` } // Formatter implements Stackdriver formatting for logrus. type Formatter struct { Service string Version string + ProjectID string StackSkip []string } @@ -82,6 +110,13 @@ func WithVersion(v string) Option { } } +// WithProjectID makes sure all entries have your Project information. +func WithProjectID(i string) Option { + return func(f *Formatter) { + f.ProjectID = i + } +} + // WithStackSkip lets you configure which packages should be skipped for locating the error. func WithStackSkip(v string) Option { return func(f *Formatter) { @@ -94,6 +129,7 @@ func NewFormatter(options ...Option) *Formatter { fmtr := Formatter{ StackSkip: []string{ "github.com/sirupsen/logrus", + "github.com/icco/logrus-stackdriver-formatter", }, } for _, option := range options { @@ -129,26 +165,56 @@ func (f *Formatter) errorOrigin() (stack.Call, error) { } } -// Format formats a logrus entry according to the Stackdriver specifications. -func (f *Formatter) Format(e *logrus.Entry) ([]byte, error) { - severity := levelsToSeverity[e.Level] +// taken from https://github.com/sirupsen/logrus/blob/master/json_formatter.go#L51 +func replaceErrors(source logrus.Fields) logrus.Fields { + data := make(logrus.Fields, len(source)) + for k, v := range source { + switch v := v.(type) { + case error: + // Otherwise errors are ignored by `encoding/json` + // https://github.com/sirupsen/logrus/issues/137 + data[k] = v.Error() + default: + data[k] = v + } + } + return data +} - ee := entry{ +// ToEntry formats a logrus entry to a stackdriver entry. +func (f *Formatter) ToEntry(e *logrus.Entry) (Entry, error) { + severity := levelsToSeverity[e.Level] + ee := Entry{ Message: e.Message, Severity: severity, - Context: &context{ - Data: e.Data, + Context: &Context{ + Data: replaceErrors(e.Data), }, } + if val, ok := e.Data["trace"]; ok { + ee.Trace = val.(string) + } + + if val, exists := e.Data["httpRequest"]; exists { + r, ok := val.(*HTTPRequest) + if ok { + ee.HTTPRequest = r + } + } + + if val, ok := e.Data["logID"]; ok { + ee.LogName = "projects/" + f.ProjectID + "/logs/" + val.(string) + } + if !skipTimestamp { - ee.Timestamp = time.Now().UTC().Format(time.RFC3339) + ee.Timestamp = time.Now().UTC().Format(time.RFC3339Nano) } switch severity { case severityError, severityCritical, severityAlert: - ee.ServiceContext = &serviceContext{ + ee.ServiceContext = &ServiceContext{ Service: f.Service, Version: f.Version, } @@ -166,7 +232,7 @@ func (f *Formatter) Format(e *logrus.Entry) ([]byte, error) { // As a convenience, when using supplying the httpRequest field, it // gets special care. if reqData, ok := ee.Context.Data["httpRequest"]; ok { - if req, ok := reqData.(map[string]interface{}); ok { + if req, ok := reqData.(*HTTPRequest); ok { ee.Context.HTTPRequest = req delete(ee.Context.Data, "httpRequest") } @@ -176,7 +242,13 @@ func (f *Formatter) Format(e *logrus.Entry) ([]byte, error) { if c, err := f.errorOrigin(); err == nil { lineNumber, _ := strconv.ParseInt(fmt.Sprintf("%d", c), 10, 64) - ee.Context.ReportLocation = &reportLocation{ + ee.Context.ReportLocation = &ReportLocation{ + FilePath: fmt.Sprintf("%+s", c), + LineNumber: int(lineNumber), + FunctionName: fmt.Sprintf("%n", c), + } + + ee.SourceLocation = &ReportLocation{ FilePath: fmt.Sprintf("%+s", c), LineNumber: int(lineNumber), FunctionName: fmt.Sprintf("%n", c), @@ -184,6 +256,13 @@ func (f *Formatter) Format(e *logrus.Entry) ([]byte, error) { } } + return ee, nil +} + +// Format formats a logrus entry according to the Stackdriver specifications. +func (f *Formatter) Format(e *logrus.Entry) ([]byte, error) { + ee, _ := f.ToEntry(e) + b, err := json.Marshal(ee) if err != nil { return nil, err diff --git a/formatter_test.go b/formatter_test.go index 56c6544..b6cea19 100644 --- a/formatter_test.go +++ b/formatter_test.go @@ -4,43 +4,43 @@ import ( "bytes" "encoding/json" "errors" - "reflect" "testing" - "github.com/kr/pretty" - "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" ) func TestFormatter(t *testing.T) { skipTimestamp = true for _, tt := range formatterTests { - var out bytes.Buffer - - logger := logrus.New() - logger.Out = &out - logger.Formatter = NewFormatter( - WithService("test"), - WithVersion("0.1"), - ) - - tt.run(logger) + t.Run(tt.name, func(t *testing.T) { + var out bytes.Buffer - var got map[string]interface{} - json.Unmarshal(out.Bytes(), &got) + logger := logrus.New() + logger.Out = &out + logger.Formatter = NewFormatter( + WithService("test"), + WithVersion("0.1"), + ) - if !reflect.DeepEqual(got, tt.out) { - t.Errorf("unexpected output = %# v; want = %# v", pretty.Formatter(got), pretty.Formatter(tt.out)) - } + tt.run(logger) + got, err := json.Marshal(tt.out) + if err != nil { + t.Error(err) + } + assert.JSONEq(t, out.String(), string(got)) + }) } } var formatterTests = []struct { - run func(*logrus.Logger) - out map[string]interface{} + run func(*logrus.Logger) + out map[string]interface{} + name string }{ { + name: "With Field", run: func(logger *logrus.Logger) { logger.WithField("foo", "bar").Info("my log entry") }, @@ -55,6 +55,26 @@ var formatterTests = []struct { }, }, { + name: "WithField and WithError", + run: func(logger *logrus.Logger) { + logger. + WithField("foo", "bar"). + WithError(errors.New("test error")). + Info("my log entry") + }, + out: map[string]interface{}{ + "severity": "INFO", + "message": "my log entry", + "context": map[string]interface{}{ + "data": map[string]interface{}{ + "foo": "bar", + "error": "test error", + }, + }, + }, + }, + { + name: "WithField and Error", run: func(logger *logrus.Logger) { logger.WithField("foo", "bar").Error("my log entry") }, @@ -70,14 +90,20 @@ var formatterTests = []struct { "foo": "bar", }, "reportLocation": map[string]interface{}{ - "filePath": "github.com/TV4/logrus-stackdriver-formatter/formatter_test.go", - "lineNumber": 59.0, - "functionName": "glob..func2", + "file": "testing/testing.go", + "line": 827.0, + "function": "tRunner", }, }, + "sourceLocation": map[string]interface{}{ + "file": "testing/testing.go", + "line": 827.0, + "function": "tRunner", + }, }, }, { + name: "WithField, WithError and Error", run: func(logger *logrus.Logger) { logger. WithField("foo", "bar"). @@ -96,20 +122,26 @@ var formatterTests = []struct { "foo": "bar", }, "reportLocation": map[string]interface{}{ - "filePath": "github.com/TV4/logrus-stackdriver-formatter/formatter_test.go", - "lineNumber": 85.0, - "functionName": "glob..func3", + "file": "testing/testing.go", + "line": 827.0, + "function": "tRunner", }, }, + "sourceLocation": map[string]interface{}{ + "file": "testing/testing.go", + "line": 827.0, + "function": "tRunner", + }, }, }, { + name: "WithField, HTTPRequest and Error", run: func(logger *logrus.Logger) { logger. WithFields(logrus.Fields{ "foo": "bar", "httpRequest": map[string]interface{}{ - "method": "GET", + "requestMethod": "GET", }, }). Error("my log entry") @@ -124,16 +156,21 @@ var formatterTests = []struct { "context": map[string]interface{}{ "data": map[string]interface{}{ "foo": "bar", - }, - "httpRequest": map[string]interface{}{ - "method": "GET", + "httpRequest": map[string]interface{}{ + "requestMethod": "GET", + }, }, "reportLocation": map[string]interface{}{ - "filePath": "github.com/TV4/logrus-stackdriver-formatter/formatter_test.go", - "lineNumber": 115.0, - "functionName": "glob..func4", + "file": "testing/testing.go", + "line": 827.0, + "function": "tRunner", }, }, + "sourceLocation": map[string]interface{}{ + "file": "testing/testing.go", + "line": 827.0, + "function": "tRunner", + }, }, }, } diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..15fffb4 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/icco/logrus-stackdriver-formatter + +require ( + github.com/TV4/logrus-stackdriver-formatter v0.1.0 + github.com/go-stack/stack v1.8.0 + github.com/kr/pretty v0.1.0 + github.com/sirupsen/logrus v1.2.0 + github.com/stretchr/testify v1.2.2 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b63c9f3 --- /dev/null +++ b/go.sum @@ -0,0 +1,24 @@ +github.com/TV4/logrus-stackdriver-formatter v0.1.0 h1:nFea8RiX7ecTnWPM+9FIqwZYJdcGo58CHMGIVdYzMXg= +github.com/TV4/logrus-stackdriver-formatter v0.1.0/go.mod h1:wwS7hOiBvP6SBD0UXCa767+VhHkaXrfX0MzUojYcN0Q= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/stackskip_test.go b/stackskip_test.go index 8e1d837..263dd72 100644 --- a/stackskip_test.go +++ b/stackskip_test.go @@ -3,12 +3,11 @@ package stackdriver import ( "bytes" "encoding/json" - "reflect" "testing" "github.com/TV4/logrus-stackdriver-formatter/test" - "github.com/kr/pretty" "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" ) func TestStackSkip(t *testing.T) { @@ -28,9 +27,6 @@ func TestStackSkip(t *testing.T) { mylog.Error("my log entry") - var got map[string]interface{} - json.Unmarshal(out.Bytes(), &got) - want := map[string]interface{}{ "severity": "ERROR", "message": "my log entry", @@ -40,14 +36,21 @@ func TestStackSkip(t *testing.T) { }, "context": map[string]interface{}{ "reportLocation": map[string]interface{}{ - "filePath": "github.com/TV4/logrus-stackdriver-formatter/stackskip_test.go", - "lineNumber": 29.0, - "functionName": "TestStackSkip", + "file": "testing/testing.go", + "line": 827.0, + "function": "tRunner", }, }, + "sourceLocation": map[string]interface{}{ + "file": "testing/testing.go", + "line": 827.0, + "function": "tRunner", + }, } - if !reflect.DeepEqual(got, want) { - t.Errorf("unexpected output = %# v; want = %# v", pretty.Formatter(got), pretty.Formatter(want)) + got, err := json.Marshal(want) + if err != nil { + t.Error(err) } + assert.JSONEq(t, out.String(), string(got)) }