A simple frame for quickly setting up api services based on [go-cloud](https://github.com/google/go-cloud) framework
Application objects that should be accessed fairly often in the lifecycle of an application are created as components and supplied to the service as options during initialization.
Whenever a frame service is started, a server to handle requests is supposed to be supplied at service creation. If non is supplied a default http server that at least handles http health check requests is created. Customizations can be done on the server by supplying a custom driver
Available servers include :
To create a http handler for the server, you can use gorilla mux
package main
import (
"fmt"
"github.com/gorilla/mux"
"github.com/pitabwire/frame"
"http"
)
func HomeHandler(w http.ResponseWriter, r *http.Request) {
_, _ = fmt.Fprintf(w, "Frame says yelloo!")
}
func main() {
r := mux.NewRouter()
r.HandleFunc("/", HomeHandler)
serverOption := frame.HttpHandler(r)
service := frame.NewService("Testing service", serverOption)
...
}
futher customizations can also be achieved by supplying custom implementations Of Server options. These can be related to
package main
import (
"fmt"
"github.com/pitabwire/frame"
"gocloud.dev/server/requestlog"
"os"
)
...
reqLogger := requestlog.NewNCSALogger(os.Stdout, func (e error) { _, _ = fmt.Fprintln(os.Stderr, e) })
requestLogOption := frame.HttpOptions(reqLogger)
...
You can utilize your grpc implementation within frame easily. Simply declare your implementation and supply it as an option to service at startup.
import(
"github.com/pitabwire/frame"
grpchello "google.golang.org/grpc/examples/helloworld/helloworld"
)
type grpcServer struct {
grpchello.UnimplementedGreeterServer
}
func (s *grpcServer) SayHello(ctx context.Context, in *grpchello.HelloRequest) (
*grpchello.HelloReply, error) {
return &grpchello.HelloReply{Message: "Hello " + in.Name + " from frame"}, nil
}
func main() {
grpcSrv := grpc.NewServer()
grpchello.RegisterGreeterServer(grpcSrv, &grpcServer{})
grpcServerOption := frame.GrpcServer(grpcSrv)
service := NewService("Testing Service Grpc", grpcServerOption)
...
}
Database access is via gorm by default and postgres is the database of choice. An orm allows for easier addition of multitenant constraints removal of boiler plate code such that as a developer one does not need to think about those constraints. See how Tod from AWS suggests handling multitenant architecture However there is always a performance hit taken while increasing productivity of a developer.
If you don’t need to use such features, or just dont like an orm on your path. You can always get the raw connection and suit yourself.
Creating a database component is straight forward
package main
import (
"context"
"github.com/pitabwire/frame"
)
func main() {
ctx := context.Background()
mainDbOption := frame.Datastore(ctx, "postgres://user:secret@primary_server/service_db", false)
readDbOption := frame.Datastore(ctx, "postgres://user:secret@secondary_server/service_db", true)
service := frame.NewService("Data service", mainDbOption, readDbOption)
...
}
Frame allows you to create multiple databases and specify whether they are read databases or write databases. If only one database is supplied frame will utilize it for both reads and writes.
Message queueing is essential for scalable systems as they allow the system to consume more than they can handle in an instant and reliably process the messages later.
Currently message publishing and subscription functionality requires all queues that are going to participate to be instantiated at startup. Out of the box the supported queues include: memory queue, nats io and gcp pubsub.
package main
import (
"context"
"github.com/pitabwire/frame"
)
type messageHandler struct {}
func (m *messageHandler) Handle(ctx context.Context, message []byte) error {
log.Printf(" Frame is handling messages: %v", string(message))
return nil
}
func main() {
ctx := context.Background()
opt := RegisterPublisher("test", "mem://topicA")
opt := RegisterSubscriber("test", "mem://topicA", 5, &messageHandler{} )
service := frame.NewService("Data service", mainDbOption, readDbOption)
...
}
Takes two arguments - reference that is going to be used within the app - url to the queue that will handle the message
Utilizing the registered publisher is as shown below.
...
err = srv.Publish(ctx, "test", []byte(" we are testing message publishing"))
...
Requires four input parameters
- A reference to be used within the app
- A url to the queue that will listen for new messages and process them
- Number of messages to process in parallel.
- An interface of [message handler](https://pkg.go.dev/github.com/pitabwire/frame#SubscribeWorker) to do the actual message processing
Note: For message queue managment frame takes the traditional approach of maintaining long running connections that are subscribed. We however recognize that there are superior implementations like what is done with Knative