231 lines
6.6 KiB
Go
231 lines
6.6 KiB
Go
|
// 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.
|
||
|
package errorreporting // import "cloud.google.com/go/errorreporting"
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"fmt"
|
||
|
"log"
|
||
|
"net/http"
|
||
|
"runtime"
|
||
|
"time"
|
||
|
|
||
|
api "cloud.google.com/go/errorreporting/apiv1beta1"
|
||
|
"cloud.google.com/go/internal/version"
|
||
|
"github.com/golang/protobuf/ptypes"
|
||
|
gax "github.com/googleapis/gax-go"
|
||
|
"golang.org/x/net/context"
|
||
|
"google.golang.org/api/option"
|
||
|
"google.golang.org/api/support/bundler"
|
||
|
erpb "google.golang.org/genproto/googleapis/devtools/clouderrorreporting/v1beta1"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
userAgent = `gcloud-golang-errorreporting/20160701`
|
||
|
)
|
||
|
|
||
|
// 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.
|
||
|
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 {
|
||
|
projectID string
|
||
|
apiClient client
|
||
|
serviceContext erpb.ServiceContext
|
||
|
bundler *bundler.Bundler
|
||
|
|
||
|
onErrorFn func(err error)
|
||
|
}
|
||
|
|
||
|
var newClient = func(ctx context.Context, opts ...option.ClientOption) (client, error) {
|
||
|
client, err := api.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,
|
||
|
projectID: "projects/" + projectID,
|
||
|
serviceContext: erpb.ServiceContext{
|
||
|
Service: cfg.ServiceName,
|
||
|
Version: cfg.ServiceVersion,
|
||
|
},
|
||
|
}
|
||
|
bundler := bundler.NewBundler((*erpb.ReportErrorEventRequest)(nil), func(bundle interface{}) {
|
||
|
reqs := bundle.([]*erpb.ReportErrorEventRequest)
|
||
|
for _, req := range reqs {
|
||
|
_, err = client.apiClient.ReportErrorEvent(ctx, req)
|
||
|
if err != nil {
|
||
|
client.onError(fmt.Errorf("failed to upload: %v", 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 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 {
|
||
|
return c.apiClient.Close()
|
||
|
}
|
||
|
|
||
|
// Report writes an error report. It doesn't block. Errors in
|
||
|
// writing the error report can be handled via Client.OnError.
|
||
|
func (c *Client) Report(e Entry) {
|
||
|
var stack string
|
||
|
if e.Stack != nil {
|
||
|
stack = string(e.Stack)
|
||
|
}
|
||
|
req := c.makeReportErrorEventRequest(e.Req, e.Error.Error(), stack)
|
||
|
c.bundler.Add(req, 1)
|
||
|
}
|
||
|
|
||
|
// ReportSync writes an error report. It blocks until the entry is written.
|
||
|
func (c *Client) ReportSync(ctx context.Context, e Entry) error {
|
||
|
var stack string
|
||
|
if e.Stack != nil {
|
||
|
stack = string(e.Stack)
|
||
|
}
|
||
|
req := c.makeReportErrorEventRequest(e.Req, e.Error.Error(), stack)
|
||
|
_, err := c.apiClient.ReportErrorEvent(ctx, req)
|
||
|
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 report the
|
||
|
// error via the (*Client).OnError handler.
|
||
|
func (c *Client) Flush() {
|
||
|
c.bundler.Flush()
|
||
|
}
|
||
|
|
||
|
func (c *Client) makeReportErrorEventRequest(r *http.Request, msg string, stack string) *erpb.ReportErrorEventRequest {
|
||
|
if stack == "" {
|
||
|
// limit the stack trace to 16k.
|
||
|
var buf [16 * 1024]byte
|
||
|
stack = chopStack(buf[0:runtime.Stack(buf[:], false)])
|
||
|
}
|
||
|
message := msg + "\n" + stack
|
||
|
|
||
|
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,
|
||
|
},
|
||
|
}
|
||
|
}
|
||
|
return &erpb.ReportErrorEventRequest{
|
||
|
ProjectName: c.projectID,
|
||
|
Event: &erpb.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 *erpb.ReportErrorEventRequest, opts ...gax.CallOption) (*erpb.ReportErrorEventResponse, error)
|
||
|
Close() error
|
||
|
}
|