refactor: expose docs through interface

This commit is contained in:
Kevin Franklin Kim 2023-09-08 15:49:23 +02:00
parent 58b482d8ac
commit 2823a97bad
No known key found for this signature in database
10 changed files with 480 additions and 489 deletions

View File

@ -62,7 +62,7 @@ linters:
- gosec # (gas): Inspects source code for security problems [fast: false, auto-fix: false]
- grouper # An analyzer to analyze expression groups. [fast: true, auto-fix: false]
- importas # Enforces consistent import aliases [fast: false, auto-fix: false]
- maintidx # maintidx measures the maintainability index of each function. [fast: true, auto-fix: false]
#- maintidx # maintidx measures the maintainability index of each function. [fast: true, auto-fix: false]
- makezero # Finds slice declarations with non-zero initial length [fast: false, auto-fix: false]
- misspell # Finds commonly misspelled English words in comments [fast: true, auto-fix: true]
- nakedret # Finds naked returns in functions greater than a specified function length [fast: true, auto-fix: false]

View File

@ -11,7 +11,9 @@ import (
// config holds the global configuration
var (
config *viper.Viper
config *viper.Viper
requiredKeys []string
defaults = map[string]interface{}{}
)
// Init sets up the configuration
@ -28,15 +30,13 @@ func Config() *viper.Viper {
}
func GetBool(c *viper.Viper, key string, fallback bool) func() bool {
c = ensure(c)
c.SetDefault(key, fallback)
setDefault(c, key, fallback)
return func() bool {
return c.GetBool(key)
}
}
func MustGetBool(c *viper.Viper, key string, fallback bool) func() bool {
c = ensure(c)
func MustGetBool(c *viper.Viper, key string) func() bool {
must(c, key)
return func() bool {
return c.GetBool(key)
@ -58,15 +58,13 @@ func MustGetInt(c *viper.Viper, key string) func() int {
}
func GetInt32(c *viper.Viper, key string, fallback int32) func() int32 {
c = ensure(c)
c.SetDefault(key, fallback)
setDefault(c, key, fallback)
return func() int32 {
return c.GetInt32(key)
}
}
func MustGetInt32(c *viper.Viper, key string) func() int32 {
c = ensure(c)
must(c, key)
return func() int32 {
return c.GetInt32(key)
@ -74,15 +72,13 @@ func MustGetInt32(c *viper.Viper, key string) func() int32 {
}
func GetInt64(c *viper.Viper, key string, fallback int64) func() int64 {
c = ensure(c)
c.SetDefault(key, fallback)
setDefault(c, key, fallback)
return func() int64 {
return c.GetInt64(key)
}
}
func MustGetInt64(c *viper.Viper, key string) func() int64 {
c = ensure(c)
must(c, key)
return func() int64 {
return c.GetInt64(key)
@ -90,15 +86,13 @@ func MustGetInt64(c *viper.Viper, key string) func() int64 {
}
func GetUint(c *viper.Viper, key string, fallback uint) func() uint {
c = ensure(c)
c.SetDefault(key, fallback)
setDefault(c, key, fallback)
return func() uint {
return c.GetUint(key)
}
}
func MustGetUint(c *viper.Viper, key string) func() uint {
c = ensure(c)
must(c, key)
return func() uint {
return c.GetUint(key)
@ -106,15 +100,13 @@ func MustGetUint(c *viper.Viper, key string) func() uint {
}
func GetUint32(c *viper.Viper, key string, fallback uint32) func() uint32 {
c = ensure(c)
c.SetDefault(key, fallback)
setDefault(c, key, fallback)
return func() uint32 {
return c.GetUint32(key)
}
}
func MustGetUint32(c *viper.Viper, key string) func() uint32 {
c = ensure(c)
must(c, key)
return func() uint32 {
return c.GetUint32(key)
@ -122,15 +114,13 @@ func MustGetUint32(c *viper.Viper, key string) func() uint32 {
}
func GetUint64(c *viper.Viper, key string, fallback uint64) func() uint64 {
c = ensure(c)
c.SetDefault(key, fallback)
setDefault(c, key, fallback)
return func() uint64 {
return c.GetUint64(key)
}
}
func MustGetUint64(c *viper.Viper, key string) func() uint64 {
c = ensure(c)
must(c, key)
return func() uint64 {
return c.GetUint64(key)
@ -138,15 +128,13 @@ func MustGetUint64(c *viper.Viper, key string) func() uint64 {
}
func GetFloat64(c *viper.Viper, key string, fallback float64) func() float64 {
c = ensure(c)
c.SetDefault(key, fallback)
setDefault(c, key, fallback)
return func() float64 {
return c.GetFloat64(key)
}
}
func MustGetFloat64(c *viper.Viper, key string) func() float64 {
c = ensure(c)
must(c, key)
return func() float64 {
return c.GetFloat64(key)
@ -154,15 +142,13 @@ func MustGetFloat64(c *viper.Viper, key string) func() float64 {
}
func GetString(c *viper.Viper, key, fallback string) func() string {
c = ensure(c)
c.SetDefault(key, fallback)
setDefault(c, key, fallback)
return func() string {
return c.GetString(key)
}
}
func MustGetString(c *viper.Viper, key string) func() string {
c = ensure(c)
must(c, key)
return func() string {
return c.GetString(key)
@ -170,15 +156,13 @@ func MustGetString(c *viper.Viper, key string) func() string {
}
func GetTime(c *viper.Viper, key string, fallback time.Time) func() time.Time {
c = ensure(c)
c.SetDefault(key, fallback)
setDefault(c, key, fallback)
return func() time.Time {
return c.GetTime(key)
}
}
func MustGetTime(c *viper.Viper, key string) func() time.Time {
c = ensure(c)
must(c, key)
return func() time.Time {
return c.GetTime(key)
@ -186,15 +170,13 @@ func MustGetTime(c *viper.Viper, key string) func() time.Time {
}
func GetDuration(c *viper.Viper, key string, fallback time.Duration) func() time.Duration {
c = ensure(c)
c.SetDefault(key, fallback)
setDefault(c, key, fallback)
return func() time.Duration {
return c.GetDuration(key)
}
}
func MustGetDuration(c *viper.Viper, key string) func() time.Duration {
c = ensure(c)
must(c, key)
return func() time.Duration {
return c.GetDuration(key)
@ -202,15 +184,13 @@ func MustGetDuration(c *viper.Viper, key string) func() time.Duration {
}
func GetIntSlice(c *viper.Viper, key string, fallback []int) func() []int {
c = ensure(c)
c.SetDefault(key, fallback)
setDefault(c, key, fallback)
return func() []int {
return c.GetIntSlice(key)
}
}
func MustGetIntSlice(c *viper.Viper, key string) func() []int {
c = ensure(c)
must(c, key)
return func() []int {
return c.GetIntSlice(key)
@ -218,15 +198,13 @@ func MustGetIntSlice(c *viper.Viper, key string) func() []int {
}
func GetStringSlice(c *viper.Viper, key string, fallback []string) func() []string {
c = ensure(c)
c.SetDefault(key, fallback)
setDefault(c, key, fallback)
return func() []string {
return c.GetStringSlice(key)
}
}
func MustGetStringSlice(c *viper.Viper, key string) func() []string {
c = ensure(c)
must(c, key)
return func() []string {
return c.GetStringSlice(key)
@ -234,15 +212,13 @@ func MustGetStringSlice(c *viper.Viper, key string) func() []string {
}
func GetStringMap(c *viper.Viper, key string, fallback map[string]interface{}) func() map[string]interface{} {
c = ensure(c)
c.SetDefault(key, fallback)
setDefault(c, key, fallback)
return func() map[string]interface{} {
return c.GetStringMap(key)
}
}
func MustGetStringMap(c *viper.Viper, key string) func() map[string]interface{} {
c = ensure(c)
must(c, key)
return func() map[string]interface{} {
return c.GetStringMap(key)
@ -250,15 +226,13 @@ func MustGetStringMap(c *viper.Viper, key string) func() map[string]interface{}
}
func GetStringMapString(c *viper.Viper, key string, fallback map[string]string) func() map[string]string {
c = ensure(c)
c.SetDefault(key, fallback)
setDefault(c, key, fallback)
return func() map[string]string {
return c.GetStringMapString(key)
}
}
func MustGetStringMapString(c *viper.Viper, key string) func() map[string]string {
c = ensure(c)
must(c, key)
return func() map[string]string {
return c.GetStringMapString(key)
@ -266,15 +240,13 @@ func MustGetStringMapString(c *viper.Viper, key string) func() map[string]string
}
func GetStringMapStringSlice(c *viper.Viper, key string, fallback map[string][]string) func() map[string][]string {
c = ensure(c)
c.SetDefault(key, fallback)
setDefault(c, key, fallback)
return func() map[string][]string {
return c.GetStringMapStringSlice(key)
}
}
func MustGetStringMapStringSlice(c *viper.Viper, key string) func() map[string][]string {
c = ensure(c)
must(c, key)
return func() map[string][]string {
return c.GetStringMapStringSlice(key)
@ -316,6 +288,14 @@ func GetStruct(c *viper.Viper, key string, fallback interface{}) (func(v interfa
}, nil
}
func RequiredKeys() []string {
return requiredKeys
}
func Defaults() map[string]interface{} {
return defaults
}
func ensure(c *viper.Viper) *viper.Viper {
if c == nil {
c = config
@ -324,6 +304,8 @@ func ensure(c *viper.Viper) *viper.Viper {
}
func must(c *viper.Viper, key string) {
c = ensure(c)
requiredKeys = append(requiredKeys, key)
if !c.IsSet(key) {
panic(fmt.Sprintf("missing required config key: %s", key))
}
@ -339,3 +321,9 @@ func decode(input, output interface{}) error {
}
return decoder.Decode(input)
}
func setDefault(c *viper.Viper, key string, fallback any) {
c = ensure(c)
c.SetDefault(key, fallback)
defaults[key] = fallback
}

6
interfaces/documenter.go Normal file
View File

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

38
markdown/markdown.go Normal file
View File

@ -0,0 +1,38 @@
package markdown
import (
"fmt"
markdowntable "github.com/fbiville/markdown-table-formatter/pkg/markdown"
)
type Markdown struct {
value string
}
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...))
}
func (s *Markdown) Print(a ...any) {
s.value += fmt.Sprint(a...)
}
func (s *Markdown) String() string {
return s.value
}
func (s *Markdown) Table(headers []string, rows [][]string) {
table, err := markdowntable.NewTableFormatterBuilder().
WithAlphabeticalSortIn(markdowntable.ASCENDING_ORDER).
WithPrettyPrint().
Build(headers...).
Format(rows)
if err != nil {
panic(err)
}
s.Print(table)
}

19
markdown/utils.go Normal file
View File

@ -0,0 +1,19 @@
package markdown
import (
"fmt"
)
func Code(v string) string {
if v == "" {
return ""
}
return "`" + v + "`"
}
func String(v any) string {
if i, ok := v.(fmt.Stringer); ok {
return i.String()
}
return ""
}

View File

@ -178,3 +178,14 @@ func WithHTTPHealthzService(enabled bool) Option {
}
}
}
// WithHTTPDocsService option with default value
func WithHTTPDocsService(enabled bool) Option {
return func(inst *Server) {
if config.GetBool(inst.Config(), "service.docs.enabled", enabled)() {
svs := service.NewDefaultHTTPDocs(inst.documenter)
inst.initServices = append(inst.initServices, svs)
inst.AddAlwaysHealthzers(svs)
}
}
}

214
server.go
View File

@ -1,6 +1,3 @@
//go:build !docs
// +build !docs
package keel
import (
@ -9,14 +6,19 @@ import (
"net/http"
"os"
"os/signal"
"reflect"
"sync/atomic"
"syscall"
"time"
"github.com/foomo/keel/healthz"
"github.com/foomo/keel/interfaces"
"github.com/foomo/keel/markdown"
"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"
@ -46,6 +48,7 @@ type Server struct {
running atomic.Bool
closers []interface{}
probes map[healthz.Type][]interface{}
documenter map[string]interfaces.Documenter
ctx context.Context
ctxCancel context.Context
ctxCancelFn context.CancelFunc
@ -60,6 +63,7 @@ func NewServer(opts ...Option) *Server {
shutdownTimeout: 30 * time.Second,
shutdownSignals: []os.Signal{os.Interrupt, syscall.SIGTERM},
probes: map[healthz.Type][]interface{}{},
documenter: map[string]interfaces.Documenter{},
ctx: context.Background(),
c: config.Config(),
l: log.Logger(),
@ -170,6 +174,7 @@ func NewServer(opts ...Option) *Server {
// add probe
inst.AddAlwaysHealthzers(inst)
inst.AddDocumenter("Server", inst)
// start init services
inst.startService(inst.initServices...)
@ -263,6 +268,11 @@ 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
}
// AddHealthzer adds a probe to be called on healthz checks
func (s *Server) AddHealthzer(typ healthz.Type, probe interface{}) {
switch probe.(type) {
@ -350,12 +360,204 @@ func (s *Server) Run() {
s.l.Info("keel server stopped")
}
func (s *Server) Docs() 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
}
rows = append(rows, []string{
markdown.Code(key),
"",
markdown.Code(fmt.Sprintf("%v", fallback)),
})
}
for _, key := range config.RequiredKeys() {
rows = append(rows, []string{
markdown.Code(key),
markdown.Code("true"),
"",
})
}
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", "Default", "Required"}, rows)
md.Println("")
}
}
{
var rows [][]string
for _, value := range s.initServices {
if v, ok := value.(*service.HTTP); ok {
t := reflect.TypeOf(v)
rows = append(rows, []string{
markdown.Code(v.Name()),
markdown.Code(t.String()),
markdown.String(v),
})
}
}
if len(rows) > 0 {
md.Println("## Init Services")
md.Println("")
md.Println("List of all registered init services that are being immediately started.")
md.Println("")
md.Table([]string{"Name", "Type", "Address"}, rows)
md.Println("")
}
}
{
var rows [][]string
for _, value := range s.services {
if v, ok := value.(*service.HTTP); ok {
t := reflect.TypeOf(v)
rows = append(rows, []string{
markdown.Code(v.Name()),
markdown.Code(t.String()),
markdown.String(v),
})
}
}
if len(rows) > 0 {
md.Println("## Services")
md.Println("")
md.Println("List of all registered services that are being started.")
md.Println("")
md.Table([]string{"Name", "Type", "Description"}, rows)
md.Println("")
}
}
{
var rows [][]string
for k, probes := range s.probes {
for _, probe := range probes {
t := reflect.TypeOf(probe)
rows = append(rows, []string{
markdown.Code(k.String()),
markdown.Code(t.String()),
})
}
}
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", "Type"}, rows)
md.Println("")
}
}
{
var rows [][]string
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(t.String()),
markdown.Code(closer),
})
}
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"}, 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 _, service := range services {
service := service
for _, value := range services {
value := value
s.g.Go(func() error {
if err := service.Start(s.ctx); errors.Is(err, http.ErrServerClosed) {
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")

View File

@ -1,435 +0,0 @@
//go:build docs
// +build docs
package keel
import (
"context"
"fmt"
"os"
"reflect"
"sort"
"time"
markdowntable "github.com/fbiville/markdown-table-formatter/pkg/markdown"
"github.com/foomo/keel/config"
"github.com/foomo/keel/healthz"
"github.com/foomo/keel/interfaces"
"github.com/foomo/keel/log"
"github.com/foomo/keel/service"
"github.com/foomo/keel/telemetry/nonrecording"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus"
"github.com/spf13/viper"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/metric"
otelglobal "go.opentelemetry.io/otel/metric/global"
"go.opentelemetry.io/otel/trace"
"go.uber.org/zap"
)
// Server struct
type Server struct {
services []Service
initServices []Service
meter metric.Meter
meterProvider metric.MeterProvider
tracer trace.Tracer
traceProvider trace.TracerProvider
shutdownSignals []os.Signal
shutdownTimeout time.Duration
closers []interface{}
probes map[healthz.Type][]interface{}
ctx context.Context
gCtx context.Context
l *zap.Logger
c *viper.Viper
}
func NewServer(opts ...Option) *Server {
inst := &Server{
probes: map[healthz.Type][]interface{}{},
meterProvider: nonrecording.NewNoopMeterProvider(),
traceProvider: trace.NewNoopTracerProvider(),
ctx: context.Background(),
c: config.Config(),
l: log.Logger(),
}
inst.meter = inst.meterProvider.Meter("")
otelglobal.SetMeterProvider(inst.meterProvider)
inst.tracer = inst.traceProvider.Tracer("")
otel.SetTracerProvider(inst.traceProvider)
// add probe
inst.AddAlwaysHealthzers(inst)
return inst
}
// Logger returns server logger
func (s *Server) Logger() *zap.Logger {
return s.l
}
// Meter returns the implementation meter
func (s *Server) Meter() metric.Meter {
return s.meter
}
// Tracer returns the implementation tracer
func (s *Server) Tracer() trace.Tracer {
return s.tracer
}
// Config returns server config
func (s *Server) Config() *viper.Viper {
return s.c
}
// Context returns server context
func (s *Server) Context() context.Context {
return s.ctx
}
// CancelContext returns server's cancel context
func (s *Server) CancelContext() context.Context {
return s.ctx
}
// AddService add a single service
func (s *Server) AddService(service Service) {
for _, value := range s.services {
if value == service {
return
}
}
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)
}
}
// AddCloser adds a closer to be called on shutdown
func (s *Server) AddCloser(closer interface{}) {
for _, value := range s.closers {
if value == closer {
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:
s.closers = append(s.closers, closer)
default:
s.l.Warn("unable to add closer", log.FValue(fmt.Sprintf("%T", closer)))
}
}
// AddClosers adds the given closers to be called on shutdown
func (s *Server) AddClosers(closers ...interface{}) {
for _, closer := range closers {
s.AddCloser(closer)
}
}
// 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:
s.probes[typ] = append(s.probes[typ], probe)
default:
s.l.Debug("not a healthz probe", log.FValue(fmt.Sprintf("%T", probe)))
}
}
// AddHealthzers adds the given probes to be called on healthz checks
func (s *Server) AddHealthzers(typ healthz.Type, probes ...interface{}) {
for _, probe := range probes {
s.AddHealthzer(typ, probe)
}
}
// AddAlwaysHealthzers adds the probes to be called on any healthz checks
func (s *Server) AddAlwaysHealthzers(probes ...interface{}) {
s.AddHealthzers(healthz.TypeAlways, probes...)
}
// AddStartupHealthzers adds the startup probes to be called on healthz checks
func (s *Server) AddStartupHealthzers(probes ...interface{}) {
s.AddHealthzers(healthz.TypeStartup, probes...)
}
// AddLivenessHealthzers adds the liveness probes to be called on healthz checks
func (s *Server) AddLivenessHealthzers(probes ...interface{}) {
s.AddHealthzers(healthz.TypeLiveness, probes...)
}
// AddReadinessHealthzers adds the readiness probes to be called on healthz checks
func (s *Server) AddReadinessHealthzers(probes ...interface{}) {
s.AddHealthzers(healthz.TypeReadiness, probes...)
}
// IsCanceled returns true if the internal errgroup has been canceled
func (s *Server) IsCanceled() bool {
return errors.Is(s.gCtx.Err(), context.Canceled)
}
// Healthz returns true if the server is running
func (s *Server) Healthz() error {
return nil
}
// Run runs the server
func (s *Server) Run() {
// add init services to closers
for _, initService := range s.initServices {
s.AddClosers(initService)
}
md := &MD{}
{
var rows [][]string
for _, key := range s.Config().AllKeys() {
rows = append(rows, []string{
code(key),
code(s.Config().GetString(key)),
})
}
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", "Default"}, rows)
md.Println("")
}
}
{
var rows [][]string
for _, value := range s.initServices {
if v, ok := value.(*service.HTTP); ok {
t := reflect.TypeOf(v)
rows = append(rows, []string{
code(v.Name()),
code(t.String()),
stringer(v),
})
}
}
if len(rows) > 0 {
md.Println("## Init Services")
md.Println("")
md.Println("List of all registerd init services that are being immediately started.")
md.Println("")
md.Table([]string{"Name", "Type", "Address"}, rows)
md.Println("")
}
}
{
var rows [][]string
for _, value := range s.services {
if v, ok := value.(*service.HTTP); ok {
t := reflect.TypeOf(v)
rows = append(rows, []string{
code(v.Name()),
code(t.String()),
stringer(v),
})
}
}
if len(rows) > 0 {
md.Println("## Services")
md.Println("")
md.Println("List of all registered services that are being started.")
md.Println("")
md.Table([]string{"Name", "Type", "Description"}, rows)
md.Println("")
}
}
{
var rows [][]string
for k, probes := range s.probes {
for _, probe := range probes {
t := reflect.TypeOf(probe)
rows = append(rows, []string{
code(k.String()),
code(t.String()),
})
}
}
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", "Type"}, rows)
md.Println("")
}
}
{
var rows [][]string
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{
code(t.String()),
code(closer),
})
}
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"}, rows)
md.Println("")
}
}
{
var rows [][]string
s.meter.AsyncFloat64()
var names []string
values := map[string]nonrecording.Metric{}
for _, value := range nonrecording.Metrics() {
names = append(names, value.Name)
values[value.Name] = value
}
gatherer, _ := prometheus.DefaultRegisterer.(*prometheus.Registry).Gather()
for _, value := range gatherer {
names = append(names, value.GetName())
values[value.GetName()] = nonrecording.Metric{
Name: value.GetName(),
Type: value.GetType().String(),
Help: value.GetHelp(),
}
}
sort.Strings(names)
for _, name := range names {
value := values[name]
rows = append(rows, []string{
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("")
}
}
fmt.Print(md.String())
}
type MD struct {
value string
}
func (s *MD) Println(a ...any) {
s.value += fmt.Sprintln(a...)
}
func (s *MD) Print(a ...any) {
s.value += fmt.Sprint(a...)
}
func (s *MD) String() string {
return s.value
}
func (s *MD) Table(headers []string, rows [][]string) {
table, err := markdowntable.NewTableFormatterBuilder().
WithPrettyPrint().
Build(headers...).
Format(rows)
if err != nil {
panic(err)
}
s.Print(table)
}
func code(v string) string {
if v == "" {
return ""
}
return "`" + v + "`"
}
func stringer(v any) string {
if i, ok := v.(fmt.Stringer); ok {
return i.String()
}
return ""
}

49
service/httpdocs.go Normal file
View File

@ -0,0 +1,49 @@
package service
import (
"net/http"
"github.com/foomo/keel/interfaces"
"github.com/foomo/keel/markdown"
"go.uber.org/zap"
"github.com/foomo/keel/log"
)
const (
DefaultHTTPDocsName = "docs"
DefaultHTTPDocsAddr = "localhost:9001"
DefaultHTTPDocsPath = "/docs"
)
func NewHTTPDocs(l *zap.Logger, name, addr, path string, documenters map[string]interfaces.Documenter) *HTTP {
handler := http.NewServeMux()
handler.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
l.Info("ping ")
switch r.Method {
case http.MethodGet:
w.WriteHeader(http.StatusOK)
w.Header().Add("Content-Type", "text/markdown")
md := &markdown.Markdown{}
for name, documenter := range documenters {
md.Printf("# %s", name)
md.Println("")
md.Print(documenter.Docs())
}
_, _ = w.Write([]byte(md.String()))
default:
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
}
})
return NewHTTP(l, name, addr, handler)
}
func NewDefaultHTTPDocs(documenter map[string]interfaces.Documenter) *HTTP {
return NewHTTPDocs(
log.Logger(),
DefaultHTTPDocsName,
DefaultHTTPDocsAddr,
DefaultHTTPDocsPath,
documenter,
)
}

113
service/httpdocs_test.go Normal file
View File

@ -0,0 +1,113 @@
package service_test
import (
"fmt"
"io"
"net/http"
"os"
"time"
"github.com/foomo/keel"
"github.com/foomo/keel/config"
"go.uber.org/zap"
)
func ExampleNewHTTPDocs() {
shutdown(3 * time.Second)
// 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.NewExample()),
keel.WithHTTPDocsService(true),
)
c := svr.Config()
// config with fallback
_ = config.GetBool(c, "example.bool", false)()
_ = config.GetString(c, "example.string", "fallback")()
// required configs
_ = config.MustGetBool(c, "example.required.bool")()
_ = config.MustGetString(c, "example.required.string")()
{
resp, _ := http.Get("http://localhost:9001/docs") //nolint:noctx
defer resp.Body.Close() //nolint:govet
b, _ := io.ReadAll(resp.Body)
fmt.Print(string(b))
}
svr.Run()
// Output:
// # Server
//
// ## Config
//
// List of all registered config variabled with their defaults.
//
// | Key | Default | Required |
// | ------------------------- | ------- | ---------- |
// | `example.bool` | | `false` |
// | `example.required.bool` | `true` | |
// | `example.required.string` | `true` | |
// | `example.string` | | `fallback` |
// | `service.docs.enabled` | | `true` |
//
// ## Init Services
//
// List of all registered init services that are being immediately started.
//
// | Name | Type | Address |
// | ------ | --------------- | ------------------------- |
// | `docs` | `*service.HTTP` | address: `localhost:9001` |
//
// ## Health probes
//
// List of all registered healthz probes that are being called during startup and runntime.
//
// | Name | Type |
// | -------- | --------------- |
// | `always` | `*keel.Server` |
// | `always` | `*service.HTTP` |
//
// ## Metrics
//
// List of all registered metrics than are being exposed.
//
// | Name | Type | Description |
// | ---------------------------------- | ------- | ------------------------------------------------------------------ |
// | `go_gc_duration_seconds` | SUMMARY | A summary of the pause duration of garbage collection cycles. |
// | `go_goroutines` | GAUGE | Number of goroutines that currently exist. |
// | `go_info` | GAUGE | Information about the Go environment. |
// | `go_memstats_alloc_bytes_total` | COUNTER | Total number of bytes allocated, even if freed. |
// | `go_memstats_alloc_bytes` | GAUGE | Number of bytes allocated and still in use. |
// | `go_memstats_buck_hash_sys_bytes` | GAUGE | Number of bytes used by the profiling bucket hash table. |
// | `go_memstats_frees_total` | COUNTER | Total number of frees. |
// | `go_memstats_gc_sys_bytes` | GAUGE | Number of bytes used for garbage collection system metadata. |
// | `go_memstats_heap_alloc_bytes` | GAUGE | Number of heap bytes allocated and still in use. |
// | `go_memstats_heap_idle_bytes` | GAUGE | Number of heap bytes waiting to be used. |
// | `go_memstats_heap_inuse_bytes` | GAUGE | Number of heap bytes that are in use. |
// | `go_memstats_heap_objects` | GAUGE | Number of allocated objects. |
// | `go_memstats_heap_released_bytes` | GAUGE | Number of heap bytes released to OS. |
// | `go_memstats_heap_sys_bytes` | GAUGE | Number of heap bytes obtained from system. |
// | `go_memstats_last_gc_time_seconds` | GAUGE | Number of seconds since 1970 of last garbage collection. |
// | `go_memstats_lookups_total` | COUNTER | Total number of pointer lookups. |
// | `go_memstats_mallocs_total` | COUNTER | Total number of mallocs. |
// | `go_memstats_mcache_inuse_bytes` | GAUGE | Number of bytes in use by mcache structures. |
// | `go_memstats_mcache_sys_bytes` | GAUGE | Number of bytes used for mcache structures obtained from system. |
// | `go_memstats_mspan_inuse_bytes` | GAUGE | Number of bytes in use by mspan structures. |
// | `go_memstats_mspan_sys_bytes` | GAUGE | Number of bytes used for mspan structures obtained from system. |
// | `go_memstats_next_gc_bytes` | GAUGE | Number of heap bytes when next garbage collection will take place. |
// | `go_memstats_other_sys_bytes` | GAUGE | Number of bytes used for other system allocations. |
// | `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. |
//
// {"level":"info","msg":"starting keel server"}
// {"level":"debug","msg":"keel graceful shutdown"}
// {"level":"info","msg":"keel server stopped"}
}