Skip to content

Stacktraces

Three calls capture a stacktrace: patrol.NewException(err), event.WithError(err) and event.WithDebug(message). All three use patrol.NewStacktrace(), which you can also call yourself and attach with event.WithStacktrace(st). Passing nil to WithStacktrace removes one.

A Stacktrace is one field, Frames []Frame, outermost first. The frame you are reading is at the top of the list, main at the bottom, which is the opposite of a Go panic dump and the same order most issue trackers use.

Field JSON What it holds
Function function The function name without its package path
Module module The import path of the package that holds it
Filename filename The file path as the runtime reported it, when that path is relative
AbsPath abs_path The absolute file path, when the runtime reported one
Lineno lineno The line in that file
InApp in_app Whether the frame looks like your code rather than a library’s

Exactly one of Filename and AbsPath is set per frame. A build with trimmed paths reports relative ones, and then Filename carries them. Both fields are omitempty, and so is every other one, so a frame in the JSON is as short as the facts allow.

At most 64 frames are captured. NewStacktrace returns nil when the runtime gives it none.

Noise, before you ever see it:

  • Every frame whose module is runtime or testing.
  • Every frame inside github.com/kataras/patrol itself, unless the file ends in _test.go. The call that captured the trace is not interesting; the code that hit the error is.
  • Every frame whose module starts with a prefix listed in patrol.SkipModuleFrames.

InApp is a guess, not a filter. It is false for an empty module and for any path containing vendor or third_party, true for main, and otherwise true when the first element of the import path contains a dot, which is how a module path is spelled and a standard library path is not. A console can grey out the rest; nothing in the library acts on it.

A framework or a middleware package between the error and the handler shows up in every trace. AddSkipModuleFrames takes prefixes, matched against a frame’s module:

package main
import (
"context"
"errors"
"log"
"github.com/kataras/patrol"
)
func init() {
// Before any stacktrace is captured: this slice is read without a lock.
patrol.AddSkipModuleFrames(
"github.com/kataras/iris/v14",
"github.com/example/internal/middleware",
)
}
func main() {
ctx := context.Background()
producer := patrol.NewProducer(patrol.ProducerOptions{ProjectName: "checkout-api"})
defer producer.Close(ctx)
if err := producer.CaptureException(ctx, errors.New("charge declined")); err != nil {
log.Fatal(err)
}
}

SkipModuleFrames is a plain exported slice and AddSkipModuleFrames appends to it without a lock. Call it from init or from the top of main, before the first event. Adding to it while another goroutine is capturing a trace is a data race, and the race detector will say so.

A frame with an AbsPath renders as a link. Slack and the email template both build vscode://file/<path>:<line> and show <path>:<line> as the text, so a frame in a Slack thread opens in the editor on the machine reading it. A frame with only a relative Filename renders as plain text, because a relative path means nothing on someone else’s disk.

That is also why a service built with -trimpath produces traces you can read but cannot click.

{
"stacktrace": {
"frames": [
{ "function": "main", "module": "main", "abs_path": "/srv/checkout/main.go", "lineno": 41, "in_app": true },
{ "function": "Handler.Charge", "module": "github.com/example/checkout/http", "abs_path": "/srv/checkout/http/handler.go", "lineno": 118, "in_app": true },
{ "function": "chargeCard", "module": "github.com/example/checkout/payments", "abs_path": "/srv/checkout/payments/stripe.go", "lineno": 64, "in_app": true }
]
}
}

Outermost first: main called the handler, the handler called chargeCard, and chargeCard is where the error was captured.

Next: pick a destination.