Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

story(issue-342): appbuilder simplify otel middleware #343

Merged
merged 4 commits into from
Dec 15, 2024
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go)
[![Go Reference](https://pkg.go.dev/badge/github.com/z5labs/bedrock.svg)](https://pkg.go.dev/github.com/z5labs/bedrock)
[![Go Report Card](https://goreportcard.com/badge/github.com/z5labs/bedrock)](https://goreportcard.com/report/github.com/z5labs/bedrock)
![Coverage](https://img.shields.io/badge/Coverage-98.6%25-brightgreen)
![Coverage](https://img.shields.io/badge/Coverage-98.2%25-brightgreen)
[![build](https://github.com/z5labs/bedrock/actions/workflows/build.yaml/badge.svg)](https://github.com/z5labs/bedrock/actions/workflows/build.yaml)

**bedrock provides a minimal, modular and composable foundation for
Expand Down
21 changes: 21 additions & 0 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,27 @@ func (f LifecycleHookFunc) Run(ctx context.Context) error {
return f(ctx)
}

// ComposeLifecycleHooks combines multiple [LifecycleHook]s into a single hook.
// Each hook is called sequentially and each hook is called irregardless if a
// previous hook returned an error or not. Any and all errors are then returned
// after all hooks have been ran.
func ComposeLifecycleHooks(hooks ...LifecycleHook) LifecycleHook {
return LifecycleHookFunc(func(ctx context.Context) error {
errs := make([]error, 0, len(hooks))
for _, hook := range hooks {
err := hook.Run(ctx)
if err == nil {
continue
}
errs = append(errs, err)
}
if len(errs) == 0 {
return nil
}
return errors.Join(errs...)
})
}

// Lifecycle
type Lifecycle struct {
// PostRun is always executed regardless if the underlying [bedrock.App]
Expand Down
33 changes: 33 additions & 0 deletions app/app_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,36 @@ func ExampleWithLifecycleHooks_unrecoveredPanic() {

// Output: ran post run hook
}

func ExampleComposeLifecycleHooks() {
var app bedrock.App = runFunc(func(ctx context.Context) error {
return nil
})

app = WithLifecycleHooks(app, Lifecycle{
PostRun: ComposeLifecycleHooks(
LifecycleHookFunc(func(ctx context.Context) error {
fmt.Println("one")
return nil
}),
LifecycleHookFunc(func(ctx context.Context) error {
fmt.Println("two")
return nil
}),
LifecycleHookFunc(func(ctx context.Context) error {
fmt.Println("three")
return nil
}),
),
})

err := app.Run(context.Background())
if err != nil {
fmt.Println(err)
return
}

// Output: one
// two
// three
}
54 changes: 54 additions & 0 deletions app/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,57 @@ func TestWithLifecycleHooks(t *testing.T) {
})
})
}

func TestComposeLifecycleHooks(t *testing.T) {
t.Run("will return an error", func(t *testing.T) {
t.Run("if a single lifecycle hook failed", func(t *testing.T) {
errHookFailed := errors.New("failed to run hook")

hook := ComposeLifecycleHooks(
LifecycleHookFunc(func(ctx context.Context) error {
return nil
}),
LifecycleHookFunc(func(ctx context.Context) error {
return errHookFailed
}),
LifecycleHookFunc(func(ctx context.Context) error {
return nil
}),
)

err := hook.Run(context.Background())
if !assert.ErrorIs(t, err, errHookFailed) {
return
}
})

t.Run("if multiple lifecycle hooks failed", func(t *testing.T) {
errHookFailedOne := errors.New("failed to run hook: one")
errHookFailedTwo := errors.New("failed to run hook: two")
errHookFailedThree := errors.New("failed to run hook: three")

hook := ComposeLifecycleHooks(
LifecycleHookFunc(func(ctx context.Context) error {
return errHookFailedOne
}),
LifecycleHookFunc(func(ctx context.Context) error {
return errHookFailedTwo
}),
LifecycleHookFunc(func(ctx context.Context) error {
return errHookFailedThree
}),
)

err := hook.Run(context.Background())
if !assert.ErrorIs(t, err, errHookFailedOne) {
return
}
if !assert.ErrorIs(t, err, errHookFailedTwo) {
return
}
if !assert.ErrorIs(t, err, errHookFailedThree) {
return
}
})
})
}
12 changes: 6 additions & 6 deletions appbuilder/appbuilder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,36 +19,36 @@ func TestRecover(t *testing.T) {
t.Run("will return an error", func(t *testing.T) {
t.Run("if the underlying App returns an error", func(t *testing.T) {
buildErr := errors.New("failed to build")
builder := Recover(bedrock.AppBuilderFunc[config](func(ctx context.Context, cfg config) (bedrock.App, error) {
builder := Recover(bedrock.AppBuilderFunc[struct{}](func(ctx context.Context, cfg struct{}) (bedrock.App, error) {
return nil, buildErr
}))

_, err := builder.Build(context.Background(), config{})
_, err := builder.Build(context.Background(), struct{}{})
if !assert.Equal(t, buildErr, err) {
return
}
})

t.Run("if the underlying App panics with an error value", func(t *testing.T) {
buildErr := errors.New("failed to build")
builder := Recover(bedrock.AppBuilderFunc[config](func(ctx context.Context, cfg config) (bedrock.App, error) {
builder := Recover(bedrock.AppBuilderFunc[struct{}](func(ctx context.Context, cfg struct{}) (bedrock.App, error) {
panic(buildErr)
return nil, nil
}))

_, err := builder.Build(context.Background(), config{})
_, err := builder.Build(context.Background(), struct{}{})
if !assert.ErrorIs(t, err, buildErr) {
return
}
})

t.Run("if the underlying App panics with a non-error value", func(t *testing.T) {
builder := Recover(bedrock.AppBuilderFunc[config](func(ctx context.Context, cfg config) (bedrock.App, error) {
builder := Recover(bedrock.AppBuilderFunc[struct{}](func(ctx context.Context, cfg struct{}) (bedrock.App, error) {
panic("hello world")
return nil, nil
}))

_, err := builder.Build(context.Background(), config{})
_, err := builder.Build(context.Background(), struct{}{})

var perr bedrock.PanicError
if !assert.ErrorAs(t, err, &perr) {
Expand Down
106 changes: 37 additions & 69 deletions appbuilder/otel.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,88 +9,56 @@ import (
"context"

"github.com/z5labs/bedrock"

"github.com/z5labs/bedrock/app"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/log"
"go.opentelemetry.io/otel/log/global"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/trace"
)

// TextMapPropagatorInitializer
type TextMapPropagatorInitializer interface {
InitTextMapPropogator(context.Context) (propagation.TextMapPropagator, error)
// OTelInitializer represents anything which can initialize the OTel SDK.
type OTelInitializer interface {
InitializeOTel(context.Context) error
}

// TracerProviderInitializer
type TracerProviderInitializer interface {
InitTracerProvider(context.Context) (trace.TracerProvider, error)
}
// OTel is a [bedrock.AppBuilder] middleware which initializes the OTel SDK.
// It also ensures that the OTel SDK is properly shutdown when the built [bedrock.App]
// stops running.
func OTel[T OTelInitializer](builder bedrock.AppBuilder[T]) bedrock.AppBuilder[T] {
return bedrock.AppBuilderFunc[T](func(ctx context.Context, cfg T) (bedrock.App, error) {
err := cfg.InitializeOTel(ctx)
if err != nil {
return nil, err
}

// MeterProviderInitializer
type MeterProviderInitializer interface {
InitMeterProvider(context.Context) (metric.MeterProvider, error)
}
base, err := builder.Build(ctx, cfg)
if err != nil {
return nil, err
}

// LoggerProviderInitializer
type LoggerProviderInitializer interface {
InitLoggerProvider(context.Context) (log.LoggerProvider, error)
base = app.WithLifecycleHooks(base, app.Lifecycle{
PostRun: app.ComposeLifecycleHooks(
tryShutdown(otel.GetTracerProvider()),
tryShutdown(otel.GetMeterProvider()),
tryShutdown(global.GetLoggerProvider()),
),
})
return base, nil
})
}

// OTelInitializer
type OTelInitializer interface {
TextMapPropagatorInitializer
TracerProviderInitializer
MeterProviderInitializer
LoggerProviderInitializer
type shutdowner interface {
Shutdown(context.Context) error
}

// OTel
func OTel[T OTelInitializer](builder bedrock.AppBuilder[T]) bedrock.AppBuilder[T] {
return bedrock.AppBuilderFunc[T](func(ctx context.Context, cfg T) (bedrock.App, error) {
fs := []func(context.Context) error{
func(ctx context.Context) error {
tmp, err := cfg.InitTextMapPropogator(ctx)
if err != nil || tmp == nil {
return err
}
otel.SetTextMapPropagator(tmp)
return nil
},
func(ctx context.Context) error {
tp, err := cfg.InitTracerProvider(ctx)
if err != nil || tp == nil {
return err
}
otel.SetTracerProvider(tp)
return nil
},
func(ctx context.Context) error {
mp, err := cfg.InitMeterProvider(ctx)
if err != nil || mp == nil {
return err
}
otel.SetMeterProvider(mp)
return nil
},
func(ctx context.Context) error {
lp, err := cfg.InitLoggerProvider(ctx)
if err != nil || lp == nil {
return err
}
global.SetLoggerProvider(lp)
return nil
},
func tryShutdown(v any) app.LifecycleHookFunc {
return func(ctx context.Context) error {
if v == nil {
return nil
}

for _, f := range fs {
err := f(ctx)
if err != nil {
return nil, err
}
s, ok := v.(shutdowner)
if !ok {
return nil
}

return builder.Build(ctx, cfg)
})
return s.Shutdown(ctx)
}
}
Loading