From e11c563c3a9c96ee8e164200b7ac92f7d327676c Mon Sep 17 00:00:00 2001 From: Ryan McGuire Date: Thu, 6 Mar 2025 17:16:27 -0500 Subject: [PATCH] add grpc support --- TODO.md | 4 +- pkg/app/app.go | 66 ++++++----------------------- pkg/app/app_init.go | 55 +++++++++++++++++++++++++ pkg/app/app_types.go | 47 +++++++++++++++++++++ pkg/config/types.go | 82 +++---------------------------------- pkg/config/types_grpc.go | 14 +++++++ pkg/config/types_http.go | 23 +++++++++++ pkg/config/types_logging.go | 42 +++++++++++++++++++ pkg/config/types_otel.go | 18 ++++++++ pkg/srv/http.go | 16 +++----- 10 files changed, 225 insertions(+), 142 deletions(-) create mode 100644 pkg/app/app_init.go create mode 100644 pkg/app/app_types.go create mode 100644 pkg/config/types_grpc.go create mode 100644 pkg/config/types_http.go create mode 100644 pkg/config/types_logging.go create mode 100644 pkg/config/types_otel.go diff --git a/TODO.md b/TODO.md index 6ca7f30..fcd15a3 100644 --- a/TODO.md +++ b/TODO.md @@ -1,8 +1,10 @@ # TODO -- [x] Pattern for extending config +- [ ] Finish implementing GRPC service support +- [ ] Expand tracing ## Done - [x] Unit tests +- [x] Pattern for extending config - [x] HTTP Logging Middleware - [x] Fix panic with OTEL disabled diff --git a/pkg/app/app.go b/pkg/app/app.go index 8ca8142..6456794 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -1,42 +1,15 @@ package app import ( - "context" "errors" - "net/http" "github.com/rs/zerolog" "go.opentelemetry.io/otel/codes" - "go.opentelemetry.io/otel/trace" "gitea.libretechconsulting.com/rmcguire/go-app/pkg/config" - "gitea.libretechconsulting.com/rmcguire/go-app/pkg/otel" - "gitea.libretechconsulting.com/rmcguire/go-app/pkg/srv" ) -type App struct { - AppContext context.Context - HTTP *AppHTTP - cfg *config.AppConfig - l *zerolog.Logger - tracer trace.Tracer - shutdownFuncs []shutdownFunc - appDone chan interface{} -} - -type AppHTTP struct { - Funcs []srv.HTTPFunc - Middleware []http.Handler - HealthChecks []srv.HealthCheckFunc - httpDone <-chan interface{} -} - -type ( - healthCheckFunc func(context.Context) error - shutdownFunc func(context.Context) error -) - -func (a *App) Done() <-chan interface{} { +func (a *App) Done() <-chan any { return a.appDone } @@ -49,8 +22,8 @@ func (a *App) MustRun() { a.cfg = config.MustFromCtx(a.AppContext) a.l = zerolog.Ctx(a.AppContext) a.shutdownFuncs = make([]shutdownFunc, 0) - a.appDone = make(chan interface{}) - a.HTTP.httpDone = make(chan interface{}) + a.appDone = make(chan any) + a.HTTP.httpDone = make(chan any) if len(a.HTTP.Funcs) < 1 { a.l.Warn().Msg("no http funcs provided, only serving health and metrics") @@ -58,11 +31,17 @@ func (a *App) MustRun() { // Start OTEL a.initOTEL() - var initSpan trace.Span - _, initSpan = a.tracer.Start(a.AppContext, "init") + ctx, initSpan := a.tracer.Start(a.AppContext, "init") + defer initSpan.End() // Start HTTP - a.initHTTP() + if err := a.initHTTP(ctx); err != nil { + initSpan.RecordError(err) + initSpan.SetStatus(codes.Error, err.Error()) + } + + // Start GRPC + a.initGRPC() // Monitor app lifecycle go a.run() @@ -74,25 +53,4 @@ func (a *App) MustRun() { Str("logLevel", a.cfg.Logging.Level). Msg("app initialized") initSpan.SetStatus(codes.Ok, "") - initSpan.End() -} - -func (a *App) initHTTP() { - var httpShutdown shutdownFunc - httpShutdown, a.HTTP.httpDone = srv.MustInitHTTPServer( - &srv.HTTPServerOpts{ - Ctx: a.AppContext, - HandleFuncs: a.HTTP.Funcs, - Middleware: a.HTTP.Middleware, - HealthCheckFuncs: a.HTTP.HealthChecks, - }, - ) - a.shutdownFuncs = append(a.shutdownFuncs, httpShutdown) -} - -func (a *App) initOTEL() { - var otelShutdown shutdownFunc - a.AppContext, otelShutdown = otel.Init(a.AppContext) - a.shutdownFuncs = append(a.shutdownFuncs, otelShutdown) - a.tracer = otel.MustTracerFromCtx(a.AppContext) } diff --git a/pkg/app/app_init.go b/pkg/app/app_init.go new file mode 100644 index 0000000..5872d98 --- /dev/null +++ b/pkg/app/app_init.go @@ -0,0 +1,55 @@ +package app + +import ( + "context" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + + "gitea.libretechconsulting.com/rmcguire/go-app/pkg/otel" + "gitea.libretechconsulting.com/rmcguire/go-app/pkg/srv" +) + +func (a *App) initGRPC() { +} + +func (a *App) initHTTP(ctx context.Context) error { + var err error + var httpShutdown shutdownFunc + + _, span := a.tracer.Start(ctx, "init.http") + defer span.End() + + span.SetAttributes( + attribute.Int("numHTTPFuncs", len(a.HTTP.Funcs)), + attribute.Int("numHTTPMiddlewares", len(a.HTTP.Middleware)), + attribute.Int("numHTTPHealthChecks", len(a.HTTP.HealthChecks)), + ) + + httpShutdown, a.HTTP.httpDone, err = srv.InitHTTPServer( + &srv.HTTPServerOpts{ + Ctx: a.AppContext, + HandleFuncs: a.HTTP.Funcs, + Middleware: a.HTTP.Middleware, + HealthCheckFuncs: a.HTTP.HealthChecks, + }, + ) + + a.shutdownFuncs = append(a.shutdownFuncs, httpShutdown) + + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } else { + span.SetStatus(codes.Ok, "") + } + + return err +} + +func (a *App) initOTEL() { + var otelShutdown shutdownFunc + a.AppContext, otelShutdown = otel.Init(a.AppContext) + a.shutdownFuncs = append(a.shutdownFuncs, otelShutdown) + a.tracer = otel.MustTracerFromCtx(a.AppContext) +} diff --git a/pkg/app/app_types.go b/pkg/app/app_types.go new file mode 100644 index 0000000..3205d3b --- /dev/null +++ b/pkg/app/app_types.go @@ -0,0 +1,47 @@ +package app + +import ( + "context" + "net/http" + + "github.com/rs/zerolog" + "go.opentelemetry.io/otel/trace" + "google.golang.org/grpc" + + "gitea.libretechconsulting.com/rmcguire/go-app/pkg/config" + "gitea.libretechconsulting.com/rmcguire/go-app/pkg/srv" +) + +type App struct { + AppContext context.Context + HTTP *AppHTTP + GRPC *AppGRPC + cfg *config.AppConfig + l *zerolog.Logger + tracer trace.Tracer + shutdownFuncs []shutdownFunc + appDone chan any +} + +type AppGRPC struct { + Services []*GRPCService + GRPCOpts []grpc.ServerOption +} + +type GRPCService struct { + Name string // Descriptive name of service + Type *grpc.ServiceDesc // Type (from protoc generated code) + Service any // Implementation of GRPCService.Type (ptr) +} + +type AppHTTP struct { + Funcs []srv.HTTPFunc + Middleware []http.Handler + HealthChecks []srv.HealthCheckFunc + httpDone <-chan any +} + +type ( + healthCheckFunc func(context.Context) error + shutdownFunc func(context.Context) error +) diff --git a/pkg/config/types.go b/pkg/config/types.go index 907e9c1..d227302 100644 --- a/pkg/config/types.go +++ b/pkg/config/types.go @@ -1,32 +1,13 @@ package config -import "time" - // Default Settings var DefaultConfig = &AppConfig{ Environment: "development", Version: getVersion(), - Logging: &LogConfig{ - Enabled: true, - Level: "info", - Format: LogFormatJSON, - Output: "stderr", - TimeFormat: TimeFormatLong, - }, - HTTP: &HTTPConfig{ - Listen: "127.0.0.1:8080", - LogRequests: false, - ReadTimeout: "10s", - WriteTimeout: "10s", - IdleTimeout: "1m", - }, - OTEL: &OTELConfig{ - Enabled: true, - PrometheusEnabled: true, - PrometheusPath: "/metrics", - StdoutEnabled: false, - MetricIntervalSecs: 30, - }, + Logging: defaultLoggingConfig, + HTTP: defaultHTTPConfig, + OTEL: defaultOTELConfig, + GRPC: defaultGRPCConfig, } type AppConfig struct { @@ -39,58 +20,5 @@ type AppConfig struct { Logging *LogConfig `yaml:"logging,omitempty"` HTTP *HTTPConfig `yaml:"http,omitempty"` OTEL *OTELConfig `yaml:"otel,omitempty"` -} - -// Logging Configuration -type LogConfig struct { - Enabled bool `yaml:"enabled,omitempty" env:"APP_LOG_ENABLED"` - Level string `yaml:"level,omitempty" env:"APP_LOG_LEVEL"` - Format LogFormat `yaml:"format,omitempty" env:"APP_LOG_FORMAT"` - Output LogOutput `yaml:"output,omitempty" env:"APP_LOG_OUTPUT"` - TimeFormat TimeFormat `yaml:"timeFormat,omitempty" env:"APP_LOG_TIME_FORMAT"` -} - -type LogFormat string - -const ( - LogFormatConsole LogFormat = "console" - LogFormatJSON LogFormat = "json" -) - -type TimeFormat string - -const ( - TimeFormatShort TimeFormat = "short" - TimeFormatLong TimeFormat = "long" - TimeFormatUnix TimeFormat = "unix" - TimeFormatRFC3339 TimeFormat = "rfc3339" - TimeFormatOff TimeFormat = "off" -) - -type LogOutput string - -const ( - LogOutputStdout LogOutput = "stdout" - LogOutputStderr LogOutput = "stderr" -) - -// HTTP Configuration -type HTTPConfig struct { - Listen string `yaml:"listen,omitempty" env:"APP_HTTP_LISTEN"` - LogRequests bool `yaml:"logRequests" env:"APP_HTTP_LOG_REQUESTS"` - ReadTimeout string `yaml:"readTimeout" env:"APP_HTTP_READ_TIMEOUT"` // Go duration (e.g. 10s) - WriteTimeout string `yaml:"writeTimeout" env:"APP_HTTP_WRITE_TIMEOUT"` // Go duration (e.g. 10s) - IdleTimeout string `yaml:"idleTimeout" env:"APP_HTTP_IDLE_TIMEOUT"` // Go duration (e.g. 10s) - rT *time.Duration - wT *time.Duration - iT *time.Duration -} - -// OTEL Configuration -type OTELConfig struct { - Enabled bool `yaml:"enabled,omitempty" env:"APP_OTEL_ENABLED"` - PrometheusEnabled bool `yaml:"prometheusEnabled,omitempty" env:"APP_OTEL_PROMETHEUS_ENABLED"` - PrometheusPath string `yaml:"prometheusPath,omitempty" env:"APP_OTEL_PROMETHEUS_PATH"` - StdoutEnabled bool `yaml:"stdoutEnabled,omitempty" env:"APP_OTEL_STDOUT_ENABLED"` - MetricIntervalSecs int `yaml:"metricIntervalSecs,omitempty" env:"APP_OTEL_METRIC_INTERVAL_SECS"` + GRPC *GRPCConfig `yaml:"grpc,omitempty"` } diff --git a/pkg/config/types_grpc.go b/pkg/config/types_grpc.go new file mode 100644 index 0000000..104f333 --- /dev/null +++ b/pkg/config/types_grpc.go @@ -0,0 +1,14 @@ +package config + +var defaultGRPCConfig = &GRPCConfig{ + LogRequests: false, + EnableReflection: true, + EnableInstrumentation: true, +} + +type GRPCConfig struct { + Listen string `yaml:"listen" env:"APP_GRPC_LISTEN"` + LogRequests bool `yaml:"logRequests" env:"APP_GRPC_LOG_REQUESTS"` + EnableReflection bool `yaml:"enableReflection" env:"APP_GRPC_ENABLE_REFLECTION"` + EnableInstrumentation bool `yaml:"enableInstrumentation" env:"APP_GRPC_ENABLE_INSTRUMENTATION"` // requires OTEL +} diff --git a/pkg/config/types_http.go b/pkg/config/types_http.go new file mode 100644 index 0000000..3fd2b93 --- /dev/null +++ b/pkg/config/types_http.go @@ -0,0 +1,23 @@ +package config + +import "time" + +var defaultHTTPConfig = &HTTPConfig{ + Listen: "127.0.0.1:8080", + LogRequests: false, + ReadTimeout: "10s", + WriteTimeout: "10s", + IdleTimeout: "1m", +} + +// HTTP Configuration +type HTTPConfig struct { + Listen string `yaml:"listen,omitempty" env:"APP_HTTP_LISTEN"` + LogRequests bool `yaml:"logRequests" env:"APP_HTTP_LOG_REQUESTS"` + ReadTimeout string `yaml:"readTimeout" env:"APP_HTTP_READ_TIMEOUT"` // Go duration (e.g. 10s) + WriteTimeout string `yaml:"writeTimeout" env:"APP_HTTP_WRITE_TIMEOUT"` // Go duration (e.g. 10s) + IdleTimeout string `yaml:"idleTimeout" env:"APP_HTTP_IDLE_TIMEOUT"` // Go duration (e.g. 10s) + rT *time.Duration + wT *time.Duration + iT *time.Duration +} diff --git a/pkg/config/types_logging.go b/pkg/config/types_logging.go new file mode 100644 index 0000000..d939943 --- /dev/null +++ b/pkg/config/types_logging.go @@ -0,0 +1,42 @@ +package config + +var defaultLoggingConfig = &LogConfig{ + Enabled: true, + Level: "info", + Format: LogFormatJSON, + Output: "stderr", + TimeFormat: TimeFormatLong, +} + +// Logging Configuration +type LogConfig struct { + Enabled bool `yaml:"enabled,omitempty" env:"APP_LOG_ENABLED"` + Level string `yaml:"level,omitempty" env:"APP_LOG_LEVEL"` + Format LogFormat `yaml:"format,omitempty" env:"APP_LOG_FORMAT"` + Output LogOutput `yaml:"output,omitempty" env:"APP_LOG_OUTPUT"` + TimeFormat TimeFormat `yaml:"timeFormat,omitempty" env:"APP_LOG_TIME_FORMAT"` +} + +type LogFormat string + +const ( + LogFormatConsole LogFormat = "console" + LogFormatJSON LogFormat = "json" +) + +type TimeFormat string + +const ( + TimeFormatShort TimeFormat = "short" + TimeFormatLong TimeFormat = "long" + TimeFormatUnix TimeFormat = "unix" + TimeFormatRFC3339 TimeFormat = "rfc3339" + TimeFormatOff TimeFormat = "off" +) + +type LogOutput string + +const ( + LogOutputStdout LogOutput = "stdout" + LogOutputStderr LogOutput = "stderr" +) diff --git a/pkg/config/types_otel.go b/pkg/config/types_otel.go new file mode 100644 index 0000000..a8cfe96 --- /dev/null +++ b/pkg/config/types_otel.go @@ -0,0 +1,18 @@ +package config + +var defaultOTELConfig = &OTELConfig{ + Enabled: true, + PrometheusEnabled: true, + PrometheusPath: "/metrics", + StdoutEnabled: false, + MetricIntervalSecs: 30, +} + +// OTEL Configuration +type OTELConfig struct { + Enabled bool `yaml:"enabled,omitempty" env:"APP_OTEL_ENABLED"` + PrometheusEnabled bool `yaml:"prometheusEnabled,omitempty" env:"APP_OTEL_PROMETHEUS_ENABLED"` + PrometheusPath string `yaml:"prometheusPath,omitempty" env:"APP_OTEL_PROMETHEUS_PATH"` + StdoutEnabled bool `yaml:"stdoutEnabled,omitempty" env:"APP_OTEL_STDOUT_ENABLED"` + MetricIntervalSecs int `yaml:"metricIntervalSecs,omitempty" env:"APP_OTEL_METRIC_INTERVAL_SECS"` +} diff --git a/pkg/srv/http.go b/pkg/srv/http.go index 8f58ef4..b81dd81 100644 --- a/pkg/srv/http.go +++ b/pkg/srv/http.go @@ -129,23 +129,19 @@ func prepHTTPServer(opts *HTTPServerOpts) *http.Server { // Returns a shutdown func and a done channel if the // server aborts abnormally. Panics on error. -func MustInitHTTPServer(opts *HTTPServerOpts) ( - func(context.Context) error, <-chan interface{}, +func InitHTTPServer(opts *HTTPServerOpts) ( + func(context.Context) error, <-chan any, error, ) { - shutdownFunc, doneChan, err := InitHTTPServer(opts) - if err != nil { - panic(err) - } - return shutdownFunc, doneChan + return initHTTPServer(opts) } // Returns a shutdown func and a done channel if the // server aborts abnormally. Returns error on failure to start -func InitHTTPServer(opts *HTTPServerOpts) ( - func(context.Context) error, <-chan interface{}, error, +func initHTTPServer(opts *HTTPServerOpts) ( + func(context.Context) error, <-chan any, error, ) { l := zerolog.Ctx(opts.Ctx) - doneChan := make(chan interface{}) + doneChan := make(chan any) var server *http.Server