Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions OPENTELEMETRY_INTEGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,12 +260,12 @@ import (

type UserHandler struct{}

func (h UserHandler) Parameters() handlers.HandlerParameters {
return handlers.HandlerParameters{
func (h UserHandler) Parameters() handlers.HandlerParameters[GetUserRequest, GetUserResponse] {
return handlers.HandlerParameters[GetUserRequest, GetUserResponse]{
Title: "GetUser",
Body: libRequest.JSON,
ValidateHeader: true,
SaveToRequest: true,
Persistence: myRequestPersister{}, // optional; nil disables persistence
Path: "/users/:id",
EnableTracing: true, // Enable tracing for this handler
TracingSpanName: "get_user_handler",
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/blockloop/scan/v2 v2.5.0
github.com/gin-contrib/cors v1.7.6
github.com/gin-gonic/gin v1.10.1
github.com/go-chi/chi/v5 v5.3.0
github.com/go-playground/locales v0.14.1
github.com/go-playground/universal-translator v0.18.1
github.com/go-playground/validator/v10 v10.27.0
Expand Down Expand Up @@ -37,6 +38,7 @@ require (
go.opentelemetry.io/otel/sdk v1.38.0
go.opentelemetry.io/otel/trace v1.38.0
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b
golang.org/x/oauth2 v0.36.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.6.0
Expand All @@ -58,7 +60,6 @@ require (
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-chi/chi/v5 v5.3.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
Expand Down Expand Up @@ -106,7 +107,6 @@ require (
golang.org/x/arch v0.20.0 // indirect
golang.org/x/crypto v0.41.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
Expand Down
42 changes: 42 additions & 0 deletions handlers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ handlers/
├── dmlHandler.go
├── ormQueryHandler.go
├── pagination.go
├── persistence.go
├── queryHandler.go
├── recovery.go
└── *_test.go
Expand Down Expand Up @@ -315,6 +316,47 @@ This allows consistent visibility across request pipelines.

---

# Request persistence (optional)

Handlers support optional request/result persistence via `RequestPersister[Req, Resp]` on generic `HandlerParameters[Req, Resp]`.

When `Persistence` is `nil` (default for built-in query/DML/call handlers), no insert or update runs. When set, the framework calls:

1. `Insert(path, req)` after parse — failure aborts the request
2. `Update(path, req)` in `Recovery` after `Finalizer` and log collection — best-effort; errors are logged only and not retried

```go
type RequestPersister[Req, Resp any] interface {
Insert(path string, req *HandlerRequest[Req, Resp]) error
Update(path string, req *HandlerRequest[Req, Resp]) error
}
```

There is no built-in persister in this package. Consumers implement `RequestPersister` when they need audit storage, for example delegating to `libRequest`:

```go
type ServiceRequestPersister[Req, Resp any] struct{}

func (ServiceRequestPersister[Req, Resp]) Insert(path string, req *handlers.HandlerRequest[Req, Resp]) error {
return req.Core.RequestTools().InitRequest(req.W, req.Title, path)
}

func (ServiceRequestPersister[Req, Resp]) Update(path string, req *handlers.HandlerRequest[Req, Resp]) error {
reqLogLocal := req.W.Parser.GetLocal("reqLog")
reqLog, ok := reqLogLocal.(libRequest.RequestPtr)
if !ok || reqLog == nil {
return nil
}
reqLog.Outgoing = req.Response
// map errors from req.W.Parser.GetLocal("errorArray") as needed
return req.Core.RequestTools().UpdateRequestWithContext(req.W.Ctx, reqLog)
}
```

SQL and table shape for the service-tier `request` table live in application setup (`libApplication/env.go`), not in handlers.

---

# Testing

The handlers package includes test coverage through files such as:
Expand Down
25 changes: 12 additions & 13 deletions handlers/baseHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@ import (
"go.opentelemetry.io/otel/trace"
)

type HandlerParameters struct {
type HandlerParameters[Req, Resp any] struct {
Title string
Body libRequest.Type
ValidateHeader bool
SaveToRequest bool
Path string
HasReceipt bool
RecoveryHandler func(any)
FileResponse bool
LogArrays []string
LogTags []string
Persistence RequestPersister[Req, Resp]
// Tracing parameters
EnableTracing bool
TracingSpanName string
Expand All @@ -37,9 +37,9 @@ type HandlerInterface[Req any, Resp any] interface {
// returns handler title
// Request Bodymode
// and validate header option
// and save to request table option
// and optional request persistence
// and url path of handler
Parameters() HandlerParameters
Parameters() HandlerParameters[Req, Resp]
// runs after validating request
Initializer(req HandlerRequest[Req, Resp]) error
// main handler runs after initialize
Expand Down Expand Up @@ -124,8 +124,9 @@ func BaseHandler[Req any, Resp any, Handler HandlerInterface[Req, Resp]](
webFramework.AddServiceRegistrationLog(params.Title)
return func(c context.Context) {
start := time.Now()
var requestInserted bool
var w webFramework.WebFramework
if params.SaveToRequest {
if params.Persistence != nil {
w = libContext.InitContext(c)
} else {
w = libContext.InitContextNoAuditTrail(c)
Expand Down Expand Up @@ -153,7 +154,7 @@ func BaseHandler[Req any, Resp any, Handler HandlerInterface[Req, Resp]](
attribute.String("handler.title", params.Title),
attribute.String("handler.path", params.Path),
attribute.Bool("handler.validate_header", params.ValidateHeader),
attribute.Bool("handler.save_to_request", params.SaveToRequest),
attribute.Bool("handler.has_persistence", params.Persistence != nil),
)
}
}
Expand All @@ -168,7 +169,7 @@ func BaseHandler[Req any, Resp any, Handler HandlerInterface[Req, Resp]](
}

defer func() {
Recovery(start, w, handler, params, trx, core)
Recovery(start, w, handler, params, &trx, core, requestInserted)
}()

// Ensure span is ended
Expand Down Expand Up @@ -214,14 +215,12 @@ func BaseHandler[Req any, Resp any, Handler HandlerInterface[Req, Resp]](
w.Parser.SetLocal(libLogger.SlogRequestBody, trx.Request)
webFramework.AddLog(w, webFramework.HandlerLogTag, slog.Any("request", trx.Request))

if params.SaveToRequest {
errInitRequest := core.RequestTools().InitRequest(trx.W, params.Title, params.Path)
if errInitRequest != nil {
core.Responder().Error(trx.W, errInitRequest)
if params.Persistence != nil {
if errInsert := params.Persistence.Insert(params.Path, &trx); errInsert != nil {
core.Responder().Error(trx.W, errInsert)
return
}
} else {
w.Parser.SetLocal("reqLog", nil)
requestInserted = true
}

var errInit error
Expand Down
99 changes: 94 additions & 5 deletions handlers/baseHanlder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package handlers

import (
"log"
"sync/atomic"
"testing"

"github.com/gin-gonic/gin"
Expand All @@ -17,20 +18,35 @@ type testResp struct {
Result string `json:"result"`
}

type testPersister[Req, Resp any] struct {
updateCalled *atomic.Bool
}

func (p testPersister[Req, Resp]) Insert(path string, req *HandlerRequest[Req, Resp]) error {
return req.Core.RequestTools().InitRequest(req.W, req.Title, path)
}

func (p testPersister[Req, Resp]) Update(_ string, _ *HandlerRequest[Req, Resp]) error {
if p.updateCalled != nil {
p.updateCalled.Store(true)
}
return nil
}

type testHandlerType[Req testReq, Resp testResp] struct {
Title string
Path string
Mode libRequest.Type
VerifyHeader bool
SaveRequest bool
Persistence RequestPersister[Req, Resp]
}

func (h testHandlerType[Req, Resp]) Parameters() HandlerParameters {
return HandlerParameters{
func (h testHandlerType[Req, Resp]) Parameters() HandlerParameters[Req, Resp] {
return HandlerParameters[Req, Resp]{
Title: h.Title,
Body: h.Mode,
ValidateHeader: h.VerifyHeader,
SaveToRequest: h.SaveRequest,
Persistence: h.Persistence,
Path: h.Path,
HasReceipt: false,
RecoveryHandler: nil,
Expand Down Expand Up @@ -58,6 +74,7 @@ func (h testHandlerType[Req, Resp]) Simulation(req HandlerRequest[Req, Resp]) (R
func (h testHandlerType[Req, Resp]) Finalizer(req HandlerRequest[Req, Resp]) {
log.Println("Finalizer")
}

func TestBaseHandler(t *testing.T) {
testCases := []testingtools.TestCase{
{
Expand Down Expand Up @@ -85,7 +102,7 @@ func TestBaseHandler(t *testing.T) {
testHandlerType[testReq, testResp]{
Title: "test",
Path: "/path/to/api",
SaveRequest: true,
Persistence: testPersister[testReq, testResp]{},
Mode: libRequest.JSON,
VerifyHeader: true,
},
Expand All @@ -100,3 +117,75 @@ func TestBaseHandler(t *testing.T) {
Silent: true,
})
}

func TestBaseHandlerPersistenceUpdate(t *testing.T) {
var updateCalled atomic.Bool

env := testingtools.GetEnvWithDB[testingtools.TestEnv](
testingtools.SampleRequestModelMock(t, nil).DB,
testingtools.DefaultAPIList,
)

handler := BaseHandler(
env.Interface,
testHandlerType[testReq, testResp]{
Title: "test",
Path: "/path/to/api",
Persistence: testPersister[testReq, testResp]{updateCalled: &updateCalled},
Mode: libRequest.JSON,
VerifyHeader: true,
},
false,
)
gin.SetMode(gin.ReleaseMode)
testingtools.TestDB(t, &testingtools.TestCase{
Name: "Valid",
Url: "/",
Request: testReq{ID: "1"},
Status: 200,
CheckBody: []string{"result", `"a"`},
}, &testingtools.TestOptions{
Path: "/",
Name: "check persistence update",
Method: "POST",
Handler: handler,
Silent: true,
})

if !updateCalled.Load() {
t.Fatal("expected Persistence.Update to be called after successful Insert")
}
}

func TestBaseHandlerNoPersistence(t *testing.T) {
env := testingtools.GetEnvWithDB[testingtools.TestEnv](
testingtools.SampleRequestModelMock(t, nil).DB,
testingtools.DefaultAPIList,
)

handler := BaseHandler(
env.Interface,
testHandlerType[testReq, testResp]{
Title: "test",
Path: "/path/to/api",
Persistence: nil,
Mode: libRequest.JSON,
VerifyHeader: true,
},
false,
)
gin.SetMode(gin.ReleaseMode)
testingtools.TestDB(t, &testingtools.TestCase{
Name: "Valid",
Url: "/",
Request: testReq{ID: "1"},
Status: 200,
CheckBody: []string{"result", `"a"`},
}, &testingtools.TestOptions{
Path: "/",
Name: "check no persistence",
Method: "POST",
Handler: handler,
Silent: true,
})
}
17 changes: 6 additions & 11 deletions handlers/consumeHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,22 +49,18 @@ type CallArgs[Req any, Resp any] struct {
RecoveryHandler func(any)
}

func (c CallArgs[Req, Resp]) Parameters() HandlerParameters {
func (c CallArgs[Req, Resp]) Parameters() HandlerParameters[Req, Resp] {
var mode libRequest.Type
if c.IsJson {
mode = libRequest.JSON
} else {
mode = libRequest.Query
}
save := false
if c.HasInitializer {
save = true
}
return HandlerParameters{
return HandlerParameters[Req, Resp]{
Title: c.Title,
Body: mode,
ValidateHeader: false,
SaveToRequest: save,
Persistence: nil,
Path: c.Path,
HasReceipt: false,
RecoveryHandler: c.RecoveryHandler,
Expand Down Expand Up @@ -178,7 +174,6 @@ type ConsumeHandlerType[Req, Resp any] struct {
Path string
Mode libRequest.Type
VerifyHeader bool
SaveToRequest bool
HasReceipt bool
Headers []string
Api string
Expand All @@ -187,12 +182,12 @@ type ConsumeHandlerType[Req, Resp any] struct {
RecoveryHandler func(any)
}

func (h *ConsumeHandlerType[Req, Resp]) Parameters() HandlerParameters {
return HandlerParameters{
func (h *ConsumeHandlerType[Req, Resp]) Parameters() HandlerParameters[Req, Resp] {
return HandlerParameters[Req, Resp]{
Title: h.Title,
Body: h.Mode,
ValidateHeader: h.VerifyHeader,
SaveToRequest: h.SaveToRequest,
Persistence: nil,
Path: h.Path,
HasReceipt: h.HasReceipt,
RecoveryHandler: h.RecoveryHandler,
Expand Down
Loading
Loading