go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/errors/annotate.go (about) 1 // Copyright 2016 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package errors 16 17 import ( 18 "bytes" 19 "context" 20 "errors" 21 "fmt" 22 "io" 23 "path/filepath" 24 "runtime" 25 "sort" 26 "strings" 27 28 "go.chromium.org/luci/common/data/stringset" 29 "go.chromium.org/luci/common/data/text/indented" 30 "go.chromium.org/luci/common/iotools" 31 "go.chromium.org/luci/common/logging" 32 "go.chromium.org/luci/common/runtime/goroutine" 33 ) 34 35 const ( 36 // renderedStackDumpFormat is used as the prefix format when renderedStack is dumped to string. 37 renderedStackDumpFormat = "goroutine %d:\n" 38 // RenderedStackDumpRe is coupled with renderedStackDumpFormat to indicate its regex expression. 39 RenderedStackDumpRe = `goroutine \d+:\n` 40 ) 41 42 type stack struct { 43 id goroutine.ID 44 frames []uintptr 45 } 46 47 func (s *stack) findPointOfDivergence(other *stack) int { 48 // TODO(iannucci): can we optimize this search routine to not overly penalize 49 // tail-recursive functions? Searching 'up' from both stacks doesn't work in 50 // the face of recursion because there will be multiple ambiguous stack 51 // frames. The algorithm below is correct, but it potentially collects and 52 // then throws away a bunch of data (e.g. if we could walk up from the current 53 // stack and then find the intersection in the captured stack, we could stop 54 // walking at that point instead of walking all the way back from the root on 55 // every Annotate call. 56 if s.id != other.id { 57 panic(fmt.Errorf( 58 "finding intersection between unrelated stacks: %d v %d", s.id, other.id)) 59 } 60 61 myIdx := len(s.frames) - 1 62 oIdx := len(other.frames) - 1 63 64 for s.frames[myIdx] == other.frames[oIdx] { 65 myIdx-- 66 oIdx-- 67 } 68 69 return myIdx 70 } 71 72 // stackContexter is the interface that an error may implement if it has data 73 // associated with a specific stack frame. 74 type stackContexter interface { 75 stackContext() stackContext 76 } 77 78 // stackFrameInfo holds a stack and an index into that stack for association 79 // with stackContexts. 80 type stackFrameInfo struct { 81 frameIdx int 82 forStack *stack 83 } 84 85 // stackContext represents the annotation data associated with an error, or an 86 // annotation of an error. 87 type stackContext struct { 88 frameInfo stackFrameInfo 89 // publicly-facing reason, and will show up in the Error() string. 90 reason string 91 92 // used for printing tracebacks, but will not show up in the Error() string. 93 internalReason string 94 95 // tags are any data associated with this frame. 96 tags map[TagKey]any 97 } 98 99 // renderPublic renders the public error.Error()-style string for this frame, 100 // combining this frame's Reason with the inner error. 101 func (s *stackContext) renderPublic(inner error) string { 102 switch { 103 case inner == nil: 104 return s.reason 105 case s.reason == "": 106 return inner.Error() 107 } 108 return fmt.Sprintf("%s: %s", s.reason, inner.Error()) 109 } 110 111 // render renders the frame as a single entry in a stack trace. This looks like: 112 // 113 // reason: "The literal content of the reason field: %(key2)d" 114 // internal reason: I am an internal reason formatted with key1: value 115 func (s *stackContext) render() lines { 116 siz := len(s.tags) 117 if s.internalReason != "" { 118 siz++ 119 } 120 if s.reason != "" { 121 siz++ 122 } 123 if siz == 0 { 124 return nil 125 } 126 127 ret := make(lines, 0, siz) 128 if s.reason != "" { 129 ret = append(ret, fmt.Sprintf("reason: %s", s.reason)) 130 } 131 if s.internalReason != "" { 132 ret = append(ret, fmt.Sprintf("internal reason: %s", s.internalReason)) 133 } 134 keys := make(tagKeySlice, 0, len(s.tags)) 135 for key := range s.tags { 136 keys = append(keys, key) 137 } 138 sort.Sort(keys) 139 for _, key := range keys { 140 if key != nil { 141 ret = append(ret, fmt.Sprintf("tag[%q]: %#v", key.description, s.tags[key])) 142 } else { 143 ret = append(ret, fmt.Sprintf("tag[nil]: %#v", s.tags[key])) 144 } 145 } 146 147 return ret 148 } 149 150 type terminalStackError struct { 151 error 152 finfo stackFrameInfo 153 tags map[TagKey]any 154 } 155 156 var _ interface { 157 error 158 stackContexter 159 } = (*terminalStackError)(nil) 160 161 func (e *terminalStackError) stackContext() stackContext { 162 return stackContext{frameInfo: e.finfo, tags: e.tags} 163 } 164 165 type annotatedError struct { 166 inner error 167 ctx stackContext 168 } 169 170 var _ interface { 171 error 172 stackContexter 173 Wrapped 174 } = (*annotatedError)(nil) 175 176 func (e *annotatedError) Error() string { return e.ctx.renderPublic(e.inner) } 177 func (e *annotatedError) stackContext() stackContext { return e.ctx } 178 func (e *annotatedError) Unwrap() error { return e.inner } 179 180 // Annotator is a builder for annotating errors. Obtain one by calling Annotate 181 // on an existing error or using Reason. 182 // 183 // See the example test for Annotate to see how this is meant to be used. 184 type Annotator struct { 185 inner error 186 ctx stackContext 187 } 188 189 // InternalReason adds a stack-trace-only internal reason string (for humans) to 190 // this error. 191 // 192 // The text here will only be visible when using `errors.Log` or 193 // `errors.RenderStack`, not when calling the .Error() method of the resulting 194 // error. 195 // 196 // The `reason` string is formatted with `args` and may contain Sprintf-style 197 // formatting directives. 198 func (a *Annotator) InternalReason(reason string, args ...any) *Annotator { 199 if a == nil { 200 return a 201 } 202 a.ctx.internalReason = fmt.Sprintf(reason, args...) 203 return a 204 } 205 206 // Tag adds a tag with an optional value to this error. 207 // 208 // `value` is a unary optional argument, and must be a simple type (i.e. has 209 // a reflect.Kind which is a base data type like bool, string, or int). 210 func (a *Annotator) Tag(tags ...TagValueGenerator) *Annotator { 211 if a == nil { 212 return a 213 } 214 tagMap := make(map[TagKey]any, len(tags)) 215 for _, t := range tags { 216 v := t.GenerateErrorTagValue() 217 tagMap[v.Key] = v.Value 218 } 219 if len(tagMap) > 0 { 220 if a.ctx.tags == nil { 221 a.ctx.tags = tagMap 222 } else { 223 for k, v := range tagMap { 224 a.ctx.tags[k] = v 225 } 226 } 227 } 228 return a 229 } 230 231 // Err returns the finalized annotated error. 232 // 233 //go:noinline 234 func (a *Annotator) Err() error { 235 if a == nil { 236 return nil 237 } 238 return (*annotatedError)(a) 239 } 240 241 // maxLogEntrySize limits log messages produced by Log. 242 // 243 // It is modified in tests of this package. 244 var maxLogEntrySize = 64 * 1024 245 246 // Log logs the full error. If this is an Annotated error, it will log the full 247 // stack information as well. 248 // 249 // This is a shortcut for logging the output of RenderStack(err). 250 // 251 // If resulting log message is large, splits it into log entries of at most 252 // 64KiB. 253 func Log(ctx context.Context, err error, excludePkgs ...string) { 254 log := logging.Get(ctx) 255 r := renderStack(err) 256 buf := strings.Builder{} 257 _, _ = r.dumpTo(&buf, excludePkgs...) // no errors can happen, only panics. 258 s := strings.TrimSuffix(buf.String(), "\n") 259 260 preamble := "" 261 for len(s) > 0 { 262 chunk := s 263 if maxLen := maxLogEntrySize - len(preamble); maxLen < len(chunk) { 264 // Make chunk end with '\n' and be smaller than maxLen. 265 chunk = strings.TrimRightFunc(chunk[:maxLen], func(r rune) bool { return r != '\n' }) 266 if len(chunk) == 0 { 267 // Chunk is 1 very long line, can't be nice about splitting it. 268 chunk = s[:maxLen] 269 } 270 } 271 s = s[len(chunk):] 272 chunk = strings.TrimSuffix(chunk, "\n") 273 log.Errorf("%s%s", preamble, chunk) 274 preamble = "(continuation of error log)\n" 275 } 276 } 277 278 // lines is just a list of printable lines. 279 // 280 // It's a type because it's most frequently used as []lines, and [][]string 281 // doesn't read well. 282 type lines []string 283 284 // renderedFrame represents a single, rendered stack frame. 285 type renderedFrame struct { 286 pkg string 287 file string 288 lineNum int 289 funcName string 290 291 // wrappers is any frame-info-less errors.Wrapped that were encountered when 292 // rendering that didn't have any associated frame info: this is the closest 293 // frame to where they were added to the error. 294 wrappers []lines 295 296 // annotations is any Annotate context associated directly with this Frame. 297 annotations []lines 298 } 299 300 var nlSlice = []byte{'\n'} 301 302 // dumpWrappersTo formats the wrappers portion of this renderedFrame. 303 func (r *renderedFrame) dumpWrappersTo(w io.Writer, from, to int) (n int, err error) { 304 return iotools.WriteTracker(w, func(rawWriter io.Writer) error { 305 w := &indented.Writer{Writer: rawWriter, UseSpaces: true} 306 fmt.Fprintf(w, "From frame %d to %d, the following wrappers were found:\n", from, to) 307 for i, wrp := range r.wrappers { 308 if i != 0 { 309 w.Write(nlSlice) 310 } 311 w.Level = 2 312 for i, line := range wrp { 313 if i == 0 { 314 fmt.Fprintf(w, "%s\n", line) 315 w.Level += 2 316 } else { 317 fmt.Fprintf(w, "%s\n", line) 318 } 319 } 320 } 321 return nil 322 }) 323 } 324 325 // dumpTo formats the Header and annotations for this renderedFrame. 326 func (r *renderedFrame) dumpTo(w io.Writer, idx int) (n int, err error) { 327 return iotools.WriteTracker(w, func(rawWriter io.Writer) error { 328 w := &indented.Writer{Writer: rawWriter, UseSpaces: true} 329 330 fmt.Fprintf(w, "#%d %s/%s:%d - %s()\n", idx, r.pkg, r.file, 331 r.lineNum, r.funcName) 332 w.Level += 2 333 switch len(r.annotations) { 334 case 0: 335 // pass 336 case 1: 337 for _, line := range r.annotations[0] { 338 fmt.Fprintf(w, "%s\n", line) 339 } 340 default: 341 for i, ann := range r.annotations { 342 fmt.Fprintf(w, "annotation #%d:\n", i) 343 w.Level += 2 344 for _, line := range ann { 345 fmt.Fprintf(w, "%s\n", line) 346 } 347 w.Level -= 2 348 } 349 } 350 return nil 351 }) 352 } 353 354 // renderedStack is a single rendered stack from one goroutine. 355 type renderedStack struct { 356 goID goroutine.ID 357 frames []*renderedFrame 358 } 359 360 // dumpTo formats the full stack. 361 func (r *renderedStack) dumpTo(w io.Writer, excludePkgs ...string) (n int, err error) { 362 excludeSet := stringset.NewFromSlice(excludePkgs...) 363 364 return iotools.WriteTracker(w, func(w io.Writer) error { 365 fmt.Fprintf(w, renderedStackDumpFormat, r.goID) 366 367 lastIdx := 0 368 needNL := false 369 skipCount := 0 370 skipPkg := "" 371 flushSkips := func(extra string) { 372 if skipCount != 0 { 373 if needNL { 374 w.Write(nlSlice) 375 needNL = false 376 } 377 fmt.Fprintf(w, "... skipped %d frames in pkg %q...\n%s", skipCount, skipPkg, extra) 378 skipCount = 0 379 skipPkg = "" 380 } 381 } 382 for i, f := range r.frames { 383 if needNL { 384 w.Write(nlSlice) 385 needNL = false 386 } 387 pkg := dropVersionSuffix(f.pkg) 388 if excludeSet.Has(pkg) { 389 if skipPkg == pkg { 390 skipCount++ 391 } else { 392 flushSkips("") 393 skipCount++ 394 skipPkg = pkg 395 } 396 continue 397 } 398 flushSkips("\n") 399 if len(f.wrappers) > 0 { 400 f.dumpWrappersTo(w, lastIdx, i) 401 w.Write(nlSlice) 402 } 403 if len(f.annotations) > 0 { 404 lastIdx = i 405 needNL = true 406 } 407 f.dumpTo(w, i) 408 } 409 flushSkips("") 410 411 return nil 412 }) 413 } 414 415 // dropVersionSuffix takes "a/b@zzz" and returns "a/b". 416 func dropVersionSuffix(pkg string) string { 417 if i := strings.LastIndexAny(pkg, "@"); i != -1 { 418 return pkg[:i] 419 } 420 return pkg 421 } 422 423 // renderedError is a series of RenderedStacks, one for each goroutine that the 424 // error was annotated on. 425 type renderedError struct { 426 // if originalError is set and `stacks` is empty, this was just a plain-old Go 427 // error without any stack context. 428 originalError string 429 stacks []*renderedStack 430 } 431 432 // toLines renders a full-information stack trace as a series of lines. 433 func (r *renderedError) toLines(excludePkgs ...string) lines { 434 if r.originalError != "" && len(r.stacks) == 0 { 435 return []string{r.originalError} 436 } 437 438 buf := bytes.Buffer{} 439 r.dumpTo(&buf, excludePkgs...) 440 return strings.Split(strings.TrimSuffix(buf.String(), "\n"), "\n") 441 } 442 443 // dumpTo writes the full-information stack trace to the writer. 444 func (r *renderedError) dumpTo(w io.Writer, excludePkgs ...string) (n int, err error) { 445 if r.originalError != "" && len(r.stacks) == 0 { 446 return w.Write([]byte(r.originalError)) 447 } 448 449 return iotools.WriteTracker(w, func(w io.Writer) error { 450 if r.originalError != "" { 451 fmt.Fprintf(w, "original error: %s\n\n", r.originalError) 452 } 453 454 for i := len(r.stacks) - 1; i >= 0; i-- { 455 if i != len(r.stacks)-1 { 456 w.Write(nlSlice) 457 } 458 r.stacks[i].dumpTo(w, excludePkgs...) 459 } 460 return nil 461 }) 462 } 463 464 func frameHeaderDetails(frm uintptr) (pkg, filename, funcName string, lineno int) { 465 // this `frm--` is to get the correct line/function information, since the 466 // Frame is actually the `return` pc. See runtime.Callers. 467 frm-- 468 469 fn := runtime.FuncForPC(frm) 470 file, lineno := fn.FileLine(frm) 471 472 var dirpath string 473 dirpath, filename = filepath.Split(file) 474 pkgTopLevelName := filepath.Base(dirpath) 475 476 fnName := fn.Name() 477 lastSlash := strings.LastIndex(fnName, "/") 478 if lastSlash == -1 { 479 funcName = fnName 480 pkg = pkgTopLevelName 481 } else { 482 funcName = fnName[lastSlash+1:] 483 pkg = fmt.Sprintf("%s/%s", fnName[:lastSlash], pkgTopLevelName) 484 } 485 return 486 } 487 488 // RenderStack renders the error to a list of lines. 489 func RenderStack(err error, excludePkgs ...string) []string { 490 return renderStack(err).toLines(excludePkgs...) 491 } 492 493 func renderStack(err error) *renderedError { 494 ret := &renderedError{} 495 496 lastAnnotatedFrame := 0 497 var wrappers []lines 498 getCurFrame := func(fi *stackFrameInfo) *renderedFrame { 499 if len(ret.stacks) == 0 || ret.stacks[len(ret.stacks)-1].goID != fi.forStack.id { 500 lastAnnotatedFrame = len(fi.forStack.frames) - 1 501 toAdd := &renderedStack{ 502 goID: fi.forStack.id, 503 frames: make([]*renderedFrame, len(fi.forStack.frames)), 504 } 505 for i, frm := range fi.forStack.frames { 506 pkgPath, filename, functionName, line := frameHeaderDetails(frm) 507 toAdd.frames[i] = &renderedFrame{ 508 pkg: pkgPath, file: filename, lineNum: line, funcName: functionName} 509 } 510 ret.stacks = append(ret.stacks, toAdd) 511 } 512 curStack := ret.stacks[len(ret.stacks)-1] 513 514 if fi.frameIdx < lastAnnotatedFrame { 515 lastAnnotatedFrame = fi.frameIdx 516 frm := curStack.frames[lastAnnotatedFrame] 517 frm.wrappers = wrappers 518 wrappers = nil 519 return frm 520 } 521 return curStack.frames[lastAnnotatedFrame] 522 } 523 524 // originalError will hold a pointer in the error tree of the last candidate 525 // error we encountered. 526 originalError := err 527 528 for err != nil { 529 // we attempt to walk the error; we expect to see a mix of stackContexters and 530 // non-stackContexters (i.e. regular Go errors, which may wrap other errors, 531 // possibly even stackContexter errors). When we hit a stackContexter, we need 532 // to select the error it contains as the next originalError, so we use this 533 // boolean to keep track. 534 needNewOriginalError := false 535 536 if sc, ok := err.(stackContexter); ok { 537 needNewOriginalError = true 538 ctx := sc.stackContext() 539 if stk := ctx.frameInfo.forStack; stk != nil { 540 frm := getCurFrame(&ctx.frameInfo) 541 if rendered := ctx.render(); len(rendered) > 0 { 542 frm.annotations = append(frm.annotations, rendered) 543 } 544 } else { 545 wrappers = append(wrappers, ctx.render()) 546 } 547 } else { 548 wrappers = append(wrappers, lines{fmt.Sprintf("unknown wrapper %T", err)}) 549 } 550 551 var inner error 552 switch x := err.(type) { 553 case MultiError: 554 // TODO(riannucci): it's kinda dumb that we have to walk the MultiError 555 // twice (once in its stackContext method, and again here). 556 inner = x.First() 557 case Wrapped: 558 inner = x.Unwrap() 559 } 560 561 if inner != nil { 562 err = inner 563 if needNewOriginalError { 564 originalError = err 565 needNewOriginalError = false 566 } 567 continue 568 } else { 569 // At this point we hit the bottom of the stack. 570 // We need to render the error with originalError in order to account for 571 // non-LUCI wrapped errors (e.g. `fmt.Errorf("something: %w", err)`). 572 ret.originalError = originalError.Error() 573 break 574 } 575 } 576 577 return ret 578 } 579 580 // Annotate captures the current stack frame and returns a new annotatable 581 // error, attaching the publicly readable `reason` format string to the error. 582 // You can add additional metadata to this error with the 'InternalReason' and 583 // 'Tag' methods, and then obtain a real `error` with the Err() function. 584 // 585 // If this is passed nil, it will return a no-op Annotator whose .Err() function 586 // will also return nil. 587 // 588 // The original error may be recovered by using Wrapped.Unwrap on the 589 // returned error. 590 // 591 // Rendering the derived error with Error() will render a summary version of all 592 // the public `reason`s as well as the initial underlying error's Error() text. 593 // It is intended that the initial underlying error and all annotated reasons 594 // only contain user-visible information, so that the accumulated error may be 595 // returned to the user without worrying about leakage. 596 // 597 // You should assume that end-users (including unauthenticated end users) may 598 // see the text in the `reason` field here. To only attach an internal reason, 599 // leave the `reason` argument blank and don't pass any additional formatting 600 // arguments. 601 // 602 // The `reason` string is formatted with `args` and may contain Sprintf-style 603 // formatting directives. 604 func Annotate(err error, reason string, args ...any) *Annotator { 605 if err == nil { 606 return nil 607 } 608 return &Annotator{err, stackContext{ 609 frameInfo: stackFrameInfoForError(1, err), 610 reason: fmt.Sprintf(reason, args...), 611 }} 612 } 613 614 // Reason builds a new Annotator starting with reason. This allows you to use 615 // all the formatting directives you would normally use with Annotate, in case 616 // your originating error needs tags or an internal reason. 617 // 618 // errors.Reason("something bad: %d", value).Tag(transient.Tag).Err() 619 // 620 // Prefer this form to errors.New(fmt.Sprintf("...")) or fmt.Errorf("...") 621 func Reason(reason string, args ...any) *Annotator { 622 currentStack := captureStack(1) 623 frameInfo := stackFrameInfo{0, currentStack} 624 return (&Annotator{nil, stackContext{ 625 frameInfo: frameInfo, 626 reason: fmt.Sprintf(reason, args...), 627 }}) 628 } 629 630 // New is an API-compatible version of the standard errors.New function. Unlike 631 // the stdlib errors.New, this will capture the current stack information at the 632 // place this error was created. 633 func New(msg string, tags ...TagValueGenerator) error { 634 tse := &terminalStackError{ 635 errors.New(msg), stackFrameInfo{forStack: captureStack(1)}, nil} 636 if len(tags) > 0 { 637 tse.tags = make(map[TagKey]any, len(tags)) 638 for _, t := range tags { 639 v := t.GenerateErrorTagValue() 640 tse.tags[v.Key] = v.Value 641 } 642 } 643 return tse 644 } 645 646 func captureStack(skip int) *stack { 647 fullStk := stack{goroutine.CurID(), nil} 648 stk := make([]uintptr, 16) 649 offset := skip + 2 650 for n := len(stk); n == len(stk); { 651 n = runtime.Callers(offset, stk) 652 offset += n 653 fullStk.frames = append(fullStk.frames, stk[:n]...) 654 } 655 return &fullStk 656 } 657 658 func getCapturedStack(err error) (ret *stack) { 659 Walk(err, func(err error) bool { 660 if sc, ok := err.(stackContexter); ok { 661 ret = sc.stackContext().frameInfo.forStack 662 return false 663 } 664 return true 665 }) 666 return 667 } 668 669 // stackFrameInfoForError returns a stackFrameInfo suitable for use to implement 670 // the stackContexter interface. 671 // 672 // It skips the provided number of frames when collecting the current trace 673 // (which should be equal to the number of functions between your error library 674 // and the user's code). 675 // 676 // The returned stackFrameInfo will find the appropriate frame in the error's 677 // existing stack information (if the error was created with errors.New), or 678 // include the current stack if it was not. 679 func stackFrameInfoForError(skip int, err error) stackFrameInfo { 680 currentStack := captureStack(skip + 1) 681 currentlyCapturedStack := getCapturedStack(err) 682 if currentlyCapturedStack == nil || currentStack.id != currentlyCapturedStack.id { 683 // This is the very first annotation on this error OR 684 // We switched goroutines. 685 return stackFrameInfo{forStack: currentStack} 686 } 687 return stackFrameInfo{ 688 frameIdx: currentlyCapturedStack.findPointOfDivergence(currentStack), 689 forStack: currentlyCapturedStack, 690 } 691 }