Skip to content

Commit dfcd83d

Browse files
rosstimothymvbrock
authored andcommitted
Vendor gravitational/trace/trail in api (#51032)
* Vendor gravitational/trace/trail in api Pulling in the trail package directly in api will allow the trace module to shed the grpc-go dependency. This needs to land prior to gravitational/trace#112 being included in a new version of trace. There should be no noticable change in the api depdency tree since it already depends on grpc-go. Some additional items from the trace/internal package were also vendored within trail as needed. Additionally, some of the public api of trail that was not being consumed has been made private. * fix: appease linters
1 parent d840625 commit dfcd83d

File tree

28 files changed

+530
-26
lines changed

28 files changed

+530
-26
lines changed

api/client/client_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@ import (
2929

3030
"github.com/google/go-cmp/cmp"
3131
"github.com/gravitational/trace"
32-
"github.com/gravitational/trace/trail"
3332
"github.com/stretchr/testify/assert"
3433
"github.com/stretchr/testify/require"
3534

3635
"github.com/gravitational/teleport/api"
3736
"github.com/gravitational/teleport/api/client/proto"
3837
"github.com/gravitational/teleport/api/defaults"
3938
"github.com/gravitational/teleport/api/metadata"
39+
"github.com/gravitational/teleport/api/trail"
4040
"github.com/gravitational/teleport/api/types"
4141
)
4242

api/client/proxy/client_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import (
3030
"github.com/google/go-cmp/cmp"
3131
"github.com/google/go-cmp/cmp/cmpopts"
3232
"github.com/gravitational/trace"
33-
"github.com/gravitational/trace/trail"
3433
"github.com/stretchr/testify/assert"
3534
"github.com/stretchr/testify/require"
3635
"golang.org/x/crypto/ssh"
@@ -44,6 +43,7 @@ import (
4443
"github.com/gravitational/teleport/api/client"
4544
"github.com/gravitational/teleport/api/client/proto"
4645
transportv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/transport/v1"
46+
"github.com/gravitational/teleport/api/trail"
4747
"github.com/gravitational/teleport/api/utils/grpc/stream"
4848
)
4949

api/client/proxy/transport/transportv1/client_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@ import (
2929
"time"
3030

3131
"github.com/gravitational/trace"
32-
"github.com/gravitational/trace/trail"
3332
"github.com/stretchr/testify/require"
3433
"golang.org/x/crypto/ssh/agent"
3534
"google.golang.org/grpc"
3635
"google.golang.org/grpc/credentials/insecure"
3736
"google.golang.org/grpc/test/bufconn"
3837

3938
transportv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/transport/v1"
39+
"github.com/gravitational/teleport/api/trail"
4040
"github.com/gravitational/teleport/api/utils/grpc/interceptors"
4141
streamutils "github.com/gravitational/teleport/api/utils/grpc/stream"
4242
)

api/client/secreport/crud.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ import (
2020
"context"
2121

2222
"github.com/gravitational/trace"
23-
"github.com/gravitational/trace/trail"
2423

2524
pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/secreports/v1"
25+
"github.com/gravitational/teleport/api/trail"
2626
"github.com/gravitational/teleport/api/types/secreports"
2727
v1 "github.com/gravitational/teleport/api/types/secreports/convert/v1"
2828
)

api/client/secreport/secreport.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ import (
2020
"context"
2121

2222
"github.com/gravitational/trace"
23-
"github.com/gravitational/trace/trail"
2423

2524
pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/secreports/v1"
25+
"github.com/gravitational/teleport/api/trail"
2626
"github.com/gravitational/teleport/api/types/secreports"
2727
v1 "github.com/gravitational/teleport/api/types/secreports/convert/v1"
2828
)

api/trail/trail.go

+313
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
/*
2+
Copyright 2016 Gravitational, Inc.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// Package trail integrates trace errors with GRPC
18+
//
19+
// Example server that sends the GRPC error and attaches metadata:
20+
//
21+
// func (s *server) Echo(ctx context.Context, message *gw.StringMessage) (*gw.StringMessage, error) {
22+
// trace.SetDebug(true) // to tell trace to start attaching metadata
23+
// // Send sends metadata via grpc header and converts error to GRPC compatible one
24+
// return nil, trail.Send(ctx, trace.AccessDenied("missing authorization"))
25+
// }
26+
//
27+
// Example client reading error and trace debug info:
28+
//
29+
// var header metadata.MD
30+
// r, err := c.Echo(context.Background(), &gw.StringMessage{Value: message}, grpc.Header(&header))
31+
// if err != nil {
32+
// // FromGRPC reads error, converts it back to trace error and attaches debug metadata
33+
// // like stack trace of the error origin back to the error
34+
// err = trail.FromGRPC(err, header)
35+
//
36+
// // this line will log original trace of the error
37+
// log.Errorf("error saying echo: %v", trace.DebugReport(err))
38+
// return
39+
// }
40+
package trail
41+
42+
import (
43+
"encoding/base64"
44+
"encoding/json"
45+
"errors"
46+
"io"
47+
"os"
48+
"runtime"
49+
50+
"github.com/gravitational/trace"
51+
"google.golang.org/grpc/codes"
52+
"google.golang.org/grpc/metadata"
53+
"google.golang.org/grpc/status"
54+
)
55+
56+
// DebugReportMetadata is a key in metadata holding debug information
57+
// about the error - stack traces and original error
58+
const debugReportMetadata = "trace-debug-report"
59+
60+
// ToGRPC converts error to GRPC-compatible error
61+
func ToGRPC(originalErr error) error {
62+
if originalErr == nil {
63+
return nil
64+
}
65+
66+
// Avoid modifying top-level gRPC errors.
67+
if _, ok := status.FromError(originalErr); ok {
68+
return originalErr
69+
}
70+
71+
code := codes.Unknown
72+
returnOriginal := false
73+
traverseErr(originalErr, func(err error) (ok bool) {
74+
if errors.Is(err, io.EOF) {
75+
// Keep legacy semantics and return the original error.
76+
returnOriginal = true
77+
return true
78+
}
79+
80+
if s, ok := status.FromError(err); ok {
81+
code = s.Code()
82+
return true
83+
}
84+
85+
// Duplicate check from trace.IsNotFound.
86+
if os.IsNotExist(err) {
87+
code = codes.NotFound
88+
return true
89+
}
90+
91+
ok = true // Assume match
92+
93+
var (
94+
accessDeniedErr *trace.AccessDeniedError
95+
alreadyExistsErr *trace.AlreadyExistsError
96+
badParameterErr *trace.BadParameterError
97+
compareFailedErr *trace.CompareFailedError
98+
connectionProblemErr *trace.ConnectionProblemError
99+
limitExceededErr *trace.LimitExceededError
100+
notFoundErr *trace.NotFoundError
101+
notImplementedErr *trace.NotImplementedError
102+
oauthErr *trace.OAuth2Error
103+
)
104+
if errors.As(err, &accessDeniedErr) {
105+
code = codes.PermissionDenied
106+
} else if errors.As(err, &alreadyExistsErr) {
107+
code = codes.AlreadyExists
108+
} else if errors.As(err, &badParameterErr) {
109+
code = codes.InvalidArgument
110+
} else if errors.As(err, &compareFailedErr) {
111+
code = codes.FailedPrecondition
112+
} else if errors.As(err, &connectionProblemErr) {
113+
code = codes.Unavailable
114+
} else if errors.As(err, &limitExceededErr) {
115+
code = codes.ResourceExhausted
116+
} else if errors.As(err, &notFoundErr) {
117+
code = codes.NotFound
118+
} else if errors.As(err, &notImplementedErr) {
119+
code = codes.Unimplemented
120+
} else if errors.As(err, &oauthErr) {
121+
code = codes.InvalidArgument
122+
} else {
123+
// *trace.RetryError not mapped.
124+
// *trace.TrustError not mapped.
125+
ok = false
126+
}
127+
128+
return ok
129+
})
130+
if returnOriginal {
131+
return originalErr
132+
}
133+
134+
return status.Error(code, trace.UserMessage(originalErr))
135+
}
136+
137+
// FromGRPC converts error from GRPC error back to trace.Error
138+
// Debug information will be retrieved from the metadata if specified in args
139+
func FromGRPC(err error, args ...interface{}) error {
140+
if err == nil {
141+
return nil
142+
}
143+
144+
statusErr := status.Convert(err)
145+
code := statusErr.Code()
146+
message := statusErr.Message()
147+
148+
var e error
149+
switch code {
150+
case codes.OK:
151+
return nil
152+
case codes.NotFound:
153+
e = &trace.NotFoundError{Message: message}
154+
case codes.AlreadyExists:
155+
e = &trace.AlreadyExistsError{Message: message}
156+
case codes.PermissionDenied:
157+
e = &trace.AccessDeniedError{Message: message}
158+
case codes.FailedPrecondition:
159+
e = &trace.CompareFailedError{Message: message}
160+
case codes.InvalidArgument:
161+
e = &trace.BadParameterError{Message: message}
162+
case codes.ResourceExhausted:
163+
e = &trace.LimitExceededError{Message: message}
164+
case codes.Unavailable:
165+
e = &trace.ConnectionProblemError{Message: message}
166+
case codes.Unimplemented:
167+
e = &trace.NotImplementedError{Message: message}
168+
default:
169+
e = err
170+
}
171+
if len(args) != 0 {
172+
if meta, ok := args[0].(metadata.MD); ok {
173+
e = decodeDebugInfo(e, meta)
174+
// We return here because if it's a trace.Error then
175+
// frames was already extracted from metadata so
176+
// there's no need to capture frames once again.
177+
var traceErr trace.Error
178+
if errors.As(e, &traceErr) {
179+
return e
180+
}
181+
}
182+
}
183+
traces := captureTraces(1)
184+
return &trace.TraceErr{Err: e, Traces: traces}
185+
}
186+
187+
// setDebugInfo adds debug metadata about error (traces, original error)
188+
// to request metadata as encoded property
189+
func setDebugInfo(err error, meta metadata.MD) {
190+
var traceErr trace.Error
191+
if !errors.As(err, &traceErr) {
192+
return
193+
}
194+
195+
out, err := json.Marshal(err)
196+
if err != nil {
197+
return
198+
}
199+
meta[debugReportMetadata] = []string{
200+
base64.StdEncoding.EncodeToString(out),
201+
}
202+
}
203+
204+
// decodeDebugInfo decodes debug information about error
205+
// from the metadata and returns error with enriched metadata about it
206+
func decodeDebugInfo(err error, meta metadata.MD) error {
207+
if len(meta) == 0 {
208+
return err
209+
}
210+
encoded, ok := meta[debugReportMetadata]
211+
if !ok || len(encoded) != 1 {
212+
return err
213+
}
214+
data, decodeErr := base64.StdEncoding.DecodeString(encoded[0])
215+
if decodeErr != nil {
216+
return err
217+
}
218+
var raw trace.RawTrace
219+
if unmarshalErr := json.Unmarshal(data, &raw); unmarshalErr != nil {
220+
return err
221+
}
222+
if len(raw.Traces) != 0 && len(raw.Err) != 0 {
223+
return &trace.TraceErr{Traces: raw.Traces, Err: err, Message: raw.Message}
224+
}
225+
return err
226+
}
227+
228+
// traverseErr traverses the err error chain until fn returns true.
229+
// Traversal stops on nil errors, fn(nil) is never called.
230+
// Returns true if fn matched, false otherwise.
231+
func traverseErr(err error, fn func(error) (ok bool)) (ok bool) {
232+
if err == nil {
233+
return false
234+
}
235+
236+
if fn(err) {
237+
return true
238+
}
239+
240+
type singleUnwrap interface {
241+
Unwrap() error
242+
}
243+
244+
type aggregateUnwrap interface {
245+
Unwrap() []error
246+
}
247+
248+
var singleErr singleUnwrap
249+
var aggregateErr aggregateUnwrap
250+
251+
if errors.As(err, &singleErr) {
252+
return traverseErr(singleErr.Unwrap(), fn)
253+
}
254+
255+
if errors.As(err, &aggregateErr) {
256+
for _, err2 := range aggregateErr.Unwrap() {
257+
if traverseErr(err2, fn) {
258+
return true
259+
}
260+
}
261+
}
262+
263+
return false
264+
}
265+
266+
// FrameCursor stores the position in a call stack
267+
type frameCursor struct {
268+
// Current specifies the current stack frame.
269+
// if omitted, rest contains the complete stack
270+
Current *runtime.Frame
271+
// Rest specifies the rest of stack frames to explore
272+
Rest *runtime.Frames
273+
// N specifies the total number of stack frames
274+
N int
275+
}
276+
277+
// CaptureTraces gets the current stack trace with some deep frames skipped
278+
func captureTraces(skip int) trace.Traces {
279+
var buf [32]uintptr
280+
// +2 means that we also skip `CaptureTraces` and `runtime.Callers` frames.
281+
n := runtime.Callers(skip+2, buf[:])
282+
pcs := buf[:n]
283+
frames := runtime.CallersFrames(pcs)
284+
cursor := frameCursor{
285+
Rest: frames,
286+
N: n,
287+
}
288+
return getTracesFromCursor(cursor)
289+
}
290+
291+
// GetTracesFromCursor gets the current stack trace from a given cursor
292+
func getTracesFromCursor(cursor frameCursor) trace.Traces {
293+
traces := make(trace.Traces, 0, cursor.N)
294+
if cursor.Current != nil {
295+
traces = append(traces, frameToTrace(*cursor.Current))
296+
}
297+
for i := 0; i < cursor.N; i++ {
298+
frame, more := cursor.Rest.Next()
299+
traces = append(traces, frameToTrace(frame))
300+
if !more {
301+
break
302+
}
303+
}
304+
return traces
305+
}
306+
307+
func frameToTrace(frame runtime.Frame) trace.Trace {
308+
return trace.Trace{
309+
Func: frame.Function,
310+
Path: frame.File,
311+
Line: frame.Line,
312+
}
313+
}

0 commit comments

Comments
 (0)