diff --git a/README.md b/README.md index 4fa8393967c..cf4b00928c0 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ Google API | Status | Package [Translation][cloud-translation] | stable | [`cloud.google.com/go/translate`][cloud-translation-ref] [Trace][cloud-trace] | alpha | [`cloud.google.com/go/trace`][cloud-trace-ref] [Video Intelligence][cloud-video]| beta | [`cloud.google.com/go/videointelligence/apiv1beta1`][cloud-video-ref] -[ErrorReporting][cloud-errors] | alpha | [`cloud.google.com/go/errors`][cloud-errors-ref] +[ErrorReporting][cloud-errors] | alpha | [`cloud.google.com/go/errorreporting`][cloud-errors-ref] > **Alpha status**: the API is still being actively developed. As a @@ -455,6 +455,6 @@ for more information. [cloud-video-ref]: https://godoc.org/cloud.google.com/go/videointelligence/apiv1beta1 [cloud-errors]: https://cloud.google.com/error-reporting/ -[cloud-errors-ref]: https://godoc.org/cloud.google.com/go/errors +[cloud-errors-ref]: https://godoc.org/cloud.google.com/go/errorreporting [default-creds]: https://developers.google.com/identity/protocols/application-default-credentials diff --git a/errorreporting/error_logging_test.go b/errorreporting/error_logging_test.go new file mode 100644 index 00000000000..ba1a3c9871e --- /dev/null +++ b/errorreporting/error_logging_test.go @@ -0,0 +1,215 @@ +// Copyright 2016 Google Inc. 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License 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 errorreporting + +import ( + "bytes" + "errors" + "log" + "strings" + "testing" + + "cloud.google.com/go/logging" + "golang.org/x/net/context" + "google.golang.org/api/option" +) + +type fakeLogger struct { + entry *logging.Entry + fail bool +} + +func (c *fakeLogger) LogSync(ctx context.Context, e logging.Entry) error { + if c.fail { + return errors.New("request failed") + } + c.entry = &e + return nil +} + +func (c *fakeLogger) Close() error { + return nil +} + +func newTestClientUsingLogging(c *fakeLogger) *Client { + newLoggerInterface = func(ctx context.Context, project string, opts ...option.ClientOption) (loggerInterface, error) { + return c, nil + } + t, err := NewClient(context.Background(), testProjectID, "myservice", "v1.000", true) + if err != nil { + panic(err) + } + t.RepanicDefault = false + return t +} + +func TestCatchNothingUsingLogging(t *testing.T) { + fl := &fakeLogger{} + c := newTestClientUsingLogging(fl) + defer func() { + e := fl.entry + if e != nil { + t.Errorf("got error report, expected none") + } + }() + defer c.Catch(ctx) +} + +func entryMessage(e *logging.Entry) string { + return e.Payload.(map[string]interface{})["message"].(string) +} + +func commonLoggingChecks(t *testing.T, e *logging.Entry, panickingFunction string) { + if e.Payload.(map[string]interface{})["serviceContext"].(map[string]string)["service"] != "myservice" { + t.Errorf("error report didn't contain service name") + } + if e.Payload.(map[string]interface{})["serviceContext"].(map[string]string)["version"] != "v1.000" { + t.Errorf("error report didn't contain version name") + } + if !strings.Contains(entryMessage(e), "hello, error") { + t.Errorf("error report didn't contain message") + } + if !strings.Contains(entryMessage(e), panickingFunction) { + t.Errorf("error report didn't contain stack trace") + } +} + +func TestCatchPanicUsingLogging(t *testing.T) { + fl := &fakeLogger{} + c := newTestClientUsingLogging(fl) + defer func() { + e := fl.entry + if e == nil { + t.Fatalf("got no error report, expected one") + } + commonLoggingChecks(t, e, "TestCatchPanic") + if !strings.Contains(entryMessage(e), "divide by zero") { + t.Errorf("error report didn't contain recovered value") + } + }() + defer c.Catch(ctx, WithMessage("hello, error")) + var x int + x = x / x +} + +func TestCatchPanicNilClientUsingLogging(t *testing.T) { + buf := new(bytes.Buffer) + log.SetOutput(buf) + defer func() { + recover() + body := buf.String() + if !strings.Contains(body, "divide by zero") { + t.Errorf("error report didn't contain recovered value") + } + if !strings.Contains(body, "hello, error") { + t.Errorf("error report didn't contain message") + } + if !strings.Contains(body, "TestCatchPanicNilClient") { + t.Errorf("error report didn't contain recovered value") + } + }() + var c *Client + defer c.Catch(ctx, WithMessage("hello, error")) + var x int + x = x / x +} + +func TestLogFailedReportsUsingLogging(t *testing.T) { + fl := &fakeLogger{fail: true} + c := newTestClientUsingLogging(fl) + buf := new(bytes.Buffer) + log.SetOutput(buf) + defer func() { + recover() + body := buf.String() + if !strings.Contains(body, "hello, error") { + t.Errorf("error report didn't contain message") + } + if !strings.Contains(body, "errorreporting.TestLogFailedReports") { + t.Errorf("error report didn't contain stack trace") + } + if !strings.Contains(body, "divide by zero") { + t.Errorf("error report didn't contain recovered value") + } + }() + defer c.Catch(ctx, WithMessage("hello, error")) + var x int + x = x / x +} + +func TestCatchNilPanicUsingLogging(t *testing.T) { + fl := &fakeLogger{} + c := newTestClientUsingLogging(fl) + defer func() { + e := fl.entry + if e == nil { + t.Fatalf("got no error report, expected one") + } + commonLoggingChecks(t, e, "TestCatchNilPanic") + if !strings.Contains(entryMessage(e), "nil") { + t.Errorf("error report didn't contain recovered value") + } + }() + b := true + defer c.Catch(ctx, WithMessage("hello, error"), PanicFlag(&b)) + panic(nil) +} + +func TestNotCatchNilPanicUsingLogging(t *testing.T) { + fl := &fakeLogger{} + c := newTestClientUsingLogging(fl) + defer func() { + e := fl.entry + if e != nil { + t.Errorf("got error report, expected none") + } + }() + defer c.Catch(ctx, WithMessage("hello, error")) + panic(nil) +} + +func TestReportUsingLogging(t *testing.T) { + fl := &fakeLogger{} + c := newTestClientUsingLogging(fl) + c.Report(ctx, nil, "hello, ", "error") + e := fl.entry + if e == nil { + t.Fatalf("got no error report, expected one") + } + commonLoggingChecks(t, e, "TestReport") +} + +func TestReportfUsingLogging(t *testing.T) { + fl := &fakeLogger{} + c := newTestClientUsingLogging(fl) + c.Reportf(ctx, nil, "hello, error 2+%d=%d", 2, 2+2) + e := fl.entry + if e == nil { + t.Fatalf("got no error report, expected one") + } + commonLoggingChecks(t, e, "TestReportf") + if !strings.Contains(entryMessage(e), "2+2=4") { + t.Errorf("error report didn't contain formatted message") + } +} + +func TestCloseUsingLogging(t *testing.T) { + fl := &fakeLogger{} + c := newTestClientUsingLogging(fl) + err := c.Close() + if err != nil { + t.Fatal(err) + } +} diff --git a/errorreporting/errors.go b/errorreporting/errors.go new file mode 100644 index 00000000000..dee6007331b --- /dev/null +++ b/errorreporting/errors.go @@ -0,0 +1,456 @@ +// Copyright 2016 Google Inc. 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License 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 errorreporting is a Google Stackdriver Error Reporting library. +// +// This package is still experimental and subject to change. +// +// See https://cloud.google.com/error-reporting/ for more information. +// +// To initialize a client, use the NewClient function. +// +// import "cloud.google.com/go/errorreporting" +// ... +// errorsClient, err = errorreporting.NewClient(ctx, projectID, "myservice", "v1.0", true) +// +// The client can recover panics in your program and report them as errors. +// To use this functionality, defer its Catch method, as you would any other +// function for recovering panics. +// +// func foo(ctx context.Context, ...) { +// defer errorsClient.Catch(ctx) +// ... +// } +// +// Catch writes an error report containing the recovered value and a stack trace +// to Stackdriver Error Reporting. +// +// There are various options you can add to the call to Catch that modify how +// panics are handled. +// +// WithMessage and WithMessagef add a custom message after the recovered value, +// using fmt.Sprint and fmt.Sprintf respectively. +// +// defer errorsClient.Catch(ctx, errorreporting.WithMessagef("x=%d", x)) +// +// WithRequest fills in various fields in the error report with information +// about an http.Request that's being handled. +// +// defer errorsClient.Catch(ctx, errorreporting.WithRequest(httpReq)) +// +// By default, after recovering a panic, Catch will panic again with the +// recovered value. You can turn off this behavior with the Repanic option. +// +// defer errorsClient.Catch(ctx, errorreporting.Repanic(false)) +// +// You can also change the default behavior for the client by changing the +// RepanicDefault field. +// +// errorsClient.RepanicDefault = false +// +// It is also possible to write an error report directly without recovering a +// panic, using Report or Reportf. +// +// if err != nil { +// errorsClient.Reportf(ctx, r, "unexpected error %v", err) +// } +// +// If you try to write an error report with a nil client, or if the client +// fails to write the report to the server, the error report is logged using +// log.Println. +package errorreporting // import "cloud.google.com/go/errorreporting" + +import ( + "bytes" + "fmt" + "log" + "net/http" + "runtime" + "strings" + "time" + + api "cloud.google.com/go/errorreporting/apiv1beta1" + "cloud.google.com/go/internal/version" + "cloud.google.com/go/logging" + "github.com/golang/protobuf/ptypes/timestamp" + gax "github.com/googleapis/gax-go" + "golang.org/x/net/context" + "google.golang.org/api/option" + erpb "google.golang.org/genproto/googleapis/devtools/clouderrorreporting/v1beta1" +) + +const ( + userAgent = `gcloud-golang-errorreporting/20160701` +) + +type apiInterface interface { + ReportErrorEvent(ctx context.Context, req *erpb.ReportErrorEventRequest, opts ...gax.CallOption) (*erpb.ReportErrorEventResponse, error) + Close() error +} + +var newApiInterface = func(ctx context.Context, opts ...option.ClientOption) (apiInterface, error) { + client, err := api.NewReportErrorsClient(ctx, opts...) + if err != nil { + return nil, err + } + client.SetGoogleClientInfo("gccl", version.Repo) + return client, nil +} + +type loggerInterface interface { + LogSync(ctx context.Context, e logging.Entry) error + Close() error +} + +type logger struct { + *logging.Logger + c *logging.Client +} + +func (l logger) Close() error { + return l.c.Close() +} + +var newLoggerInterface = func(ctx context.Context, projectID string, opts ...option.ClientOption) (loggerInterface, error) { + lc, err := logging.NewClient(ctx, projectID, opts...) + if err != nil { + return nil, fmt.Errorf("creating Logging client: %v", err) + } + l := lc.Logger("errorreports") + return logger{l, lc}, nil +} + +type sender interface { + send(ctx context.Context, r *http.Request, message string) + close() error +} + +// errorApiSender sends error reports using the Stackdriver Error Reporting API. +type errorApiSender struct { + apiClient apiInterface + projectID string + serviceContext erpb.ServiceContext +} + +// loggingSender sends error reports using the Stackdriver Logging API. +type loggingSender struct { + logger loggerInterface + projectID string + serviceContext map[string]string +} + +// Client represents a Google Cloud Error Reporting client. +type Client struct { + sender + // RepanicDefault determines whether Catch will re-panic after recovering a + // panic. This behavior can be overridden for an individual call to Catch using + // the Repanic option. + RepanicDefault bool +} + +// NewClient returns a new error reporting client. Generally you will want +// to create a client on program initialization and use it through the lifetime +// of the process. +// +// The service name and version string identify the running program, and are +// included in error reports. The version string can be left empty. +// +// Set useLogging to report errors also using Stackdriver Logging, +// which will result in errors appearing in both the logs and the error +// dashboard. This is useful if you are already a user of Stackdriver Logging. +func NewClient(ctx context.Context, projectID, serviceName, serviceVersion string, useLogging bool, opts ...option.ClientOption) (*Client, error) { + if useLogging { + l, err := newLoggerInterface(ctx, projectID, opts...) + if err != nil { + return nil, fmt.Errorf("creating Logging client: %v", err) + } + sender := &loggingSender{ + logger: l, + projectID: projectID, + serviceContext: map[string]string{ + "service": serviceName, + }, + } + if serviceVersion != "" { + sender.serviceContext["version"] = serviceVersion + } + c := &Client{ + sender: sender, + RepanicDefault: true, + } + return c, nil + } else { + a, err := newApiInterface(ctx, opts...) + if err != nil { + return nil, fmt.Errorf("creating Error Reporting client: %v", err) + } + c := &Client{ + sender: &errorApiSender{ + apiClient: a, + projectID: "projects/" + projectID, + serviceContext: erpb.ServiceContext{ + Service: serviceName, + Version: serviceVersion, + }, + }, + RepanicDefault: true, + } + return c, nil + } +} + +// Close closes any resources held by the client. +// Close should be called when the client is no longer needed. +// It need not be called at program exit. +func (c *Client) Close() error { + err := c.sender.close() + c.sender = nil + return err +} + +// An Option is an optional argument to Catch. +type Option interface { + isOption() +} + +// PanicFlag returns an Option that can inform Catch that a panic has occurred. +// If *p is true when Catch is called, an error report is made even if recover +// returns nil. This allows Catch to report an error for panic(nil). +// If p is nil, the option is ignored. +// +// Here is an example of how to use PanicFlag: +// +// func foo(ctx context.Context, ...) { +// hasPanicked := true +// defer errorsClient.Catch(ctx, errorreporting.PanicFlag(&hasPanicked)) +// ... +// ... +// // We have reached the end of the function, so we're not panicking. +// hasPanicked = false +// } +func PanicFlag(p *bool) Option { return panicFlag{p} } + +type panicFlag struct { + *bool +} + +func (h panicFlag) isOption() {} + +// Repanic returns an Option that determines whether Catch will re-panic after +// it reports an error. This overrides the default in the client. +func Repanic(r bool) Option { return repanic(r) } + +type repanic bool + +func (r repanic) isOption() {} + +// WithRequest returns an Option that informs Catch or Report of an http.Request +// that is being handled. Information from the Request is included in the error +// report, if one is made. +func WithRequest(r *http.Request) Option { return withRequest{r} } + +type withRequest struct { + *http.Request +} + +func (w withRequest) isOption() {} + +// WithMessage returns an Option that sets a message to be included in the error +// report, if one is made. v is converted to a string with fmt.Sprint. +func WithMessage(v ...interface{}) Option { return message(v) } + +type message []interface{} + +func (m message) isOption() {} + +// WithMessagef returns an Option that sets a message to be included in the error +// report, if one is made. format and v are converted to a string with fmt.Sprintf. +func WithMessagef(format string, v ...interface{}) Option { return messagef{format, v} } + +type messagef struct { + format string + v []interface{} +} + +func (m messagef) isOption() {} + +// Catch tries to recover a panic; if it succeeds, it writes an error report. +// It should be called by deferring it, like any other function for recovering +// panics. +// +// Catch can be called concurrently with other calls to Catch, Report or Reportf. +func (c *Client) Catch(ctx context.Context, opt ...Option) { + panicked := false + for _, o := range opt { + switch o := o.(type) { + case panicFlag: + panicked = panicked || o.bool != nil && *o.bool + } + } + x := recover() + if x == nil && !panicked { + return + } + var ( + r *http.Request + shouldRepanic = true + messages = []string{fmt.Sprint(x)} + ) + if c != nil { + shouldRepanic = c.RepanicDefault + } + for _, o := range opt { + switch o := o.(type) { + case repanic: + shouldRepanic = bool(o) + case withRequest: + r = o.Request + case message: + messages = append(messages, fmt.Sprint(o...)) + case messagef: + messages = append(messages, fmt.Sprintf(o.format, o.v...)) + } + } + c.logInternal(ctx, r, true, strings.Join(messages, " ")) + if shouldRepanic { + panic(x) + } +} + +// Report writes an error report unconditionally, instead of only when a panic +// occurs. +// If r is non-nil, information from the Request is included in the error report. +// +// Report can be called concurrently with other calls to Catch, Report or Reportf. +func (c *Client) Report(ctx context.Context, r *http.Request, v ...interface{}) { + c.logInternal(ctx, r, false, fmt.Sprint(v...)) +} + +// Reportf writes an error report unconditionally, instead of only when a panic +// occurs. +// If r is non-nil, information from the Request is included in the error report. +// +// Reportf can be called concurrently with other calls to Catch, Report or Reportf. +func (c *Client) Reportf(ctx context.Context, r *http.Request, format string, v ...interface{}) { + c.logInternal(ctx, r, false, fmt.Sprintf(format, v...)) +} + +func (c *Client) logInternal(ctx context.Context, r *http.Request, isPanic bool, msg string) { + // limit the stack trace to 16k. + var buf [16384]byte + stack := buf[0:runtime.Stack(buf[:], false)] + message := msg + "\n" + chopStack(stack, isPanic) + if c == nil { + log.Println("Error report used nil client:", message) + return + } + c.send(ctx, r, message) +} + +func (s *loggingSender) send(ctx context.Context, r *http.Request, message string) { + payload := map[string]interface{}{ + "eventTime": time.Now().In(time.UTC).Format(time.RFC3339Nano), + "message": message, + "serviceContext": s.serviceContext, + } + if r != nil { + payload["context"] = map[string]interface{}{ + "httpRequest": map[string]interface{}{ + "method": r.Method, + "url": r.Host + r.RequestURI, + "userAgent": r.UserAgent(), + "referrer": r.Referer(), + "remoteIp": r.RemoteAddr, + }, + } + } + e := logging.Entry{ + Severity: logging.Error, + Payload: payload, + } + err := s.logger.LogSync(ctx, e) + if err != nil { + log.Println("Error writing error report:", err, "report:", payload) + } +} + +func (s *loggingSender) close() error { + return s.logger.Close() +} + +func (s *errorApiSender) send(ctx context.Context, r *http.Request, message string) { + time := time.Now() + var errorContext *erpb.ErrorContext + if r != nil { + errorContext = &erpb.ErrorContext{ + HttpRequest: &erpb.HttpRequestContext{ + Method: r.Method, + Url: r.Host + r.RequestURI, + UserAgent: r.UserAgent(), + Referrer: r.Referer(), + RemoteIp: r.RemoteAddr, + }, + } + } + req := erpb.ReportErrorEventRequest{ + ProjectName: s.projectID, + Event: &erpb.ReportedErrorEvent{ + EventTime: ×tamp.Timestamp{ + Seconds: time.Unix(), + Nanos: int32(time.Nanosecond()), + }, + ServiceContext: &s.serviceContext, + Message: message, + Context: errorContext, + }, + } + _, err := s.apiClient.ReportErrorEvent(ctx, &req) + if err != nil { + log.Println("Error writing error report:", err, "report:", message) + } +} + +func (s *errorApiSender) close() error { + return s.apiClient.Close() +} + +// chopStack trims a stack trace so that the function which panics or calls +// Report is first. +func chopStack(s []byte, isPanic bool) string { + var f []byte + if isPanic { + f = []byte("panic(") + } else { + f = []byte("cloud.google.com/go/errorreporting.(*Client).Report") + } + + lfFirst := bytes.IndexByte(s, '\n') + if lfFirst == -1 { + return string(s) + } + stack := s[lfFirst:] + panicLine := bytes.Index(stack, f) + if panicLine == -1 { + return string(s) + } + stack = stack[panicLine+1:] + for i := 0; i < 2; i++ { + nextLine := bytes.IndexByte(stack, '\n') + if nextLine == -1 { + return string(s) + } + stack = stack[nextLine+1:] + } + return string(s[:lfFirst+1]) + string(stack) +} diff --git a/errorreporting/errors_test.go b/errorreporting/errors_test.go new file mode 100644 index 00000000000..c5cda8815d2 --- /dev/null +++ b/errorreporting/errors_test.go @@ -0,0 +1,212 @@ +// Copyright 2016 Google Inc. 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License 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 errorreporting + +import ( + "bytes" + "errors" + "log" + "strings" + "testing" + + gax "github.com/googleapis/gax-go" + + "golang.org/x/net/context" + "google.golang.org/api/option" + erpb "google.golang.org/genproto/googleapis/devtools/clouderrorreporting/v1beta1" +) + +const testProjectID = "testproject" + +type fakeReportErrorsClient struct { + req *erpb.ReportErrorEventRequest + fail bool +} + +func (c *fakeReportErrorsClient) ReportErrorEvent(ctx context.Context, req *erpb.ReportErrorEventRequest, _ ...gax.CallOption) (*erpb.ReportErrorEventResponse, error) { + if c.fail { + return nil, errors.New("request failed") + } + c.req = req + return &erpb.ReportErrorEventResponse{}, nil +} + +func (c *fakeReportErrorsClient) Close() error { + return nil +} + +func newTestClient(c *fakeReportErrorsClient) *Client { + newApiInterface = func(ctx context.Context, opts ...option.ClientOption) (apiInterface, error) { + return c, nil + } + t, err := NewClient(context.Background(), testProjectID, "myservice", "v1.000", false) + if err != nil { + panic(err) + } + t.RepanicDefault = false + return t +} + +var ctx context.Context + +func init() { + ctx = context.Background() +} + +func TestCatchNothing(t *testing.T) { + fc := &fakeReportErrorsClient{} + c := newTestClient(fc) + defer func() { + r := fc.req + if r != nil { + t.Errorf("got error report, expected none") + } + }() + defer c.Catch(ctx) +} + +func commonChecks(t *testing.T, req *erpb.ReportErrorEventRequest, panickingFunction string) { + if req.Event.ServiceContext.Service != "myservice" { + t.Errorf("error report didn't contain service name") + } + if req.Event.ServiceContext.Version != "v1.000" { + t.Errorf("error report didn't contain version name") + } + if !strings.Contains(req.Event.Message, "hello, error") { + t.Errorf("error report didn't contain message") + } + if !strings.Contains(req.Event.Message, panickingFunction) { + t.Errorf("error report didn't contain stack trace") + } +} + +func TestCatchPanic(t *testing.T) { + fc := &fakeReportErrorsClient{} + c := newTestClient(fc) + defer func() { + r := fc.req + if r == nil { + t.Fatalf("got no error report, expected one") + } + commonChecks(t, r, "errorreporting.TestCatchPanic") + if !strings.Contains(r.Event.Message, "divide by zero") { + t.Errorf("error report didn't contain recovered value") + } + }() + defer c.Catch(ctx, WithMessage("hello, error")) + var x int + x = x / x +} + +func TestCatchPanicNilClient(t *testing.T) { + buf := new(bytes.Buffer) + log.SetOutput(buf) + defer func() { + recover() + body := buf.String() + if !strings.Contains(body, "divide by zero") { + t.Errorf("error report didn't contain recovered value") + } + if !strings.Contains(body, "hello, error") { + t.Errorf("error report didn't contain message") + } + if !strings.Contains(body, "TestCatchPanicNilClient") { + t.Errorf("error report didn't contain recovered value") + } + }() + var c *Client + defer c.Catch(ctx, WithMessage("hello, error")) + var x int + x = x / x +} + +func TestLogFailedReports(t *testing.T) { + fc := &fakeReportErrorsClient{fail: true} + c := newTestClient(fc) + buf := new(bytes.Buffer) + log.SetOutput(buf) + defer func() { + recover() + body := buf.String() + if !strings.Contains(body, "hello, error") { + t.Errorf("error report didn't contain message") + } + if !strings.Contains(body, "errorreporting.TestLogFailedReports") { + t.Errorf("error report didn't contain stack trace") + } + if !strings.Contains(body, "divide by zero") { + t.Errorf("error report didn't contain recovered value") + } + }() + defer c.Catch(ctx, WithMessage("hello, error")) + var x int + x = x / x +} + +func TestCatchNilPanic(t *testing.T) { + fc := &fakeReportErrorsClient{} + c := newTestClient(fc) + defer func() { + r := fc.req + if r == nil { + t.Fatalf("got no error report, expected one") + } + commonChecks(t, r, "errorreporting.TestCatchNilPanic") + if !strings.Contains(r.Event.Message, "nil") { + t.Errorf("error report didn't contain recovered value") + } + }() + b := true + defer c.Catch(ctx, WithMessage("hello, error"), PanicFlag(&b)) + panic(nil) +} + +func TestNotCatchNilPanic(t *testing.T) { + fc := &fakeReportErrorsClient{} + c := newTestClient(fc) + defer func() { + r := fc.req + if r != nil { + t.Errorf("got error report, expected none") + } + }() + defer c.Catch(ctx, WithMessage("hello, error")) + panic(nil) +} + +func TestReport(t *testing.T) { + fc := &fakeReportErrorsClient{} + c := newTestClient(fc) + c.Report(ctx, nil, "hello, ", "error") + r := fc.req + if r == nil { + t.Fatalf("got no error report, expected one") + } + commonChecks(t, r, "errorreporting.TestReport") +} + +func TestReportf(t *testing.T) { + fc := &fakeReportErrorsClient{} + c := newTestClient(fc) + c.Reportf(ctx, nil, "hello, error 2+%d=%d", 2, 2+2) + r := fc.req + if r == nil { + t.Fatalf("got no error report, expected one") + } + commonChecks(t, r, "errorreporting.TestReportf") + if !strings.Contains(r.Event.Message, "2+2=4") { + t.Errorf("error report didn't contain formatted message") + } +} diff --git a/errorreporting/stack_test.go b/errorreporting/stack_test.go new file mode 100644 index 00000000000..950534f25ab --- /dev/null +++ b/errorreporting/stack_test.go @@ -0,0 +1,118 @@ +// Copyright 2016 Google Inc. 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License 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 errorreporting + +import "testing" + +func TestChopStack(t *testing.T) { + for _, test := range []struct { + name string + in []byte + expected string + isPanic bool + }{ + { + name: "Catch", + in: []byte(`goroutine 20 [running]: +runtime/debug.Stack() + /gopath/src/runtime/debug/stack.go:24 +0x79 +cloud.google.com/go/errorreporting.(*Client).logInternal() + /gopath/src/cloud.google.com/go/errorreporting/errors.go:259 +0x18b +cloud.google.com/go/errorreporting.(*Client).Catch() + /gopath/src/cloud.google.com/go/errorreporting/errors.go:219 +0x6ed +panic() + /gopath/src/runtime/panic.go:458 +0x243 +cloud.google.com/go/errorreporting.TestCatchPanic() + /gopath/src/cloud.google.com/go/errorreporting/errors_test.go:93 +0x171 +testing.tRunner() + /gopath/src/testing/testing.go:610 +0x81 +created by testing.(*T).Run + /gopath/src/testing/testing.go:646 +0x2ec +`), + expected: `goroutine 20 [running]: +cloud.google.com/go/errorreporting.TestCatchPanic() + /gopath/src/cloud.google.com/go/errorreporting/errors_test.go:93 +0x171 +testing.tRunner() + /gopath/src/testing/testing.go:610 +0x81 +created by testing.(*T).Run + /gopath/src/testing/testing.go:646 +0x2ec +`, + isPanic: true, + }, + { + name: "function not found", + in: []byte(`goroutine 20 [running]: +runtime/debug.Stack() + /gopath/src/runtime/debug/stack.go:24 +0x79 +cloud.google.com/go/errorreporting.(*Client).logInternal() + /gopath/src/cloud.google.com/go/errorreporting/errors.go:259 +0x18b +cloud.google.com/go/errorreporting.(*Client).Catch() + /gopath/src/cloud.google.com/go/errorreporting/errors.go:219 +0x6ed +cloud.google.com/go/errorreporting.TestCatchPanic() + /gopath/src/cloud.google.com/go/errorreporting/errors_test.go:93 +0x171 +testing.tRunner() + /gopath/src/testing/testing.go:610 +0x81 +created by testing.(*T).Run + /gopath/src/testing/testing.go:646 +0x2ec +`), + expected: `goroutine 20 [running]: +runtime/debug.Stack() + /gopath/src/runtime/debug/stack.go:24 +0x79 +cloud.google.com/go/errorreporting.(*Client).logInternal() + /gopath/src/cloud.google.com/go/errorreporting/errors.go:259 +0x18b +cloud.google.com/go/errorreporting.(*Client).Catch() + /gopath/src/cloud.google.com/go/errorreporting/errors.go:219 +0x6ed +cloud.google.com/go/errorreporting.TestCatchPanic() + /gopath/src/cloud.google.com/go/errorreporting/errors_test.go:93 +0x171 +testing.tRunner() + /gopath/src/testing/testing.go:610 +0x81 +created by testing.(*T).Run + /gopath/src/testing/testing.go:646 +0x2ec +`, + isPanic: true, + }, + { + name: "Report", + in: []byte(` goroutine 39 [running]: +runtime/debug.Stack() + /gopath/runtime/debug/stack.go:24 +0x79 +cloud.google.com/go/errorreporting.(*Client).logInternal() + /gopath/cloud.google.com/go/errorreporting/errors.go:259 +0x18b +cloud.google.com/go/errorreporting.(*Client).Report() + /gopath/cloud.google.com/go/errorreporting/errors.go:248 +0x4ed +cloud.google.com/go/errorreporting.TestReport() + /gopath/cloud.google.com/go/errorreporting/errors_test.go:137 +0x2a1 +testing.tRunner() + /gopath/testing/testing.go:610 +0x81 +created by testing.(*T).Run + /gopath/testing/testing.go:646 +0x2ec +`), + expected: ` goroutine 39 [running]: +cloud.google.com/go/errorreporting.TestReport() + /gopath/cloud.google.com/go/errorreporting/errors_test.go:137 +0x2a1 +testing.tRunner() + /gopath/testing/testing.go:610 +0x81 +created by testing.(*T).Run + /gopath/testing/testing.go:646 +0x2ec +`, + isPanic: false, + }, + } { + out := chopStack(test.in, test.isPanic) + if out != test.expected { + t.Errorf("case %q: chopStack(%q, %t): got %q want %q", test.name, test.in, test.isPanic, out, test.expected) + } + } +} diff --git a/errors/errors.go b/errors/errors.go index b02c4832daa..a4c83640307 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -69,6 +69,8 @@ // If you try to write an error report with a nil client, or if the client // fails to write the report to the server, the error report is logged using // log.Println. +// +// Deprecated: Use cloud.google.com/go/errorreporting instead. package errors // import "cloud.google.com/go/errors" import (