Skip to content

Commit

Permalink
story(readme): update to reflect new minimal implementation (#341)
Browse files Browse the repository at this point in the history
  • Loading branch information
Zaba505 authored Dec 15, 2024
1 parent fb43aeb commit fa2e9cf
Showing 1 changed file with 96 additions and 119 deletions.
215 changes: 96 additions & 119 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,28 @@
[![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
quickly developing services and more use case specific frameworks in Go.**
quickly developing more use case specific frameworks in Go.**

# Core Concepts
# Building custom frameworks with bedrock

One of the guiding principals for [bedrock](https://pkg.go.dev/github.com/z5labs/bedrock) is to be composable.
This principal comes from the experience gained from working with custom, tailor made frameworks which
over their lifetime within an organization are unable to adapt to changing
development and deployment patterns. Eventually, these frameworks are abandoned
for new ones or completely rewritten to reflect the current state of the organization.

[bedrock](https://pkg.go.dev/github.com/z5labs/bedrock) defines a small set of types and carefully
chooses its opinions to balance composability and functionality, as much as it can. The result is, in fact, a framework
that isn't necessarily designed for building services directly, but instead meant for building
more custom, use case specific frameworks.

For example, [bedrock](https://pkg.go.dev/github.com/z5labs/bedrock) could be used by your organizations
platform engineering or framework team(s) to quickly develop internal frameworks which abstract over all of
your organizations requirements e.g. OpenTelemetry, Logging, Authenticated endpoints, etc. Then, due to the
high composibility of [bedrock](https://pkg.go.dev/github.com/z5labs/bedrock), any changes within your
organization would then be very easy to adapt to within your internal framework.

## Core Concepts

```go
type App interface {
Expand Down Expand Up @@ -57,145 +76,103 @@ provides is the [Run](https://pkg.go.dev/github.com/z5labs/bedrock#Run) function
handles the orchestration of config parsing, app building and, lastly, app execution by relying
on the other core abstractions noted above.

# Building services with bedrock
## Putting them altogether

Below is a tiny and simplistic example of all the core concepts of [bedrock](https://pkg.go.dev/github.com/z5labs/bedrock).

[bedrock](https://pkg.go.dev/github.com/z5labs/bedrock) conveniently comes with a couple of
[App](https://pkg.go.dev/github.com/z5labs/bedrock#App)s already implemented for you.
This can significantly aid in shortening your overall development time, as well as,
provide an example for how to implement your own custom [App](https://pkg.go.dev/github.com/z5labs/bedrock#App).
### config.yaml

```yaml
logging:
min_level: {{env "MIN_LOG_LEVEL"}}
```
For example, the below shows how simple it is to implement a RESTful API leveraging
the [rest.App](https://pkg.go.dev/github.com/z5labs/bedrock/rest#App).
### main.go
```go
package main

import (
"context"
"encoding/json"
"net/http"
"strings"

"github.com/z5labs/bedrock"
"github.com/z5labs/bedrock/pkg/config"
"github.com/z5labs/bedrock/rest"
"github.com/z5labs/bedrock/rest/endpoint"
"bytes"
"context"
_ "embed"
"log/slog"
"os"

"github.com/z5labs/bedrock"
"github.com/z5labs/bedrock/app"
"github.com/z5labs/bedrock/appbuilder"
"github.com/z5labs/bedrock/config"
)

type echoService struct{}

type EchoRequest struct {
Msg string `json:"msg"`
}

func (EchoRequest) ContentType() string {
return "application/json"
}

func (req *EchoRequest) UnmarshalBinary(b []byte) error {
return json.Unmarshal(b, req)
}

type EchoResponse struct {
Msg string `json:"msg"`
}

func (EchoResponse) ContentType() string {
return "application/json"
}
//go:embed config.yaml
var configBytes []byte

func (resp EchoResponse) MarshalBinary() ([]byte, error) {
return json.Marshal(resp)
func main() {
os.Exit(run())
}

func (echoService) Handle(ctx context.Context, req *EchoRequest) (*EchoResponse, error) {
return &EchoResponse{Msg: req.Msg}, nil
func run() int {
// bedrock does not handle process exiting for you. This is mostly
// to aid framework developers in unit testing their usages of bedrock
// by validating the returned error.
err := bedrock.Run(
context.Background(),
appbuilder.Recover(
bedrock.AppBuilderFunc[myConfig](initApp),
),
config.FromYaml(
config.RenderTextTemplate(
bytes.NewReader(configBytes),
config.TemplateFunc("env", os.Getenv),
),
),
)
if err == nil {
return 0
}
return 1
}

type Config struct {
Title string `config:"title"`
Version string `config:"version"`

Http struct {
Port uint `config:"port"`
} `config:"http"`
// myConfig can contain anything you like. The only thing you must
// remember is to always use the tag name, "config". If that tag
// name is not used then the bedrock config package will not know
// how to properly unmarshal the config source(s) into your custom
// config struct.
type myConfig struct {
Logging struct {
MinLevel slog.Level `config:"min_level"`
} `config:"logging"`
}

// here we're defining our AppBuilder as a simple function
// remember bedrock.Run handles config unmarshalling for us
// so we get to work with our custom config type, Config, directly.
func buildRestApp(ctx context.Context, cfg Config) (bedrock.App, error) {
app := rest.NewApp(
rest.ListenOn(cfg.Http.Port),
rest.Title(cfg.Title),
rest.Version(cfg.Version),
rest.Endpoint(
http.MethodPost,
"/",
endpoint.NewOperation(
echoService{},
),
),
)
return app, nil
type myApp struct {
log *slog.Logger
}

// would recommend this being a separate file you could use
// go:embed on or use a config.Source that could fetch it
// from a remote store.
var config = `{
"title": "My Example API",
"version": "v0.0.0",
"http": {
"port": 8080
// initApp is a function implementation of the bedrock.AppBuilder interface.
func initApp(ctx context.Context, cfg myConfig) (bedrock.App, error) {
var base bedrock.App = &myApp{
log: slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: cfg.Logging.MinLevel,
})),
}
}`
base = app.Recover(base)
return base, nil
}

func main() {
builder := bedrock.AppBuilderFunc[Config](buildRestApp)
// Run implements the bedrock.App interface.
func (a *myApp) Run(ctx context.Context) error {
// Do something here like:
// - run an HTTP server
// - start the AWS lambda runtime,
// - run goroutines to consume from Kafka
// etc.

// Note: Should actually handle error in your code
_ = bedrock.Run(
context.Background(),
builder,
config.FromJson(strings.NewReader(config)),
)
a.log.InfoContext(ctx, "running my app")
return nil
}
```

There you go, an entire RESTful API in less than 100 lines!

This incredibly simple example can theb easily be extended (aka made more production-ready) by leveraging a middleware
approach to the [App](https://pkg.go.dev/github.com/z5labs/bedrock#App) returned by your
[AppBuilder](https://pkg.go.dev/github.com/z5labs/bedrock#AppBuilder). Conventiently,
[bedrock](https://pkg.go.dev/github.com/z5labs/bedrock) already has a couple common middlewares
defined for your use in [package app](https://pkg.go.dev/github.com/z5labs/bedrock/pkg/app). Two
notable middleware implementations are:

- [WithOTel](https://pkg.go.dev/github.com/z5labs/bedrock/pkg/app#WithOTel) initializes
the various global [OpenTelemetry](https://opentelemetry.io/) types (e.g. TracerProvider, MeterProvider, etc.)
before executing your actual [App](https://pkg.go.dev/github.com/z5labs/bedrock#App).
- [WithSignalNotifications](https://pkg.go.dev/github.com/z5labs/bedrock/pkg/app#WithSignalNotifications)
wraps your [App](https://pkg.go.dev/github.com/z5labs/bedrock#App) and will execute it with a
"child" [context.Context](https://pkg.go.dev/context#Context) which will automatically be cancelled
if any of the provided [os.Signal](https://pkg.go.dev/os#Signal)s are received.

# Building custom frameworks with bedrock

One of the guiding principals for [bedrock](https://pkg.go.dev/github.com/z5labs/bedrock) is to be composable.
This principal comes from the experience gained from working with custom, tailor made frameworks which
over their lifetime within an organization are unable to adapt to changing
development and deployment patterns. Eventually, these frameworks are abandoned
for new ones or completely rewritten to reflect the current state of the organization.

[bedrock](https://pkg.go.dev/github.com/z5labs/bedrock) defines a small set of types and carefully
chooses its opinions to balance composability and functionality, as much as it can. The result is, in fact, a framework
that isn't necessarily designed for building services directly, but instead meant for building
more custom, use case specific frameworks.
# Built with bedrock

For example, [bedrock](https://pkg.go.dev/github.com/z5labs/bedrock) could be used by your organizations
platform engineering or framework team(s) to quickly develop internal frameworks which abstract over all of
your organizations requirements e.g. OpenTelemetry, Logging, Authenticated endpoints, etc. Then, due to the
high composibility of [bedrock](https://pkg.go.dev/github.com/z5labs/bedrock), any changes within your
organization would then be very easy to adapt to within your internal framework. A more concrete example of
how a custom framework could look like can be found in [example/custom_framework](https://github.com/z5labs/bedrock/tree/main/example/custom_framework).
- [z5labs/humus](https://github.com/z5labs/humus)

0 comments on commit fa2e9cf

Please sign in to comment.