blob: dbf00bba310f38c9148a455b04803c7ed9cd29bb [file]
// Copyright 2016 The LUCI Authors.
//
// 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
import (
"context"
"errors"
"fmt"
"regexp"
"slices"
"strings"
"go.chromium.org/luci/common/errors/errtag"
"go.chromium.org/luci/common/errors/errtag/stacktag"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/common/runtime/debugstack"
)
// ErrorWrapper describes a method which can accept an error and return an
// optionally wrapped version of that error.
//
// Implementations should return `nil` if provided `nil`.
type ErrorWrapper interface {
Apply(err error) (wrapped error)
}
// Log logs the full error. If this is an Annotated error, it will log the full
// stack information as well.
//
// Does nothing if err is nil.
func Log(ctx context.Context, err error, excludePkgs ...string) {
if err == nil {
return
}
stack := stacktag.Tag.ValueOrDefault(err)
if stack == "" {
// No stack in error, just log it normally.
if tags := errtag.Collect(err, stacktag.Tag); len(tags) > 0 {
logging.Errorf(ctx, "errtags:\n%s", strings.Join(tags.Format(" "), "\n"))
}
logging.Errorf(ctx, "%s", err)
} else {
// This has a stacktrace. Report it with a stack trace attached.
stack := logging.StackTrace{
Standard: stack,
Textual: RenderStack(err, excludePkgs...),
}
logging.ErrorWithStackTrace(ctx, stack, "error: %s", err)
}
}
func dropFrames(stack string, excludePkgs ...string) string {
if len(excludePkgs) == 0 {
return stack
}
filters := make([]*regexp.Regexp, len(excludePkgs))
for i, pkg := range excludePkgs {
filters[i] = regexp.MustCompile(fmt.Sprintf("^%s$", regexp.QuoteMeta(pkg)))
}
parsed := debugstack.ParseString(stack).Filter(debugstack.CompileRules(
debugstack.Rule{
ApplyTo: debugstack.StackFrameKind,
DropIfInPkg: filters,
},
), true)
return parsed.String()
}
// RenderStack renders the error message and a stack to a list of lines.
//
// Uses a compact stack format.
func RenderStack(err error, excludePkgs ...string) string {
if err == nil {
return ""
}
ret := err.Error()
if tags := errtag.Collect(err, stacktag.Tag); len(tags) > 0 {
ret += fmt.Sprintf("\n\nerrtags:\n%s\n", strings.Join(tags.Format(" "), "\n"))
}
if stack := stacktag.Tag.ValueOrDefault(err); stack != "" {
stack = dropFrames(stack, excludePkgs...)
ret += fmt.Sprintf("\n%s", stack)
}
return ret
}
// RenderGoStack renders the error to a Go-style stacktrace.
//
// If `onlyInner` is true, this will only return the inner-most stack in case
// this error was annotated by multiple goroutines.
//
// If it's false, then all goroutines which annotated this error will have their
// stacks combined into a single trace.
//
// If `err` is not annotated, returns the empty string.
func RenderGoStack(err error, onlyInner bool, excludePkgs ...string) string {
return dropFrames(stacktag.Tag.ValueOrDefault(err), excludePkgs...)
}
// Annotate is a legacy API.
//
// It is exactly equivalent to WrapIf
var Annotate = WrapIf
// Reason is a legacy API.
//
// Exactly equivalent to `errors.New` with just a reason
// or `errors.Fmt` with one or more args.
func Reason(reason string, args ...any) error {
var ret error
if len(args) == 0 {
ret = errors.New(reason)
} else {
ret = fmt.Errorf(reason, args...)
}
// The 1 is because this is a helper function we don't want to see in the
// captured trace.
return stacktag.Capture(ret, 1)
}
// New is an API-compatible version of the standard errors.New function, except
// that it also allows you to apply wrappers (like from the errtag library).
//
// This function does capture the stack, but ONLY if this is not used at
// init()-time. This is to avoid module level errors having a mostly-useless
// stack attached.
func New(msg string, wrappers ...ErrorWrapper) error {
ret := errors.New(msg)
for _, wrapper := range wrappers {
ret = wrapper.Apply(ret)
}
// The 1 is because this is a helper function we don't want to see in the
// captured trace.
return stacktag.Capture(ret, 1)
}
// Fmt is an API-compatible version of the standard fmt.Errorf function, except
// that it captures the stack, but ONLY if this is not used at init()-time. This
// is to avoid module level errors having a mostly-useless stack attached.
func Fmt(format string, args ...any) error {
// The 1 is because this is a helper function we don't want to see in the
// captured trace.
return stacktag.Capture(fmt.Errorf(format, args...), 1)
}
// WrapIf is the same as Fmt, except that:
//
// - It explicitly checks that `err` is not nil. If it is, then this returns
// `nil`.
// - It adds ": %w" to format and err to args.
//
// This is meant to be used like:
//
// return errors.WrapIf(someCall(i), "while doing someCall(%d)", i)
//
// Instead of:
//
// if err := someCall(i); err != nil {
// return errors.Fmt("while doing someCall(%d): %w", i, err)
// }
// return nil
//
// These two code snippets have identical behavior.
//
// This is convenient, but is mostly an affordance to safely convert a previous
// `errors.Annotate` api which had this semantic.
func WrapIf(err error, format string, args ...any) error {
if err == nil {
return nil
}
// The 1 is because this is a helper function we don't want to see in the
// captured trace.
//
// Clip is necessary to avoid mutating a passed-in `args`.
return stacktag.Capture(fmt.Errorf(format+": %w", append(slices.Clip(args), err)...), 1)
}