improve shutdown and app service boilerplate
All checks were successful
Build and Publish / check-chart (push) Successful in 14s
Build and Publish / helm-release (push) Has been skipped
Build and Publish / release (push) Successful in 4m30s

This commit is contained in:
2025-07-20 18:16:12 -04:00
parent 378df897b2
commit c1c296a0c0
11 changed files with 185 additions and 98 deletions

54
pkg/demo/demo.go Normal file
View File

@ -0,0 +1,54 @@
// Package demo provides a reference implementation
// of service.AppService. It packages out the GRPC and HTTP
// functionality, for the sake of structure.
package demo
import (
"context"
optsgrpc "gitea.libretechconsulting.com/rmcguire/go-app/pkg/srv/grpc/opts"
optshttp "gitea.libretechconsulting.com/rmcguire/go-app/pkg/srv/http/opts"
"gitea.libretechconsulting.com/rmcguire/go-server-with-otel/pkg/config"
"gitea.libretechconsulting.com/rmcguire/go-server-with-otel/pkg/demo/demogrpc"
"gitea.libretechconsulting.com/rmcguire/go-server-with-otel/pkg/demo/demohttp"
"gitea.libretechconsulting.com/rmcguire/go-server-with-otel/pkg/service"
)
type DemoService struct {
ctx context.Context
config *config.ServiceConfig
http *demohttp.DemoHTTPServer
grpc *demogrpc.DemoGRPCServer
}
func (d *DemoService) Init(ctx context.Context, config *config.ServiceConfig,
) (service.ShutdownFunc, error) {
d.config = config
d.ctx = ctx
// These don't HAVE to be split into separate packages
// This is, after all, a demo app. Just implement the interface.
d.http = demohttp.NewDemoHTTPServer(ctx, config)
d.grpc = demogrpc.NewDemoGRPCServer(ctx, config)
// TODO: This should actually do shutdown stuff
return func(_ context.Context) (string, error) {
return "DemoService", nil
}, nil
}
func (d *DemoService) GetGRPC() *optsgrpc.AppGRPC {
return &optsgrpc.AppGRPC{
Services: d.grpc.GetServices(),
GRPCDialOpts: d.grpc.GetDialOpts(),
}
}
func (d *DemoService) GetHTTP() *optshttp.AppHTTP {
return &optshttp.AppHTTP{
Ctx: d.ctx,
Funcs: d.http.GetHandleFuncs(),
HealthChecks: d.http.GetHealthCheckFuncs(),
}
}

86
pkg/demo/demogrpc/demo.go Normal file
View File

@ -0,0 +1,86 @@
package demogrpc
import (
"context"
"fmt"
"github.com/go-resty/resty/v2"
"github.com/rs/zerolog"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
grpccodes "google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"gitea.libretechconsulting.com/rmcguire/go-app/pkg/otel"
pb "gitea.libretechconsulting.com/rmcguire/go-server-with-otel/api/demo/app/v1alpha1"
"gitea.libretechconsulting.com/rmcguire/go-server-with-otel/pkg/config"
)
const (
defaultFactType = config.TypeRandom
factsBaseURI = "https://uselessfacts.jsph.pl/api/v2/facts"
)
type DemoGRPCServer struct {
tracer trace.Tracer
ctx context.Context
cfg *config.ServiceConfig
client *resty.Client
pb.UnimplementedDemoAppServiceServer
}
func NewDemoGRPCServer(ctx context.Context, cfg *config.ServiceConfig) *DemoGRPCServer {
if cfg.Opts == nil {
cfg.Opts = &config.DemoOpts{}
}
if cfg.Opts.FactType == "" {
cfg.Opts.FactType = config.TypeRandom
}
return &DemoGRPCServer{
ctx: ctx,
cfg: cfg,
tracer: otel.GetTracer(ctx, "demoGRPCServer"),
client: resty.New().SetBaseURL(factsBaseURI),
}
}
func (d *DemoGRPCServer) GetDemo(ctx context.Context, req *pb.GetDemoRequest) (
*pb.GetDemoResponse, error,
) {
ctx, span := d.tracer.Start(ctx, "getDemo", trace.WithAttributes(
attribute.String("language", req.GetLanguage()),
))
defer span.End()
r := d.client.R().SetContext(ctx)
if req.GetLanguage() != "" {
r = r.SetQueryParam("language", req.GetLanguage())
}
fact := new(RandomFact)
r.SetResult(fact)
resp, err := r.Get(fmt.Sprintf("/%s", d.cfg.Opts.FactType))
if err != nil {
span.SetStatus(codes.Error, err.Error())
span.RecordError(err)
zerolog.Ctx(d.ctx).Err(err).Send()
return nil, status.Errorf(grpccodes.Unknown, "%s: %s",
err.Error(), string(resp.Body()))
}
span.SetAttributes(attribute.String("fact", fact.Text))
span.SetStatus(codes.Ok, "")
zerolog.Ctx(d.ctx).Debug().Str("fact", fact.Text).Msg("retrieved fact")
return fact.FactToProto(), nil
}

25
pkg/demo/demogrpc/fact.go Normal file
View File

@ -0,0 +1,25 @@
package demogrpc
import (
"google.golang.org/protobuf/types/known/timestamppb"
pb "gitea.libretechconsulting.com/rmcguire/go-server-with-otel/api/demo/app/v1alpha1"
)
type RandomFact struct {
ID string `json:"id,omitempty"`
Text string `json:"text,omitempty"`
Source string `json:"source,omitempty"`
SourceURL string `json:"source_url,omitempty"`
Language string `json:"language,omitempty"`
Permalink string `json:"permalink,omitempty"`
}
func (f *RandomFact) FactToProto() *pb.GetDemoResponse {
return &pb.GetDemoResponse{
Timestamp: timestamppb.Now(),
Fact: f.Text,
Source: f.SourceURL,
Language: f.Language,
}
}

View File

@ -0,0 +1,30 @@
package demogrpc
import (
"gitea.libretechconsulting.com/rmcguire/go-app/pkg/srv/grpc/opts"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
demoAppPb "gitea.libretechconsulting.com/rmcguire/go-server-with-otel/api/demo/app/v1alpha1"
)
func (ds *DemoGRPCServer) GetDialOpts() []grpc.DialOption {
return []grpc.DialOption{
// NOTE: Necessary for grpc-gateway to connect to grpc server
// Update if grpc service has credentials, tls, etc..
grpc.WithTransportCredentials(insecure.NewCredentials()),
}
}
func (ds *DemoGRPCServer) GetServices() []*opts.GRPCService {
return []*opts.GRPCService{
{
Name: "Demo App",
Type: &demoAppPb.DemoAppService_ServiceDesc,
Service: ds,
GwRegistrationFuncs: []opts.GwRegistrationFunc{
demoAppPb.RegisterDemoAppServiceHandler,
},
},
}
}

View File

@ -0,0 +1,44 @@
package demohttp
import (
"context"
"net/http"
"gitea.libretechconsulting.com/rmcguire/go-app/pkg/srv/http/opts"
"gitea.libretechconsulting.com/rmcguire/go-server-with-otel/pkg/config"
)
type DemoHTTPServer struct {
ctx context.Context
cfg *config.ServiceConfig
}
func NewDemoHTTPServer(ctx context.Context, cfg *config.ServiceConfig) *DemoHTTPServer {
return &DemoHTTPServer{
ctx: ctx,
cfg: cfg,
}
}
func (dh *DemoHTTPServer) GetHandleFuncs() []opts.HTTPFunc {
// TODO: Implement real http handle funcs
return []opts.HTTPFunc{
{
Path: "/test",
HandlerFunc: func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotImplemented)
w.Write([]byte("unimplemented demo app"))
},
},
}
}
func (dh *DemoHTTPServer) GetHealthCheckFuncs() []opts.HealthCheckFunc {
return []opts.HealthCheckFunc{
func(ctx context.Context) error {
// TODO: Implement real health checks
return nil
},
}
}