feat: add mongo and refactor readme

This commit is contained in:
Kevin Franklin Kim 2023-09-08 22:24:31 +02:00
parent 36a26d875d
commit 9983e267e2
No known key found for this signature in database
18 changed files with 576 additions and 257 deletions

29
closer.go Normal file
View File

@ -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
}
}

71
config/readme.go Normal file
View File

@ -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()
}

View File

@ -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
}

55
env/env.go vendored
View File

@ -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 ""
}

43
env/readme.go vendored Normal file
View File

@ -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()
}

20
healthz.go Normal file
View File

@ -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
}
}

View File

@ -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()

View File

@ -1,6 +0,0 @@
package interfaces
// Documenter interface
type Documenter interface {
Docs() string
}

6
interfaces/namer.go Normal file
View File

@ -0,0 +1,6 @@
package interfaces
// Namer interface
type Namer interface {
Name() string
}

6
interfaces/readmer.go Normal file
View File

@ -0,0 +1,6 @@
package interfaces
// Readmer interface
type Readmer interface {
Readme() string
}

View File

@ -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...))
}

40
metrics/readme.go Normal file
View File

@ -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()
}

View File

@ -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)
}

View File

@ -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{

View File

@ -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()
}

349
server.go
View File

@ -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
})
}
}

View File

@ -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,
)
}

View File

@ -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. |