// Copyright 2016 Google LLC // // 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. package errorreporting // import "cloud.google.com/go/errorreporting" import ( "bytes" "context" "fmt" "log" "net/http" "runtime" "time" vkit "cloud.google.com/go/errorreporting/apiv1beta1" "cloud.google.com/go/internal/version" "github.com/golang/protobuf/ptypes" gax "github.com/googleapis/gax-go/v2" "google.golang.org/api/option" "google.golang.org/api/support/bundler" pb "google.golang.org/genproto/googleapis/devtools/clouderrorreporting/v1beta1" ) // Config is additional configuration for Client. type Config struct { // ServiceName identifies the running program and is included in the error reports. // Optional. ServiceName string // ServiceVersion identifies the version of the running program and is // included in the error reports. // Optional. ServiceVersion string // OnError is the function to call if any background // tasks errored. By default, errors are logged. OnError func(err error) } // Entry holds information about the reported error. type Entry struct { Error error Req *http.Request // if error is associated with a request. User string // an identifier for the user affected by the error Stack []byte // if user does not provide a stack trace, runtime.Stack will be called } // Client represents a Google Cloud Error Reporting client. type Client struct { projectName string apiClient client serviceContext *pb.ServiceContext bundler *bundler.Bundler onErrorFn func(err error) } var newClient = func(ctx context.Context, opts ...option.ClientOption) (client, error) { client, err := vkit.NewReportErrorsClient(ctx, opts...) if err != nil { return nil, err } client.SetGoogleClientInfo("gccl", version.Repo) return client, nil } // 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. func NewClient(ctx context.Context, projectID string, cfg Config, opts ...option.ClientOption) (*Client, error) { if cfg.ServiceName == "" { cfg.ServiceName = "goapp" } c, err := newClient(ctx, opts...) if err != nil { return nil, fmt.Errorf("creating client: %v", err) } client := &Client{ apiClient: c, projectName: "projects/" + projectID, serviceContext: &pb.ServiceContext{ Service: cfg.ServiceName, Version: cfg.ServiceVersion, }, onErrorFn: cfg.OnError, } bundler := bundler.NewBundler((*pb.ReportErrorEventRequest)(nil), func(bundle interface{}) { reqs := bundle.([]*pb.ReportErrorEventRequest) for _, req := range reqs { _, err = client.apiClient.ReportErrorEvent(ctx, req) if err != nil { client.onError(err) } } }) // TODO(jbd): Optimize bundler limits. bundler.DelayThreshold = 2 * time.Second bundler.BundleCountThreshold = 100 bundler.BundleByteThreshold = 1000 bundler.BundleByteLimit = 1000 bundler.BufferedByteLimit = 10000 client.bundler = bundler return client, nil } func (c *Client) onError(err error) { if c.onErrorFn != nil { c.onErrorFn(err) return } log.Println(err) } // Close calls Flush, then closes any resources held by the client. // Close should be called when the client is no longer needed. func (c *Client) Close() error { c.Flush() return c.apiClient.Close() } // Report writes an error report. It doesn't block. Errors in // writing the error report can be handled via Config.OnError. func (c *Client) Report(e Entry) { c.bundler.Add(c.newRequest(e), 1) } // ReportSync writes an error report. It blocks until the entry is written. func (c *Client) ReportSync(ctx context.Context, e Entry) error { _, err := c.apiClient.ReportErrorEvent(ctx, c.newRequest(e)) return err } // Flush blocks until all currently buffered error reports are sent. // // If any errors occurred since the last call to Flush, or the // creation of the client if this is the first call, then Flush reports the // error via the Config.OnError handler. func (c *Client) Flush() { c.bundler.Flush() } func (c *Client) newRequest(e Entry) *pb.ReportErrorEventRequest { var stack string if e.Stack != nil { stack = string(e.Stack) } else { // limit the stack trace to 16k. var buf [16 * 1024]byte stack = chopStack(buf[0:runtime.Stack(buf[:], false)]) } message := e.Error.Error() + "\n" + stack var errorContext *pb.ErrorContext if r := e.Req; r != nil { errorContext = &pb.ErrorContext{ HttpRequest: &pb.HttpRequestContext{ Method: r.Method, Url: r.Host + r.RequestURI, UserAgent: r.UserAgent(), Referrer: r.Referer(), RemoteIp: r.RemoteAddr, }, } } if e.User != "" { if errorContext == nil { errorContext = &pb.ErrorContext{} } errorContext.User = e.User } return &pb.ReportErrorEventRequest{ ProjectName: c.projectName, Event: &pb.ReportedErrorEvent{ EventTime: ptypes.TimestampNow(), ServiceContext: c.serviceContext, Message: message, Context: errorContext, }, } } // chopStack trims a stack trace so that the function which panics or calls // Report is first. func chopStack(s []byte) string { 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) } type client interface { ReportErrorEvent(ctx context.Context, req *pb.ReportErrorEventRequest, opts ...gax.CallOption) (*pb.ReportErrorEventResponse, error) Close() error }