pax_global_header00006660000000000000000000000064140333637360014522gustar00rootroot0000000000000052 comment=bc7ebb404927e77834a451ed594f1756cf5eed24 trace-1.1.15/000077500000000000000000000000001403336373600127055ustar00rootroot00000000000000trace-1.1.15/.gitignore000066400000000000000000000004121403336373600146720ustar00rootroot00000000000000# Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof trace-1.1.15/LICENSE000066400000000000000000000260631403336373600137210ustar00rootroot00000000000000Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2015 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.trace-1.1.15/README.md000066400000000000000000000024471403336373600141730ustar00rootroot00000000000000# Trace [![GoDoc](https://godoc.org/github.com/gravitational/trace?status.png)](https://godoc.org/github.com/gravitational/trace) Package for error handling and error reporting Read more here: http://gravitational.com/blog/golang_error_handling/ ### Capture file, line and function ```golang import ( "github.com/gravitational/trace" ) func someFunc() error { return trace.Wrap(err) } func main() { err := someFunc() fmt.Println(err.Error()) // prints file, line and function } ``` ### Emit structured logs to Elastic search using udpbeat **Add trace's document template to your ElasticSearch cluster** ```shell curl -XPUT 'http://localhost:9200/_template/trace' -d@udbbeat/template.json ``` **Start udpbeat UDP logs collector and emitter** ```shell go get github.com/gravitational/udpbeat udpbeat ``` **Hook up logger to UDP collector** In your code, attach a logrus hook to use udpbeat: ```golang import ( "github.com/gravitational/trace" log "github.com/sirupsen/logrus" ) func main() { hook, err := trace.NewUDPHook() if err != nil { log.Fatalf(err) } log.SetHook(hook) } ``` Done! You will get structured logs capturing output, log and error message. You can edit `udpbeat/template.json` to modify emitted fields to whatever makes sense to your app. trace-1.1.15/errors.go000066400000000000000000000315741403336373600145620ustar00rootroot00000000000000/* Copyright 2015 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 trace import ( "crypto/x509" "fmt" "io" "net" "net/url" "os" ) // NotFound returns new instance of not found error func NotFound(message string, args ...interface{}) Error { return newTrace(&NotFoundError{ Message: fmt.Sprintf(message, args...), }, 2) } // NotFoundError indicates that object has not been found type NotFoundError struct { Message string `json:"message"` } // IsNotFoundError returns true to indicate that is NotFoundError func (e *NotFoundError) IsNotFoundError() bool { return true } // Error returns log friendly description of an error func (e *NotFoundError) Error() string { if e.Message != "" { return e.Message } return "object not found" } // OrigError returns original error (in this case this is the error itself) func (e *NotFoundError) OrigError() error { return e } // IsNotFound returns whether this error is of NotFoundError type func IsNotFound(err error) bool { err = Unwrap(err) _, ok := err.(interface { IsNotFoundError() bool }) if !ok { return os.IsNotExist(err) } return true } // AlreadyExists returns a new instance of AlreadyExists error func AlreadyExists(message string, args ...interface{}) Error { return newTrace(&AlreadyExistsError{ Message: fmt.Sprintf(message, args...), }, 2) } // AlreadyExistsError indicates that there's a duplicate object that already // exists in the storage/system type AlreadyExistsError struct { Message string `json:"message"` } // Error returns log friendly description of an error func (n *AlreadyExistsError) Error() string { if n.Message != "" { return n.Message } return "object already exists" } // IsAlreadyExistsError indicates that this error of the AlreadyExistsError type func (AlreadyExistsError) IsAlreadyExistsError() bool { return true } // OrigError returns original error (in this case this is the error itself) func (e *AlreadyExistsError) OrigError() error { return e } // IsAlreadyExists returns whether this is error indicating that object // already exists func IsAlreadyExists(e error) bool { type ae interface { IsAlreadyExistsError() bool } _, ok := Unwrap(e).(ae) return ok } // BadParameter returns a new instance of BadParameterError func BadParameter(message string, args ...interface{}) Error { return newTrace(&BadParameterError{ Message: fmt.Sprintf(message, args...), }, 2) } // BadParameterError indicates that something is wrong with passed // parameter to API method type BadParameterError struct { Message string `json:"message"` } // Error returns log friendly description of an error func (b *BadParameterError) Error() string { return b.Message } // OrigError returns original error (in this case this is the error itself) func (b *BadParameterError) OrigError() error { return b } // IsBadParameterError indicates that this error is of BadParameterError type func (b *BadParameterError) IsBadParameterError() bool { return true } // IsBadParameter returns whether this error is of BadParameterType func IsBadParameter(e error) bool { type bp interface { IsBadParameterError() bool } _, ok := Unwrap(e).(bp) return ok } // NotImplemented returns a new instance of NotImplementedError func NotImplemented(message string, args ...interface{}) Error { return newTrace(&NotImplementedError{ Message: fmt.Sprintf(message, args...), }, 2) } // NotImplementedError defines an error condition to describe the result // of a call to an unimplemented API type NotImplementedError struct { Message string `json:"message"` } // Error returns log friendly description of an error func (e *NotImplementedError) Error() string { return e.Message } // OrigError returns original error func (e *NotImplementedError) OrigError() error { return e } // IsNotImplementedError indicates that this error is of NotImplementedError type func (e *NotImplementedError) IsNotImplementedError() bool { return true } // IsNotImplemented returns whether this error is of NotImplementedError type func IsNotImplemented(e error) bool { type ni interface { IsNotImplementedError() bool } err, ok := Unwrap(e).(ni) return ok && err.IsNotImplementedError() } // CompareFailed returns new instance of CompareFailedError func CompareFailed(message string, args ...interface{}) Error { return newTrace(&CompareFailedError{ Message: fmt.Sprintf(message, args...), }, 2) } // CompareFailedError indicates a failed comparison (e.g. bad password or hash) type CompareFailedError struct { // Message is user-friendly error message Message string `json:"message"` } // Error is debug - friendly message func (e *CompareFailedError) Error() string { if e.Message != "" { return e.Message } return "compare failed" } // OrigError returns original error (in this case this is the error itself) func (e *CompareFailedError) OrigError() error { return e } // IsCompareFailedError indicates that this is CompareFailedError func (e *CompareFailedError) IsCompareFailedError() bool { return true } // IsCompareFailed detects if this error is of CompareFailed type func IsCompareFailed(e error) bool { type cf interface { IsCompareFailedError() bool } _, ok := Unwrap(e).(cf) return ok } // AccessDenied returns new instance of AccessDeniedError func AccessDenied(message string, args ...interface{}) Error { return newTrace(&AccessDeniedError{ Message: fmt.Sprintf(message, args...), }, 2) } // AccessDeniedError indicates denied access type AccessDeniedError struct { Message string `json:"message"` } // Error is debug - friendly error message func (e *AccessDeniedError) Error() string { if e.Message != "" { return e.Message } return "access denied" } // IsAccessDeniedError indicates that this error is of AccessDeniedError type func (e *AccessDeniedError) IsAccessDeniedError() bool { return true } // OrigError returns original error (in this case this is the error itself) func (e *AccessDeniedError) OrigError() error { return e } // IsAccessDenied detects if this error is of AccessDeniedError type func IsAccessDenied(err error) bool { _, ok := Unwrap(err).(interface { IsAccessDeniedError() bool }) return ok } // ConvertSystemError converts system error to appropriate trace error // if it is possible, otherwise, returns original error func ConvertSystemError(err error) error { innerError := Unwrap(err) if os.IsExist(innerError) { return newTrace(&AlreadyExistsError{ Message: innerError.Error(), }, 2) } if os.IsNotExist(innerError) { return newTrace(&NotFoundError{ Message: innerError.Error(), }, 2) } if os.IsPermission(innerError) { return newTrace(&AccessDeniedError{ Message: innerError.Error(), }, 2) } switch realErr := innerError.(type) { case *net.OpError: return newTrace(&ConnectionProblemError{ Err: realErr, }, 2) case *os.PathError: message := fmt.Sprintf("failed to execute command %v error: %v", realErr.Path, realErr.Err) return newTrace(&AccessDeniedError{ Message: message, }, 2) case x509.SystemRootsError, x509.UnknownAuthorityError: return newTrace(&TrustError{Err: innerError}, 2) } if _, ok := innerError.(net.Error); ok { return newTrace(&ConnectionProblemError{ Err: innerError, }, 2) } return err } // ConnectionProblem returns new instance of ConnectionProblemError func ConnectionProblem(err error, message string, args ...interface{}) Error { return newTrace(&ConnectionProblemError{ Message: fmt.Sprintf(message, args...), Err: err, }, 2) } // ConnectionProblemError indicates a network related problem type ConnectionProblemError struct { Message string `json:"message"` Err error `json:"-"` } // Error is debug - friendly error message func (c *ConnectionProblemError) Error() string { if c.Message != "" { return c.Message } return UserMessage(c.Err) } // IsConnectionProblemError indicates that this error is of ConnectionProblemError type func (c *ConnectionProblemError) IsConnectionProblemError() bool { return true } // Unwrap returns the wrapped error if any func (c *ConnectionProblemError) Unwrap() error { return c.Err } // OrigError returns original error func (c *ConnectionProblemError) OrigError() error { if c.Err != nil { return c.Err } return c } // IsConnectionProblem returns whether this error is of ConnectionProblemError func IsConnectionProblem(e error) bool { type ad interface { IsConnectionProblemError() bool } _, ok := Unwrap(e).(ad) return ok } // LimitExceeded returns whether new instance of LimitExceededError func LimitExceeded(message string, args ...interface{}) Error { return newTrace(&LimitExceededError{ Message: fmt.Sprintf(message, args...), }, 2) } // LimitExceededError indicates rate limit or connection limit problem type LimitExceededError struct { Message string `json:"message"` } // Error is debug - friendly error message func (c *LimitExceededError) Error() string { return c.Message } // IsLimitExceededError indicates that this error is of ConnectionProblem func (c *LimitExceededError) IsLimitExceededError() bool { return true } // OrigError returns original error (in this case this is the error itself) func (c *LimitExceededError) OrigError() error { return c } // IsLimitExceeded detects if this error is of LimitExceededError func IsLimitExceeded(e error) bool { type ad interface { IsLimitExceededError() bool } _, ok := Unwrap(e).(ad) return ok } // Trust returns new instance of TrustError func Trust(err error, message string, args ...interface{}) Error { return newTrace(&TrustError{ Message: fmt.Sprintf(message, args...), Err: err, }, 2) } // TrustError indicates trust-related validation error (e.g. untrusted cert) type TrustError struct { // Err is original error Err error `json:"-"` Message string `json:"message"` } // Error returns log-friendly error description func (t *TrustError) Error() string { if t.Message != "" { return t.Message } return UserMessage(t.Err) } // IsTrustError indicates that this error is of TrustError type func (*TrustError) IsTrustError() bool { return true } // Unwrap returns the wrapped error if any func (t *TrustError) Unwrap() error { return t.Err } // OrigError returns original error (in this case this is the error itself) func (t *TrustError) OrigError() error { if t.Err != nil { return t.Err } return t } // IsTrustError returns if this is a trust error func IsTrustError(e error) bool { type te interface { IsTrustError() bool } _, ok := Unwrap(e).(te) return ok } // OAuth2 returns new instance of OAuth2Error func OAuth2(code, message string, query url.Values) Error { return newTrace(&OAuth2Error{ Code: code, Message: message, Query: query, }, 2) } // OAuth2Error defined an error used in OpenID Connect Flow (OIDC) type OAuth2Error struct { Code string `json:"code"` Message string `json:"message"` Query url.Values `json:"query"` } //Error returns log friendly description of an error func (o *OAuth2Error) Error() string { return fmt.Sprintf("OAuth2 error code=%v, message=%v", o.Code, o.Message) } // IsOAuth2Error returns whether this error of OAuth2Error type func (o *OAuth2Error) IsOAuth2Error() bool { return true } // IsOAuth2 returns if this is a OAuth2-related error func IsOAuth2(e error) bool { type oe interface { IsOAuth2Error() bool } _, ok := Unwrap(e).(oe) return ok } // IsEOF returns true if the passed error is io.EOF func IsEOF(e error) bool { return Unwrap(e) == io.EOF } // Retry returns new instance of RetryError which indicates a transient error type func Retry(err error, message string, args ...interface{}) Error { return newTrace(&RetryError{ Message: fmt.Sprintf(message, args...), Err: err, }, 2) } // RetryError indicates a transient error type type RetryError struct { Message string `json:"message"` Err error `json:"-"` } // Error is debug-friendly error message func (c *RetryError) Error() string { if c.Message != "" { return c.Message } return UserMessage(c.Err) } // IsRetryError indicates that this error is of RetryError type func (c *RetryError) IsRetryError() bool { return true } // Unwrap returns the wrapped error if any func (c *RetryError) Unwrap() error { return c.Err } // OrigError returns original error (in this case this is the error itself) func (c *RetryError) OrigError() error { if c.Err != nil { return c.Err } return c } // IsRetryError returns whether this error is of ConnectionProblemError func IsRetryError(e error) bool { type ad interface { IsRetryError() bool } _, ok := Unwrap(e).(ad) return ok } trace-1.1.15/httplib.go000066400000000000000000000062431403336373600147070ustar00rootroot00000000000000package trace import ( "encoding/json" "fmt" "net/http" ) // WriteError sets up HTTP error response and writes it to writer w func WriteError(w http.ResponseWriter, err error) { if !IsAggregate(err) { replyJSON(w, ErrorToCode(err), err) return } for i := 0; i < maxHops; i++ { var aggErr Aggregate var ok bool if aggErr, ok = Unwrap(err).(Aggregate); !ok { break } errors := aggErr.Errors() if len(errors) == 0 { break } err = errors[0] } replyJSON(w, ErrorToCode(err), err) } // ErrorToCode returns an appropriate HTTP status code based on the provided error type func ErrorToCode(err error) int { switch { case IsAggregate(err): return http.StatusGatewayTimeout case IsNotFound(err): return http.StatusNotFound case IsBadParameter(err) || IsOAuth2(err): return http.StatusBadRequest case IsNotImplemented(err): return http.StatusNotImplemented case IsCompareFailed(err): return http.StatusPreconditionFailed case IsAccessDenied(err): return http.StatusForbidden case IsAlreadyExists(err): return http.StatusConflict case IsLimitExceeded(err): return http.StatusTooManyRequests case IsConnectionProblem(err): return http.StatusGatewayTimeout default: return http.StatusInternalServerError } } // ReadError converts http error to internal error type // based on HTTP response code and HTTP body contents // if status code does not indicate error, it will return nil func ReadError(statusCode int, respBytes []byte) error { if statusCode >= http.StatusOK && statusCode < http.StatusBadRequest { return nil } var err error switch statusCode { case http.StatusNotFound: err = &NotFoundError{} case http.StatusBadRequest: err = &BadParameterError{} case http.StatusNotImplemented: err = &NotImplementedError{} case http.StatusPreconditionFailed: err = &CompareFailedError{} case http.StatusForbidden: err = &AccessDeniedError{} case http.StatusConflict: err = &AlreadyExistsError{} case http.StatusTooManyRequests: err = &LimitExceededError{} case http.StatusGatewayTimeout: err = &ConnectionProblemError{} default: err = &RawTrace{} } return wrapProxy(unmarshalError(err, respBytes)) } func replyJSON(w http.ResponseWriter, code int, err error) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(code) var out []byte // wrap regular errors in order to achieve unification // and provide structurally consistent responses var obj interface{} = err if _, ok := err.(*TraceErr); !ok { obj = &TraceErr{Err: err} } out, err = json.MarshalIndent(obj, "", " ") if err != nil { out = []byte(fmt.Sprintf(`{"error": {"message": "internal marshal error: %v"}}`, err)) } w.Write(out) } func unmarshalError(err error, responseBody []byte) error { if len(responseBody) == 0 { return err } var raw RawTrace if err2 := json.Unmarshal(responseBody, &raw); err2 != nil { return err } if len(raw.Traces) != 0 && len(raw.Err) != 0 { err2 := json.Unmarshal(raw.Err, err) if err2 != nil { return err } return &TraceErr{ Traces: raw.Traces, Err: err, Message: raw.Message, Messages: raw.Messages, Fields: raw.Fields, } } json.Unmarshal(responseBody, err) return err } trace-1.1.15/httplib_test.go000066400000000000000000000025221403336373600157420ustar00rootroot00000000000000/* Copyright 2020 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 trace import ( "errors" "net/http/httptest" . "gopkg.in/check.v1" ) type HttplibSuite struct{} var _ = Suite(&HttplibSuite{}) var ( errCode = 400 errText = "test error" expectedErrorResponse = "" + "{\n" + " \"error\": {\n" + " \"message\": \"" + errText + "\"\n" + " }\n" + "}" ) func (s *HttplibSuite) TestRegularErrorResponseJSON(c *C) { recorder := httptest.NewRecorder() replyJSON(recorder, errCode, errors.New(errText)) c.Assert(recorder.Body.String(), Equals, expectedErrorResponse) } func (s *HttplibSuite) TestTraceErrorResponseJSON(c *C) { recorder := httptest.NewRecorder() replyJSON(recorder, errCode, &TraceErr{Err: errors.New(errText)}) c.Assert(recorder.Body.String(), Equals, expectedErrorResponse) } trace-1.1.15/internal/000077500000000000000000000000001403336373600145215ustar00rootroot00000000000000trace-1.1.15/internal/frames.go000066400000000000000000000066371403336373600163410ustar00rootroot00000000000000/* Copyright 2021 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 internal import ( "fmt" "path/filepath" "runtime" "strings" ) // Trace stores structured trace entry, including file line and path type Trace struct { // Path is a full file path Path string `json:"path"` // Func is a function name Func string `json:"func"` // Line is a code line number Line int `json:"line"` } // 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 } // Traces is a list of trace entries type Traces []Trace // CaptureTraces gets the current stack trace with some deep frames skipped func CaptureTraces(skip int) 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) Traces { traces := make(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 { return Trace{ Func: frame.Function, Path: frame.File, Line: frame.Line, } } // SetTraces adds new traces to the list func (s Traces) SetTraces(traces ...Trace) { s = append(s, traces...) } // Func returns first function in trace list func (s Traces) Func() string { if len(s) == 0 { return "" } return s[0].Func } // Func returns just function name func (s Traces) FuncName() string { if len(s) == 0 { return "" } fn := filepath.ToSlash(s[0].Func) idx := strings.LastIndex(fn, "/") if idx == -1 || idx == len(fn)-1 { return fn } return fn[idx+1:] } // Loc points to file/line location in the code func (s Traces) Loc() string { if len(s) == 0 { return "" } return s[0].String() } // String returns debug-friendly representaton of trace stack func (s Traces) String() string { if len(s) == 0 { return "" } out := make([]string, len(s)) for i, t := range s { out[i] = fmt.Sprintf("\t%v:%v %v", t.Path, t.Line, t.Func) } return strings.Join(out, "\n") } // String returns debug-friendly representation of this trace func (t *Trace) String() string { dir, file := filepath.Split(t.Path) dirs := strings.Split(filepath.ToSlash(filepath.Clean(dir)), "/") if len(dirs) != 0 { file = filepath.Join(dirs[len(dirs)-1], file) } return fmt.Sprintf("%v:%v", file, t.Line) } trace-1.1.15/log.go000066400000000000000000000164161403336373600140250ustar00rootroot00000000000000/* Copyright 2015 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 trace implements utility functions for capturing logs package trace import ( "bytes" "fmt" "io" "os" "reflect" "regexp" "runtime" rundebug "runtime/debug" "sort" "strconv" "strings" "time" "github.com/gravitational/trace/internal" log "github.com/sirupsen/logrus" "golang.org/x/crypto/ssh/terminal" ) const ( // FileField is a field with code file added to structured traces FileField = "file" // FunctionField is a field with function name FunctionField = "func" // LevelField returns logging level as set by logrus LevelField = "level" // Component is a field that represents component - e.g. service or // function Component = "trace.component" // ComponentFields is a fields component ComponentFields = "trace.fields" // DefaultComponentPadding is a default padding for component field DefaultComponentPadding = 11 // DefaultLevelPadding is a default padding for level field DefaultLevelPadding = 4 ) // IsTerminal checks whether writer is a terminal func IsTerminal(w io.Writer) bool { switch v := w.(type) { case *os.File: return terminal.IsTerminal(int(v.Fd())) default: return false } } // TextFormatter is logrus-compatible formatter and adds // file and line details to every logged entry. type TextFormatter struct { // DisableTimestamp disables timestamp output (useful when outputting to // systemd logs) DisableTimestamp bool // ComponentPadding is a padding to pick when displaying // and formatting component field, defaults to DefaultComponentPadding ComponentPadding int // EnableColors enables colored output EnableColors bool // FormatCaller is a function to return (part) of source file path for output. // Defaults to filePathAndLine() if unspecified FormatCaller func() (caller string) } // Format implements logrus.Formatter interface and adds file and line func (tf *TextFormatter) Format(e *log.Entry) (data []byte, err error) { defer func() { if r := recover(); r != nil { data = append([]byte("panic in log formatter\n"), rundebug.Stack()...) return } }() formatCaller := tf.FormatCaller if formatCaller == nil { formatCaller = formatCallerWithPathAndLine } caller := formatCaller() w := &writer{} // time if !tf.DisableTimestamp { w.writeField(e.Time.Format(time.RFC3339), noColor) } // level color := noColor if tf.EnableColors { switch e.Level { case log.DebugLevel: color = gray case log.WarnLevel: color = yellow case log.ErrorLevel, log.FatalLevel, log.PanicLevel: color = red default: color = blue } } w.writeField(strings.ToUpper(padMax(e.Level.String(), DefaultLevelPadding)), color) // always output the component field if available padding := DefaultComponentPadding if tf.ComponentPadding != 0 { padding = tf.ComponentPadding } if w.Len() > 0 { w.WriteByte(' ') } value := e.Data[Component] var component string if reflect.ValueOf(value).IsValid() { component = fmt.Sprintf("[%v]", value) } component = strings.ToUpper(padMax(component, padding)) if component[len(component)-1] != ' ' { component = component[:len(component)-1] + "]" } w.WriteString(component) // message if e.Message != "" { w.writeField(e.Message, noColor) } // rest of the fields if len(e.Data) > 0 { w.writeMap(e.Data) } // caller, if present, always last if caller != "" { w.writeField(caller, noColor) } w.WriteByte('\n') data = w.Bytes() return } // JSONFormatter implements logrus.Formatter interface and adds file and line // properties to JSON entries type JSONFormatter struct { log.JSONFormatter } // Format implements logrus.Formatter interface func (j *JSONFormatter) Format(e *log.Entry) ([]byte, error) { if cursor := findFrame(); cursor != nil { t := internal.GetTracesFromCursor(*cursor) new := e.WithFields(log.Fields{ FileField: t.Loc(), FunctionField: t.FuncName(), }) new.Time = e.Time new.Level = e.Level new.Message = e.Message e = new } return j.JSONFormatter.Format(e) } // formatCallerWithPathAndLine formats the caller in the form path/segment: // for output in the log func formatCallerWithPathAndLine() (path string) { if cursor := findFrame(); cursor != nil { t := internal.GetTracesFromCursor(*cursor) return t.Loc() } return "" } var frameIgnorePattern = regexp.MustCompile(`github\.com/(S|s)irupsen/logrus`) // findFrames positions the stack pointer to the first // function that does not match the frameIngorePattern // and returns the rest of the stack frames func findFrame() *internal.FrameCursor { var buf [32]uintptr // Skip enough frames to start at user code. // This number is a mere hint to the following loop // to start as close to user code as possible and getting it right is not mandatory. // The skip count might need to get updated if the call to findFrame is // moved up/down the call stack n := runtime.Callers(4, buf[:]) pcs := buf[:n] frames := runtime.CallersFrames(pcs) for i := 0; i < n; i++ { frame, _ := frames.Next() if !frameIgnorePattern.MatchString(frame.File) { return &internal.FrameCursor{ Current: &frame, Rest: frames, N: n, } } } return nil } const ( noColor = -1 red = 31 yellow = 33 blue = 36 gray = 37 ) type writer struct { bytes.Buffer } func (w *writer) writeField(value interface{}, color int) { if w.Len() > 0 { w.WriteByte(' ') } w.writeValue(value, color) } func (w *writer) writeValue(value interface{}, color int) { var s string switch v := value.(type) { case string: s = v if needsQuoting(s) { s = fmt.Sprintf("%q", v) } default: s = fmt.Sprintf("%v", v) } if color != noColor { s = fmt.Sprintf("\x1b[%dm%s\x1b[0m", color, s) } w.WriteString(s) } func (w *writer) writeError(value interface{}) { switch err := value.(type) { case Error: w.WriteString(fmt.Sprintf("[%v]", err.DebugReport())) default: w.WriteString(fmt.Sprintf("[%v]", value)) } } func (w *writer) writeKeyValue(key string, value interface{}) { if w.Len() > 0 { w.WriteByte(' ') } w.WriteString(key) w.WriteByte(':') if key == log.ErrorKey { w.writeError(value) return } w.writeValue(value, noColor) } func (w *writer) writeMap(m map[string]interface{}) { if len(m) == 0 { return } keys := make([]string, 0, len(m)) for key := range m { keys = append(keys, key) } sort.Strings(keys) for _, key := range keys { if key == Component { continue } switch value := m[key].(type) { case log.Fields: w.writeMap(value) default: w.writeKeyValue(key, value) } } } func needsQuoting(text string) bool { for _, r := range text { if !strconv.IsPrint(r) { return true } } return false } func padMax(in string, chars int) string { switch { case len(in) < chars: return in + strings.Repeat(" ", chars-len(in)) default: return in[:chars] } } trace-1.1.15/trace.go000066400000000000000000000337771403336373600143530ustar00rootroot00000000000000/* Copyright 2015-2019 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 trace implements utility functions for capturing debugging // information about file and line in error reports and logs. package trace import ( "bytes" "encoding/json" "fmt" "html/template" "strings" "sync/atomic" "github.com/gravitational/trace/internal" "golang.org/x/net/context" ) var debug int32 // SetDebug turns on/off debugging mode, that causes Fatalf to panic func SetDebug(enabled bool) { if enabled { atomic.StoreInt32(&debug, 1) } else { atomic.StoreInt32(&debug, 0) } } // IsDebug returns true if debug mode is on func IsDebug() bool { return atomic.LoadInt32(&debug) == 1 } // Wrap takes the original error and wraps it into the Trace struct // memorizing the context of the error. func Wrap(err error, args ...interface{}) Error { if err == nil { return nil } var trace Error if traceErr, ok := err.(Error); ok { trace = traceErr } else { trace = newTrace(err, 2) } if len(args) > 0 { trace = trace.AddUserMessage(args[0], args[1:]...) } return trace } // Unwrap returns the original error the given error wraps func Unwrap(err error) error { if err, ok := err.(ErrorWrapper); ok { return err.OrigError() } return err } // UserMessager returns a user message associated with the error type UserMessager interface { // UserMessage returns the user message associated with the error if any UserMessage() string } // ErrorWrapper wraps another error type ErrorWrapper interface { // OrigError returns the wrapped error OrigError() error } // DebugReporter formats an error for display type DebugReporter interface { // DebugReport formats an error for display DebugReport() string } // UserMessage returns user-friendly part of the error func UserMessage(err error) string { if err == nil { return "" } if wrap, ok := err.(UserMessager); ok { return wrap.UserMessage() } return err.Error() } // UserMessageWithFields returns user-friendly error with key-pairs as part of the message func UserMessageWithFields(err error) string { if err == nil { return "" } if wrap, ok := err.(Error); ok { if len(wrap.GetFields()) == 0 { return wrap.UserMessage() } var kvps []string for k, v := range wrap.GetFields() { kvps = append(kvps, fmt.Sprintf("%v=%q", k, v)) } return fmt.Sprintf("%v %v", strings.Join(kvps, " "), wrap.UserMessage()) } return err.Error() } // DebugReport returns debug report with all known information // about the error including stack trace if it was captured func DebugReport(err error) string { if err == nil { return "" } if reporter, ok := err.(DebugReporter); ok { return reporter.DebugReport() } return err.Error() } // GetFields returns any fields that have been added to the error message func GetFields(err error) map[string]interface{} { if err == nil { return map[string]interface{}{} } if wrap, ok := err.(Error); ok { return wrap.GetFields() } return map[string]interface{}{} } // WrapWithMessage wraps the original error into Error and adds user message if any func WrapWithMessage(err error, message interface{}, args ...interface{}) Error { var trace Error if traceErr, ok := err.(Error); ok { trace = traceErr } else { trace = newTrace(err, 2) } trace.AddUserMessage(message, args...) return trace } // Errorf is similar to fmt.Errorf except that it captures // more information about the origin of error, such as // callee, line number and function that simplifies debugging func Errorf(format string, args ...interface{}) (err error) { err = fmt.Errorf(format, args...) return newTrace(err, 2) } // Fatalf - If debug is false Fatalf calls Errorf. If debug is // true Fatalf calls panic func Fatalf(format string, args ...interface{}) error { if IsDebug() { panic(fmt.Sprintf(format, args...)) } else { return Errorf(format, args...) } } func newTrace(err error, depth int) *TraceErr { traces := internal.CaptureTraces(depth) return &TraceErr{Err: err, Traces: traces} } type Traces = internal.Traces type Trace = internal.Trace // MarshalJSON marshals this error as JSON-encoded payload func (r *TraceErr) MarshalJSON() ([]byte, error) { if r == nil { return nil, nil } type marshalableError TraceErr err := marshalableError(*r) err.Err = &RawTrace{Message: r.Err.Error()} return json.Marshal(err) } // TraceErr contains error message and some additional // information about the error origin type TraceErr struct { // Err is the underlying error that TraceErr wraps Err error `json:"error"` // Traces is a slice of stack trace entries for the error Traces `json:"traces,omitempty"` // Message is an optional message that can be wrapped with the original error. // // This field is obsolete, replaced by messages list below. Message string `json:"message,omitempty"` // Messages is a list of user messages added to this error. Messages []string `json:"messages,omitempty"` // Fields is a list of key-value-pairs that can be wrapped with the error to give additional context Fields map[string]interface{} `json:"fields,omitempty"` } // Fields maps arbitrary keys to values inside an error type Fields map[string]interface{} // Error returns the error message this trace describes. // Implements error func (r *RawTrace) Error() string { return r.Message } // RawTrace describes the error trace on the wire type RawTrace struct { // Err specifies the original error Err json.RawMessage `json:"error,omitempty"` // Traces lists the stack traces at the moment the error was recorded Traces `json:"traces,omitempty"` // Message specifies the optional user-facing message Message string `json:"message,omitempty"` // Messages is a list of user messages added to this error. Messages []string `json:"messages,omitempty"` // Fields is a list of key-value-pairs that can be wrapped with the error to give additional context Fields map[string]interface{} `json:"fields,omitempty"` } // AddUserMessage adds user-friendly message describing the error nature func (e *TraceErr) AddUserMessage(formatArg interface{}, rest ...interface{}) *TraceErr { newMessage := fmt.Sprintf(fmt.Sprintf("%v", formatArg), rest...) e.Messages = append(e.Messages, newMessage) return e } // AddFields adds the given map of fields to the error being reported func (e *TraceErr) AddFields(fields map[string]interface{}) *TraceErr { if e.Fields == nil { e.Fields = make(map[string]interface{}, len(fields)) } for k, v := range fields { e.Fields[k] = v } return e } // AddField adds a single field to the error wrapper as context for the error func (e *TraceErr) AddField(k string, v interface{}) *TraceErr { if e.Fields == nil { e.Fields = make(map[string]interface{}, 1) } e.Fields[k] = v return e } // UserMessage returns user-friendly error message func (e *TraceErr) UserMessage() string { if len(e.Messages) > 0 { // Format all collected messages in the reverse order, with each error // on its own line with appropriate indentation so they form a tree and // it's easy to see the cause and effect. var buf bytes.Buffer fmt.Fprintln(&buf, e.Messages[len(e.Messages)-1]) index, indent := len(e.Messages)-1, 1 for ; index > 0; index, indent = index-1, indent+1 { fmt.Fprintf(&buf, "%v%v\n", strings.Repeat("\t", indent), e.Messages[index-1]) } fmt.Fprintf(&buf, "%v%v", strings.Repeat("\t", indent), UserMessage(e.Err)) return buf.String() } if e.Message != "" { // For backwards compatibility return the old user message if it's present. return e.Message } return UserMessage(e.Err) } // DebugReport returns developer-friendly error report func (e *TraceErr) DebugReport() string { var buf bytes.Buffer err := reportTemplate.Execute(&buf, errorReport{ OrigErrType: fmt.Sprintf("%T", e.Err), OrigErrMessage: e.Err.Error(), Fields: e.Fields, StackTrace: e.Traces.String(), UserMessage: e.UserMessage(), }) if err != nil { return fmt.Sprint("error generating debug report: ", err.Error()) } return buf.String() } // Error returns user-friendly error message when not in debug mode func (e *TraceErr) Error() string { return e.UserMessage() } func (e *TraceErr) GetFields() map[string]interface{} { return e.Fields } // Unwrap returns the error this TraceErr wraps. The returned error may also // wrap another one, Unwrap doesn't recursively get the inner-most error like // OrigError does. func (e *TraceErr) Unwrap() error { return e.Err } // OrigError returns original wrapped error func (e *TraceErr) OrigError() error { err := e.Err // this is not an endless loop because I'm being // paranoid, this is a safe protection against endless // loops for i := 0; i < maxHops; i++ { newerr, ok := err.(Error) if !ok { break } next := newerr.OrigError() if next == nil || next == err { break } err = next } return err } // GoString formats this trace object for use with // with the "%#v" format string func (e *TraceErr) GoString() string { return e.DebugReport() } // maxHops is a max supported nested depth for errors const maxHops = 50 // Error is an interface that helps to adapt usage of trace in the code // When applications define new error types, they can implement the interface // // Error handlers can use Unwrap() to retrieve error from the wrapper, or // errors.Is()/As() to compare it to another value. type Error interface { error ErrorWrapper DebugReporter UserMessager // AddMessage adds formatted user-facing message // to the error, depends on the implementation, // usually works as fmt.Sprintf(formatArg, rest...) // but implementations can choose another way, e.g. treat // arguments as structured args AddUserMessage(formatArg interface{}, rest ...interface{}) *TraceErr // AddField adds additional field information to the error AddField(key string, value interface{}) *TraceErr // AddFields adds a map of additional fields to the error AddFields(fields map[string]interface{}) *TraceErr // GetFields returns any fields that have been added to the error GetFields() map[string]interface{} } // NewAggregate creates a new aggregate instance from the specified // list of errors func NewAggregate(errs ...error) error { // filter out possible nil values var nonNils []error for _, err := range errs { if err != nil { nonNils = append(nonNils, err) } } if len(nonNils) == 0 { return nil } return newTrace(aggregate(nonNils), 2) } // NewAggregateFromChannel creates a new aggregate instance from the provided // errors channel. // // A context.Context can be passed in so the caller has the ability to cancel // the operation. If this is not desired, simply pass context.Background(). func NewAggregateFromChannel(errCh chan error, ctx context.Context) error { var errs []error Loop: for { select { case err, ok := <-errCh: if !ok { // the channel is closed, time to exit break Loop } errs = append(errs, err) case <-ctx.Done(): break Loop } } return NewAggregate(errs...) } // Aggregate interface combines several errors into one error type Aggregate interface { error // Errors obtains the list of errors this aggregate combines Errors() []error } // aggregate implements Aggregate type aggregate []error // Error implements the error interface func (r aggregate) Error() string { if len(r) == 0 { return "" } output := r[0].Error() for i := 1; i < len(r); i++ { output = fmt.Sprintf("%v, %v", output, r[i]) } return output } // Errors obtains the list of errors this aggregate combines func (r aggregate) Errors() []error { return []error(r) } // IsAggregate returns whether this error of Aggregate error type func IsAggregate(err error) bool { _, ok := Unwrap(err).(Aggregate) return ok } // wrapProxy wraps the specified error as a new error trace func wrapProxy(err error) Error { if err == nil { return nil } return proxyError{ // Do not include ReadError in the trace TraceErr: newTrace(err, 3), } } // DebugReport formats the underlying error for display // Implements DebugReporter func (r proxyError) DebugReport() string { var wrappedErr *TraceErr var ok bool if wrappedErr, ok = r.TraceErr.Err.(*TraceErr); !ok { return DebugReport(r.TraceErr) } var buf bytes.Buffer //nolint:errcheck reportTemplate.Execute(&buf, errorReport{ OrigErrType: fmt.Sprintf("%T", wrappedErr.Err), OrigErrMessage: wrappedErr.Err.Error(), Fields: wrappedErr.Fields, StackTrace: wrappedErr.Traces.String(), UserMessage: wrappedErr.UserMessage(), Caught: r.TraceErr.Traces.String(), }) return buf.String() } // GoString formats this trace object for use with // with the "%#v" format string func (r proxyError) GoString() string { return r.DebugReport() } // proxyError wraps another error type proxyError struct { *TraceErr } type errorReport struct { // OrigErrType specifies the error type as text OrigErrType string // OrigErrMessage specifies the original error's message OrigErrMessage string // Fields lists any additional fields attached to the error Fields map[string]interface{} // StackTrace specifies the call stack StackTrace string // UserMessage is the user-facing message (if any) UserMessage string // Caught optionally specifies the stack trace where the error // has been recorded after coming over the wire Caught string } var reportTemplate = template.Must(template.New("debugReport").Parse(reportTemplateText)) var reportTemplateText = ` ERROR REPORT: Original Error: {{.OrigErrType}} {{.OrigErrMessage}} {{if .Fields}}Fields: {{range $key, $value := .Fields}} {{$key}}: {{$value}} {{end}}{{end}}Stack Trace: {{.StackTrace}} {{if .Caught}}Caught: {{.Caught}} User Message: {{.UserMessage}} {{else}}User Message: {{.UserMessage}}{{end}}` trace-1.1.15/trace_test.go000066400000000000000000000426561403336373600154060ustar00rootroot00000000000000/* Copyright 2015-2019 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 trace import ( "bytes" "errors" "fmt" "io" "net/http" "os" "strings" "testing" log "github.com/sirupsen/logrus" "golang.org/x/net/context" . "gopkg.in/check.v1" ) func TestTrace(t *testing.T) { TestingT(t) } type TraceSuite struct{} var _ = Suite(&TraceSuite{}) func (s *TraceSuite) TestEmpty(c *C) { c.Assert(DebugReport(nil), Equals, "") c.Assert(UserMessage(nil), Equals, "") c.Assert(UserMessageWithFields(nil), Equals, "") c.Assert(GetFields(nil), DeepEquals, map[string]interface{}{}) } func (s *TraceSuite) TestWrap(c *C) { testErr := &testError{Param: "param"} err := Wrap(Wrap(testErr)) c.Assert(line(DebugReport(err)), Matches, ".*trace_test.go.*") c.Assert(line(DebugReport(err)), Not(Matches), ".*trace.go.*") c.Assert(line(UserMessage(err)), Not(Matches), ".*trace_test.go.*") c.Assert(line(UserMessage(err)), Matches, ".*param.*") } func (s *TraceSuite) TestOrigError(c *C) { testErr := fmt.Errorf("some error") err := Wrap(Wrap(testErr)) c.Assert(err.OrigError(), Equals, testErr) } func (s *TraceSuite) TestIsEOF(c *C) { c.Assert(IsEOF(io.EOF), Equals, true) c.Assert(IsEOF(Wrap(io.EOF)), Equals, true) } func (s *TraceSuite) TestWrapUserMessage(c *C) { testErr := fmt.Errorf("description") err := Wrap(testErr, "user message") c.Assert(line(DebugReport(err)), Matches, "*.trace_test.go.*") c.Assert(line(DebugReport(err)), Not(Matches), "*.trace.go.*") c.Assert(line(UserMessage(err)), Equals, "user message\tdescription") err = Wrap(err, "user message 2") c.Assert(line(UserMessage(err)), Equals, "user message 2\tuser message\t\tdescription") } func (s *TraceSuite) TestWrapWithMessage(c *C) { testErr := fmt.Errorf("description") err := WrapWithMessage(testErr, "user message") c.Assert(line(UserMessage(err)), Equals, "user message\tdescription") c.Assert(line(DebugReport(err)), Matches, "*.trace_test.go.*") c.Assert(line(DebugReport(err)), Not(Matches), "*.trace.go.*") } func (s *TraceSuite) TestUserMessageWithFields(c *C) { testErr := fmt.Errorf("description") c.Assert(UserMessageWithFields(testErr), Equals, testErr.Error()) err := Wrap(testErr, "user message") c.Assert(line(UserMessageWithFields(err)), Equals, "user message\tdescription") err.AddField("test_key", "test_value") c.Assert(line(UserMessageWithFields(err)), Equals, "test_key=\"test_value\" user message\tdescription") } func (s *TraceSuite) TestGetFields(c *C) { testErr := fmt.Errorf("description") c.Assert(GetFields(testErr), DeepEquals, map[string]interface{}{}) fields := map[string]interface{}{ "test_key": "test_value", } err := Wrap(testErr).AddFields(fields) c.Assert(GetFields(err), DeepEquals, fields) } func (s *TraceSuite) TestWrapNil(c *C) { err1 := Wrap(nil, "message: %v", "extra") c.Assert(err1, IsNil) var err2 error err2 = nil err3 := Wrap(err2) c.Assert(err3, IsNil) err4 := Wrap(err3) c.Assert(err4, IsNil) } func (s *TraceSuite) TestWrapStdlibErrors(c *C) { c.Assert(IsNotFound(os.ErrNotExist), Equals, true) } func (s *TraceSuite) TestLogFormatter(c *C) { for _, f := range []log.Formatter{&TextFormatter{}, &JSONFormatter{}} { log.SetFormatter(f) // check case with global Infof var buf bytes.Buffer log.SetOutput(&buf) log.Infof("hello") c.Assert(line(buf.String()), Matches, ".*trace_test.go.*") // check case with embedded Infof buf.Reset() log.WithFields(log.Fields{"a": "b"}).Infof("hello") c.Assert(line(buf.String()), Matches, ".*trace_test.go.*") } } type panicker string func (p panicker) String() string { panic(p) } func (s *TraceSuite) TestTextFormatter(c *C) { padding := 6 f := &TextFormatter{ DisableTimestamp: true, ComponentPadding: padding, } log.SetFormatter(f) type testCase struct { log func() match string comment string } testCases := []testCase{ { comment: "padding fits in", log: func() { log.WithFields(log.Fields{ Component: "test", }).Infof("hello") }, match: `^INFO \[TEST\] hello.*`, }, { comment: "padding overflow", log: func() { log.WithFields(log.Fields{ Component: "longline", }).Infof("hello") }, match: `^INFO \[LONG\] hello.*`, }, { comment: "padded with extra spaces", log: func() { log.WithFields(log.Fields{ Component: "abc", }).Infof("hello") }, match: `^INFO \[ABC\] hello.*`, }, { comment: "missing component will be padded", log: func() { log.Infof("hello") }, match: `^INFO hello.*`, }, { comment: "panic in component is handled", log: func() { log.WithFields(log.Fields{ Component: panicker("panic"), }).Infof("hello") }, match: `.*panic.*`, }, { comment: "nested fields are reflected", log: func() { log.WithFields(log.Fields{ ComponentFields: log.Fields{"key": "value"}, }).Infof("hello") }, match: `.*key:value.*`, }, { comment: "fields are reflected", log: func() { log.WithFields(log.Fields{ "a": "b", }).Infof("hello") }, match: `.*a:b.*`, }, { comment: "non control characters are quoted", log: func() { log.Infof("\n") }, match: `.*"\\n".*`, }, { comment: "printable strings are not quoted", log: func() { log.Infof("printable string") }, match: `.*[^"]printable string[^"].*`, }, } for i, tc := range testCases { comment := Commentf("test case %v %v, expected match: %v", i+1, tc.comment, tc.match) buf := &bytes.Buffer{} log.SetOutput(buf) tc.log() c.Assert(line(buf.String()), Matches, tc.match, comment) } } func (s *TraceSuite) TestTextFormatterWithColors(c *C) { padding := 6 f := &TextFormatter{ DisableTimestamp: true, ComponentPadding: padding, EnableColors: true, } log.SetFormatter(f) type testCase struct { log func() match string comment string } testCases := []testCase{ { comment: "test info color", log: func() { log.WithFields(log.Fields{ Component: "test", }).Info("hello") }, match: `^\x1b\[36mINFO\x1b\[0m \[TEST\] hello.*`, }, { comment: "info color padding overflow", log: func() { log.WithFields(log.Fields{ Component: "longline", }).Info("hello") }, match: `^\x1b\[36mINFO\x1b\[0m \[LONG\] hello.*`, }, { comment: "test debug color", log: func() { log.WithFields(log.Fields{ Component: "test", }).Debug("hello") }, match: `^\x1b\[37mDEBU\x1b\[0m \[TEST\] hello.*`, }, { comment: "test warn color", log: func() { log.WithFields(log.Fields{ Component: "test", }).Warning("hello") }, match: `^\x1b\[33mWARN\x1b\[0m \[TEST\] hello.*`, }, { comment: "test error color", log: func() { log.WithFields(log.Fields{ Component: "test", }).Error("hello") }, match: `^\x1b\[31mERRO\x1b\[0m \[TEST\] hello.*`, }, } for i, tc := range testCases { comment := Commentf("test case %v %v, expected match: %v", i+1, tc.comment, tc.match) buf := &bytes.Buffer{} log.SetOutput(buf) log.SetLevel(log.DebugLevel) tc.log() c.Assert(line(buf.String()), Matches, tc.match, comment) } } func (s *TraceSuite) TestGenericErrors(c *C) { testCases := []struct { Err Error Predicate func(error) bool StatusCode int comment string }{ { Err: NotFound("not found"), Predicate: IsNotFound, StatusCode: http.StatusNotFound, comment: "not found error", }, { Err: AlreadyExists("already exists"), Predicate: IsAlreadyExists, StatusCode: http.StatusConflict, comment: "already exists error", }, { Err: BadParameter("is bad"), Predicate: IsBadParameter, StatusCode: http.StatusBadRequest, comment: "bad parameter error", }, { Err: CompareFailed("is bad"), Predicate: IsCompareFailed, StatusCode: http.StatusPreconditionFailed, comment: "comparison failed error", }, { Err: AccessDenied("denied"), Predicate: IsAccessDenied, StatusCode: http.StatusForbidden, comment: "access denied error", }, { Err: ConnectionProblem(nil, "prob"), Predicate: IsConnectionProblem, StatusCode: http.StatusRequestTimeout, comment: "connection error", }, { Err: LimitExceeded("limit exceeded"), Predicate: IsLimitExceeded, StatusCode: http.StatusTooManyRequests, comment: "limit exceeded error", }, { Err: NotImplemented("not implemented"), Predicate: IsNotImplemented, StatusCode: http.StatusNotImplemented, comment: "not implemented error", }, } for _, testCase := range testCases { comment := Commentf(testCase.comment) SetDebug(true) err := testCase.Err var traceErr *TraceErr var ok bool if traceErr, ok = err.(*TraceErr); !ok { c.Fatal("Expected error to be of type *TraceErr") } c.Assert(len(traceErr.Traces), Not(Equals), 0, comment) c.Assert(line(DebugReport(err)), Matches, "*.trace_test.go.*", comment) c.Assert(line(DebugReport(err)), Not(Matches), "*.errors.go.*", comment) c.Assert(line(DebugReport(err)), Not(Matches), "*.trace.go.*", comment) c.Assert(testCase.Predicate(err), Equals, true, comment) w := newTestWriter() WriteError(w, err) outErr := ReadError(w.StatusCode, w.Body) if _, ok := outErr.(proxyError); !ok { c.Fatal("Expected error to be of type proxyError") } c.Assert(testCase.Predicate(outErr), Equals, true, comment) SetDebug(false) w = newTestWriter() WriteError(w, err) outErr = ReadError(w.StatusCode, w.Body) c.Assert(testCase.Predicate(outErr), Equals, true, comment) } } // Make sure we write some output produced by standard errors func (s *TraceSuite) TestWriteExternalErrors(c *C) { err := Wrap(fmt.Errorf("snap!")) SetDebug(true) w := newTestWriter() WriteError(w, err) extErr := ReadError(w.StatusCode, w.Body) c.Assert(w.StatusCode, Equals, http.StatusInternalServerError) c.Assert(strings.Replace(string(w.Body), "\n", "", -1), Matches, "*.snap.*") c.Assert(err.Error(), Equals, extErr.Error()) SetDebug(false) w = newTestWriter() WriteError(w, err) extErr = ReadError(w.StatusCode, w.Body) c.Assert(w.StatusCode, Equals, http.StatusInternalServerError) c.Assert(strings.Replace(string(w.Body), "\n", "", -1), Matches, "*.snap.*") c.Assert(err.Error(), Equals, extErr.Error()) } type netError struct { } func (e *netError) Error() string { return "net" } func (e *netError) Timeout() bool { return true } func (e *netError) Temporary() bool { return true } func (s *TraceSuite) TestConvert(c *C) { err := ConvertSystemError(&netError{}) c.Assert(IsConnectionProblem(err), Equals, true, Commentf("failed to detect network error")) dir := c.MkDir() err = os.Mkdir(dir, 0770) err = ConvertSystemError(err) c.Assert(IsAlreadyExists(err), Equals, true, Commentf("expected AlreadyExists error, got %T", err)) } func (s *TraceSuite) TestAggregates(c *C) { err1 := Errorf("failed one") err2 := Errorf("failed two") err := NewAggregate(err1, err2) c.Assert(IsAggregate(err), Equals, true) agg := Unwrap(err).(Aggregate) c.Assert(agg.Errors(), DeepEquals, []error{err1, err2}) c.Assert(err.Error(), DeepEquals, "failed one, failed two") } func (s *TraceSuite) TestErrorf(c *C) { err := Errorf("error") c.Assert(line(DebugReport(err)), Matches, "*.trace_test.go.*") c.Assert(line(DebugReport(err)), Not(Matches), "*.Fields.*") c.Assert(err.(*TraceErr).Messages, DeepEquals, []string(nil)) } func (s *TraceSuite) TestWithField(c *C) { err := Wrap(Errorf("error")).AddField("testfield", true) c.Assert(line(DebugReport(err)), Matches, "*.testfield.*") } func (s *TraceSuite) TestWithFields(c *C) { err := Wrap(Errorf("error")).AddFields(map[string]interface{}{ "testfield1": true, "testfield2": "value2", }) c.Assert(line(DebugReport(err)), Matches, "*.Fields.*") c.Assert(line(DebugReport(err)), Matches, "*.testfield1: true.*") c.Assert(line(DebugReport(err)), Matches, "*.testfield2: value2.*") } func (s *TraceSuite) TestAggregateConvertsToCommonErrors(c *C) { testCases := []struct { Err error Predicate func(error) bool RoundtripPredicate func(error) bool StatusCode int comment string }{ { comment: "Aggregate unwraps to first aggregated error", Err: NewAggregate( BadParameter("invalid value of foo"), LimitExceeded("limit exceeded"), ), Predicate: IsAggregate, RoundtripPredicate: IsBadParameter, StatusCode: http.StatusBadRequest, }, { comment: "Nested aggregate unwraps recursively", Err: NewAggregate( NewAggregate( BadParameter("invalid value of foo"), LimitExceeded("limit exceeded"), ), ), Predicate: IsAggregate, RoundtripPredicate: IsBadParameter, StatusCode: http.StatusBadRequest, }, } for _, testCase := range testCases { comment := Commentf(testCase.comment) SetDebug(true) err := testCase.Err c.Assert(line(DebugReport(err)), Matches, "*.trace_test.go.*", comment) c.Assert(testCase.Predicate(err), Equals, true, comment) w := newTestWriter() WriteError(w, err) outErr := ReadError(w.StatusCode, w.Body) c.Assert(testCase.RoundtripPredicate(outErr), Equals, true, comment) SetDebug(false) w = newTestWriter() WriteError(w, err) outErr = ReadError(w.StatusCode, w.Body) c.Assert(testCase.RoundtripPredicate(outErr), Equals, true, comment) } } func (s *TraceSuite) TestAggregateThrowAwayNils(c *C) { err := NewAggregate(fmt.Errorf("error1"), nil, fmt.Errorf("error2")) c.Assert(err.Error(), Not(Matches), ".*nil.*") } func (s *TraceSuite) TestAggregateAllNils(c *C) { c.Assert(NewAggregate(nil, nil, nil), IsNil) } func (s *TraceSuite) TestAggregateFromChannel(c *C) { errCh := make(chan error, 3) errCh <- fmt.Errorf("Snap!") errCh <- fmt.Errorf("BAM") errCh <- fmt.Errorf("omg") close(errCh) err := NewAggregateFromChannel(errCh, context.Background()) c.Assert(err.Error(), Matches, ".*Snap!.*") c.Assert(err.Error(), Matches, ".*BAM.*") c.Assert(err.Error(), Matches, ".*omg.*") } func (s *TraceSuite) TestAggregateFromChannelCancel(c *C) { errCh := make(chan error, 3) errCh <- fmt.Errorf("Snap!") errCh <- fmt.Errorf("BAM") errCh <- fmt.Errorf("omg") ctx, cancel := context.WithCancel(context.Background()) // we never closed the channel so we just need to make sure // the function exits when we cancel it cancel() NewAggregateFromChannel(errCh, ctx) } func (s *TraceSuite) TestCompositeErrorsCanProperlyUnwrap(c *C) { var testCases = []struct { err error message string wrappedMessage string }{ { err: ConnectionProblem(fmt.Errorf("internal error"), "failed to connect"), message: "failed to connect", wrappedMessage: "internal error", }, { err: Retry(fmt.Errorf("transient error"), "connection refused"), message: "connection refused", wrappedMessage: "transient error", }, { err: Trust(fmt.Errorf("access denied"), "failed to validate"), message: "failed to validate", wrappedMessage: "access denied", }, } var wrapper ErrorWrapper for _, tt := range testCases { c.Assert(tt.err.Error(), Equals, tt.message) c.Assert(Unwrap(tt.err), Implements, &wrapper) c.Assert(Unwrap(tt.err).(ErrorWrapper).OrigError().Error(), Equals, tt.wrappedMessage) } } type testError struct { Param string } func (n *testError) Error() string { return fmt.Sprintf("TestError(param=%v)", n.Param) } func (n *testError) OrigError() error { return n } func newTestWriter() *testWriter { return &testWriter{ H: make(http.Header), } } type testWriter struct { H http.Header Body []byte StatusCode int } func (tw *testWriter) Header() http.Header { return tw.H } func (tw *testWriter) Write(body []byte) (int, error) { tw.Body = body return len(tw.Body), nil } func (tw *testWriter) WriteHeader(code int) { tw.StatusCode = code } func line(s string) string { return strings.Replace(s, "\n", "", -1) } func TestStdlibCompat(t *testing.T) { rootErr := BadParameter("root error") var err error = rootErr for i := 0; i < 10; i++ { err = Wrap(err) } for i := 0; i < 10; i++ { err = WrapWithMessage(err, "wrap message %d", i) } if !errors.Is(err, rootErr) { t.Error("trace.Is(err, rootErr): got false, want true") } otherErr := CompareFailed("other error") if errors.Is(err, otherErr) { t.Error("trace.Is(err, otherErr): got true, want false") } var bpErr *BadParameterError if !errors.As(err, &bpErr) { t.Error("trace.As(err, BadParameterEror): got false, want true") } var cpErr *ConnectionProblemError if errors.As(err, &cpErr) { t.Error("trace.As(err, ConnectivityProblemError): got true, want false") } expectedErr := errors.New("wrapped error message") err = &ConnectionProblemError{Err: expectedErr, Message: "error message"} wrappedErr := errors.Unwrap(err) if wrappedErr == nil { t.Errorf("trace.Unwrap(err): got nil, want %v", expectedErr) } wrappedErrorMessage := wrappedErr.Error() if wrappedErrorMessage != expectedErr.Error() { t.Errorf("got %q, want %q", wrappedErrorMessage, expectedErr.Error()) } } trace-1.1.15/trail/000077500000000000000000000000001403336373600140205ustar00rootroot00000000000000trace-1.1.15/trail/trail.go000066400000000000000000000134331403336373600154660ustar00rootroot00000000000000/* 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" "github.com/gravitational/trace" "github.com/gravitational/trace/internal" "golang.org/x/net/context" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" ) // Send is a high level function that: // * converts error to GRPC error // * attaches debug metadata to existing metadata if possible // * sends the header to GRPC func Send(ctx context.Context, err error) error { meta, ok := metadata.FromIncomingContext(ctx) if !ok { meta = metadata.New(nil) } if trace.IsDebug() { SetDebugInfo(err, meta) } if len(meta) != 0 { sendErr := grpc.SendHeader(ctx, meta) if sendErr != nil { return trace.NewAggregate(err, sendErr) } } return ToGRPC(err) } // 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(err error) error { if err == nil { return nil } userMessage := trace.UserMessage(err) if trace.IsNotFound(err) { return grpc.Errorf(codes.NotFound, userMessage) } if trace.IsAlreadyExists(err) { return grpc.Errorf(codes.AlreadyExists, userMessage) } if trace.IsAccessDenied(err) { return grpc.Errorf(codes.PermissionDenied, userMessage) } if trace.IsCompareFailed(err) { return grpc.Errorf(codes.FailedPrecondition, userMessage) } if trace.IsBadParameter(err) || trace.IsOAuth2(err) { return grpc.Errorf(codes.InvalidArgument, userMessage) } if trace.IsLimitExceeded(err) { return grpc.Errorf(codes.ResourceExhausted, userMessage) } if trace.IsConnectionProblem(err) { return grpc.Errorf(codes.Unavailable, userMessage) } if trace.IsNotImplemented(err) { return grpc.Errorf(codes.Unimplemented, userMessage) } return grpc.Errorf(codes.Unknown, userMessage) } // 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 } code := grpc.Code(err) message := grpc.ErrorDesc(err) 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 := internal.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 } trace-1.1.15/trail/trail_test.go000066400000000000000000000060401403336373600165210ustar00rootroot00000000000000/* 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 import ( "io" "strings" "testing" "github.com/gravitational/trace" "google.golang.org/grpc" "google.golang.org/grpc/metadata" . "gopkg.in/check.v1" ) func TestTrail(t *testing.T) { TestingT(t) } type TrailSuite struct { } var _ = Suite(&TrailSuite{}) // TestConversion makes sure we convert all trace supported errors // to and back from GRPC codes func (s *TrailSuite) TestConversion(c *C) { type TestCase struct { Error error Message string Predicate func(error) bool } testCases := []TestCase{ { Error: trace.AccessDenied("access denied"), Predicate: trace.IsAccessDenied, }, { Error: trace.ConnectionProblem(nil, "problem"), Predicate: trace.IsConnectionProblem, }, { Error: trace.NotFound("not found"), Predicate: trace.IsNotFound, }, { Error: trace.BadParameter("bad parameter"), Predicate: trace.IsBadParameter, }, { Error: trace.CompareFailed("compare failed"), Predicate: trace.IsCompareFailed, }, { Error: trace.AccessDenied("denied"), Predicate: trace.IsAccessDenied, }, { Error: trace.LimitExceeded("exceeded"), Predicate: trace.IsLimitExceeded, }, { Error: trace.NotImplemented("not implemented"), Predicate: trace.IsNotImplemented, }, } for i, tc := range testCases { comment := Commentf("test case #v", i+1) grpcError := ToGRPC(tc.Error) c.Assert(grpc.ErrorDesc(grpcError), Equals, tc.Error.Error(), comment) out := FromGRPC(grpcError) c.Assert(tc.Predicate(out), Equals, true, comment) c.Assert(line(trace.DebugReport(out)), Matches, ".*trail_test.go.*") c.Assert(line(trace.DebugReport(out)), Not(Matches), ".*trail.go.*") } } // TestNil makes sure conversions of nil to and from GRPC are no-op func (s *TrailSuite) TestNil(c *C) { out := FromGRPC(ToGRPC(nil)) c.Assert(out, IsNil) } // TestFromEOF makes sure that non-grpc error such as io.EOF is preserved well. func (s *TrailSuite) TestFromEOF(c *C) { out := FromGRPC(trace.Wrap(io.EOF)) c.Assert(trace.IsEOF(out), Equals, true) } // TestTraces makes sure we pass traces via metadata and can decode it back func (s *TrailSuite) TestTraces(c *C) { err := trace.BadParameter("param") meta := metadata.New(nil) SetDebugInfo(err, meta) err2 := FromGRPC(ToGRPC(err), meta) c.Assert(line(trace.DebugReport(err)), Matches, ".*trail_test.go.*") c.Assert(line(trace.DebugReport(err2)), Matches, ".*trail_test.go.*") } func line(s string) string { return strings.Replace(s, "\n", "", -1) } trace-1.1.15/udphook.go000066400000000000000000000047241403336373600147140ustar00rootroot00000000000000package trace import ( "encoding/json" "net" "time" "github.com/gravitational/trace/internal" "github.com/jonboulle/clockwork" log "github.com/sirupsen/logrus" ) const ( // UDPDefaultAddr is a default address to emit logs to UDPDefaultAddr = "127.0.0.1:5000" // UDPDefaultNet is a default network UDPDefaultNet = "udp" ) // UDPOptionSetter represents functional arguments passed to ELKHook type UDPOptionSetter func(f *UDPHook) // NewUDPHook returns logrus-compatible hook that sends data to UDP socket func NewUDPHook(opts ...UDPOptionSetter) (*UDPHook, error) { f := &UDPHook{} for _, o := range opts { o(f) } if f.Clock == nil { f.Clock = clockwork.NewRealClock() } if f.clientNet == "" { f.clientNet = UDPDefaultNet } if f.clientAddr == "" { f.clientAddr = UDPDefaultAddr } addr, err := net.ResolveUDPAddr(f.clientNet, f.clientAddr) if err != nil { return nil, Wrap(err) } conn, err := net.ListenPacket("udp", ":0") if err != nil { return nil, Wrap(err) } f.addr = addr f.conn = conn.(*net.UDPConn) return f, nil } type UDPHook struct { Clock clockwork.Clock clientNet string clientAddr string addr *net.UDPAddr conn *net.UDPConn } type Frame struct { Time time.Time `json:"time"` Type string `json:"type"` Entry map[string]interface{} `json:"entry"` Message string `json:"message"` Level string `json:"level"` } // Fire fires the event to the ELK beat func (elk *UDPHook) Fire(e *log.Entry) error { // Make a copy to safely modify entry := e.WithFields(nil) if cursor := findFrame(); cursor != nil { t := internal.GetTracesFromCursor(*cursor) entry.Data[FileField] = t.String() entry.Data[FunctionField] = t.Func() } data, err := json.Marshal(Frame{ Time: elk.Clock.Now().UTC(), Type: "trace", Entry: entry.Data, Message: entry.Message, Level: entry.Level.String(), }) if err != nil { return Wrap(err) } conn, err := net.ListenPacket("udp", ":0") if err != nil { return Wrap(err) } defer conn.Close() resolvedAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:5000") if err != nil { return Wrap(err) } _, err = (conn.(*net.UDPConn)).WriteToUDP(data, resolvedAddr) return Wrap(err) } // Levels returns logging levels supported by logrus func (elk *UDPHook) Levels() []log.Level { return []log.Level{ log.PanicLevel, log.FatalLevel, log.ErrorLevel, log.WarnLevel, log.InfoLevel, log.DebugLevel, } } trace-1.1.15/udphook_test.go000066400000000000000000000021251403336373600157440ustar00rootroot00000000000000/* Copyright 2015 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 trace import ( "io/ioutil" "github.com/jonboulle/clockwork" log "github.com/sirupsen/logrus" . "gopkg.in/check.v1" ) type HooksSuite struct{} var _ = Suite(&HooksSuite{}) func (s *HooksSuite) TestSafeForConcurrentAccess(c *C) { logger := log.New() logger.Out = ioutil.Discard entry := logger.WithFields(log.Fields{"foo": "bar"}) logger.Hooks.Add(&UDPHook{Clock: clockwork.NewFakeClock()}) for i := 0; i < 3; i++ { go func(entry *log.Entry) { for i := 0; i < 1000; i++ { entry.Infof("test") } }(entry) } }