diff --git a/closer.go b/closer.go new file mode 100644 index 0000000..fe583bd --- /dev/null +++ b/closer.go @@ -0,0 +1,29 @@ +package keel + +import ( + "github.com/foomo/keel/interfaces" +) + +func IsCloser(v any) bool { + switch v.(type) { + case interfaces.Closer, + interfaces.ErrorCloser, + interfaces.CloserWithContext, + interfaces.ErrorCloserWithContext, + interfaces.Shutdowner, + interfaces.ErrorShutdowner, + interfaces.ShutdownerWithContext, + interfaces.ErrorShutdownerWithContext, + interfaces.Stopper, + interfaces.ErrorStopper, + interfaces.StopperWithContext, + interfaces.ErrorStopperWithContext, + interfaces.Unsubscriber, + interfaces.ErrorUnsubscriber, + interfaces.UnsubscriberWithContext, + interfaces.ErrorUnsubscriberWithContext: + return true + default: + return false + } +} diff --git a/config/readme.go b/config/readme.go new file mode 100644 index 0000000..22bbc12 --- /dev/null +++ b/config/readme.go @@ -0,0 +1,71 @@ +package config + +import ( + "fmt" + + "github.com/foomo/keel/markdown" +) + +func Readme() string { + var configRows [][]string + var remoteRows [][]string + c := Config() + md := &markdown.Markdown{} + + { + keys := c.AllKeys() + for _, key := range keys { + var fallback interface{} + if v, ok := defaults[key]; ok { + fallback = v + } + configRows = append(configRows, []string{ + markdown.Code(key), + markdown.Code(TypeOf(key)), + "", + markdown.Code(fmt.Sprintf("%v", fallback)), + }) + } + + for _, key := range requiredKeys { + configRows = append(configRows, []string{ + markdown.Code(key), + markdown.Code(TypeOf(key)), + markdown.Code("true"), + "", + }) + } + } + + { + for _, remote := range remotes { + remoteRows = append(remoteRows, []string{ + markdown.Code(remote.provider), + markdown.Code(remote.path), + }) + } + } + + if len(configRows) > 0 || len(remoteRows) > 0 { + md.Println("### Config") + md.Println("") + } + + if len(configRows) > 0 { + md.Println("List of all registered config variabled with their defaults.") + md.Println("") + md.Table([]string{"Key", "Type", "Required", "Default"}, configRows) + md.Println("") + } + + if len(remoteRows) > 0 { + md.Println("#### Remotes") + md.Println("") + md.Println("List of remote config providers that are being watched.") + md.Println("") + md.Table([]string{"Provider", "Path"}, remoteRows) + md.Println("") + } + + return md.String() +} diff --git a/config/remote.go b/config/remote.go index 462c13c..ab85c67 100644 --- a/config/remote.go +++ b/config/remote.go @@ -6,6 +6,12 @@ import ( _ "github.com/spf13/viper/remote" ) +var remotes []struct { + provider string + endpoint string + path string +} + func WithRemoteConfig(c *viper.Viper, provider, endpoint string, path string) error { if err := c.AddRemoteProvider(provider, endpoint, path); err != nil { return err @@ -19,5 +25,11 @@ func WithRemoteConfig(c *viper.Viper, provider, endpoint string, path string) er return errors.Wrap(err, "failed to watch remote config") } + remotes = append(remotes, struct { + provider string + endpoint string + path string + }{provider: provider, endpoint: endpoint, path: path}) + return nil } diff --git a/env/env.go b/env/env.go index 1fabba3..31b5c93 100644 --- a/env/env.go +++ b/env/env.go @@ -3,10 +3,17 @@ package env import ( "fmt" "os" + "slices" "strconv" "strings" ) +var ( + defaults = map[string]interface{}{} + requiredKeys []string + types = map[string]string{} +) + // Exists return true if env var is defined func Exists(key string) bool { _, ok := os.LookupEnv(key) @@ -15,13 +22,20 @@ func Exists(key string) bool { // MustExists panics if not exists func MustExists(key string) { - if _, ok := os.LookupEnv(key); !ok { - panic(fmt.Sprintf("required environment variable %s does not exist", key)) + if !Exists(key) { + panic(fmt.Sprintf("required environment variable `%s` does not exist", key)) + } + if !slices.Contains(requiredKeys, key) { + requiredKeys = append(requiredKeys, key) } } // Get env var or fallback func Get(key, fallback string) string { + defaults[key] = fallback + if _, ok := types[key]; !ok { + types[key] = "string" + } if v, ok := os.LookupEnv(key); ok { return v } @@ -36,6 +50,9 @@ func MustGet(key string) string { // GetInt env var or fallback as int func GetInt(key string, fallback int) int { + if _, ok := types[key]; !ok { + types[key] = "int" + } if value, err := strconv.Atoi(Get(key, "")); err == nil { return value } @@ -50,6 +67,9 @@ func MustGetInt(key string) int { // GetInt64 env var or fallback as int64 func GetInt64(key string, fallback int64) int64 { + if _, ok := types[key]; !ok { + types[key] = "int64" + } if value, err := strconv.ParseInt(Get(key, ""), 10, 64); err == nil { return value } @@ -64,6 +84,9 @@ func MustGetInt64(key string) int64 { // GetFloat64 env var or fallback as float64 func GetFloat64(key string, fallback float64) float64 { + if _, ok := types[key]; !ok { + types[key] = "float64" + } if value, err := strconv.ParseFloat(Get(key, ""), 64); err == nil { return value } @@ -78,6 +101,9 @@ func MustGetFloat64(key string) float64 { // GetBool env var or fallback as bool func GetBool(key string, fallback bool) bool { + if _, ok := types[key]; !ok { + types[key] = "bool" + } if val, err := strconv.ParseBool(Get(key, "")); err == nil { return val } @@ -92,6 +118,9 @@ func MustGetBool(key string) bool { // GetStringSlice env var or fallback as []string func GetStringSlice(key string, fallback []string) []string { + if _, ok := types[key]; !ok { + types[key] = "[]string" + } if v := Get(key, ""); v != "" { return strings.Split(v, ",") } @@ -106,6 +135,9 @@ func MustGetStringSlice(key string) []string { // GetIntSlice env var or fallback as []string func GetIntSlice(key string, fallback []int) []int { + if _, ok := types[key]; !ok { + types[key] = "[]int" + } if v := Get(key, ""); v != "" { elements := strings.Split(v, ",") ret := make([]int, len(elements)) @@ -125,3 +157,22 @@ func MustGetGetIntSlice(key string) []int { MustExists(key) return GetIntSlice(key, nil) } + +func RequiredKeys() []string { + return requiredKeys +} + +func Defaults() map[string]interface{} { + return defaults +} + +func Types() map[string]string { + return types +} + +func TypeOf(key string) string { + if v, ok := types[key]; ok { + return v + } + return "" +} diff --git a/env/readme.go b/env/readme.go new file mode 100644 index 0000000..909d1bf --- /dev/null +++ b/env/readme.go @@ -0,0 +1,43 @@ +package env + +import ( + "fmt" + + "github.com/foomo/keel/markdown" +) + +func Readme() string { + var rows [][]string + md := &markdown.Markdown{} + + { + for key, fallback := range defaults { + rows = append(rows, []string{ + markdown.Code(key), + markdown.Code(TypeOf(key)), + "", + markdown.Code(fmt.Sprintf("%v", fallback)), + }) + } + + for _, key := range requiredKeys { + rows = append(rows, []string{ + markdown.Code(key), + markdown.Code(TypeOf(key)), + markdown.Code("true"), + "", + }) + } + } + + if len(rows) > 0 { + md.Println("### Env") + md.Println("") + md.Println("List of all accessed environment variables.") + md.Println("") + md.Table([]string{"Key", "Type", "Required", "Default"}, rows) + md.Println("") + } + + return md.String() +} diff --git a/healthz.go b/healthz.go new file mode 100644 index 0000000..7b43776 --- /dev/null +++ b/healthz.go @@ -0,0 +1,20 @@ +package keel + +import ( + "github.com/foomo/keel/healthz" + "github.com/foomo/keel/interfaces" +) + +func IsHealthz(v any) bool { + switch v.(type) { + case healthz.BoolHealthzer, + healthz.BoolHealthzerWithContext, + healthz.ErrorHealthzer, + healthz.ErrorHealthzWithContext, + interfaces.ErrorPinger, + interfaces.ErrorPingerWithContext: + return true + default: + return false + } +} diff --git a/interfaces/closer.go b/interfaces/closer.go index fe62a5b..abbc719 100644 --- a/interfaces/closer.go +++ b/interfaces/closer.go @@ -4,16 +4,6 @@ import ( "context" ) -type closer struct { - handle func(context.Context) error -} - -func NewCloserFn(handle func(context.Context) error) closer { - return closer{ - handle: handle, - } -} - // Closer interface type Closer interface { Close() diff --git a/interfaces/documenter.go b/interfaces/documenter.go deleted file mode 100644 index 1747c4f..0000000 --- a/interfaces/documenter.go +++ /dev/null @@ -1,6 +0,0 @@ -package interfaces - -// Documenter interface -type Documenter interface { - Docs() string -} diff --git a/interfaces/namer.go b/interfaces/namer.go new file mode 100644 index 0000000..65250a1 --- /dev/null +++ b/interfaces/namer.go @@ -0,0 +1,6 @@ +package interfaces + +// Namer interface +type Namer interface { + Name() string +} diff --git a/interfaces/readmer.go b/interfaces/readmer.go new file mode 100644 index 0000000..64e9a42 --- /dev/null +++ b/interfaces/readmer.go @@ -0,0 +1,6 @@ +package interfaces + +// Readmer interface +type Readmer interface { + Readme() string +} diff --git a/markdown/markdown.go b/markdown/markdown.go index f84f060..fd9aa1b 100644 --- a/markdown/markdown.go +++ b/markdown/markdown.go @@ -13,6 +13,7 @@ type Markdown struct { func (s *Markdown) Println(a ...any) { s.value += fmt.Sprintln(a...) } + func (s *Markdown) Printf(format string, a ...any) { s.Println(fmt.Sprintf(format, a...)) } diff --git a/metrics/readme.go b/metrics/readme.go new file mode 100644 index 0000000..bb64f91 --- /dev/null +++ b/metrics/readme.go @@ -0,0 +1,40 @@ +package metrics + +import ( + "github.com/foomo/keel/markdown" + "github.com/foomo/keel/telemetry/nonrecording" + "github.com/prometheus/client_golang/prometheus" +) + +func Readme() string { + md := markdown.Markdown{} + values := nonrecording.Metrics() + + gatherer, _ := prometheus.DefaultRegisterer.(*prometheus.Registry).Gather() + for _, value := range gatherer { + values = append(values, nonrecording.Metric{ + Name: value.GetName(), + Type: value.GetType().String(), + Help: value.GetHelp(), + }) + } + + rows := make([][]string, 0, len(values)) + for _, value := range values { + rows = append(rows, []string{ + markdown.Code(value.Name), + value.Type, + value.Help, + }) + } + if len(rows) > 0 { + md.Println("### Metrics") + md.Println("") + md.Println("List of all registered metrics than are being exposed.") + md.Println("") + md.Table([]string{"Name", "Type", "Description"}, rows) + md.Println("") + } + + return md.String() +} diff --git a/option.go b/option.go index 9a4e002..cde3b8b 100644 --- a/option.go +++ b/option.go @@ -179,11 +179,11 @@ func WithHTTPHealthzService(enabled bool) Option { } } -// WithHTTPDocsService option with default value -func WithHTTPDocsService(enabled bool) Option { +// WithHTTPReadmeService option with default value +func WithHTTPReadmeService(enabled bool) Option { return func(inst *Server) { - if config.GetBool(inst.Config(), "service.docs.enabled", enabled)() { - svs := service.NewDefaultHTTPDocs(inst.Logger(), inst.documenter) + if config.GetBool(inst.Config(), "service.readme.enabled", enabled)() { + svs := service.NewDefaultHTTPReadme(inst.Logger(), &inst.readmers) inst.initServices = append(inst.initServices, svs) inst.AddAlwaysHealthzers(svs) } diff --git a/persistence/mongo/collection.go b/persistence/mongo/collection.go index a933575..4b39597 100644 --- a/persistence/mongo/collection.go +++ b/persistence/mongo/collection.go @@ -2,6 +2,7 @@ package keelmongo import ( "context" + "slices" "time" keelerrors "github.com/foomo/keel/errors" @@ -113,6 +114,11 @@ func CollectionWithIndexesCommitQuorumVotingMembers(v context.Context) Collectio // ~ Constructor // ------------------------------------------------------------------------------------------------ +var ( + dbs = map[string][]string{} + indices = map[string]map[string][]string{} +) + func NewCollection(db *mongo.Database, name string, opts ...CollectionOption) (*Collection, error) { o := DefaultCollectionOptions() for _, opt := range opts { @@ -120,11 +126,22 @@ func NewCollection(db *mongo.Database, name string, opts ...CollectionOption) (* } col := db.Collection(name, o.CollectionOptions) + if !slices.Contains(dbs[db.Name()], name) { + dbs[db.Name()] = append(dbs[db.Name()], name) + } if len(o.Indexes) > 0 { if _, err := col.Indexes().CreateMany(o.IndexesContext, o.Indexes, o.CreateIndexesOptions); err != nil { return nil, err } + if _, ok := indices[db.Name()]; !ok { + indices[db.Name()] = map[string][]string{} + } + for _, index := range o.Indexes { + if index.Options.Name != nil { + indices[db.Name()][name] = append(indices[db.Name()][name], *index.Options.Name) + } + } } return &Collection{ diff --git a/persistence/mongo/readme.go b/persistence/mongo/readme.go new file mode 100644 index 0000000..6ad2580 --- /dev/null +++ b/persistence/mongo/readme.go @@ -0,0 +1,37 @@ +package keelmongo + +import ( + "strings" + + "github.com/foomo/keel/markdown" +) + +func Readme() string { + var rows [][]string + md := &markdown.Markdown{} + + for db, collections := range dbs { + for _, collection := range collections { + var i string + if v, ok := indices[db][collection]; ok { + i += strings.Join(v, "`, `") + } + rows = append(rows, []string{ + markdown.Code(db), + markdown.Code(collection), + markdown.Code(i), + }) + } + } + + if len(rows) > 0 { + md.Println("### Mongo") + md.Println("") + md.Println("List of all used mongo collections including the configured indices options.") + md.Println("") + md.Table([]string{"Database", "Collection", "Indices"}, rows) + md.Println("") + } + + return md.String() +} diff --git a/server.go b/server.go index 54d5b3e..d616d9b 100644 --- a/server.go +++ b/server.go @@ -7,6 +7,7 @@ import ( "os" "os/signal" "reflect" + "slices" "sync" "sync/atomic" "syscall" @@ -15,11 +16,11 @@ import ( "github.com/foomo/keel/healthz" "github.com/foomo/keel/interfaces" "github.com/foomo/keel/markdown" + "github.com/foomo/keel/metrics" + keelmongo "github.com/foomo/keel/persistence/mongo" "github.com/foomo/keel/service" - "github.com/foomo/keel/telemetry/nonrecording" "github.com/go-logr/logr" "github.com/pkg/errors" - "github.com/prometheus/client_golang/prometheus" "github.com/spf13/viper" otelhost "go.opentelemetry.io/contrib/instrumentation/host" otelruntime "go.opentelemetry.io/contrib/instrumentation/runtime" @@ -49,8 +50,8 @@ type Server struct { running atomic.Bool closers []interface{} closersLock sync.Mutex + readmers []interfaces.Readmer probes map[healthz.Type][]interface{} - documenter map[string]interfaces.Documenter ctx context.Context ctxCancel context.Context ctxCancelFn context.CancelFunc @@ -64,8 +65,8 @@ func NewServer(opts ...Option) *Server { inst := &Server{ shutdownTimeout: 30 * time.Second, shutdownSignals: []os.Signal{os.Interrupt, syscall.SIGTERM}, + readmers: []interfaces.Readmer{}, probes: map[healthz.Type][]interface{}{}, - documenter: map[string]interfaces.Documenter{}, ctx: context.Background(), c: config.Config(), l: log.Logger(), @@ -178,7 +179,7 @@ func NewServer(opts ...Option) *Server { // add probe inst.AddAlwaysHealthzers(inst) - inst.AddDocumenter("Keel Server", inst) + inst.AddReadmer(inst) // start init services inst.startService(inst.initServices...) @@ -218,20 +219,17 @@ func (s *Server) CancelContext() context.Context { // AddService add a single service func (s *Server) AddService(service Service) { - for _, value := range s.services { - if value == service { - return - } + if !slices.Contains(s.services, service) { + s.services = append(s.services, service) + s.AddAlwaysHealthzers(service) + s.AddCloser(service) } - s.services = append(s.services, service) - s.AddAlwaysHealthzers(service) - s.AddCloser(service) } // AddServices adds multiple service func (s *Server) AddServices(services ...Service) { - for _, service := range services { - s.AddService(service) + for _, value := range services { + s.AddService(value) } } @@ -244,25 +242,9 @@ func (s *Server) AddCloser(closer interface{}) { return } } - switch closer.(type) { - case interfaces.Closer, - interfaces.ErrorCloser, - interfaces.CloserWithContext, - interfaces.ErrorCloserWithContext, - interfaces.Shutdowner, - interfaces.ErrorShutdowner, - interfaces.ShutdownerWithContext, - interfaces.ErrorShutdownerWithContext, - interfaces.Stopper, - interfaces.ErrorStopper, - interfaces.StopperWithContext, - interfaces.ErrorStopperWithContext, - interfaces.Unsubscriber, - interfaces.ErrorUnsubscriber, - interfaces.UnsubscriberWithContext, - interfaces.ErrorUnsubscriberWithContext: + if IsCloser(closer) { s.closers = append(s.closers, closer) - default: + } else { s.l.Warn("unable to add closer", log.FValue(fmt.Sprintf("%T", closer))) } } @@ -274,22 +256,25 @@ func (s *Server) AddClosers(closers ...interface{}) { } } -// AddDocumenter adds a dcoumenter to beadded to the exposed docs -func (s *Server) AddDocumenter(name string, documenter interfaces.Documenter) { - s.documenter[name] = documenter +// AddReadmer adds a readmer to be added to the exposed readme +func (s *Server) AddReadmer(readmer interfaces.Readmer) { + if !slices.Contains(s.readmers, readmer) { + s.readmers = append(s.readmers, readmer) + } +} + +// AddReadmers adds readmers to be added to the exposed readme +func (s *Server) AddReadmers(readmers ...interfaces.Readmer) { + for _, readmer := range readmers { + s.AddCloser(readmer) + } } // AddHealthzer adds a probe to be called on healthz checks func (s *Server) AddHealthzer(typ healthz.Type, probe interface{}) { - switch probe.(type) { - case healthz.BoolHealthzer, - healthz.BoolHealthzerWithContext, - healthz.ErrorHealthzer, - healthz.ErrorHealthzWithContext, - interfaces.ErrorPinger, - interfaces.ErrorPingerWithContext: + if IsHealthz(probe) { s.probes[typ] = append(s.probes[typ], probe) - default: + } else { s.l.Debug("not a healthz probe", log.FValue(fmt.Sprintf("%T", probe))) } } @@ -366,43 +351,131 @@ func (s *Server) Run() { s.l.Info("keel server stopped") } -// Docs returns the self-documenting string -func (s *Server) Docs() string { +// Readme returns the self-documenting string +func (s *Server) Readme() string { md := &markdown.Markdown{} - { - var rows [][]string - keys := s.Config().AllKeys() - defaults := config.Defaults() - for _, key := range keys { - var fallback interface{} - if v, ok := defaults[key]; ok { - fallback = v + md.Print(env.Readme()) + md.Print(config.Readme()) + md.Println(s.readmeServices()) + md.Println(s.readmeHealthz()) + md.Print(s.readmeCloser()) + md.Print(keelmongo.Readme()) + md.Print(metrics.Readme()) + + return md.String() +} + +// ------------------------------------------------------------------------------------------------ +// ~ Private methods +// ------------------------------------------------------------------------------------------------ + +// startService starts the given services +func (s *Server) startService(services ...Service) { + for _, value := range services { + value := value + s.g.Go(func() error { + if err := value.Start(s.ctx); errors.Is(err, http.ErrServerClosed) { + log.WithError(s.l, err).Debug("server has closed") + } else if err != nil { + log.WithError(s.l, err).Error("failed to start service") + return err } - rows = append(rows, []string{ - markdown.Code(key), - markdown.Code(config.TypeOf(key)), - "", - markdown.Code(fmt.Sprintf("%v", fallback)), - }) + return nil + }) + } +} + +func (s *Server) readmeCloser() string { + md := &markdown.Markdown{} + rows := make([][]string, 0, len(s.closers)) + s.closersLock.Lock() + defer s.closersLock.Unlock() + for _, value := range s.closers { + t := reflect.TypeOf(value) + var closer string + switch value.(type) { + case interfaces.Closer: + closer = "Closer" + case interfaces.ErrorCloser: + closer = "ErrorCloser" + case interfaces.CloserWithContext: + closer = "CloserWithContext" + case interfaces.ErrorCloserWithContext: + closer = "ErrorCloserWithContext" + case interfaces.Shutdowner: + closer = "Shutdowner" + case interfaces.ErrorShutdowner: + closer = "ErrorShutdowner" + case interfaces.ShutdownerWithContext: + closer = "ShutdownerWithContext" + case interfaces.ErrorShutdownerWithContext: + closer = "ErrorShutdownerWithContext" + case interfaces.Stopper: + closer = "Stopper" + case interfaces.ErrorStopper: + closer = "ErrorStopper" + case interfaces.StopperWithContext: + closer = "StopperWithContext" + case interfaces.ErrorStopperWithContext: + closer = "ErrorStopperWithContext" + case interfaces.Unsubscriber: + closer = "Unsubscriber" + case interfaces.ErrorUnsubscriber: + closer = "ErrorUnsubscriber" + case interfaces.UnsubscriberWithContext: + closer = "UnsubscriberWithContext" + case interfaces.ErrorUnsubscriberWithContext: + closer = "ErrorUnsubscriberWithContext" } - for _, key := range config.RequiredKeys() { + rows = append(rows, []string{ + markdown.Code(markdown.Name(value)), + markdown.Code(t.String()), + markdown.Code(closer), + markdown.String(value), + }) + } + if len(rows) > 0 { + md.Println("### Closers") + md.Println("") + md.Println("List of all registered closers that are being called during graceful shutdown.") + md.Println("") + md.Table([]string{"Name", "Type", "Closer", "Description"}, rows) + md.Println("") + } + + return md.String() +} + +func (s *Server) readmeHealthz() string { + var rows [][]string + md := &markdown.Markdown{} + + for k, probes := range s.probes { + for _, probe := range probes { + t := reflect.TypeOf(probe) rows = append(rows, []string{ - markdown.Code(key), - markdown.Code(config.TypeOf(key)), - markdown.Code("true"), - "", + markdown.Code(markdown.Name(probe)), + markdown.Code(k.String()), + markdown.Code(t.String()), + markdown.String(probe), }) } - if len(rows) > 0 { - md.Println("### Config") - md.Println("") - md.Println("List of all registered config variabled with their defaults.") - md.Println("") - md.Table([]string{"Key", "Type", "Required", "Default"}, rows) - md.Println("") - } } + if len(rows) > 0 { + md.Println("### Health probes") + md.Println("") + md.Println("List of all registered healthz probes that are being called during startup and runntime.") + md.Println("") + md.Table([]string{"Name", "Probe", "Type", "Description"}, rows) + md.Println("") + } + + return md.String() +} + +func (s *Server) readmeServices() string { + md := &markdown.Markdown{} { var rows [][]string @@ -446,137 +519,5 @@ func (s *Server) Docs() string { } } - { - var rows [][]string - for k, probes := range s.probes { - for _, probe := range probes { - t := reflect.TypeOf(probe) - rows = append(rows, []string{ - markdown.Code(markdown.Name(probe)), - markdown.Code(k.String()), - markdown.Code(t.String()), - markdown.String(probe), - }) - } - } - if len(rows) > 0 { - md.Println("### Health probes") - md.Println("") - md.Println("List of all registered healthz probes that are being called during startup and runntime.") - md.Println("") - md.Table([]string{"Name", "Probe", "Type", "Description"}, rows) - md.Println("") - } - } - - { - var rows [][]string - s.closersLock.Lock() - defer s.closersLock.Unlock() - for _, value := range s.closers { - t := reflect.TypeOf(value) - var closer string - switch value.(type) { - case interfaces.Closer: - closer = "Closer" - case interfaces.ErrorCloser: - closer = "ErrorCloser" - case interfaces.CloserWithContext: - closer = "CloserWithContext" - case interfaces.ErrorCloserWithContext: - closer = "ErrorCloserWithContext" - case interfaces.Shutdowner: - closer = "Shutdowner" - case interfaces.ErrorShutdowner: - closer = "ErrorShutdowner" - case interfaces.ShutdownerWithContext: - closer = "ShutdownerWithContext" - case interfaces.ErrorShutdownerWithContext: - closer = "ErrorShutdownerWithContext" - case interfaces.Stopper: - closer = "Stopper" - case interfaces.ErrorStopper: - closer = "ErrorStopper" - case interfaces.StopperWithContext: - closer = "StopperWithContext" - case interfaces.ErrorStopperWithContext: - closer = "ErrorStopperWithContext" - case interfaces.Unsubscriber: - closer = "Unsubscriber" - case interfaces.ErrorUnsubscriber: - closer = "ErrorUnsubscriber" - case interfaces.UnsubscriberWithContext: - closer = "UnsubscriberWithContext" - case interfaces.ErrorUnsubscriberWithContext: - closer = "ErrorUnsubscriberWithContext" - } - rows = append(rows, []string{ - markdown.Code(markdown.Name(value)), - markdown.Code(t.String()), - markdown.Code(closer), - markdown.String(value), - }) - } - if len(rows) > 0 { - md.Println("### Closers") - md.Println("") - md.Println("List of all registered closers that are being called during graceful shutdown.") - md.Println("") - md.Table([]string{"Name", "Type", "Closer", "Description"}, rows) - md.Println("") - } - } - - { - var rows [][]string - s.meter.AsyncFloat64() - - values := nonrecording.Metrics() - - gatherer, _ := prometheus.DefaultRegisterer.(*prometheus.Registry).Gather() - for _, value := range gatherer { - values = append(values, nonrecording.Metric{ - Name: value.GetName(), - Type: value.GetType().String(), - Help: value.GetHelp(), - }) - } - for _, value := range values { - rows = append(rows, []string{ - markdown.Code(value.Name), - value.Type, - value.Help, - }) - } - if len(rows) > 0 { - md.Println("### Metrics") - md.Println("") - md.Println("List of all registered metrics than are being exposed.") - md.Println("") - md.Table([]string{"Name", "Type", "Description"}, rows) - md.Println("") - } - } - return md.String() } - -// ------------------------------------------------------------------------------------------------ -// ~ Private methods -// ------------------------------------------------------------------------------------------------ - -// startService starts the given services -func (s *Server) startService(services ...Service) { - for _, value := range services { - value := value - s.g.Go(func() error { - if err := value.Start(s.ctx); errors.Is(err, http.ErrServerClosed) { - log.WithError(s.l, err).Debug("server has closed") - } else if err != nil { - log.WithError(s.l, err).Error("failed to start service") - return err - } - return nil - }) - } -} diff --git a/service/httpdocs.go b/service/httpreadme.go similarity index 53% rename from service/httpdocs.go rename to service/httpreadme.go index 44892d3..3cb5026 100644 --- a/service/httpdocs.go +++ b/service/httpreadme.go @@ -9,23 +9,21 @@ import ( ) const ( - DefaultHTTPDocsName = "docs" - DefaultHTTPDocsAddr = "localhost:9001" - DefaultHTTPDocsPath = "/docs" + DefaultHTTPReadmeName = "readme" + DefaultHTTPReadmeAddr = "localhost:9001" + DefaultHTTPReadmePath = "/readme" ) -func NewHTTPDocs(l *zap.Logger, name, addr, path string, documenters map[string]interfaces.Documenter) *HTTP { +func NewHTTPReadme(l *zap.Logger, name, addr, path string, readmers *[]interfaces.Readmer) *HTTP { handler := http.NewServeMux() handler.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - w.WriteHeader(http.StatusOK) w.Header().Add("Content-Type", "text/markdown") + w.WriteHeader(http.StatusOK) md := &markdown.Markdown{} - for name, documenter := range documenters { - md.Printf("## %s", name) - md.Println("") - md.Print(documenter.Docs()) + for _, readmer := range *readmers { + md.Print(readmer.Readme()) } _, _ = w.Write([]byte(md.String())) default: @@ -35,12 +33,12 @@ func NewHTTPDocs(l *zap.Logger, name, addr, path string, documenters map[string] return NewHTTP(l, name, addr, handler) } -func NewDefaultHTTPDocs(l *zap.Logger, documenter map[string]interfaces.Documenter) *HTTP { - return NewHTTPDocs( +func NewDefaultHTTPReadme(l *zap.Logger, readmers *[]interfaces.Readmer) *HTTP { + return NewHTTPReadme( l, - DefaultHTTPDocsName, - DefaultHTTPDocsAddr, - DefaultHTTPDocsPath, - documenter, + DefaultHTTPReadmeName, + DefaultHTTPReadmeAddr, + DefaultHTTPReadmePath, + readmers, ) } diff --git a/service/httpdocs_test.go b/service/httpreadme_test.go similarity index 65% rename from service/httpdocs_test.go rename to service/httpreadme_test.go index 013e6a7..c7a6236 100644 --- a/service/httpdocs_test.go +++ b/service/httpreadme_test.go @@ -6,23 +6,34 @@ import ( "io" "net/http" "os" + "time" "github.com/foomo/keel" "github.com/foomo/keel/config" + "github.com/foomo/keel/env" + "github.com/foomo/keel/examples/persistence/mongo/store" + "github.com/foomo/keel/log" + keelmongo "github.com/foomo/keel/persistence/mongo" "github.com/foomo/keel/service" "go.uber.org/zap" ) -func ExampleNewHTTPDocs() { +func ExampleNewHTTPReadme() { // define vars so it does not panic _ = os.Setenv("EXAMPLE_REQUIRED_BOOL", "true") _ = os.Setenv("EXAMPLE_REQUIRED_STRING", "foo") svr := keel.NewServer( keel.WithLogger(zap.NewNop()), - keel.WithHTTPDocsService(true), + keel.WithHTTPReadmeService(true), ) + // access some env vars + _ = env.Get("EXAMPLE_STRING", "demo") + _ = env.GetBool("EXAMPLE_BOOL", false) + _ = env.MustGet("EXAMPLE_REQUIRED_STRING") + _ = env.MustGetBool("EXAMPLE_REQUIRED_BOOL") + l := svr.Logger() c := svr.Config() @@ -33,18 +44,40 @@ func ExampleNewHTTPDocs() { _ = config.MustGetBool(c, "example.required.bool") _ = config.MustGetString(c, "example.required.string") + // create persistor + persistor, err := keelmongo.New(svr.Context(), "mongodb://localhost:27017/dummy") + log.Must(l, err, "failed to create persistor") + + // ensure to add the persistor to the closers + svr.AddClosers(persistor) + + // create repositories + _, err = persistor.Collection( + "dummy", + // define indexes but beware of changes on large dbs + keelmongo.CollectionWithIndexes( + store.EntityIndex, + store.EntityWithVersionsIndex, + ), + // define max time for index creation + keelmongo.CollectionWithIndexesMaxTime(time.Minute), + ) + log.Must(l, err, "failed to create collection") + + // add http service svr.AddService(service.NewHTTP(l, "demp-http", "localhost:8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("OK")) }))) + // add go routine service svr.AddService(service.NewGoRoutine(l, "demo-goroutine", func(ctx context.Context, l *zap.Logger) error { return nil })) go func() { - resp, _ := http.Get("http://localhost:9001/docs") //nolint:noctx - defer resp.Body.Close() //nolint:govet + resp, _ := http.Get("http://localhost:9001/readme") //nolint:noctx + defer resp.Body.Close() //nolint:govet b, _ := io.ReadAll(resp.Body) fmt.Print(string(b)) shutdown() @@ -53,7 +86,26 @@ func ExampleNewHTTPDocs() { svr.Run() // Output: - // ## Keel Server + // ### Env + // + // List of all accessed environment variables. + // + // | Key | Type | Required | Default | + // | --------------------------------------- | -------- | -------- | ------- | + // | `EXAMPLE_BOOL` | `bool` | | | + // | `EXAMPLE_REQUIRED_BOOL` | `bool` | | | + // | `EXAMPLE_REQUIRED_BOOL` | `bool` | `true` | | + // | `EXAMPLE_REQUIRED_STRING` | `string` | | | + // | `EXAMPLE_REQUIRED_STRING` | `string` | `true` | | + // | `EXAMPLE_STRING` | `string` | | `demo` | + // | `LOG_DISABLE_CALLER` | `bool` | | | + // | `LOG_DISABLE_STACKTRACE` | `bool` | | | + // | `LOG_ENCODING` | `string` | | `json` | + // | `LOG_LEVEL` | `string` | | `info` | + // | `LOG_MODE` | `string` | | `prod` | + // | `OTEL_ENABLED` | `bool` | | | + // | `OTEL_MONGO_COMMAND_ATTRIBUTE_DISABLED` | `bool` | | | + // | `OTEL_MONGO_ENABLED` | `bool` | | | // // ### Config // @@ -65,15 +117,15 @@ func ExampleNewHTTPDocs() { // | `example.required.bool` | `bool` | `true` | | // | `example.required.string` | `string` | `true` | | // | `example.string` | `string` | | `fallback` | - // | `service.docs.enabled` | `bool` | | `true` | + // | `service.readme.enabled` | `bool` | | `true` | // // ### Init Services // // List of all registered init services that are being immediately started. // - // | Name | Type | Address | - // | ------ | --------------- | ------------------------------------ | - // | `docs` | `*service.HTTP` | `*http.ServeMux` on `localhost:9001` | + // | Name | Type | Address | + // | -------- | --------------- | ------------------------------------ | + // | `readme` | `*service.HTTP` | `*http.ServeMux` on `localhost:9001` | // // ### Services // @@ -84,6 +136,7 @@ func ExampleNewHTTPDocs() { // | `demo-goroutine` | `*service.GoRoutine` | parallel: `1` | // | `demp-http` | `*service.HTTP` | `http.HandlerFunc` on `localhost:8080` | // + // // ### Health probes // // List of all registered healthz probes that are being called during startup and runntime. @@ -93,17 +146,27 @@ func ExampleNewHTTPDocs() { // | | `always` | `*keel.Server` | | // | `demo-goroutine` | `always` | `*service.GoRoutine` | parallel: `1` | // | `demp-http` | `always` | `*service.HTTP` | `http.HandlerFunc` on `localhost:8080` | - // | `docs` | `always` | `*service.HTTP` | `*http.ServeMux` on `localhost:9001` | + // | `readme` | `always` | `*service.HTTP` | `*http.ServeMux` on `localhost:9001` | + // // // ### Closers // // List of all registered closers that are being called during graceful shutdown. // - // | Name | Type | Closer | Description | - // | ---------------- | -------------------- | ------------------------ | -------------------------------------- | - // | `demo-goroutine` | `*service.GoRoutine` | `ErrorCloserWithContext` | parallel: `1` | - // | `demp-http` | `*service.HTTP` | `ErrorCloserWithContext` | `http.HandlerFunc` on `localhost:8080` | - // | `docs` | `*service.HTTP` | `ErrorCloserWithContext` | `*http.ServeMux` on `localhost:9001` | + // | Name | Type | Closer | Description | + // | ---------------- | ---------------------- | ------------------------ | -------------------------------------- | + // | | `*keelmongo.Persistor` | `ErrorCloserWithContext` | | + // | `demo-goroutine` | `*service.GoRoutine` | `ErrorCloserWithContext` | parallel: `1` | + // | `demp-http` | `*service.HTTP` | `ErrorCloserWithContext` | `http.HandlerFunc` on `localhost:8080` | + // | `readme` | `*service.HTTP` | `ErrorCloserWithContext` | `*http.ServeMux` on `localhost:9001` | + // + // ### Mongo + // + // List of all used mongo collections. + // + // | Database | Collection | Indices | + // | -------- | ---------- | ------------------------ | + // | `dummy` | `dummy` | `id_1`, `id_1_version_1` | // // ### Metrics // @@ -137,7 +200,7 @@ func ExampleNewHTTPDocs() { // | `go_memstats_stack_inuse_bytes` | GAUGE | Number of bytes in use by the stack allocator. | // | `go_memstats_stack_sys_bytes` | GAUGE | Number of bytes obtained from system for stack allocator. | // | `go_memstats_sys_bytes` | GAUGE | Number of bytes obtained from system. | - // | `go_threads` | GAUGE | Number of OS threads created. | + // | `go_threads` | GAUGE | Number of OS threads created. |// // | `process_cpu_seconds_total` | COUNTER | Total user and system CPU time spent in seconds. | // | `process_max_fds` | GAUGE | Maximum number of open file descriptors. | // | `process_open_fds` | GAUGE | Number of open file descriptors. |