blob: e2375c2f4bec923613a6a10f3e1064a48410bd33 [file] [log] [blame]
// 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.
//
// 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"
"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 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", 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
}