Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 1 addition & 1 deletion .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -222,4 +222,4 @@ linters-settings:
disabled: true

misspell:
locale: "US" # Fix the colour => color, and co
locale: "UK" # Fix the colour => color, and co
31 changes: 14 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,37 +18,34 @@ Goliath is an opinionated set of libraries to build resilient, scalable and main

# 🚀 Features

- [apperror](/apperror) - create elegant application errors
- [appcontext](/appcontext/) - wrapper around the native `ctx.Context`
- [app](/app) - set of common utilities such as elegant errors and enriched `ctx.Context`
- [retry](/retry/) - retry a specific task securely
- [clock](/clock) - wrapper around `time.Now` to help during testing
- [sleep](/sleep) - wrapper around `time.Sleep` for testing
- [httperror](/httperror) - implementation of the [RFC7807 Problem Details](https://datatracker.ietf.org/doc/html/rfc7807)

# 👀 Examples

### apperror
### app
```go
// create application errors
err := apperror.NewValidation("validate_user", "Error Validating User")
err.AddValidationError(NewValidationError("name", "name is empty"))
err.AddValidationError(NewValidationError("age", "user is under 18", "user must be an adult"))
// create elegant application errors
err := app.NewValidation("validate_user", "Error Validating User")
err.AddValidationError(NewFieldValidationError("name", "name is empty"))
err.AddValidationError(NewFieldValidationError("age", "user is under 18", "user must be an adult"))

// wrap the inner cause of the error
conn, err := db.Connect(...)

if err != nil {
return apperror.NewInternal("database_connection", "Error connecting to the database").SetSeverity(apperror.Critical).Wrap(err)
return app.NewErrorInternal("database_connection", "Error connecting to the database").SetSeverity(app.ErrorSeverityCritical).Wrap(err)
}

// create a raw error
err := apperror.New(("error_code", apperror.Internal, apperror.High, "Error message"))
```
err := app.NewError(("error_code", app.ErrorInternal, app.ErrorSeverityHigh, "Error message"))

### appcontext
```go
// enriched context
func Hello(w http.ResponseWriter, r *http.Request) {
ctx := appcontext.FromContext(r.Context())
ctx := app.NewContext(r.Context())
traceId := appCtx.TraceID()
userID, checkUser := ctx.UserID()

Expand Down Expand Up @@ -93,10 +90,10 @@ func Hello(w http.ResponseWriter, r *http.Request) {

### httperror
```go
appCtx := appcontext.FromContext(ctx)
err := apperror.NewValidation("invalid_payment_data", "The payment request is invalid")
err.AddValidationError(apperror.NewValidationError("amount", "Amount needs to be positive"))
err.AddValidationError(apperror.NewValidationError("currency", "currency is required"))
appCtx := app.NewContext(ctx)
err := app.NewValidationError("invalid_payment_data", "The payment request is invalid")
err.AddValidationError(app.NewFieldValidationError("amount", "Amount needs to be positive"))
err.AddValidationError(app.NewFieldValidationError("currency", "currency is required"))

httpErr := New(appCtx, err, "/payments")
```
Expand Down
79 changes: 79 additions & 0 deletions app/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package app

import (
"context"

"github.com/google/uuid"
)

type ContextKey string

const (
TraceIDKey ContextKey = "trace_id"
UserIDKey ContextKey = "user_id"
TenantIDKey ContextKey = "tenant_id"
)

// Context carries the context of the current execution.
type Context struct {
// original context
context.Context
}

// UserID returns the user id
func (sc *Context) UserID() (string, bool) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsure why you return the bool about the presence, you are not doing it for TraceID

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is that TraceID() always exist, so I set it in the New() if it is absent. To allow debugging easily.

userID := sc.Context.Value(UserIDKey)
id, ok := userID.(string)
return id, ok
}

// SetUserID sets the user id
func (sc *Context) SetUserID(userID string) *Context {
sc.Context = context.WithValue(sc.Context, UserIDKey, userID)

return sc
}

// TenantID returns the tenant id
func (sc *Context) TenantID() (string, bool) {
tenantIDKey := sc.Context.Value(TenantIDKey)
id, ok := tenantIDKey.(string)
return id, ok
}

// SetTenantID sets the user id
func (sc *Context) SetTenantID(tenantID string) *Context {
sc.Context = context.WithValue(sc.Context, TenantIDKey, tenantID)

return sc
}

// SetTraceID sets the trace id
func (sc *Context) SetTraceID(traceID string) *Context {
sc.Context = context.WithValue(sc.Context, TraceIDKey, traceID)

return sc
}

// TraceID returns the trace identifier for the current flow
func (sc *Context) TraceID() string {
traceID := sc.Context.Value(TraceIDKey)
id, ok := traceID.(string)

if !ok {
return ""
}

return id
}

// NewContext returns a new Context from a context.Context
func NewContext(ctx context.Context) Context {
appCtx := Context{Context: ctx}

if _, ok := ctx.Value(TraceIDKey).(string); !ok {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have called TraceID, and define one if the if was == "", my problem is that you somehow define two places where you check if you have a context.

This comment is made on your current code.

I think you could also consider to do not play/check/set with traceid in app.NewContext, but to define one when none is present in GetTraceID.

It would also fix an issue that someone with your current GetTraceID implementation.

Someone could define a context like this

ctx : = app.Context{Context: context.Background()} // possible as app.Context cannot be nothing but Exported

traceID := ctx.TraceID() // this would be empty

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you never call the TraceID() and pass the context as a value instead of a reference, the TraceID value will be mismatched.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, but the points I raised are still valid, no?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I added in the constructor and getter but it does not feel right 🤔

appCtx.SetTraceID(uuid.NewString())
}

return appCtx
}
44 changes: 22 additions & 22 deletions appcontext/app_context_test.go → app/context_test.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
package appcontext
package app

import (
"context"
"testing"

"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestNewAppContext(t *testing.T) {
func TestNewContext(t *testing.T) {
parentCtx := context.Background()

ctx := NewAppContext(parentCtx)
ctx := NewContext(parentCtx)

assert.NotEmpty(t, ctx.TraceID())
require.NotEmpty(t, ctx.TraceID())

_, err := uuid.Parse(ctx.TraceID())

assert.NoError(t, err)
require.NoError(t, err)
}

func TestFromContext(t *testing.T) {
func TestNewContextWithValues(t *testing.T) {
expectedTraceID := "asd-asd-123-asd"
expectedUserID := "c6d7dc51-c2a5-4aed-91fc-6f151342f9e2"
expectedTenantID := "c6ad12dc51-c2a5-asd-91fc-6f151342f9e2"
Expand All @@ -29,40 +29,40 @@ func TestFromContext(t *testing.T) {
parentCtx = context.WithValue(parentCtx, UserIDKey, expectedUserID)
parentCtx = context.WithValue(parentCtx, TenantIDKey, expectedTenantID)

ctx := FromContext(parentCtx)
ctx := NewContext(parentCtx)

assert.Equal(t, expectedTraceID, ctx.TraceID())
require.Equal(t, expectedTraceID, ctx.TraceID())

userID, checkUser := ctx.UserID()
assert.Equal(t, expectedUserID, userID)
assert.True(t, checkUser)
require.Equal(t, expectedUserID, userID)
require.True(t, checkUser)

tenantID, checkTenant := ctx.TenantID()
assert.Equal(t, expectedTenantID, tenantID)
assert.True(t, checkTenant)
require.Equal(t, expectedTenantID, tenantID)
require.True(t, checkTenant)
}

func TestFromEmptyContext(t *testing.T) {
parentCtx := context.Background()
ctx := FromContext(parentCtx)
ctx := NewContext(parentCtx)

assert.NotEmpty(t, ctx.TraceID())
require.NotEmpty(t, ctx.TraceID())

userID, checkUser := ctx.UserID()
assert.Equal(t, "", userID)
assert.False(t, checkUser)
require.Empty(t, userID)
require.False(t, checkUser)

tenantID, checkTenant := ctx.TenantID()
assert.Equal(t, "", tenantID)
assert.False(t, checkTenant)
require.Empty(t, tenantID)
require.False(t, checkTenant)
}

func TestAppContextCancellation(t *testing.T) {
func TestContextCancellation(t *testing.T) {
parentCtx, cancel := context.WithCancel(context.Background())

ctx := NewAppContext(parentCtx)
ctx := NewContext(parentCtx)

cancel()

assert.Equal(t, context.Canceled, ctx.Context().Err())
require.Equal(t, context.Canceled, ctx.Err())
}
Loading