-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
1 parent
e4e09a1
commit a08909a
Showing
28 changed files
with
505 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,287 @@ | ||
/* | ||
Copyright 2016 Gravitational, Inc. | ||
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 trail integrates trace errors with GRPC | ||
// | ||
// Example server that sends the GRPC error and attaches metadata: | ||
// | ||
// func (s *server) Echo(ctx context.Context, message *gw.StringMessage) (*gw.StringMessage, error) { | ||
// trace.SetDebug(true) // to tell trace to start attaching metadata | ||
// // Send sends metadata via grpc header and converts error to GRPC compatible one | ||
// return nil, trail.Send(ctx, trace.AccessDenied("missing authorization")) | ||
// } | ||
// | ||
// Example client reading error and trace debug info: | ||
// | ||
// var header metadata.MD | ||
// r, err := c.Echo(context.Background(), &gw.StringMessage{Value: message}, grpc.Header(&header)) | ||
// if err != nil { | ||
// // FromGRPC reads error, converts it back to trace error and attaches debug metadata | ||
// // like stack trace of the error origin back to the error | ||
// err = trail.FromGRPC(err, header) | ||
// | ||
// // this line will log original trace of the error | ||
// log.Errorf("error saying echo: %v", trace.DebugReport(err)) | ||
// return | ||
// } | ||
package trail | ||
|
||
import ( | ||
"encoding/base64" | ||
"encoding/json" | ||
"io" | ||
"os" | ||
"runtime" | ||
|
||
"github.com/gravitational/trace" | ||
|
||
"google.golang.org/grpc/codes" | ||
"google.golang.org/grpc/metadata" | ||
"google.golang.org/grpc/status" | ||
) | ||
|
||
// DebugReportMetadata is a key in metadata holding debug information | ||
// about the error - stack traces and original error | ||
const debugReportMetadata = "trace-debug-report" | ||
|
||
// ToGRPC converts error to GRPC-compatible error | ||
func ToGRPC(originalErr error) error { | ||
if originalErr == nil { | ||
return nil | ||
} | ||
|
||
// Avoid modifying top-level gRPC errors. | ||
if _, ok := status.FromError(originalErr); ok { | ||
return originalErr | ||
} | ||
|
||
code := codes.Unknown | ||
returnOriginal := false | ||
traverseErr(originalErr, func(err error) (ok bool) { | ||
if err == io.EOF { | ||
// Keep legacy semantics and return the original error. | ||
returnOriginal = true | ||
return true | ||
} | ||
|
||
if s, ok := status.FromError(err); ok { | ||
code = s.Code() | ||
return true | ||
} | ||
|
||
// Duplicate check from trace.IsNotFound. | ||
if os.IsNotExist(err) { | ||
code = codes.NotFound | ||
return true | ||
} | ||
|
||
ok = true // Assume match | ||
switch err.(type) { | ||
case *trace.AccessDeniedError: | ||
code = codes.PermissionDenied | ||
case *trace.AlreadyExistsError: | ||
code = codes.AlreadyExists | ||
case *trace.BadParameterError: | ||
code = codes.InvalidArgument | ||
case *trace.CompareFailedError: | ||
code = codes.FailedPrecondition | ||
case *trace.ConnectionProblemError: | ||
code = codes.Unavailable | ||
case *trace.LimitExceededError: | ||
code = codes.ResourceExhausted | ||
case *trace.NotFoundError: | ||
code = codes.NotFound | ||
case *trace.NotImplementedError: | ||
code = codes.Unimplemented | ||
case *trace.OAuth2Error: | ||
code = codes.InvalidArgument | ||
// *trace.RetryError not mapped. | ||
// *trace.TrustError not mapped. | ||
default: | ||
ok = false | ||
} | ||
return ok | ||
}) | ||
if returnOriginal { | ||
return originalErr | ||
} | ||
|
||
return status.Error(code, trace.UserMessage(originalErr)) | ||
} | ||
|
||
// FromGRPC converts error from GRPC error back to trace.Error | ||
// Debug information will be retrieved from the metadata if specified in args | ||
func FromGRPC(err error, args ...interface{}) error { | ||
if err == nil { | ||
return nil | ||
} | ||
|
||
statusErr := status.Convert(err) | ||
code := statusErr.Code() | ||
message := statusErr.Message() | ||
|
||
var e error | ||
switch code { | ||
case codes.OK: | ||
return nil | ||
case codes.NotFound: | ||
e = &trace.NotFoundError{Message: message} | ||
case codes.AlreadyExists: | ||
e = &trace.AlreadyExistsError{Message: message} | ||
case codes.PermissionDenied: | ||
e = &trace.AccessDeniedError{Message: message} | ||
case codes.FailedPrecondition: | ||
e = &trace.CompareFailedError{Message: message} | ||
case codes.InvalidArgument: | ||
e = &trace.BadParameterError{Message: message} | ||
case codes.ResourceExhausted: | ||
e = &trace.LimitExceededError{Message: message} | ||
case codes.Unavailable: | ||
e = &trace.ConnectionProblemError{Message: message} | ||
case codes.Unimplemented: | ||
e = &trace.NotImplementedError{Message: message} | ||
default: | ||
e = err | ||
} | ||
if len(args) != 0 { | ||
if meta, ok := args[0].(metadata.MD); ok { | ||
e = decodeDebugInfo(e, meta) | ||
// We return here because if it's a trace.Error then | ||
// frames was already extracted from metadata so | ||
// there's no need to capture frames once again. | ||
if _, ok := e.(trace.Error); ok { | ||
return e | ||
} | ||
} | ||
} | ||
traces := captureTraces(1) | ||
return &trace.TraceErr{Err: e, Traces: traces} | ||
} | ||
|
||
// setDebugInfo adds debug metadata about error (traces, original error) | ||
// to request metadata as encoded property | ||
func setDebugInfo(err error, meta metadata.MD) { | ||
if _, ok := err.(*trace.TraceErr); !ok { | ||
return | ||
} | ||
out, err := json.Marshal(err) | ||
if err != nil { | ||
return | ||
} | ||
meta[debugReportMetadata] = []string{ | ||
base64.StdEncoding.EncodeToString(out), | ||
} | ||
} | ||
|
||
// decodeDebugInfo decodes debug information about error | ||
// from the metadata and returns error with enriched metadata about it | ||
func decodeDebugInfo(err error, meta metadata.MD) error { | ||
if len(meta) == 0 { | ||
return err | ||
} | ||
encoded, ok := meta[debugReportMetadata] | ||
if !ok || len(encoded) != 1 { | ||
return err | ||
} | ||
data, decodeErr := base64.StdEncoding.DecodeString(encoded[0]) | ||
if decodeErr != nil { | ||
return err | ||
} | ||
var raw trace.RawTrace | ||
if unmarshalErr := json.Unmarshal(data, &raw); unmarshalErr != nil { | ||
return err | ||
} | ||
if len(raw.Traces) != 0 && len(raw.Err) != 0 { | ||
return &trace.TraceErr{Traces: raw.Traces, Err: err, Message: raw.Message} | ||
} | ||
return err | ||
} | ||
|
||
// traverseErr traverses the err error chain until fn returns true. | ||
// Traversal stops on nil errors, fn(nil) is never called. | ||
// Returns true if fn matched, false otherwise. | ||
func traverseErr(err error, fn func(error) (ok bool)) (ok bool) { | ||
if err == nil { | ||
return false | ||
} | ||
|
||
if fn(err) { | ||
return true | ||
} | ||
|
||
switch err := err.(type) { | ||
case interface{ Unwrap() error }: | ||
return traverseErr(err.Unwrap(), fn) | ||
|
||
case interface{ Unwrap() []error }: | ||
for _, err2 := range err.Unwrap() { | ||
if traverseErr(err2, fn) { | ||
return true | ||
} | ||
} | ||
} | ||
|
||
return false | ||
} | ||
|
||
// FrameCursor stores the position in a call stack | ||
type frameCursor struct { | ||
// Current specifies the current stack frame. | ||
// if omitted, rest contains the complete stack | ||
Current *runtime.Frame | ||
// Rest specifies the rest of stack frames to explore | ||
Rest *runtime.Frames | ||
// N specifies the total number of stack frames | ||
N int | ||
} | ||
|
||
// CaptureTraces gets the current stack trace with some deep frames skipped | ||
func captureTraces(skip int) trace.Traces { | ||
var buf [32]uintptr | ||
// +2 means that we also skip `CaptureTraces` and `runtime.Callers` frames. | ||
n := runtime.Callers(skip+2, buf[:]) | ||
pcs := buf[:n] | ||
frames := runtime.CallersFrames(pcs) | ||
cursor := frameCursor{ | ||
Rest: frames, | ||
N: n, | ||
} | ||
return getTracesFromCursor(cursor) | ||
} | ||
|
||
// GetTracesFromCursor gets the current stack trace from a given cursor | ||
func getTracesFromCursor(cursor frameCursor) trace.Traces { | ||
traces := make(trace.Traces, 0, cursor.N) | ||
if cursor.Current != nil { | ||
traces = append(traces, frameToTrace(*cursor.Current)) | ||
} | ||
for i := 0; i < cursor.N; i++ { | ||
frame, more := cursor.Rest.Next() | ||
traces = append(traces, frameToTrace(frame)) | ||
if !more { | ||
break | ||
} | ||
} | ||
return traces | ||
} | ||
|
||
func frameToTrace(frame runtime.Frame) trace.Trace { | ||
return trace.Trace{ | ||
Func: frame.Function, | ||
Path: frame.File, | ||
Line: frame.Line, | ||
} | ||
} |
Oops, something went wrong.