| // 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 Cloud Error Reporting library. | |
| // | |
| // Any provided stacktraces must match the format produced by https://golang.org/pkg/runtime/#Stack | |
| // or as per https://cloud.google.com/error-reporting/reference/rest/v1beta1/projects.events/report#ReportedErrorEvent | |
| // for language specific stacktrace formats. | |
| // | |
| // 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" | |
| pb "cloud.google.com/go/errorreporting/apiv1beta1/errorreportingpb" | |
| "cloud.google.com/go/errorreporting/internal" | |
| "github.com/golang/protobuf/ptypes" | |
| gax "github.com/googleapis/gax-go/v2" | |
| "google.golang.org/api/option" | |
| "google.golang.org/api/support/bundler" | |
| ) | |
| // 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 specifies the stacktrace and call sequence correlated with | |
| // the error. Stack's content must match the format specified by | |
| // https://cloud.google.com/error-reporting/reference/rest/v1beta1/projects.events/report#ReportedErrorEvent.message | |
| // or at least for Go programs, it must match the format produced | |
| // by https://golang.org/pkg/runtime/debug/#Stack. | |
| // | |
| // If Stack is blank, the result of runtime.Stack will be used instead. | |
| Stack []byte | |
| } | |
| // 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", internal.Version) | |
| 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: %w", 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 | |
| } |