Skip to content

Commit

Permalink
feat: fault
Browse files Browse the repository at this point in the history
  • Loading branch information
chronark committed Jan 23, 2025
1 parent e7a2d1a commit d2d30e7
Show file tree
Hide file tree
Showing 29 changed files with 782 additions and 393 deletions.
4 changes: 3 additions & 1 deletion go/.golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ linters-settings:
- "^google.golang.org/protobuf/.+Options$"
- "^gopkg.in/yaml.v3.Node$"

lll:
line-length: 100
funlen:
# Checks the number of lines in a function.
# If lower than 0, disable the check.
Expand Down Expand Up @@ -252,7 +254,7 @@ linters:
# - goprintffuncname # checks that printf-like functions are named with f at the end
# - gosec # inspects source code for security problems
# - intrange # finds places where for loops could make use of an integer range
# - lll # reports long lines
- lll # reports long lines
# - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap)
# - makezero # finds slice declarations with non-zero initial length
# - mirror # reports wrong mirror patterns of bytes/strings usage
Expand Down
18 changes: 16 additions & 2 deletions go/cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import (
"runtime/debug"
"syscall"

"github.com/unkeyed/unkey/go/pkg/api/server"
"github.com/unkeyed/unkey/go/pkg/config"
"github.com/unkeyed/unkey/go/pkg/logging"
"github.com/unkeyed/unkey/go/pkg/uid"
"github.com/unkeyed/unkey/go/pkg/version"
"github.com/unkeyed/unkey/go/pkg/zen"
"github.com/unkeyed/unkey/go/pkg/zen/validation"
"github.com/urfave/cli/v2"
)

Expand Down Expand Up @@ -69,7 +70,7 @@ func run(c *cli.Context) error {

logger.Info(c.Context, "configration loaded", slog.String("file", configFile))

srv, err := server.New(server.Config{
srv, err := zen.New(zen.Config{
NodeId: cfg.NodeId,
Logger: logger,
Clickhouse: nil,
Expand All @@ -78,6 +79,19 @@ func run(c *cli.Context) error {
return err
}

validator, err := validation.New()
if err != nil {
return err
}

srv.SetGlobalMiddleware(
// metrics should always run first, so it can capture the latency of the entire request
zen.WithMetrics(nil), // TODO: add eventbuffer
zen.WithLogging(logger),
zen.WithErrorHandling(),
zen.WithValidation(validator),
)

go func() {
listenErr := srv.Listen(c.Context, fmt.Sprintf(":%s", cfg.Port))
if listenErr != nil {
Expand Down
27 changes: 14 additions & 13 deletions go/pkg/clickhouse/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,19 +69,20 @@ func New(config Config) (*Clickhouse, error) {
}
},
}),
keyVerifications: batch.New[schema.KeyVerificationRequestV1](batch.Config[schema.KeyVerificationRequestV1]{
BatchSize: 1000,
BufferSize: 100000,
FlushInterval: time.Second,
Consumers: 4,
Flush: func(ctx context.Context, rows []schema.KeyVerificationRequestV1) {
table := "raw_key_verifications_v1"
err := flush(ctx, conn, table, rows)
if err != nil {
config.Logger.Error().Err(err).Str("table", table).Msg("failed to flush batch")
}
},
}),
keyVerifications: batch.New[schema.KeyVerificationRequestV1](
batch.Config[schema.KeyVerificationRequestV1]{
BatchSize: 1000,
BufferSize: 100000,
FlushInterval: time.Second,
Consumers: 4,
Flush: func(ctx context.Context, rows []schema.KeyVerificationRequestV1) {
table := "raw_key_verifications_v1"
err := flush(ctx, conn, table, rows)
if err != nil {
config.Logger.Error().Err(err).Str("table", table).Msg("failed to flush batch")
}
},
}),
}

// err = c.conn.Ping(context.Background())
Expand Down
3 changes: 2 additions & 1 deletion go/pkg/ctxutil/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ const (
request_id contextKey = "request_id"
)

// getValue returns the value for the given key from the context or its zero value if it doesn't exist.
// getValue returns the value for the given key from the context or its zero
// value if it doesn't exist.
func getValue[T any](ctx context.Context, key contextKey) T {
val, ok := ctx.Value(key).(T)
if !ok {
Expand Down
95 changes: 95 additions & 0 deletions go/pkg/fault/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<div align="center">
<h1 align="center">Fault</h1>
</div>


Fault is an error handling package for Go, providing rich context preservation
and safe error reporting in applications. It combines debugging capabilities with
secure user communication patterns.

## Inspiration

I goodartistscopygreatartistssteal'd a lot from [Southclaws/fault](https://github.com/Southclaws/fault).

## Features

Fault addresses common challenges in production error handling:

- Separating internal error details from user-facing messages
- Maintaining complete error context for debugging
- Providing consistent error classification
- Automatic source location tracking
- Safe error chain unwrapping and inspection

### Dual-Message Pattern
Maintain separate internal and public error messages:
```go
fault.Wrap(err,
fault.With(
"database error: connection timeout", // internal message
"Service temporarily unavailable" // public message
),
)
```

### Error Chain Tracking
Preserve complete error context with source locations:
```go
steps := fault.Unwind(err)
for _, step := range steps {
fmt.Printf("Error at %s: %s\n", step.Location, step.Message)
}
```

### Error Classification
Tag errors for consistent handling:
```go
var DATABASE_ERROR = fault.Tag("DATABASE_ERROR")

err := fault.New("connection failed",
fault.WithTag(DATABASE_ERROR),
)

switch fault.GetTag(err) {
case DATABASE_ERROR:
// handle
default:
// handle
}
```

### Location Tracking
Automatic capture of error locations:
```go
err := fault.New("initial error") // captures location
err = fault.Wrap(err, fault.With(...)) // captures new location
```

## Philosophy

Fault is built on these principles:

Fault embraces Go’s simplicity and power. By focusing only on essential
abstractions, it keeps your code clean, maintainable, and in harmony with
Go’s design principles.

## Usage

Fault integrates seamlessly with existing Go code:

```go
func ProcessOrder(id string) error {
order, err := db.FindOrder(id)
if err != nil {
return fault.Wrap(err,
fault.WithTag(DATABASE_ERROR),
fault.With(
fmt.Sprintf("failed to find order %s", id),
"Order not found",
),
)
}

// ... process order ...
}
```
72 changes: 72 additions & 0 deletions go/pkg/fault/chain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package fault

import (
"errors"
"slices"
)

// Step represents a single step in an error chain, containing both
// the error message and the location where it occurred.
type Step struct {
// Message contains the error message for this step
Message string
// Location contains the file:line where this error occurred
Location string
}

// Unwind extracts all the steps from an error chain, producing a slice
// of Steps that can be used for detailed error reporting and debugging.
// It preserves both the error messages and the locations where they occurred.
//
// Example:
//
// err := New("base error")
// err = fault.Wrap(err, fault.With("missing paramter", "The request is missing a parameter"))
// steps := fault.Unwind(err)
// for _, step := range steps {
// fmt.Printf("Error at %s: %s\n", step.Location, step.Message)
// }
func Unwind(err error) []Step {
if err == nil {
return []Step{}
}

chain := []error{}

for err != nil {
chain = append(chain, err)
err = errors.Unwrap(err)
}

lastLocation := ""
steps := []Step{}
for i := len(chain) - 1; i >= 0; i-- {
err := chain[i]
var next error
if i+1 < len(chain) {
next = chain[i+1]
}

switch unwithLocation := err.(type) {
case *withLocation:
{
_, ok := next.(*withLocation)
if ok && unwithLocation.location != "" {
steps = append(steps, Step{Message: "", Location: unwithLocation.location})
}
lastLocation = unwithLocation.location
}
case *root:
{
steps = append(steps, Step{Message: unwithLocation.message, Location: unwithLocation.location})
lastLocation = ""
}
default:
{
steps = append(steps, Step{Message: err.Error(), Location: lastLocation})
}
}
}
slices.Reverse(steps)
return steps
}
32 changes: 32 additions & 0 deletions go/pkg/fault/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Package fault provides a comprehensive error handling system designed for
// building robust applications with rich error context and debugging
// capabilities. It implements a multi-layered error handling approach with
// several key features:
//
// - Dual-message error pattern: Separate internal (debug) and
// public (user-facing) error messages.
// - Error chain tracking: Capture and preserve complete error context with
// source locations.
// - Error tagging: Flexible error classification system for consistent error
// handling.
// - Stack trace preservation: Automatic capture of error locations throughout
// the chain.
//
// It builds upon Go's standard error handling patterns while adding structured
// context and safety mechanisms.
//
// This package is heavily inspired/copied from [fault]. We just wanted a
// slightly different API.
//
// Basic usage:
//
// err := fault.New("database connection failed")
// if err != nil {
// return fault.Wrap(err,
// fault.WithTag(DATABASE_ERROR),
// fault.With("init failed: %v", "Service temporarily unavailable"),
// )
// }
//
// [fault] https://github.com/Southclaws/fault
package fault
54 changes: 54 additions & 0 deletions go/pkg/fault/location.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package fault

import (
"fmt"
"runtime"
"strings"
)

// withLocation represents an error with an associated call location.
// It's used internally to track where in the code errors occur.
type withLocation struct {
// err is the underlying error being withLocation
err error
// location contains the file and line number where the error was withLocation
location string
}

// Error implements the error interface by returning the underlying error messages
// in the order they were added to the chain, from oldest to newest.
//
// Example:
//
// baseErr := fault.New("initial error")
// withLocation := fault.Wrap(baseErr, fault.With })
// fmt.Println(withLocation.Error()) // Output will contain all messages in chain
func (w *withLocation) Error() string {
errs := []string{}
chain := Unwind(w)

for i := len(chain) - 1; i >= 0; i-- {
errs = append(errs, chain[i].Message)
}
return strings.Join(errs, ": ")

}

// getLocation returns a string containing the file name and line number
// where it was called. It skips 3 frames in the call stack to get the
// actual location where the error was created rather than the internal
// function calls.
//
// The returned string is in the format "filename:linenumber"
//
// Example:
//
// loc := getLocation() // might return "main.go:42"
func getLocation() string {
pc := make([]uintptr, 1)
runtime.Callers(3, pc)
cf := runtime.CallersFrames(pc)
f, _ := cf.Next()

return fmt.Sprintf("%s:%d", f.File, f.Line)
}
Loading

0 comments on commit d2d30e7

Please sign in to comment.