From cc439bf5684861095b85129aac123b959f35431a Mon Sep 17 00:00:00 2001 From: Mufthi Ryanda <77824812+mufthiryanda@users.noreply.github.com> Date: Mon, 12 Jan 2026 17:27:50 +0700 Subject: [PATCH 1/8] feat : add auto log runtime --- stderr/error.go | 23 ++++++++++++++++++++++- stdresp/standard.go | 13 +++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/stderr/error.go b/stderr/error.go index 56240af..b7e7ee1 100644 --- a/stderr/error.go +++ b/stderr/error.go @@ -25,7 +25,28 @@ func (e err) Error() string { // // Each value can be retrieved by helper func such as GetCode, GetMsg, GetMeta. func Err(code string, msg string, httpCode int) error { - return errWithDebug(code, msg, httpCode) + switch code { + case ERROR_CODE_ACCESS_PERMISSION: + return ErrPermission(msg) + case ERROR_CODE_DATA_NOT_FOUND: + return ErrDataNotFound() + case ERROR_CODE_INVALID_RULE: + return ErrInvRule(msg) + case ERROR_CODE_PARAMETER: + return ErrParam(msg) + case ERROR_CODE_WAITING_STATUS: + return ErrWaiting(msg) + case ERROR_CODE_THIRD_PARTY: + return ErrThirdParty(msg) + case ERROR_CODE_UNSUPPORTED: + return ErrUnsupported(msg) + case ERROR_CODE_INVALID_HEADER: + return ErrInvHeader(msg) + case ERROR_CODE_SYSTEM: + return ErrRuntime(msg) + default: + return ErrRuntime(msg) + } } // ErrValidation error in validation, it's not recommended to be used directly. diff --git a/stdresp/standard.go b/stdresp/standard.go index e4f90de..fddea61 100644 --- a/stdresp/standard.go +++ b/stdresp/standard.go @@ -2,6 +2,8 @@ package stdresp import ( "github.com/goravel/framework/contracts/http" + "github.com/spotlibs/go-lib/ctx" + "github.com/spotlibs/go-lib/log" "github.com/spotlibs/go-lib/stderr" ) @@ -25,6 +27,17 @@ func Resp(c http.Context, code, desc string, opts ...StdOpt) http.Response { for _, opt := range opts { opt(&res) } + + // Auto Logging Error + if res.ResponseCode == stderr.ERROR_CODE_SYSTEM { + mt := ctx.Get(c) // Retrieve X-Request-Id + log.Runtime(c).Error(log.Map{ + "code": res.ResponseCode, + "message": res.ResponseDesc, + "requestID": mt.ReqId, + }) + } + return c.Response().Json(res.httpCode, res) } From 471c340f30faaaf22a086f8ec782d6bf4463c65d Mon Sep 17 00:00:00 2001 From: Mufthi Ryanda <77824812+mufthiryanda@users.noreply.github.com> Date: Thu, 15 Jan 2026 18:09:16 +0700 Subject: [PATCH 2/8] feat : add stack trace --- debug/stack_trace_brispot.go | 16 ++++++---------- stderr/debug.go | 18 ++++++++++-------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/debug/stack_trace_brispot.go b/debug/stack_trace_brispot.go index ceb2a2f..2d09e7c 100644 --- a/debug/stack_trace_brispot.go +++ b/debug/stack_trace_brispot.go @@ -22,34 +22,30 @@ func GetStackTraceOnDebug(pick ...int) string { // May be used when just want to get the stack trace without caring the debug // state. func GetStackTraceInString(pick ...int) string { - stack := make([]uintptr, 2<<6) // 128 - length := runtime.Callers(0, stack) // skip no frames + stack := make([]uintptr, 2<<6) + length := runtime.Callers(0, stack) var pickAll bool - // set default to capture the first found line if len(pick) < 1 { pickAll = true } trackPicked := 1 - var allStackTrace strings.Builder + var traces []string for i := 0; i < length; i++ { funcPtr := runtime.FuncForPC(stack[i]) file, line := funcPtr.FileLine(stack[i]) if strings.Contains(file, "/app/") { + s := fmt.Sprintf("[%s:%d]", file, line) - s := fmt.Sprintf("%s:%d\n", file, line) - // capture the matched pick if !pickAll && trackPicked == pick[0] { return s } - allStackTrace.WriteString(s) - + traces = append(traces, s) trackPicked++ - } } - return allStackTrace.String() + return strings.Join(traces, " ") } diff --git a/stderr/debug.go b/stderr/debug.go index 1e96861..ad29a7e 100644 --- a/stderr/debug.go +++ b/stderr/debug.go @@ -14,14 +14,16 @@ var debugDepthLevel atomic.Int32 func errWithDebug(code string, msg string, httpCode int, metadata ...string) error { e := err{code: code, msg: msg, httpCode: httpCode, metadata: metadata} - switch debugDepthLevel.Load() { - case -1: - e.stackTrc = debug.GetStackTraceOnDebug() - case 0: // the default, set to 1 so that at least it can print one line - e.stackTrc = debug.GetStackTraceOnDebug(1) - default: - e.stackTrc = debug.GetStackTraceOnDebug(int(debugDepthLevel.Load())) - } + //switch debugDepthLevel.Load() { + //case -1: + // e.stackTrc = debug.GetStackTraceOnDebug() + //case 0: // the default, set to 1 so that at least it can print one line + // e.stackTrc = debug.GetStackTraceOnDebug(1) + //default: + // e.stackTrc = debug.GetStackTraceOnDebug(int(debugDepthLevel.Load())) + //} + + e.stackTrc = debug.GetStackTraceOnDebug() return e } From 31a84a322a05236e5ae99685ac280ba988ddc363 Mon Sep 17 00:00:00 2001 From: Mufthi Ryanda <77824812+mufthiryanda@users.noreply.github.com> Date: Mon, 19 Jan 2026 11:15:31 +0700 Subject: [PATCH 3/8] feat : add mocks --- mocks/errors.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 mocks/errors.go diff --git a/mocks/errors.go b/mocks/errors.go new file mode 100644 index 0000000..8691a69 --- /dev/null +++ b/mocks/errors.go @@ -0,0 +1,19 @@ +package mocks + +import "errors" + +const ( + ErrExpectedNetwork = "called error expected network" + ErrExpectedInfra = "called error expected infra" +) + +func ProduceSampleError(errs ...error) error { + var errMsg string + if len(errs) == 0 { + for _, err := range errs { + errMsg += err.Error() + "\n" + } + return errors.New(errMsg) + } + return errors.New(ErrExpectedNetwork) +} From 9666771051f8b352ad48dd6de02e6b25ee4b18ca Mon Sep 17 00:00:00 2001 From: Mufthi Ryanda <77824812+mufthiryanda@users.noreply.github.com> Date: Mon, 19 Jan 2026 12:40:59 +0700 Subject: [PATCH 4/8] chore : change namming --- {mocks => mocking}/errors.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename {mocks => mocking}/errors.go (95%) diff --git a/mocks/errors.go b/mocking/errors.go similarity index 95% rename from mocks/errors.go rename to mocking/errors.go index 8691a69..a581d4e 100644 --- a/mocks/errors.go +++ b/mocking/errors.go @@ -1,4 +1,4 @@ -package mocks +package mocking import "errors" From 28d8b76182d32f7edc02be9f3b97b4a36a0555f8 Mon Sep 17 00:00:00 2001 From: Mufthi Ryanda <77824812+mufthiryanda@users.noreply.github.com> Date: Mon, 19 Jan 2026 16:36:37 +0700 Subject: [PATCH 5/8] feat : limit duplicate stack trace --- debug/stack_trace_brispot.go | 16 ++++++++++------ helper/array.go | 34 ++++++++++++++++++++++++++++++++++ mocking/errors.go | 2 +- 3 files changed, 45 insertions(+), 7 deletions(-) create mode 100644 helper/array.go diff --git a/debug/stack_trace_brispot.go b/debug/stack_trace_brispot.go index 2d09e7c..dc29fc5 100644 --- a/debug/stack_trace_brispot.go +++ b/debug/stack_trace_brispot.go @@ -4,6 +4,8 @@ import ( "fmt" "runtime" "strings" + + "github.com/spotlibs/go-lib/helper" ) // GetStackTraceOnDebug return information from GetStackTraceInString if the @@ -32,18 +34,20 @@ func GetStackTraceInString(pick ...int) string { trackPicked := 1 var traces []string + seen := helper.NewStringSet() for i := 0; i < length; i++ { funcPtr := runtime.FuncForPC(stack[i]) file, line := funcPtr.FileLine(stack[i]) if strings.Contains(file, "/app/") { s := fmt.Sprintf("[%s:%d]", file, line) - - if !pickAll && trackPicked == pick[0] { - return s + if !seen.Exists(s) { + seen.Add(s) + if !pickAll && trackPicked == pick[0] { + return s + } + traces = append(traces, s) + trackPicked++ } - - traces = append(traces, s) - trackPicked++ } } diff --git a/helper/array.go b/helper/array.go new file mode 100644 index 0000000..54f64ad --- /dev/null +++ b/helper/array.go @@ -0,0 +1,34 @@ +package helper + +type StringSet struct { + items map[string]struct{} +} + +func NewStringSet() *StringSet { + return &StringSet{ + items: make(map[string]struct{}), + } +} + +func NewStringSetFromSlice(strings []string) *StringSet { + s := &StringSet{ + items: make(map[string]struct{}, len(strings)), + } + for _, str := range strings { + s.items[str] = struct{}{} + } + return s +} + +func (s *StringSet) Add(str string) { + s.items[str] = struct{}{} +} + +func (s *StringSet) Exists(str string) bool { + _, exists := s.items[str] + return exists +} + +func (s *StringSet) Size() int { + return len(s.items) +} diff --git a/mocking/errors.go b/mocking/errors.go index a581d4e..ee8e706 100644 --- a/mocking/errors.go +++ b/mocking/errors.go @@ -9,7 +9,7 @@ const ( func ProduceSampleError(errs ...error) error { var errMsg string - if len(errs) == 0 { + if len(errs) > 0 { for _, err := range errs { errMsg += err.Error() + "\n" } From 4c43b5c21ebca53f8221a5e010447557b696ed02 Mon Sep 17 00:00:00 2001 From: Mufthi Ryanda <77824812+mufthiryanda@users.noreply.github.com> Date: Mon, 19 Jan 2026 17:03:18 +0700 Subject: [PATCH 6/8] feat : add UT --- tests/helper/array_test.go | 108 ++++++++++++++++++++++++++++++++++++ tests/mocking/error_test.go | 90 ++++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 tests/helper/array_test.go create mode 100644 tests/mocking/error_test.go diff --git a/tests/helper/array_test.go b/tests/helper/array_test.go new file mode 100644 index 0000000..fe73d76 --- /dev/null +++ b/tests/helper/array_test.go @@ -0,0 +1,108 @@ +package helper_test + +import ( + "testing" + + "github.com/spotlibs/go-lib/helper" + "github.com/stretchr/testify/assert" +) + +func TestNewStringSet(t *testing.T) { + set := helper.NewStringSet() + assert.NotNil(t, set) + assert.Equal(t, 0, set.Size()) +} + +func TestNewStringSetFromSlice(t *testing.T) { + items := []string{"apple", "banana", "cherry"} + set := helper.NewStringSetFromSlice(items) + + assert.Equal(t, 3, set.Size()) + assert.True(t, set.Exists("apple")) + assert.True(t, set.Exists("banana")) + assert.True(t, set.Exists("cherry")) +} + +func TestNewStringSetFromSlice_Empty(t *testing.T) { + set := helper.NewStringSetFromSlice([]string{}) + assert.Equal(t, 0, set.Size()) +} + +func TestNewStringSetFromSlice_Duplicates(t *testing.T) { + items := []string{"apple", "apple", "banana"} + set := helper.NewStringSetFromSlice(items) + + assert.Equal(t, 2, set.Size()) + assert.True(t, set.Exists("apple")) + assert.True(t, set.Exists("banana")) +} + +func TestStringSet_Add(t *testing.T) { + set := helper.NewStringSet() + + set.Add("test") + assert.Equal(t, 1, set.Size()) + assert.True(t, set.Exists("test")) +} + +func TestStringSet_Add_Multiple(t *testing.T) { + set := helper.NewStringSet() + + set.Add("first") + set.Add("second") + set.Add("third") + + assert.Equal(t, 3, set.Size()) + assert.True(t, set.Exists("first")) + assert.True(t, set.Exists("second")) + assert.True(t, set.Exists("third")) +} + +func TestStringSet_Add_Duplicate(t *testing.T) { + set := helper.NewStringSet() + + set.Add("duplicate") + set.Add("duplicate") + + assert.Equal(t, 1, set.Size()) +} + +func TestStringSet_Exists(t *testing.T) { + set := helper.NewStringSet() + set.Add("exists") + + assert.True(t, set.Exists("exists")) + assert.False(t, set.Exists("notexists")) +} + +func TestStringSet_Exists_EmptyString(t *testing.T) { + set := helper.NewStringSet() + set.Add("") + + assert.True(t, set.Exists("")) +} + +func TestStringSet_Size(t *testing.T) { + set := helper.NewStringSet() + assert.Equal(t, 0, set.Size()) + + set.Add("one") + assert.Equal(t, 1, set.Size()) + + set.Add("two") + assert.Equal(t, 2, set.Size()) + + set.Add("one") // duplicate + assert.Equal(t, 2, set.Size()) +} + +func TestStringSet_ChineseCharacters(t *testing.T) { + set := helper.NewStringSet() + set.Add("你好") + set.Add("世界") + + assert.Equal(t, 2, set.Size()) + assert.True(t, set.Exists("你好")) + assert.True(t, set.Exists("世界")) + assert.False(t, set.Exists("测试")) +} diff --git a/tests/mocking/error_test.go b/tests/mocking/error_test.go new file mode 100644 index 0000000..3b52b58 --- /dev/null +++ b/tests/mocking/error_test.go @@ -0,0 +1,90 @@ +package mocking_test + +import ( + "errors" + "strings" + "testing" + + "github.com/spotlibs/go-lib/mocking" + "github.com/stretchr/testify/assert" +) + +func TestProduceSampleError_NoArgs(t *testing.T) { + err := mocking.ProduceSampleError() + + assert.Error(t, err) + assert.Equal(t, mocking.ErrExpectedNetwork, err.Error()) +} + +func TestProduceSampleError_SingleError(t *testing.T) { + inputErr := errors.New("single error") + err := mocking.ProduceSampleError(inputErr) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "single error") +} + +func TestProduceSampleError_MultipleErrors(t *testing.T) { + err1 := errors.New("error one") + err2 := errors.New("error two") + err3 := errors.New("error three") + + err := mocking.ProduceSampleError(err1, err2, err3) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "error one") + assert.Contains(t, err.Error(), "error two") + assert.Contains(t, err.Error(), "error three") +} + +func TestProduceSampleError_WithNewlines(t *testing.T) { + err1 := errors.New("first") + err2 := errors.New("second") + + err := mocking.ProduceSampleError(err1, err2) + + lines := strings.Split(err.Error(), "\n") + assert.True(t, len(lines) >= 2) +} + +func TestProduceSampleError_InfraError(t *testing.T) { + infraErr := errors.New(mocking.ErrExpectedInfra) + err := mocking.ProduceSampleError(infraErr) + + assert.Error(t, err) + assert.Contains(t, err.Error(), mocking.ErrExpectedInfra) +} + +func TestProduceSampleError_NetworkError(t *testing.T) { + networkErr := errors.New(mocking.ErrExpectedNetwork) + err := mocking.ProduceSampleError(networkErr) + + assert.Error(t, err) + assert.Contains(t, err.Error(), mocking.ErrExpectedNetwork) +} + +func TestProduceSampleError_EmptySlice(t *testing.T) { + err := mocking.ProduceSampleError([]error{}...) + + assert.Error(t, err) + assert.Equal(t, mocking.ErrExpectedNetwork, err.Error()) +} + +func TestProduceSampleError_MixedErrors(t *testing.T) { + err1 := errors.New("database error") + err2 := errors.New("network timeout") + err3 := errors.New("invalid input") + + err := mocking.ProduceSampleError(err1, err2, err3) + + assert.Error(t, err) + errStr := err.Error() + assert.Contains(t, errStr, "database error") + assert.Contains(t, errStr, "network timeout") + assert.Contains(t, errStr, "invalid input") +} + +func TestConstants(t *testing.T) { + assert.Equal(t, "called error expected network", mocking.ErrExpectedNetwork) + assert.Equal(t, "called error expected infra", mocking.ErrExpectedInfra) +} From 48fc5fea45fb5087e21a4bab60faa4f6b0870fc5 Mon Sep 17 00:00:00 2001 From: Mufthi Ryanda <77824812+mufthiryanda@users.noreply.github.com> Date: Mon, 19 Jan 2026 17:15:47 +0700 Subject: [PATCH 7/8] feat : change duplication --- debug/stack_trace_brispot.go | 9 ++- helper/array.go | 34 ----------- tests/helper/array_test.go | 108 ----------------------------------- tests/stderr/error_test.go | 4 +- 4 files changed, 6 insertions(+), 149 deletions(-) delete mode 100644 helper/array.go delete mode 100644 tests/helper/array_test.go diff --git a/debug/stack_trace_brispot.go b/debug/stack_trace_brispot.go index dc29fc5..de56b05 100644 --- a/debug/stack_trace_brispot.go +++ b/debug/stack_trace_brispot.go @@ -3,9 +3,8 @@ package debug import ( "fmt" "runtime" + "slices" "strings" - - "github.com/spotlibs/go-lib/helper" ) // GetStackTraceOnDebug return information from GetStackTraceInString if the @@ -34,14 +33,14 @@ func GetStackTraceInString(pick ...int) string { trackPicked := 1 var traces []string - seen := helper.NewStringSet() + var seenFiles []string for i := 0; i < length; i++ { funcPtr := runtime.FuncForPC(stack[i]) file, line := funcPtr.FileLine(stack[i]) if strings.Contains(file, "/app/") { s := fmt.Sprintf("[%s:%d]", file, line) - if !seen.Exists(s) { - seen.Add(s) + if !slices.Contains(seenFiles, file) { + seenFiles = append(seenFiles, file) if !pickAll && trackPicked == pick[0] { return s } diff --git a/helper/array.go b/helper/array.go deleted file mode 100644 index 54f64ad..0000000 --- a/helper/array.go +++ /dev/null @@ -1,34 +0,0 @@ -package helper - -type StringSet struct { - items map[string]struct{} -} - -func NewStringSet() *StringSet { - return &StringSet{ - items: make(map[string]struct{}), - } -} - -func NewStringSetFromSlice(strings []string) *StringSet { - s := &StringSet{ - items: make(map[string]struct{}, len(strings)), - } - for _, str := range strings { - s.items[str] = struct{}{} - } - return s -} - -func (s *StringSet) Add(str string) { - s.items[str] = struct{}{} -} - -func (s *StringSet) Exists(str string) bool { - _, exists := s.items[str] - return exists -} - -func (s *StringSet) Size() int { - return len(s.items) -} diff --git a/tests/helper/array_test.go b/tests/helper/array_test.go deleted file mode 100644 index fe73d76..0000000 --- a/tests/helper/array_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package helper_test - -import ( - "testing" - - "github.com/spotlibs/go-lib/helper" - "github.com/stretchr/testify/assert" -) - -func TestNewStringSet(t *testing.T) { - set := helper.NewStringSet() - assert.NotNil(t, set) - assert.Equal(t, 0, set.Size()) -} - -func TestNewStringSetFromSlice(t *testing.T) { - items := []string{"apple", "banana", "cherry"} - set := helper.NewStringSetFromSlice(items) - - assert.Equal(t, 3, set.Size()) - assert.True(t, set.Exists("apple")) - assert.True(t, set.Exists("banana")) - assert.True(t, set.Exists("cherry")) -} - -func TestNewStringSetFromSlice_Empty(t *testing.T) { - set := helper.NewStringSetFromSlice([]string{}) - assert.Equal(t, 0, set.Size()) -} - -func TestNewStringSetFromSlice_Duplicates(t *testing.T) { - items := []string{"apple", "apple", "banana"} - set := helper.NewStringSetFromSlice(items) - - assert.Equal(t, 2, set.Size()) - assert.True(t, set.Exists("apple")) - assert.True(t, set.Exists("banana")) -} - -func TestStringSet_Add(t *testing.T) { - set := helper.NewStringSet() - - set.Add("test") - assert.Equal(t, 1, set.Size()) - assert.True(t, set.Exists("test")) -} - -func TestStringSet_Add_Multiple(t *testing.T) { - set := helper.NewStringSet() - - set.Add("first") - set.Add("second") - set.Add("third") - - assert.Equal(t, 3, set.Size()) - assert.True(t, set.Exists("first")) - assert.True(t, set.Exists("second")) - assert.True(t, set.Exists("third")) -} - -func TestStringSet_Add_Duplicate(t *testing.T) { - set := helper.NewStringSet() - - set.Add("duplicate") - set.Add("duplicate") - - assert.Equal(t, 1, set.Size()) -} - -func TestStringSet_Exists(t *testing.T) { - set := helper.NewStringSet() - set.Add("exists") - - assert.True(t, set.Exists("exists")) - assert.False(t, set.Exists("notexists")) -} - -func TestStringSet_Exists_EmptyString(t *testing.T) { - set := helper.NewStringSet() - set.Add("") - - assert.True(t, set.Exists("")) -} - -func TestStringSet_Size(t *testing.T) { - set := helper.NewStringSet() - assert.Equal(t, 0, set.Size()) - - set.Add("one") - assert.Equal(t, 1, set.Size()) - - set.Add("two") - assert.Equal(t, 2, set.Size()) - - set.Add("one") // duplicate - assert.Equal(t, 2, set.Size()) -} - -func TestStringSet_ChineseCharacters(t *testing.T) { - set := helper.NewStringSet() - set.Add("你好") - set.Add("世界") - - assert.Equal(t, 2, set.Size()) - assert.True(t, set.Exists("你好")) - assert.True(t, set.Exists("世界")) - assert.False(t, set.Exists("测试")) -} diff --git a/tests/stderr/error_test.go b/tests/stderr/error_test.go index c7d964d..6e9c4e7 100644 --- a/tests/stderr/error_test.go +++ b/tests/stderr/error_test.go @@ -10,7 +10,7 @@ func TestErr(t *testing.T) { err := stderr.Err("77", "oops", 200) // assert the code - if stderr.GetCode(err) != "77" { + if stderr.GetCode(err) != "99" { t.Errorf("expect %s, got %s", "77", stderr.GetCode(err)) } // assert the message @@ -25,7 +25,7 @@ func TestErr(t *testing.T) { func TestErr_Error(t *testing.T) { err := stderr.Err("77", "oops", 200) - if err.Error() != "77 oops" { + if err.Error() != "99 oops" { // -> Map to ErrRuntime, for code except stdCode t.Errorf("expect %s, got %s", "77 oops", err.Error()) } } From b0508ddd517f75a7e65759c4ca61c388825a3840 Mon Sep 17 00:00:00 2001 From: Made Mas Adi Winata Date: Thu, 29 Jan 2026 09:53:29 +0700 Subject: [PATCH 8/8] add env file for facades use --- .env | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .env diff --git a/.env b/.env new file mode 100644 index 0000000..740b5c0 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +APP_KEY=JzLRl2YHe1Ec7MCGqkAJ4byaF08uKLfs +APP_DEBUG=true \ No newline at end of file