423 lines
12 KiB
Go
423 lines
12 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 errors 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. Generally you will want
|
||
|
// to do this on program initialization. The NewClient function takes as
|
||
|
// arguments a context, the project name, a service name, and a version string.
|
||
|
// The service name and version string identify the running program, and are
|
||
|
// included in error reports. The version string can be left empty. NewClient
|
||
|
// also takes a bool that indicates whether to report errors 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.
|
||
|
//
|
||
|
// import "cloud.google.com/go/errors"
|
||
|
// ...
|
||
|
// errorsClient, err = errors.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, errors.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, errors.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, errors.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 errors // import "cloud.google.com/go/errors"
|
||
|
|
||
|
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"
|
||
|
"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) (*erpb.ReportErrorEventResponse, 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
|
||
|
}
|
||
|
|
||
|
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 l, nil
|
||
|
}
|
||
|
|
||
|
type sender interface {
|
||
|
send(ctx context.Context, r *http.Request, message string)
|
||
|
}
|
||
|
|
||
|
// 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
|
||
|
}
|
||
|
|
||
|
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
|
||
|
}
|
||
|
|
||
|
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
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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, errors.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 *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)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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/errors.(*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)
|
||
|
}
|