snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
This commit is contained in:
129
server/pkg/errors/error.go
Normal file
129
server/pkg/errors/error.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type (
|
||||
Error struct {
|
||||
// serves as base error differentiator and informs error handling process
|
||||
// if it makes sense to retry later or with different payload (or not at all)
|
||||
kind kind
|
||||
|
||||
// simple error message with basic information
|
||||
message string
|
||||
|
||||
// captured call stack
|
||||
stack []*frame
|
||||
|
||||
// additional details
|
||||
meta meta
|
||||
|
||||
// if error wraps another error
|
||||
wrap error
|
||||
}
|
||||
|
||||
ErrorHandler func(error) error
|
||||
)
|
||||
|
||||
const (
|
||||
stackFrameSkip = 4
|
||||
)
|
||||
|
||||
func err(k kind, m string) *Error {
|
||||
return &Error{
|
||||
kind: k,
|
||||
message: m,
|
||||
|
||||
// skip 3 steps when collecting frames
|
||||
stack: collectStack(stackFrameSkip),
|
||||
}
|
||||
}
|
||||
|
||||
// Plain returns plain *Error, without call stack and formatted message
|
||||
func Plain(k kind, m string, a ...interface{}) *Error {
|
||||
return &Error{
|
||||
kind: k,
|
||||
message: fmt.Sprintf(m, a...),
|
||||
}
|
||||
}
|
||||
|
||||
// New returns *Error
|
||||
func New(k kind, m string, fn ...mfn) *Error {
|
||||
return err(k, m).Apply(fn...)
|
||||
}
|
||||
|
||||
// New returns *Error with formatted message
|
||||
func Newf(k kind, m string, a ...interface{}) *Error {
|
||||
return err(k, fmt.Sprintf(m, a...))
|
||||
}
|
||||
|
||||
// safe to show details of this error
|
||||
func (Error) Safe() bool { return true }
|
||||
|
||||
// Error message
|
||||
func (e Error) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
// Unwrap wrapped error
|
||||
func (e Error) Unwrap() error {
|
||||
return e.wrap
|
||||
}
|
||||
|
||||
// Wrap error
|
||||
func (e *Error) Wrap(err error) *Error {
|
||||
e.wrap = err
|
||||
return e
|
||||
}
|
||||
|
||||
// Recollect stack frames
|
||||
func (e *Error) Stack(skip int) *Error {
|
||||
e.stack = collectStack(skip)
|
||||
return e
|
||||
}
|
||||
|
||||
// Returns error's meta
|
||||
func (e Error) Meta() meta {
|
||||
return e.meta
|
||||
}
|
||||
|
||||
// Apply applies modifier functions
|
||||
func (e *Error) Apply(ffn ...mfn) *Error {
|
||||
for _, fn := range ffn {
|
||||
fn(e)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// Is provided Is() method for equality checking with errors.Is
|
||||
func (e *Error) Is(target error) bool {
|
||||
t, ok := target.(*Error)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return t.kind == e.kind && t.message == e.message
|
||||
}
|
||||
|
||||
// Unwrap is alias for errors.Unwrap so users can avoid importing both errors packages
|
||||
//
|
||||
// This function DOES NOT SUPPRESS errors if they are not wrapped!
|
||||
func Unwrap(err error) error {
|
||||
if err != nil && errors.Unwrap(err) != nil {
|
||||
return errors.Unwrap(err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Is is alias for errors.Is so users can avoid importing both errors packages
|
||||
func Is(err, target error) bool {
|
||||
return errors.Is(err, target)
|
||||
}
|
||||
|
||||
// As is alias for errors.As so users can avoid importing both errors packages
|
||||
func As(err error, target interface{}) bool {
|
||||
return errors.As(err, target)
|
||||
}
|
||||
167
server/pkg/errors/http.go
Normal file
167
server/pkg/errors/http.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/locale"
|
||||
)
|
||||
|
||||
type (
|
||||
translatable interface {
|
||||
Translate(tr func(string, string, ...string) string) error
|
||||
}
|
||||
)
|
||||
|
||||
// ServeHTTP Prepares and encodes given error for HTTP transport
|
||||
//
|
||||
// mask arg hides extra/debug info
|
||||
//
|
||||
// Proper HTTP status codes are generally not used in the API due to compatibility issues
|
||||
// This should be addressed in the future versions when/if we restructure the API
|
||||
func ServeHTTP(w http.ResponseWriter, r *http.Request, err error, mask bool) {
|
||||
// due to backward compatibility,
|
||||
// custom HTTP statuses are disabled for now.
|
||||
ServeHTTPWithCode(w, r, http.StatusOK, err, mask)
|
||||
}
|
||||
|
||||
// ProperlyServeHTTP Prepares and encodes given error for HTTP transport, same as ServeHTTP but with proper status codes
|
||||
func ProperlyServeHTTP(w http.ResponseWriter, r *http.Request, err error, mask bool) {
|
||||
var (
|
||||
code = http.StatusInternalServerError
|
||||
)
|
||||
|
||||
if e, is := err.(*Error); is {
|
||||
code = e.kind.httpStatus()
|
||||
}
|
||||
|
||||
ServeHTTPWithCode(w, r, code, err, mask)
|
||||
}
|
||||
|
||||
// Serves error via
|
||||
func ServeHTTPWithCode(w http.ResponseWriter, r *http.Request, code int, err error, mask bool) {
|
||||
var (
|
||||
// Very naive approach on parsing accept headers
|
||||
acceptsJson = strings.Contains(r.Header.Get("accept"), "application/json")
|
||||
)
|
||||
|
||||
if !mask && !acceptsJson {
|
||||
// Prettify error for plain text debug output
|
||||
w.Header().Set("Content-Type", "plain/text")
|
||||
w.WriteHeader(code)
|
||||
writeHttpPlain(w, err, mask)
|
||||
_, _ = fmt.Fprintln(w, "Note: you are seeing this because system is running in development mode")
|
||||
_, _ = fmt.Fprintln(w, "and HTTP request is made without \"Accept: .../json\" headers")
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
writeHttpJSON(r.Context(), w, err, mask)
|
||||
}
|
||||
|
||||
func writeHttpPlain(w io.Writer, err error, mask bool) {
|
||||
var (
|
||||
dlmt = strings.Repeat("-", 80)
|
||||
|
||||
write = func(s string, a ...interface{}) {
|
||||
if _, err := fmt.Fprintf(w, s, a...); err != nil {
|
||||
panic(fmt.Errorf("failed to write HTTP response: %w", err))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// handle client-aborted connections with no output
|
||||
if errors.Is(err, syscall.EPIPE) || errors.Is(err, syscall.ECONNRESET) {
|
||||
return
|
||||
}
|
||||
|
||||
write("Error: ")
|
||||
write(err.Error())
|
||||
write("\n")
|
||||
|
||||
if _, is := err.(interface{ Safe() bool }); !is || mask {
|
||||
// do not output any details on un-safe errors or
|
||||
// when a masked output is preferred
|
||||
return
|
||||
}
|
||||
|
||||
if err, is := err.(*Error); is {
|
||||
write(dlmt + "\n")
|
||||
|
||||
var (
|
||||
ml, kk = err.meta.StringKeys()
|
||||
)
|
||||
|
||||
sort.Strings(kk)
|
||||
|
||||
for _, key := range kk {
|
||||
write("%s:", key)
|
||||
write(strings.Repeat(" ", ml-len(key)+1))
|
||||
write("%v\n", err.meta[key])
|
||||
}
|
||||
|
||||
if len(err.stack) > 0 {
|
||||
write(dlmt + "\n")
|
||||
write("Call stack:\n")
|
||||
for i, f := range err.stack {
|
||||
write("%3d. %s:%d\n %s()\n", len(err.stack)-i, f.File, f.Line, f.Func)
|
||||
}
|
||||
}
|
||||
|
||||
if we := err.Unwrap(); we != nil {
|
||||
write(dlmt + "\n")
|
||||
write("Wrapped error:\n\n")
|
||||
writeHttpPlain(w, we, mask)
|
||||
}
|
||||
}
|
||||
write(dlmt + "\n")
|
||||
|
||||
}
|
||||
|
||||
// writeHttpJSON
|
||||
func writeHttpJSON(ctx context.Context, w io.Writer, err error, mask bool) {
|
||||
var (
|
||||
wrap = struct {
|
||||
Error interface{} `json:"error"`
|
||||
}{}
|
||||
)
|
||||
|
||||
// handle client-aborted connections with no output
|
||||
if errors.Is(err, syscall.EPIPE) || errors.Is(err, syscall.ECONNRESET) {
|
||||
return
|
||||
}
|
||||
|
||||
if se, is := err.(interface{ Safe() bool }); !is || !se.Safe() || mask {
|
||||
// trim error details when not debugging or error is not safe or maske
|
||||
err = errors.New(err.Error())
|
||||
}
|
||||
|
||||
// if error is translatable, pass in the lambda that returns
|
||||
// translated error
|
||||
//
|
||||
// we're using global locale service directly for now
|
||||
if tr, is := err.(translatable); is {
|
||||
err = tr.Translate(func(ns string, key string, pairs ...string) string {
|
||||
return locale.Global().T(ctx, ns, key, pairs...)
|
||||
})
|
||||
}
|
||||
|
||||
if c, is := err.(json.Marshaler); is {
|
||||
// take advantage of JSON marshaller on error
|
||||
wrap.Error = c
|
||||
} else {
|
||||
wrap.Error = map[string]string{"message": err.Error()}
|
||||
}
|
||||
|
||||
if err = json.NewEncoder(w).Encode(wrap); err != nil {
|
||||
panic(fmt.Errorf("failed to encode error: %w", err))
|
||||
}
|
||||
}
|
||||
90
server/pkg/errors/http_test.go
Normal file
90
server/pkg/errors/http_test.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Example_writeHttpPlain() {
|
||||
writeHttpPlain(os.Stdout, fmt.Errorf("dummy error"), true)
|
||||
|
||||
// Output:
|
||||
// Error: dummy error
|
||||
}
|
||||
|
||||
func Example_writeHttpJSON() {
|
||||
writeHttpJSON(context.Background(), os.Stdout, fmt.Errorf("dummy error"), true)
|
||||
|
||||
// Output:
|
||||
// {"error":{"message":"dummy error"}}
|
||||
}
|
||||
|
||||
func Example_writeHttpJSON_clientAbortedConnectionReset() {
|
||||
writeHttpJSON(context.Background(), os.Stdout, syscall.ECONNRESET, true)
|
||||
|
||||
// Output:
|
||||
}
|
||||
|
||||
func Example_writeHttpPlain_clientAbortedConnectionReset() {
|
||||
writeHttpPlain(os.Stdout, syscall.ECONNRESET, true)
|
||||
|
||||
// Output:
|
||||
}
|
||||
|
||||
func Example_writeHttpJSON_clientAbortedConnectionPipe() {
|
||||
writeHttpJSON(context.Background(), os.Stdout, syscall.EPIPE, true)
|
||||
|
||||
// Output:
|
||||
}
|
||||
|
||||
func Example_writeHttpPlain_clientAbortedConnectionPipe() {
|
||||
writeHttpPlain(os.Stdout, syscall.EPIPE, true)
|
||||
|
||||
// Output:
|
||||
}
|
||||
|
||||
func Example_writeHttpPlain_masked() {
|
||||
err := New(0, "dummy error", Meta("a", "b"), Meta(&Error{}, "nope"))
|
||||
err.stack = nil // will not test the stack as file path & line numbers might change
|
||||
writeHttpPlain(os.Stdout, err, true)
|
||||
// Output:
|
||||
// Error: dummy error
|
||||
}
|
||||
|
||||
func Example_writeHttpPlain_unmasked() {
|
||||
err := New(0, "dummy error", Meta("a", "b"), Meta(&Error{}, "nope"))
|
||||
err.stack = nil // will not test the stack as file path & line numbers might change
|
||||
writeHttpPlain(os.Stdout, err, false)
|
||||
// Output:
|
||||
// Error: dummy error
|
||||
// --------------------------------------------------------------------------------
|
||||
// a: b
|
||||
// --------------------------------------------------------------------------------
|
||||
}
|
||||
|
||||
func Test_writeHttpJSON(t *testing.T) {
|
||||
var (
|
||||
err = New(0, "dummy error", Meta("meta", "meta"))
|
||||
buf = bytes.NewBuffer(nil)
|
||||
req = require.New(t)
|
||||
)
|
||||
|
||||
buf.Truncate(0)
|
||||
writeHttpJSON(context.Background(), buf, err, false)
|
||||
req.Contains(buf.String(), "dummy error")
|
||||
req.Contains(buf.String(), "meta")
|
||||
req.Contains(buf.String(), "stack")
|
||||
|
||||
// when errors are masked (production env) we do not add meta or stack
|
||||
buf.Truncate(0)
|
||||
writeHttpJSON(context.Background(), buf, err, true)
|
||||
req.Contains(buf.String(), "dummy error")
|
||||
req.NotContains(buf.String(), "meta")
|
||||
req.NotContains(buf.String(), "stack")
|
||||
}
|
||||
29
server/pkg/errors/json.go
Normal file
29
server/pkg/errors/json.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/locale"
|
||||
)
|
||||
|
||||
func (e Error) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(struct {
|
||||
Message string `json:"message"`
|
||||
Meta meta `json:"meta,omitempty"`
|
||||
Stack []*frame `json:"stack,omitempty"`
|
||||
Wrap error `json:"wrap,omitempty"`
|
||||
}{
|
||||
Message: e.Error(),
|
||||
Meta: e.meta,
|
||||
Stack: e.stack,
|
||||
Wrap: e.wrap,
|
||||
})
|
||||
}
|
||||
|
||||
func (e Error) Translate(tr func(string, string, ...string) string) error {
|
||||
if msg := tr(e.meta.AsString(locale.ErrorMetaNamespace{}), e.meta.AsString(locale.ErrorMetaKey{}), e.meta.pairs()...); len(msg) > 0 {
|
||||
e.message = msg
|
||||
}
|
||||
|
||||
return &e
|
||||
}
|
||||
192
server/pkg/errors/kind.go
Normal file
192
server/pkg/errors/kind.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type (
|
||||
kind uint8
|
||||
)
|
||||
|
||||
const (
|
||||
// internal (unspecified) error
|
||||
// these errors yield HTTP status 500 and hide
|
||||
// details unless in development environment
|
||||
KindInternal kind = iota
|
||||
|
||||
// Data validation error
|
||||
KindInvalidData
|
||||
|
||||
// Requested data not found
|
||||
KindNotFound
|
||||
|
||||
// Stale data submitted
|
||||
// Internally data was already updated or is locked in another session
|
||||
KindStaleData
|
||||
|
||||
// Data already exists
|
||||
KindDuplicateData
|
||||
|
||||
// Data sensitivity related issue
|
||||
KindSensitiveData
|
||||
|
||||
// Access control
|
||||
KindUnauthorized
|
||||
|
||||
// Expecting authenticated user
|
||||
KindUnauthenticated
|
||||
|
||||
// External system failure
|
||||
KindExternal
|
||||
|
||||
// store error
|
||||
KindStore
|
||||
|
||||
// object store (file upload/download)
|
||||
KindObjStore
|
||||
|
||||
// automation error
|
||||
KindAutomation
|
||||
)
|
||||
|
||||
// translates error kind into http status
|
||||
func (k kind) httpStatus() int {
|
||||
switch k {
|
||||
case KindInvalidData, KindSensitiveData:
|
||||
return http.StatusBadRequest
|
||||
|
||||
case KindNotFound:
|
||||
return http.StatusNotFound
|
||||
|
||||
case KindStaleData:
|
||||
return http.StatusConflict
|
||||
|
||||
case KindDuplicateData:
|
||||
return http.StatusConflict
|
||||
|
||||
case KindUnauthorized:
|
||||
return http.StatusUnauthorized
|
||||
|
||||
case KindUnauthenticated:
|
||||
return http.StatusForbidden
|
||||
|
||||
default:
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
}
|
||||
|
||||
func Internal(m string, aa ...interface{}) *Error {
|
||||
return err(KindInternal, fmt.Sprintf(m, aa...))
|
||||
}
|
||||
|
||||
func Store(m string, aa ...interface{}) *Error {
|
||||
return err(KindStore, fmt.Sprintf(m, aa...))
|
||||
}
|
||||
|
||||
func ObjStore(m string, aa ...interface{}) *Error {
|
||||
return err(KindObjStore, fmt.Sprintf(m, aa...))
|
||||
}
|
||||
|
||||
func InvalidData(m string, aa ...interface{}) *Error {
|
||||
return err(KindInvalidData, fmt.Sprintf(m, aa...))
|
||||
}
|
||||
|
||||
func NotFound(m string, aa ...interface{}) *Error {
|
||||
return err(KindNotFound, fmt.Sprintf(m, aa...))
|
||||
}
|
||||
|
||||
func StaleData(m string, aa ...interface{}) *Error {
|
||||
return err(KindStaleData, fmt.Sprintf(m, aa...))
|
||||
}
|
||||
|
||||
func DuplicateData(m string, aa ...interface{}) *Error {
|
||||
return err(KindDuplicateData, fmt.Sprintf(m, aa...))
|
||||
}
|
||||
|
||||
func SensitiveData(m string, aa ...interface{}) *Error {
|
||||
return err(KindSensitiveData, fmt.Sprintf(m, aa...))
|
||||
}
|
||||
|
||||
func Unauthorized(m string, aa ...interface{}) *Error {
|
||||
return err(KindUnauthorized, fmt.Sprintf(m, aa...))
|
||||
}
|
||||
|
||||
func Unauthenticated(m string, aa ...interface{}) *Error {
|
||||
return err(KindUnauthenticated, fmt.Sprintf(m, aa...))
|
||||
}
|
||||
|
||||
func External(m string, aa ...interface{}) *Error {
|
||||
return err(KindExternal, fmt.Sprintf(m, aa...))
|
||||
}
|
||||
|
||||
func Automation(m string, aa ...interface{}) *Error {
|
||||
return err(KindAutomation, fmt.Sprintf(m, aa...))
|
||||
}
|
||||
|
||||
func IsKind(err error, k kind) bool {
|
||||
t, ok := err.(*Error)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return t.kind == k
|
||||
}
|
||||
|
||||
// IsAny returns true if error is of type *Error
|
||||
func IsAny(err error) bool {
|
||||
var is bool
|
||||
if err != nil {
|
||||
_, is = err.(*Error)
|
||||
}
|
||||
|
||||
return is
|
||||
}
|
||||
|
||||
func IsInternal(err error) bool {
|
||||
return IsKind(err, KindInternal)
|
||||
}
|
||||
|
||||
func IsStore(err error) bool {
|
||||
return IsKind(err, KindStore)
|
||||
}
|
||||
|
||||
func IsObjStore(err error) bool {
|
||||
return IsKind(err, KindObjStore)
|
||||
}
|
||||
|
||||
func IsInvalidData(err error) bool {
|
||||
return IsKind(err, KindInvalidData)
|
||||
}
|
||||
|
||||
func IsNotFound(err error) bool {
|
||||
return IsKind(err, KindNotFound)
|
||||
}
|
||||
|
||||
func IsStaleData(err error) bool {
|
||||
return IsKind(err, KindStaleData)
|
||||
}
|
||||
|
||||
func IsDuplicateData(err error) bool {
|
||||
return IsKind(err, KindDuplicateData)
|
||||
}
|
||||
|
||||
func IsSensitiveData(err error) bool {
|
||||
return IsKind(err, KindSensitiveData)
|
||||
}
|
||||
|
||||
func IsUnauthorized(err error) bool {
|
||||
return IsKind(err, KindUnauthorized)
|
||||
}
|
||||
|
||||
func IsUnauthenticated(err error) bool {
|
||||
return IsKind(err, KindUnauthenticated)
|
||||
}
|
||||
|
||||
func IsExternal(err error) bool {
|
||||
return IsKind(err, KindExternal)
|
||||
}
|
||||
|
||||
func IsAutomation(err error) bool {
|
||||
return IsKind(err, KindAutomation)
|
||||
}
|
||||
86
server/pkg/errors/meta.go
Normal file
86
server/pkg/errors/meta.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
type (
|
||||
meta map[interface{}]interface{}
|
||||
)
|
||||
|
||||
// StringKeys returns max length of (string) keys and slice of all strings
|
||||
func (m meta) StringKeys() (int, []string) {
|
||||
var (
|
||||
l, ml int
|
||||
key string
|
||||
kk = make([]string, 0, len(m))
|
||||
)
|
||||
|
||||
// collecting keys so that we can sort them to ensure
|
||||
// stable order and find out max length of the key
|
||||
for k := range m {
|
||||
switch c := k.(type) {
|
||||
case string:
|
||||
key = c
|
||||
case fmt.Stringer:
|
||||
key = c.String()
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
kk = append(kk, key)
|
||||
if l = len(key); l > ml {
|
||||
ml = l
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(kk)
|
||||
return ml, kk
|
||||
}
|
||||
|
||||
func (m meta) AsString(key interface{}) string {
|
||||
if _, has := m[key]; has {
|
||||
if s, ok := m[key].(string); ok {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m meta) AsInt(key interface{}) int {
|
||||
if _, has := m[key]; has {
|
||||
if s, ok := m[key].(int); ok {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m meta) MarshalJSON() ([]byte, error) {
|
||||
o := make(map[string]interface{})
|
||||
_, kk := m.StringKeys()
|
||||
for _, k := range kk {
|
||||
o[k] = m[k]
|
||||
}
|
||||
|
||||
return json.Marshal(o)
|
||||
}
|
||||
|
||||
// returns string pairs (both key & value are strings=
|
||||
func (m meta) pairs() []string {
|
||||
out := make([]string, 0, len(m)*2)
|
||||
|
||||
for k := range m {
|
||||
if key, ok := k.(string); ok {
|
||||
out = append(out, key, cast.ToString(m[k]))
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
74
server/pkg/errors/modifiers.go
Normal file
74
server/pkg/errors/modifiers.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package errors
|
||||
|
||||
type (
|
||||
mfn func(*Error)
|
||||
)
|
||||
|
||||
// Adds meta
|
||||
func Meta(k, v interface{}) mfn {
|
||||
return func(e *Error) {
|
||||
if e.meta == nil {
|
||||
e.meta = meta{}
|
||||
}
|
||||
|
||||
e.meta[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// Trim all keys from meta
|
||||
func MetaTrim(kk ...interface{}) mfn {
|
||||
return func(e *Error) {
|
||||
for _, k := range kk {
|
||||
delete(e.meta, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// StackSkip skips n frames in the stack
|
||||
//
|
||||
func StackSkip(n int) mfn {
|
||||
return func(e *Error) {
|
||||
if n > len(e.stack) {
|
||||
e.stack = nil
|
||||
}
|
||||
|
||||
e.stack = e.stack[n:]
|
||||
}
|
||||
}
|
||||
|
||||
// StackTrim removes n frames from the end of the stack
|
||||
func StackTrim(n int) mfn {
|
||||
return func(e *Error) {
|
||||
if len(e.stack) < n {
|
||||
e.stack = nil
|
||||
}
|
||||
|
||||
e.stack = e.stack[:len(e.stack)-n]
|
||||
}
|
||||
}
|
||||
|
||||
// StackTrimAtFn removes all frames after (inclusive) the (1st) function match
|
||||
func StackTrimAtFn(fn string) mfn {
|
||||
return func(e *Error) {
|
||||
for i := range e.stack {
|
||||
if i > 2 && e.stack[i].Func == fn {
|
||||
e.stack = e.stack[:i]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap embeds error
|
||||
func Wrap(w error) mfn {
|
||||
return func(e *Error) {
|
||||
e.wrap = w
|
||||
}
|
||||
}
|
||||
|
||||
// Converts and attaches node.js stack to error
|
||||
func AddNodeStack(stack []string) mfn {
|
||||
return func(e *Error) {
|
||||
e.stack = convertNodeStack(stack)
|
||||
}
|
||||
}
|
||||
85
server/pkg/errors/stack.go
Normal file
85
server/pkg/errors/stack.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
frame struct {
|
||||
Func string `json:"func"`
|
||||
File string `json:"file"`
|
||||
Line int `json:"line"`
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
stackBuf = 128
|
||||
)
|
||||
|
||||
func collectStack(skip int) []*frame {
|
||||
var (
|
||||
stack = make([]*frame, 0, stackBuf)
|
||||
cc = make([]uintptr, stackBuf)
|
||||
collected = runtime.Callers(skip, cc)
|
||||
frames = runtime.CallersFrames(cc[:collected])
|
||||
)
|
||||
|
||||
for {
|
||||
f, n := frames.Next()
|
||||
if !n {
|
||||
break
|
||||
}
|
||||
|
||||
aux := &frame{
|
||||
Func: f.Function,
|
||||
File: f.File,
|
||||
Line: f.Line,
|
||||
}
|
||||
|
||||
if li := strings.LastIndex(f.Function, "/"); li > 0 {
|
||||
// remove prefix
|
||||
aux.Func = aux.Func[li+1:]
|
||||
}
|
||||
|
||||
stack = append(stack, aux)
|
||||
}
|
||||
|
||||
return stack
|
||||
}
|
||||
|
||||
// Converts node stack trace (from Error().stack) to internal structure
|
||||
//
|
||||
// Node stack traces are received with errors from Corredor (node.js) automation server
|
||||
func convertNodeStack(nf []string) []*frame {
|
||||
conv := make([]*frame, len(nf))
|
||||
for i := range nf {
|
||||
var (
|
||||
f = &frame{}
|
||||
file, lno string
|
||||
)
|
||||
|
||||
if p := strings.Index(nf[i], " ("); p > 0 {
|
||||
f.Func = nf[i][:p]
|
||||
|
||||
file = nf[i][p+2 : len(nf[i])-1]
|
||||
} else {
|
||||
file = nf[i]
|
||||
}
|
||||
|
||||
parts := strings.Split(file, ":")
|
||||
f.File = parts[0]
|
||||
|
||||
if len(parts) >= 2 {
|
||||
lno = parts[1]
|
||||
}
|
||||
|
||||
if lno != "" {
|
||||
f.Line, _ = strconv.Atoi(lno)
|
||||
}
|
||||
|
||||
conv[i] = f
|
||||
}
|
||||
return conv
|
||||
}
|
||||
47
server/pkg/errors/stack_test.go
Normal file
47
server/pkg/errors/stack_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_convertNodeStack(t *testing.T) {
|
||||
converted := convertNodeStack([]string{
|
||||
"Object.exec (/dev/corteza/server-corredor/usr/testing/server-scripts/foo.js:12:11)",
|
||||
"Object.<anonymous> (/dev/corteza/server-corredor/node_modules/@cortezaproject/corteza-js/src/corredor/exec.ts:26:35)",
|
||||
"Generator.next (<anonymous>)",
|
||||
"/dev/corteza/server-corredor/node_modules/@cortezaproject/corteza-js/dist/index.js:277:71",
|
||||
"new Promise (<anonymous>)",
|
||||
"__awaiter (/dev/corteza/server-corredor/node_modules/@cortezaproject/corteza-js/dist/index.js:273:12)",
|
||||
"Object.Exec (/dev/corteza/server-corredor/node_modules/@cortezaproject/corteza-js/dist/index.js:483:12)",
|
||||
"ServerScripts.exec (/dev/corteza/server-corredor/src/services/server-scripts.ts:86:17)",
|
||||
"Object.Exec (/dev/corteza/server-corredor/src/grpc-handlers/server-scripts.ts:142:11)",
|
||||
"/dev/corteza/server-corredor/node_modules/grpc/src/server.js:593:13",
|
||||
})
|
||||
|
||||
expecting := []*frame{
|
||||
{"Object.exec", "/dev/corteza/server-corredor/usr/testing/server-scripts/foo.js", 12},
|
||||
{"Object.<anonymous>", "/dev/corteza/server-corredor/node_modules/@cortezaproject/corteza-js/src/corredor/exec.ts", 26},
|
||||
{"Generator.next", "<anonymous>", 0},
|
||||
{"", "/dev/corteza/server-corredor/node_modules/@cortezaproject/corteza-js/dist/index.js", 277},
|
||||
{"new Promise", "<anonymous>", 0},
|
||||
{"__awaiter", "/dev/corteza/server-corredor/node_modules/@cortezaproject/corteza-js/dist/index.js", 273},
|
||||
{"Object.Exec", "/dev/corteza/server-corredor/node_modules/@cortezaproject/corteza-js/dist/index.js", 483},
|
||||
{"ServerScripts.exec", "/dev/corteza/server-corredor/src/services/server-scripts.ts", 86},
|
||||
{"Object.Exec", "/dev/corteza/server-corredor/src/grpc-handlers/server-scripts.ts", 142},
|
||||
{"", "/dev/corteza/server-corredor/node_modules/grpc/src/server.js", 593},
|
||||
}
|
||||
|
||||
if len(expecting) != len(converted) {
|
||||
t.Errorf("converted stack length (%d) not match expectations (%d)", len(converted), len(expecting))
|
||||
}
|
||||
|
||||
for i := range expecting {
|
||||
if !reflect.DeepEqual(converted[i], expecting[i]) {
|
||||
t.Errorf("converted stack frame %d does not match\nexpecting: %v\ngot: %v", i, expecting[i], converted[i])
|
||||
} else {
|
||||
t.Logf("converted stack frame %d ok", i)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user