diff --git a/go.mod b/go.mod index 6f99d22..85cc0b4 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.1 github.com/prometheus/client_golang v1.21.1 github.com/rs/zerolog v1.33.0 + github.com/swaggest/jsonschema-go v0.3.73 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 go.opentelemetry.io/otel v1.35.0 @@ -39,6 +40,7 @@ require ( github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect + github.com/swaggest/refl v1.3.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect diff --git a/go.sum b/go.sum index 8e723a9..9285923 100644 --- a/go.sum +++ b/go.sum @@ -75,6 +75,10 @@ github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/swaggest/jsonschema-go v0.3.73 h1:gU1pBzF3pkZ1GDD3dRMdQoCjrA0sldJ+QcM7aSSPgvc= +github.com/swaggest/jsonschema-go v0.3.73/go.mod h1:qp+Ym2DIXHlHzch3HKz50gPf2wJhKOrAB/VYqLS2oJU= +github.com/swaggest/refl v1.3.0 h1:PEUWIku+ZznYfsoyheF97ypSduvMApYyGkYF3nabS0I= +github.com/swaggest/refl v1.3.0/go.mod h1:3Ujvbmh1pfSbDYjC6JGG7nMgPvpG0ehQL4iNonnLNbg= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= diff --git a/pkg/app/schema.go b/pkg/app/schema.go new file mode 100644 index 0000000..6508565 --- /dev/null +++ b/pkg/app/schema.go @@ -0,0 +1,40 @@ +package app + +import ( + "encoding/json" + + js "github.com/swaggest/jsonschema-go" +) + +// Generates json schema for app's config.AppConfig +func (app *App) Schema() ([]byte, error) { + r := js.Reflector{} + + s, err := r.Reflect(*app.cfg) + if err != nil { + return nil, err + } + + return json.MarshalIndent(s, "", " ") +} + +// Generates json schema for custom config +// which embeds *config.AppConfig into it +// Panics if no *config.AppConfig is embedded into custom +// config type +// +// See swaggest/jsonschema-go for struct tag docs +func CustomSchema[T any](target T) ([]byte, error) { + if err := HasAppConfig(target); err != nil { + panic(err.Error()) + } + + r := js.Reflector{} + + s, err := r.Reflect(target) + if err != nil { + return nil, err + } + + return json.MarshalIndent(s, "", " ") +} diff --git a/pkg/app/setup_custom.go b/pkg/app/setup_custom.go index 5f8e258..ca5b882 100644 --- a/pkg/app/setup_custom.go +++ b/pkg/app/setup_custom.go @@ -19,7 +19,7 @@ import ( // are up to the caller. func MustLoadConfigInto[T any](ctx context.Context, into T) (context.Context, T) { // Step 1: Check our custom type for required *config.AppConfig - if err := hasAppConfig(into); err != nil { + if err := HasAppConfig(into); err != nil { panic(err) } @@ -66,7 +66,7 @@ func setAppConfig[T any](target T, appConfig *config.AppConfig) error { } // Replace *config.AppConfig - for i := 0; i < v.NumField(); i++ { + for i := range v.NumField() { field := v.Field(i) if field.Type() == reflect.TypeOf((*config.AppConfig)(nil)) { if !field.CanSet() { @@ -80,7 +80,7 @@ func setAppConfig[T any](target T, appConfig *config.AppConfig) error { return errors.New("no *config.AppConfig field found in target struct") } -func hasAppConfig[T any](target T) error { +func HasAppConfig[T any](target T) error { v := reflect.ValueOf(target) if v.Kind() != reflect.Ptr || v.IsNil() { return errors.New("target must be a non-nil pointer to a struct") @@ -92,7 +92,7 @@ func hasAppConfig[T any](target T) error { } hasAppConfig := false - for i := 0; i < v.NumField(); i++ { + for i := range v.NumField() { field := v.Type().Field(i) if field.Type == reflect.TypeOf((*config.AppConfig)(nil)) { hasAppConfig = true diff --git a/pkg/config/types.go b/pkg/config/types.go index fa940dc..f0ec01e 100644 --- a/pkg/config/types.go +++ b/pkg/config/types.go @@ -11,16 +11,16 @@ var DefaultConfig = &AppConfig{ } type AppConfig struct { - Name string `yaml:"name,omitempty" env:"APP_NAME"` - Environment string `yaml:"environment,omitempty" env:"APP_ENVIRONMENT"` + Name string `yaml:"name,omitempty" env:"APP_NAME" json:"name,omitempty"` + Environment string `yaml:"environment,omitempty" env:"APP_ENVIRONMENT" json:"environment,omitempty"` // This should either be set by ldflags, such as with // go build -ldflags "-X gitea.libretechconsulting.com/rmcguire/go-app/pkg/config.Version=$(VERSION)" // or allow this to use build meta. Will default to (devel) - Version string `yaml:"version,omitempty" env:"APP_VERSION"` - Logging *LogConfig `yaml:"logging,omitempty"` - HTTP *HTTPConfig `yaml:"http,omitempty"` - OTEL *OTELConfig `yaml:"otel,omitempty"` - GRPC *GRPCConfig `yaml:"grpc,omitempty"` + Version string `yaml:"version,omitempty" env:"APP_VERSION" json:"version,omitempty"` + Logging *LogConfig `yaml:"logging,omitempty" json:"logging,omitempty"` + HTTP *HTTPConfig `yaml:"http,omitempty" json:"http,omitempty"` + OTEL *OTELConfig `yaml:"otel,omitempty" json:"otel,omitempty"` + GRPC *GRPCConfig `yaml:"grpc,omitempty" json:"grpc,omitempty"` } func (ac *AppConfig) HTTPEnabled() bool { diff --git a/pkg/config/types_grpc.go b/pkg/config/types_grpc.go index 4385510..bf19310 100644 --- a/pkg/config/types_grpc.go +++ b/pkg/config/types_grpc.go @@ -10,9 +10,9 @@ var defaultGRPCConfig = &GRPCConfig{ } type GRPCConfig struct { - Enabled bool `yaml:"enabled" env:"APP_GRPC_ENABLED"` - 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 + Enabled bool `yaml:"enabled" env:"APP_GRPC_ENABLED" json:"enabled,omitempty"` + Listen string `yaml:"listen" env:"APP_GRPC_LISTEN" json:"listen,omitempty"` + LogRequests bool `yaml:"logRequests" env:"APP_GRPC_LOG_REQUESTS" json:"logRequests,omitempty"` + EnableReflection bool `yaml:"enableReflection" env:"APP_GRPC_ENABLE_REFLECTION" json:"enableReflection,omitempty"` + EnableInstrumentation bool `yaml:"enableInstrumentation" env:"APP_GRPC_ENABLE_INSTRUMENTATION" json:"enableInstrumentation,omitempty"` // requires OTEL } diff --git a/pkg/config/types_http.go b/pkg/config/types_http.go index a64944d..a453a58 100644 --- a/pkg/config/types_http.go +++ b/pkg/config/types_http.go @@ -14,13 +14,13 @@ var defaultHTTPConfig = &HTTPConfig{ // HTTP Configuration type HTTPConfig struct { - Enabled bool `yaml:"enabled" env:"APP_HTTP_ENABLED"` - 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 `yaml:"rT" env:"rT"` - wT *time.Duration `yaml:"wT" env:"wT"` - iT *time.Duration `yaml:"iT" env:"iT"` + Enabled bool `yaml:"enabled" env:"APP_HTTP_ENABLED" json:"enabled,omitempty"` + Listen string `yaml:"listen,omitempty" env:"APP_HTTP_LISTEN" json:"listen,omitempty"` + LogRequests bool `yaml:"logRequests" env:"APP_HTTP_LOG_REQUESTS" json:"logRequests,omitempty"` + ReadTimeout string `yaml:"readTimeout" env:"APP_HTTP_READ_TIMEOUT" json:"readTimeout,omitempty"` // Go duration (e.g. 10s) + WriteTimeout string `yaml:"writeTimeout" env:"APP_HTTP_WRITE_TIMEOUT" json:"writeTimeout,omitempty"` // Go duration (e.g. 10s) + IdleTimeout string `yaml:"idleTimeout" env:"APP_HTTP_IDLE_TIMEOUT" json:"idleTimeout,omitempty"` // 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 index d939943..605fcb1 100644 --- a/pkg/config/types_logging.go +++ b/pkg/config/types_logging.go @@ -10,11 +10,11 @@ var defaultLoggingConfig = &LogConfig{ // 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"` + Enabled bool `yaml:"enabled,omitempty" env:"APP_LOG_ENABLED" json:"enabled,omitempty"` + Level string `yaml:"level,omitempty" env:"APP_LOG_LEVEL" json:"level,omitempty"` + Format LogFormat `yaml:"format,omitempty" env:"APP_LOG_FORMAT" json:"format,omitempty"` + Output LogOutput `yaml:"output,omitempty" env:"APP_LOG_OUTPUT" json:"output,omitempty"` + TimeFormat TimeFormat `yaml:"timeFormat,omitempty" env:"APP_LOG_TIME_FORMAT" json:"timeFormat,omitempty"` } type LogFormat string diff --git a/pkg/config/types_otel.go b/pkg/config/types_otel.go index a8cfe96..458d7de 100644 --- a/pkg/config/types_otel.go +++ b/pkg/config/types_otel.go @@ -10,9 +10,9 @@ var defaultOTELConfig = &OTELConfig{ // 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"` + Enabled bool `yaml:"enabled,omitempty" env:"APP_OTEL_ENABLED" json:"enabled,omitempty"` + PrometheusEnabled bool `yaml:"prometheusEnabled,omitempty" env:"APP_OTEL_PROMETHEUS_ENABLED" json:"prometheusEnabled,omitempty"` + PrometheusPath string `yaml:"prometheusPath,omitempty" env:"APP_OTEL_PROMETHEUS_PATH" json:"prometheusPath,omitempty"` + StdoutEnabled bool `yaml:"stdoutEnabled,omitempty" env:"APP_OTEL_STDOUT_ENABLED" json:"stdoutEnabled,omitempty"` + MetricIntervalSecs int `yaml:"metricIntervalSecs,omitempty" env:"APP_OTEL_METRIC_INTERVAL_SECS" json:"metricIntervalSecs,omitempty"` } diff --git a/pkg/logging/logging.go b/pkg/logging/logging.go index 2c8b047..4ff82a2 100644 --- a/pkg/logging/logging.go +++ b/pkg/logging/logging.go @@ -38,7 +38,7 @@ func configureLogger(cfg *config.LogConfig) (*zerolog.Logger, error) { TimeFormat: zerolog.TimeFieldFormat, } if cfg.TimeFormat == config.TimeFormatOff { - consoleWriter.FormatTimestamp = func(_ interface{}) string { + consoleWriter.FormatTimestamp = func(_ any) string { return "" } }