Skip to content

Commit abef583

Browse files
committed
Add panictoerror middleware that recovers panics and returns them as errors
This one encapsulates the middleware contributed here [1]. I've rewritten it fairly exhaustively and added tests, but it works the same way as the original. [1] riverqueue/river#1073 (comment)
1 parent b0128f1 commit abef583

9 files changed

Lines changed: 422 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Add `panictoerror` middleware that recovers panics and returns them as errors to middlewares up the stack. [PR #32](https://github.com/riverqueue/rivercontrib/pull/32).
13+
1014
### Changed
1115

1216
- More complete example test for `nilerror` package. [PR #27](https://github.com/riverqueue/rivercontrib/pull/27).

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ See:
77
* [`datadogriver`](../datadogriver): Package containing examples of using `otelriver` with [DataDog](https://www.datadoghq.com/).
88
* [`nilerror`](../nilerror): Package containing a River hook for detecting a common accidental Go problem where a nil struct value is wrapped in a non-nil interface value.
99
* [`otelriver`](../otelriver): Package for use with [OpenTelemetry](https://opentelemetry.io/).
10+
* [`panictoerror`](../panictoerror): Provides acontaining a middleware that recovers panics that may have occurred deeper in the middleware stack (i.e. an inner middleware or the worker itself), converts those panics to errors, and returns those errors up the stack.

go.work

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ use (
44
./datadogriver
55
./otelriver
66
./nilerror
7+
./panictoerror
78
)

panictoerror/README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# panictoerror [![Build Status](https://github.com/riverqueue/rivercontrib/actions/workflows/ci.yaml/badge.svg?branch=master)](https://github.com/riverqueue/rivercontrib/actions) [![Go Reference](https://pkg.go.dev/badge/github.com/riverqueue/rivercontrib.svg)](https://pkg.go.dev/github.com/riverqueue/rivercontrib/nilerror)
2+
3+
Provides a `rivertype.WorkerMiddleware` that recovers panics that may have occurred deeper in the middleware stack (i.e. an inner middleware or the worker itself), converts those panics to errors, and returns those errors up the stack. This may be convenient in some cases so that middleware further up the stack need only have one way to handle either return errors or panic values.
4+
5+
``` go
6+
// A worker implementation which will always panic.
7+
func (w *PanicErrorWorker) Work(ctx context.Context, job *river.Job[PanicErrorArgs]) error {
8+
panic("this worker always panics!")
9+
}
10+
11+
riverClient, err := river.NewClient(riverpgxv5.New(dbPool), &river.Config{
12+
Middleware: []rivertype.Middleware{
13+
// This middleware further up the stack always receives an error instead
14+
// of a panic because `panictoerror.Middleware` is nested below it.
15+
river.WorkerMiddlewareFunc(func(ctx context.Context, job *rivertype.JobRow, doInner func(ctx context.Context) error) error {
16+
if err := doInner(ctx); err != nil {
17+
panicErr := err.(*panictoerror.PanicError)
18+
fmt.Printf("error from doInner: %s", panicErr.Cause)
19+
}
20+
return nil
21+
}),
22+
23+
// This middleware coverts the panic to an error.
24+
panictoerror.NewMiddleware(nil),
25+
},
26+
}
27+
```
28+
29+
Based [on work](https://github.com/riverqueue/river/issues/1073#issuecomment-3515520394) from [@jerbob92](https://github.com/jerbob92).
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package panictoerror_test
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log/slog"
7+
8+
"github.com/jackc/pgx/v5/pgxpool"
9+
"github.com/riverqueue/river"
10+
"github.com/riverqueue/river/riverdbtest"
11+
"github.com/riverqueue/river/riverdriver/riverpgxv5"
12+
"github.com/riverqueue/river/rivershared/riversharedtest"
13+
"github.com/riverqueue/river/rivershared/util/slogutil"
14+
"github.com/riverqueue/river/rivershared/util/testutil"
15+
"github.com/riverqueue/river/rivertype"
16+
"github.com/riverqueue/rivercontrib/panictoerror"
17+
)
18+
19+
type PanicErrorArgs struct{}
20+
21+
func (PanicErrorArgs) Kind() string { return "custom_error" }
22+
23+
type PanicErrorWorker struct {
24+
river.WorkerDefaults[PanicErrorArgs]
25+
}
26+
27+
func (w *PanicErrorWorker) Work(ctx context.Context, job *river.Job[PanicErrorArgs]) error {
28+
panic("this worker always panics!")
29+
}
30+
31+
func ExampleMiddleware() {
32+
ctx := context.Background()
33+
34+
dbPool, err := pgxpool.New(ctx, riversharedtest.TestDatabaseURL())
35+
if err != nil {
36+
panic(err)
37+
}
38+
defer dbPool.Close()
39+
40+
workers := river.NewWorkers()
41+
river.AddWorker(workers, &PanicErrorWorker{})
42+
43+
riverClient, err := river.NewClient(riverpgxv5.New(dbPool), &river.Config{
44+
Logger: slog.New(&slogutil.SlogMessageOnlyHandler{Level: slog.LevelWarn}),
45+
Middleware: []rivertype.Middleware{
46+
// Layer a middleware above panictoerror.Middleware that takes a
47+
// return error and prints it to stdout for the purpose of this test.
48+
river.WorkerMiddlewareFunc(func(ctx context.Context, job *rivertype.JobRow, doInner func(ctx context.Context) error) error {
49+
if err := doInner(ctx); err != nil {
50+
panicErr := err.(*panictoerror.PanicError)
51+
fmt.Printf("error from doInner: %s", panicErr.Cause)
52+
}
53+
return nil
54+
}),
55+
56+
// This middleware coverts the panic to an error.
57+
panictoerror.NewMiddleware(nil),
58+
},
59+
Queues: map[string]river.QueueConfig{
60+
river.QueueDefault: {MaxWorkers: 100},
61+
},
62+
Schema: riverdbtest.TestSchema(ctx, testutil.PanicTB(), riverpgxv5.New(dbPool), nil), // only necessary for the example test
63+
TestOnly: true, // suitable only for use in tests; remove for live environments
64+
Workers: workers,
65+
})
66+
if err != nil {
67+
panic(err)
68+
}
69+
70+
// Out of example scope, but used to wait until a job is worked.
71+
subscribeChan, subscribeCancel := riverClient.Subscribe(river.EventKindJobCompleted)
72+
defer subscribeCancel()
73+
74+
if _, err = riverClient.Insert(ctx, PanicErrorArgs{}, nil); err != nil {
75+
panic(err)
76+
}
77+
78+
if err := riverClient.Start(ctx); err != nil {
79+
panic(err)
80+
}
81+
82+
// Wait for jobs to complete. Only needed for purposes of the example test.
83+
riversharedtest.WaitOrTimeoutN(testutil.PanicTB(), subscribeChan, 1)
84+
85+
if err := riverClient.Stop(ctx); err != nil {
86+
panic(err)
87+
}
88+
89+
// Output:
90+
// error from doInner: this worker always panics!
91+
}

panictoerror/go.mod

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
module github.com/riverqueue/rivercontrib/panictoerror
2+
3+
go 1.24.2
4+
5+
require (
6+
github.com/riverqueue/river v0.26.0
7+
github.com/riverqueue/river/rivershared v0.26.0
8+
github.com/riverqueue/river/rivertype v0.26.0
9+
github.com/stretchr/testify v1.11.1
10+
)
11+
12+
require (
13+
github.com/davecgh/go-spew v1.1.1 // indirect
14+
github.com/jackc/pgpassfile v1.0.0 // indirect
15+
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
16+
github.com/jackc/pgx/v5 v5.7.6 // indirect
17+
github.com/jackc/puddle/v2 v2.2.2 // indirect
18+
github.com/pmezard/go-difflib v1.0.0 // indirect
19+
github.com/riverqueue/river/riverdriver v0.26.0 // indirect
20+
github.com/tidwall/gjson v1.18.0 // indirect
21+
github.com/tidwall/match v1.1.1 // indirect
22+
github.com/tidwall/pretty v1.2.1 // indirect
23+
github.com/tidwall/sjson v1.2.5 // indirect
24+
go.uber.org/goleak v1.3.0 // indirect
25+
golang.org/x/crypto v0.37.0 // indirect
26+
golang.org/x/sync v0.17.0 // indirect
27+
golang.org/x/text v0.29.0 // indirect
28+
gopkg.in/yaml.v3 v3.0.1 // indirect
29+
)

panictoerror/go.sum

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4+
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0=
5+
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
6+
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
7+
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
8+
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
9+
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
10+
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
11+
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
12+
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
13+
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
14+
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
15+
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
16+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
17+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
18+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
19+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
20+
github.com/riverqueue/river v0.26.0 h1:Lykh7L6iDBNxku3NXrnL5RXUGk7FgEnk5CdN/ak3lko=
21+
github.com/riverqueue/river v0.26.0/go.mod h1:w8+9lbnPQe/vlmBsIG7T1TObTm94Rvx63ZLUZHPmcR8=
22+
github.com/riverqueue/river/riverdriver v0.26.0 h1:hMW/OOEjAkyvkTIzTf/zqZChThJCQQO0Mi2aMvgcFzg=
23+
github.com/riverqueue/river/riverdriver v0.26.0/go.mod h1:qRLS0bFTrwmCevlpaMje5jhQK6aCDMJ9i8hRFbXAgTo=
24+
github.com/riverqueue/river/riverdriver/riverpgxv5 v0.26.0 h1:M5t0t9wZJwOIO0f6Gsbn5LmNLUQlk9K1gL0DhkZvd6k=
25+
github.com/riverqueue/river/riverdriver/riverpgxv5 v0.26.0/go.mod h1:+fkIOQtVOaUaDyJyVFK3R3bA1sg6DqGEQ0F9D47sG48=
26+
github.com/riverqueue/river/rivershared v0.26.0 h1:tsMvxTIdG58GoYXd3788DwjNq87Y7CcfRlV7TAzeuhw=
27+
github.com/riverqueue/river/rivershared v0.26.0/go.mod h1:/BEdbdGEqfcFP9FtChwK81e2AWF8e82RC6z5mwQ3y1g=
28+
github.com/riverqueue/river/rivertype v0.26.0 h1:C3GdCMH8khTUUKH+OkTSQv1kdsSAXWL8n7M7Rq2r4yE=
29+
github.com/riverqueue/river/rivertype v0.26.0/go.mod h1:rWpgI59doOWS6zlVocROcwc00fZ1RbzRwsRTU8CDguw=
30+
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
31+
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
32+
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
33+
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
34+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
35+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
36+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
37+
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
38+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
39+
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
40+
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
41+
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
42+
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
43+
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
44+
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
45+
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
46+
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
47+
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
48+
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
49+
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
50+
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
51+
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
52+
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
53+
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
54+
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
55+
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
56+
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
57+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
58+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
59+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
60+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
61+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
62+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

panictoerror/middleware.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// Package panictoerror provides a rivertype.WorkerMiddleware that recovers
2+
// panics that may have occurred deeper in the middleware stack (i.e. an inner
3+
// middleware or the worker itself), converts those panics to errors, and
4+
// returns those errors up the stack. This may be convenient in some cases so
5+
// that middleware further up the stack need only have one way to handle either
6+
// return errors or panic values.
7+
package panictoerror
8+
9+
import (
10+
"context"
11+
"fmt"
12+
"runtime"
13+
"strings"
14+
15+
"github.com/riverqueue/river"
16+
"github.com/riverqueue/river/rivershared/baseservice"
17+
"github.com/riverqueue/river/rivertype"
18+
)
19+
20+
// Verify interface compliance.
21+
var _ rivertype.WorkerMiddleware = &Middleware{}
22+
23+
// PanicError is a pnaic that's been converted to an error.
24+
type PanicError struct {
25+
// Cause is the value recovered with `recover()`.
26+
Cause any
27+
28+
// Trace is up to the stop 100 stack frames when the panic occurred. The
29+
// middleware attempts to remove internal frames on top so that user code is
30+
// the first stack frame.
31+
Trace []*runtime.Frame
32+
}
33+
34+
func (e *PanicError) Error() string {
35+
var sb strings.Builder
36+
for _, frame := range e.Trace {
37+
sb.WriteString(fmt.Sprintf("%s\n\t%s:%d\n", frame.Function, frame.File, frame.Line))
38+
}
39+
40+
return fmt.Sprintf("PanicError: %v\n%s", e.Cause, sb.String())
41+
}
42+
43+
func (e *PanicError) Is(target error) bool {
44+
_, ok := target.(*PanicError)
45+
return ok
46+
}
47+
48+
// MiddlewareConfig is configuration for the panictoerror middleware.
49+
//
50+
// Currently empty, but reserved for future use.
51+
type MiddlewareConfig struct{}
52+
53+
// Middleware is a rivertype.WorkerMiddleware that recovers panics that may have
54+
// occurred deeper in the middleware stack (i.e. an inner middleware or the
55+
// worker itself), converts those panics to errors, and returns those errors up
56+
// the stack.
57+
type Middleware struct {
58+
baseservice.BaseService
59+
river.MiddlewareDefaults
60+
config *MiddlewareConfig
61+
}
62+
63+
// NewMiddleware initializes a new River panictoerror middleare.
64+
//
65+
// config may be nil.
66+
func NewMiddleware(config *MiddlewareConfig) *Middleware {
67+
if config == nil {
68+
config = &MiddlewareConfig{}
69+
}
70+
71+
return &Middleware{
72+
config: config,
73+
}
74+
}
75+
76+
func (s *Middleware) Work(ctx context.Context, job *rivertype.JobRow, doInner func(context.Context) error) (err error) {
77+
defer func() {
78+
if recovery := recover(); recovery != nil {
79+
err = &PanicError{
80+
Cause: recovery,
81+
82+
// Skip (1) Callers, (2) captureStackTraceSkipFrames, (3) Work (this function), and (4) panic.go.
83+
//
84+
// runtime.Callers
85+
// /opt/homebrew/Cellar/go/1.25.0/libexec/src/runtime/extern.go:345
86+
// github.com/riverqueue/rivercontrib/panictoerror.captureStackTraceSkipFrames
87+
// /Users/brandur/Documents/projects/rivercontrib/panictoerror/middleware.go:77
88+
// github.com/riverqueue/rivercontrib/panictoerror.(*Middleware).Work.func1
89+
// /Users/brandur/Documents/projects/rivercontrib/panictoerror/middleware.go:58
90+
// runtime.gopanic
91+
// /opt/homebrew/Cellar/go/1.25.0/libexec/src/runtime/panic.go:783
92+
Trace: captureStackFrames(4),
93+
}
94+
}
95+
}()
96+
97+
err = doInner(ctx)
98+
return err
99+
}
100+
101+
// captureStackFrames captures the current stack trace, skipping the top
102+
// numSkipped frames.
103+
func captureStackFrames(numSkipped int) []*runtime.Frame {
104+
var (
105+
// Allocate room for up to 100 callers; adjust as needed.
106+
callers = make([]uintptr, 100)
107+
108+
// Skip the specified number of frames.
109+
numFrames = runtime.Callers(numSkipped, callers)
110+
111+
frames = runtime.CallersFrames(callers[:numFrames])
112+
)
113+
114+
trace := make([]*runtime.Frame, 0, numFrames)
115+
for {
116+
frame, more := frames.Next()
117+
trace = append(trace, &frame)
118+
if !more {
119+
break
120+
}
121+
}
122+
return trace
123+
}

0 commit comments

Comments
 (0)