diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4222c9b..b0990b7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,15 +6,14 @@ on: - v*.*.* workflow_dispatch: -env: - GOFLAGS: -mod=readonly - GOPROXY: https://proxy.golang.org +permissions: + contents: write jobs: release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 @@ -22,8 +21,7 @@ jobs: - uses: actions/setup-go@v4 with: - check-latest: true - go-version-file: 'go.mod' + go-version: 'stable' - uses: goreleaser/goreleaser-action@v4 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 11838f7..b1c0152 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,15 +4,8 @@ on: push: branches: [ main ] pull_request: - branches: [ main ] - merge_group: - branches: [ main ] workflow_dispatch: -env: - GOFLAGS: -mod=readonly - GOPROXY: https://proxy.golang.org - concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true @@ -21,15 +14,24 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-go@v4 with: - check-latest: true - go-version-file: 'go.mod' + go-version: 'stable' + + - uses: gotesttools/gotestfmt-action@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} - uses: golangci/golangci-lint-action@v3 + with: + version: latest + args: --timeout=5m - - name: Run tests - run: go test -v ./... + - run: make test + + - uses: coverallsapp/github-action@v2 + with: + file: coverage.out diff --git a/.gitignore b/.gitignore index f14e605..5c03997 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .* *.log +*.out !.github/ !.husky/ !.editorconfig @@ -7,6 +8,4 @@ !.golangci.yml !.goreleaser.yml !.husky.yaml -/coverage.out -/coverage.html /tmp/ diff --git a/.golangci.yml b/.golangci.yml index edc3188..f3f5ee7 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,5 +1,6 @@ run: - timeout: 5m + skip-dirs: + - tmp linters-settings: # https://golangci-lint.run/usage/linters/#revive @@ -61,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] diff --git a/.goreleaser.yml b/.goreleaser.yml index 65027d3..fbb8188 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -2,33 +2,4 @@ builds: - skip: true changelog: - filters: - exclude: - - "^wip" - - "^test" - - "^docs" - - "^chore" - - "^style" - - "go mod tidy" - - "merge conflict" - - "Merge pull request" - - "Merge remote-tracking branch" - - "Merge branch" - groups: - - title: Features - regexp: '^.*?feat(\([[:word:]]+\))??!?:.+$' - order: 0 - - title: Dependency updates - regexp: '^.*?(feat|fix)\(deps\)!?:.+$' - order: 100 - - title: "Bug fixes" - regexp: '^.*?fix(\([[:word:]]+\))??!?:.+$' - order: 150 - - title: "Security" - regexp: '^.*?sec(\([[:word:]]+\))??!?:.+$' - order: 200 - - title: "Performace" - regexp: '^.*?perf(\([[:word:]]+\))??!?:.+$' - order: 250 - - title: Other - order: 999 + use: github-native diff --git a/.husky.yaml b/.husky.yaml index fb2448e..2bbd935 100644 --- a/.husky.yaml +++ b/.husky.yaml @@ -9,9 +9,7 @@ hooks: lint-staged: '*.go': - goimports -l -w - - gofmt -l -w lint-commit: - email: '^(.+@bestbytes.com)$' types: '^(feat|fix|build|chore|docs|perf|refactor|revert|style|test|wip)$' header: '^(?P\w+)(\((?P[\w/.-]+)\))?(?P!)?:( +)?(?P
.+)' diff --git a/Makefile b/Makefile index a2ace4a..08b42e1 100644 --- a/Makefile +++ b/Makefile @@ -27,14 +27,7 @@ doc: .PHONY: test ## Run tests test: - @go test -v ./... - -.PHONY: test.cover -## Run tests with coverage -test.cover: - @go test -v -coverprofile=coverage.out ./... - @go tool cover -func=coverage.out - @go tool cover -html=coverage.out + @go test -coverprofile=coverage.out -race -json ./... | gotestfmt .PHONY: lint ## Run linter diff --git a/README.md b/README.md index 02690fe..a5341aa 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # keel +[![Build Status](https://github.com/foomo/keel/actions/workflows/test.yml/badge.svg?branch=main&event=push)](https://github.com/foomo/keel/actions/workflows/test.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/foomo/keel)](https://goreportcard.com/report/github.com/foomo/keel) -[![godoc](https://godoc.org/github.com/foomo/keel?status.svg)](https://godoc.org/github.com/foomo/keel) -[![GitHub Super-Linter](https://github.com/foomo/keel/workflows/CI/badge.svg)](https://github.com/marketplace/actions/super-linter) +[![Coverage Status](https://coveralls.io/repos/github/foomo/keel/badge.svg?branch=main&)](https://coveralls.io/github/foomo/keel?branch=main) +[![GoDoc](https://godoc.org/github.com/foomo/keel?status.svg)](https://godoc.org/github.com/foomo/keel) > Opinionated way to run services. @@ -25,6 +26,7 @@ import ( "net/http" "github.com/foomo/keel" + "github.com/foomo/keel/service" ) func main() { @@ -39,7 +41,7 @@ func main() { svs := newService() svr.AddService( - keel.NewServiceHTTP(l, "demo", ":8080", svs), + service.NewHTTP(l, "demo", "localhost:8080", svs), ) svr.Run() diff --git a/closer.go b/closer.go index 9de714d..fe583bd 100644 --- a/closer.go +++ b/closer.go @@ -1,37 +1,29 @@ package keel -import "context" +import ( + "github.com/foomo/keel/interfaces" +) -type closer struct { - handle func(context.Context) error -} - -func NewCloserFn(handle func(context.Context) error) closer { - return closer{ - handle: handle, +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 } } - -func (h healther) Close(ctx context.Context) error { - return h.handle(ctx) -} - -// Closer interface -type Closer interface { - Close() -} - -// ErrorCloser interface -type ErrorCloser interface { - Close() error -} - -// CloserWithContext interface -type CloserWithContext interface { - Close(ctx context.Context) -} - -// ErrorCloserWithContext interface -type ErrorCloserWithContext interface { - Close(ctx context.Context) error -} diff --git a/config/config.go b/config/config.go index 7c4b26a..195d670 100644 --- a/config/config.go +++ b/config/config.go @@ -11,7 +11,10 @@ import ( // config holds the global configuration var ( - config *viper.Viper + config *viper.Viper + requiredKeys []string + defaults = map[string]interface{}{} + types = map[string]string{} ) // Init sets up the configuration @@ -28,254 +31,224 @@ 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, "bool", fallback) return func() bool { return c.GetBool(key) } } -func MustGetBool(c *viper.Viper, key string, fallback bool) func() bool { - c = ensure(c) - must(c, key) +func MustGetBool(c *viper.Viper, key string) func() bool { + must(c, key, "bool") return func() bool { return c.GetBool(key) } } func GetInt(c *viper.Viper, key string, fallback int) func() int { - c.SetDefault(key, fallback) + setDefault(c, key, "int", fallback) return func() int { return c.GetInt(key) } } func MustGetInt(c *viper.Viper, key string) func() int { - must(c, key) + must(c, key, "int") return func() int { return c.GetInt(key) } } func GetInt32(c *viper.Viper, key string, fallback int32) func() int32 { - c = ensure(c) - c.SetDefault(key, fallback) + setDefault(c, key, "int32", fallback) return func() int32 { return c.GetInt32(key) } } func MustGetInt32(c *viper.Viper, key string) func() int32 { - c = ensure(c) - must(c, key) + must(c, key, "int32") return func() int32 { return c.GetInt32(key) } } func GetInt64(c *viper.Viper, key string, fallback int64) func() int64 { - c = ensure(c) - c.SetDefault(key, fallback) + setDefault(c, key, "int64", fallback) return func() int64 { return c.GetInt64(key) } } func MustGetInt64(c *viper.Viper, key string) func() int64 { - c = ensure(c) - must(c, key) + must(c, key, "int64") return func() int64 { return c.GetInt64(key) } } func GetUint(c *viper.Viper, key string, fallback uint) func() uint { - c = ensure(c) - c.SetDefault(key, fallback) + setDefault(c, key, "uint", fallback) return func() uint { return c.GetUint(key) } } func MustGetUint(c *viper.Viper, key string) func() uint { - c = ensure(c) - must(c, key) + must(c, key, "uint") return func() uint { return c.GetUint(key) } } func GetUint32(c *viper.Viper, key string, fallback uint32) func() uint32 { - c = ensure(c) - c.SetDefault(key, fallback) + setDefault(c, key, "uint32", fallback) return func() uint32 { return c.GetUint32(key) } } func MustGetUint32(c *viper.Viper, key string) func() uint32 { - c = ensure(c) - must(c, key) + must(c, key, "uint32") return func() uint32 { return c.GetUint32(key) } } func GetUint64(c *viper.Viper, key string, fallback uint64) func() uint64 { - c = ensure(c) - c.SetDefault(key, fallback) + setDefault(c, key, "uint64", fallback) return func() uint64 { return c.GetUint64(key) } } func MustGetUint64(c *viper.Viper, key string) func() uint64 { - c = ensure(c) - must(c, key) + must(c, key, "uint64") return func() uint64 { return c.GetUint64(key) } } func GetFloat64(c *viper.Viper, key string, fallback float64) func() float64 { - c = ensure(c) - c.SetDefault(key, fallback) + setDefault(c, key, "float64", fallback) return func() float64 { return c.GetFloat64(key) } } func MustGetFloat64(c *viper.Viper, key string) func() float64 { - c = ensure(c) - must(c, key) + must(c, key, "float64") return func() float64 { return c.GetFloat64(key) } } func GetString(c *viper.Viper, key, fallback string) func() string { - c = ensure(c) - c.SetDefault(key, fallback) + setDefault(c, key, "string", fallback) return func() string { return c.GetString(key) } } func MustGetString(c *viper.Viper, key string) func() string { - c = ensure(c) - must(c, key) + must(c, key, "string") return func() string { return c.GetString(key) } } func GetTime(c *viper.Viper, key string, fallback time.Time) func() time.Time { - c = ensure(c) - c.SetDefault(key, fallback) + setDefault(c, key, "time.Time", 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) + must(c, key, "time.Time") return func() time.Time { return c.GetTime(key) } } func GetDuration(c *viper.Viper, key string, fallback time.Duration) func() time.Duration { - c = ensure(c) - c.SetDefault(key, fallback) + setDefault(c, key, "time.Duration", 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) + must(c, key, "time.Duration") return func() time.Duration { return c.GetDuration(key) } } func GetIntSlice(c *viper.Viper, key string, fallback []int) func() []int { - c = ensure(c) - c.SetDefault(key, fallback) + setDefault(c, key, "[]int", fallback) return func() []int { return c.GetIntSlice(key) } } func MustGetIntSlice(c *viper.Viper, key string) func() []int { - c = ensure(c) - must(c, key) + must(c, key, "[]int") return func() []int { return c.GetIntSlice(key) } } func GetStringSlice(c *viper.Viper, key string, fallback []string) func() []string { - c = ensure(c) - c.SetDefault(key, fallback) + setDefault(c, key, "[]string", fallback) return func() []string { return c.GetStringSlice(key) } } func MustGetStringSlice(c *viper.Viper, key string) func() []string { - c = ensure(c) - must(c, key) + must(c, key, "[]string") return func() []string { return c.GetStringSlice(key) } } 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, "map[string]interface{}", 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) + must(c, key, "map[string]interface{}") return func() map[string]interface{} { return c.GetStringMap(key) } } 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, "map[string]string", 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) + must(c, key, "map[string]string") return func() map[string]string { return c.GetStringMapString(key) } } 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, "map[string][]string", 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) + must(c, key, "map[string][]string") return func() map[string][]string { return c.GetStringMapStringSlice(key) } @@ -316,6 +289,25 @@ 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 Types() map[string]string { + return types +} + +func TypeOf(key string) string { + if v, ok := types[key]; ok { + return v + } + return "" +} + func ensure(c *viper.Viper) *viper.Viper { if c == nil { c = config @@ -323,7 +315,10 @@ func ensure(c *viper.Viper) *viper.Viper { return c } -func must(c *viper.Viper, key string) { +func must(c *viper.Viper, key, typeof string) { + c = ensure(c) + types[key] = typeof + requiredKeys = append(requiredKeys, key) if !c.IsSet(key) { panic(fmt.Sprintf("missing required config key: %s", key)) } @@ -339,3 +334,10 @@ func decode(input, output interface{}) error { } return decoder.Decode(input) } + +func setDefault(c *viper.Viper, key, typeof string, fallback any) { + c = ensure(c) + c.SetDefault(key, fallback) + defaults[key] = fallback + types[key] = typeof +} diff --git a/config/readme.go b/config/readme.go new file mode 100644 index 0000000..bbcad9f --- /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 variables 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/errors.go b/errors.go index f9d28eb..02d3e6f 100644 --- a/errors.go +++ b/errors.go @@ -5,6 +5,5 @@ import ( ) var ( - ErrServerNotRunning = errors.New("server not running") - ErrServiceNotRunning = errors.New("service not running") + ErrServerNotRunning = errors.New("server not running") ) diff --git a/examples/config/main.go b/examples/config/main.go index bf89a65..19e1f86 100644 --- a/examples/config/main.go +++ b/examples/config/main.go @@ -7,6 +7,7 @@ import ( "time" "github.com/davecgh/go-spew/spew" + "github.com/foomo/keel/service" "github.com/foomo/keel" "github.com/foomo/keel/config" @@ -85,7 +86,7 @@ func main() { }) svr.AddService( - keel.NewServiceHTTP(l, "demo", "localhost:8081", svs), + service.NewHTTP(l, "demo", "localhost:8081", svs), ) svr.Run() diff --git a/examples/graceful/main.go b/examples/graceful/main.go index 7d030d6..4286086 100644 --- a/examples/graceful/main.go +++ b/examples/graceful/main.go @@ -6,6 +6,7 @@ import ( "sync" "time" + "github.com/foomo/keel/service" "go.uber.org/zap" "github.com/foomo/keel" @@ -32,7 +33,7 @@ func main() { }) svr.AddService( - keel.NewServiceHTTP(l, "demo", "localhost:8080", svs), + service.NewHTTP(l, "demo", "localhost:8080", svs), ) svr.Run() diff --git a/examples/healthz/main.go b/examples/healthz/main.go index cbf5ce8..8c5588e 100644 --- a/examples/healthz/main.go +++ b/examples/healthz/main.go @@ -8,6 +8,8 @@ import ( "github.com/foomo/keel" "github.com/foomo/keel/examples/healthz/handler" + "github.com/foomo/keel/healthz" + "github.com/foomo/keel/service" ) // See k8s for probe documentation @@ -46,7 +48,7 @@ func main() { svr.AddReadinessHealthzers(rh) // add inline probe e.g. in case you start go routines - svr.AddAlwaysHealthzers(keel.NewHealthzerFn(func(ctx context.Context) error { + svr.AddAlwaysHealthzers(healthz.NewHealthzerFn(func(ctx context.Context) error { l.Info("healther fn") return nil })) @@ -69,7 +71,7 @@ func main() { // add services svr.AddService( - keel.NewServiceHTTP(l, "demo", "localhost:8080", svs), + service.NewHTTP(l, "demo", "localhost:8080", svs), ) // start serer diff --git a/examples/logging/main.go b/examples/logging/main.go index 8ef5df5..00027fa 100644 --- a/examples/logging/main.go +++ b/examples/logging/main.go @@ -7,6 +7,7 @@ import ( "github.com/foomo/keel" "github.com/foomo/keel/log" + "github.com/foomo/keel/service" ) type CustomError struct { @@ -46,7 +47,7 @@ func main() { }) svr.AddService( - keel.NewServiceHTTP(l, "demo", "localhost:8080", svs), + service.NewHTTP(l, "demo", "localhost:8080", svs), ) svr.Run() diff --git a/examples/middlewares/basicauth/main.go b/examples/middlewares/basicauth/main.go index 4497761..f231b07 100644 --- a/examples/middlewares/basicauth/main.go +++ b/examples/middlewares/basicauth/main.go @@ -6,6 +6,7 @@ import ( "github.com/foomo/keel" "github.com/foomo/keel/log" "github.com/foomo/keel/net/http/middleware" + "github.com/foomo/keel/service" httputils "github.com/foomo/keel/utils/net/http" ) @@ -29,7 +30,7 @@ func main() { log.Must(l, err, "failed to hash password") svr.AddService( - keel.NewServiceHTTP(l, "demo", "localhost:8080", svs, + service.NewHTTP(l, "demo", "localhost:8080", svs, middleware.BasicAuth( username, passwordHash, diff --git a/examples/middlewares/cors/main.go b/examples/middlewares/cors/main.go index 1ab29b7..f54aad5 100644 --- a/examples/middlewares/cors/main.go +++ b/examples/middlewares/cors/main.go @@ -6,6 +6,7 @@ import ( "github.com/foomo/keel" keelhttp "github.com/foomo/keel/net/http" "github.com/foomo/keel/net/http/middleware" + "github.com/foomo/keel/service" ) func main() { @@ -22,7 +23,7 @@ func main() { }) svr.AddService( - keel.NewServiceHTTP(l, "demo", "localhost:8080", svs, + service.NewHTTP(l, "demo", "localhost:8080", svs, middleware.CORS( middleware.CORSWithAllowOrigins("example.com"), middleware.CORSWithAllowMethods(http.MethodGet, http.MethodPost), diff --git a/examples/middlewares/jwtfromcookie/main.go b/examples/middlewares/jwtfromcookie/main.go index a224541..2443648 100644 --- a/examples/middlewares/jwtfromcookie/main.go +++ b/examples/middlewares/jwtfromcookie/main.go @@ -6,6 +6,7 @@ import ( "net/http" "strings" + "github.com/foomo/keel/service" jwt2 "github.com/golang-jwt/jwt" "go.uber.org/zap" @@ -75,7 +76,7 @@ func main() { }) svr.AddService( - keel.NewServiceHTTP(l, "demo", "localhost:8080", svs, + service.NewHTTP(l, "demo", "localhost:8080", svs, middleware.Skip( middleware.JWT( jwtInst, diff --git a/examples/middlewares/jwtfromtoken/main.go b/examples/middlewares/jwtfromtoken/main.go index 0876339..915e530 100644 --- a/examples/middlewares/jwtfromtoken/main.go +++ b/examples/middlewares/jwtfromtoken/main.go @@ -5,6 +5,7 @@ import ( "crypto/rsa" "net/http" + "github.com/foomo/keel/service" jwt2 "github.com/golang-jwt/jwt" "github.com/foomo/keel" @@ -66,7 +67,7 @@ func main() { }) svr.AddService( - keel.NewServiceHTTP(l, "demo", "localhost:8080", svs, + service.NewHTTP(l, "demo", "localhost:8080", svs, middleware.Skip( middleware.JWT( jwtInst, diff --git a/examples/middlewares/logger/main.go b/examples/middlewares/logger/main.go index 99d3c39..3d702f1 100644 --- a/examples/middlewares/logger/main.go +++ b/examples/middlewares/logger/main.go @@ -6,6 +6,7 @@ import ( "github.com/foomo/keel" keelhttp "github.com/foomo/keel/net/http" "github.com/foomo/keel/net/http/middleware" + "github.com/foomo/keel/service" ) func main() { @@ -22,7 +23,7 @@ func main() { }) svr.AddService( - keel.NewServiceHTTP(l, "demo", "localhost:8080", svs, + service.NewHTTP(l, "demo", "localhost:8080", svs, middleware.Logger(), ), ) diff --git a/examples/middlewares/recover/main.go b/examples/middlewares/recover/main.go index 8e9a5db..1931772 100644 --- a/examples/middlewares/recover/main.go +++ b/examples/middlewares/recover/main.go @@ -5,6 +5,7 @@ import ( "github.com/foomo/keel" "github.com/foomo/keel/net/http/middleware" + "github.com/foomo/keel/service" ) func main() { @@ -23,7 +24,7 @@ func main() { }) svr.AddService( - keel.NewServiceHTTP(l, "demo", "localhost:8080", svs, + service.NewHTTP(l, "demo", "localhost:8080", svs, middleware.Recover( middleware.RecoverWithDisablePrintStack(true), ), diff --git a/examples/middlewares/requestid/main.go b/examples/middlewares/requestid/main.go index ae058e3..c2cf576 100644 --- a/examples/middlewares/requestid/main.go +++ b/examples/middlewares/requestid/main.go @@ -6,6 +6,7 @@ import ( "github.com/foomo/keel" keelhttp "github.com/foomo/keel/net/http" "github.com/foomo/keel/net/http/middleware" + "github.com/foomo/keel/service" ) func main() { @@ -27,7 +28,7 @@ func main() { }) svr.AddService( - keel.NewServiceHTTP(l, "demo", "localhost:8080", svs, + service.NewHTTP(l, "demo", "localhost:8080", svs, middleware.RequestID( middleware.RequestIDWithSetResponseHeader(true), middleware.RequestIDWithGenerator(requestIDGenerator), diff --git a/examples/middlewares/responsetime/main.go b/examples/middlewares/responsetime/main.go index 70c72de..44a60f3 100644 --- a/examples/middlewares/responsetime/main.go +++ b/examples/middlewares/responsetime/main.go @@ -6,6 +6,7 @@ import ( "github.com/foomo/keel" "github.com/foomo/keel/net/http/middleware" + "github.com/foomo/keel/service" ) func main() { @@ -27,7 +28,7 @@ func main() { }) svr.AddService( - keel.NewServiceHTTP(l, "demo", "localhost:8080", svs, + service.NewHTTP(l, "demo", "localhost:8080", svs, middleware.ResponseTime( // automatically set cookie if not exists middleware.ResponseTimeWithMaxDuration(time.Millisecond*500), diff --git a/examples/middlewares/sessionid/main.go b/examples/middlewares/sessionid/main.go index 49cb4a2..984f53e 100644 --- a/examples/middlewares/sessionid/main.go +++ b/examples/middlewares/sessionid/main.go @@ -8,6 +8,7 @@ import ( keelhttp "github.com/foomo/keel/net/http" "github.com/foomo/keel/net/http/cookie" "github.com/foomo/keel/net/http/middleware" + "github.com/foomo/keel/service" ) func main() { @@ -44,7 +45,7 @@ func main() { }) svr.AddService( - keel.NewServiceHTTP(l, "demo", "localhost:8080", svs, + service.NewHTTP(l, "demo", "localhost:8080", svs, middleware.SessionID( // automatically set cookie if not exists middleware.SessionIDWithSetCookie(true), diff --git a/examples/middlewares/skip/main.go b/examples/middlewares/skip/main.go index 9402df8..4d03e13 100644 --- a/examples/middlewares/skip/main.go +++ b/examples/middlewares/skip/main.go @@ -3,6 +3,7 @@ package main import ( "net/http" + "github.com/foomo/keel/service" "go.uber.org/zap" "github.com/foomo/keel" @@ -28,7 +29,7 @@ func main() { svr.AddServices( // with URI blacklist - keel.NewServiceHTTP(l, "demo", "localhost:8080", svs, + service.NewHTTP(l, "demo", "localhost:8080", svs, middleware.Skip( func(l *zap.Logger, name string, next http.Handler) http.Handler { return http.NotFoundHandler() @@ -38,7 +39,7 @@ func main() { ), // with URI whitelist - keel.NewServiceHTTP(l, "demo", ":8081", svs, + service.NewHTTP(l, "demo", "localhost:8081", svs, middleware.Skip( func(l *zap.Logger, name string, next http.Handler) http.Handler { return http.NotFoundHandler() diff --git a/examples/middlewares/telemetry/main.go b/examples/middlewares/telemetry/main.go index 14f90b4..d43e2ec 100644 --- a/examples/middlewares/telemetry/main.go +++ b/examples/middlewares/telemetry/main.go @@ -5,6 +5,7 @@ import ( "github.com/foomo/keel" "github.com/foomo/keel/net/http/middleware" + "github.com/foomo/keel/service" ) func main() { @@ -23,7 +24,7 @@ func main() { }) svr.AddService( - keel.NewServiceHTTP(l, "demo", "localhost:8080", svs, + service.NewHTTP(l, "demo", "localhost:8080", svs, middleware.Telemetry( middleware.TelemetryWithInjectPropagationHeader(true), ), diff --git a/examples/middlewares/tokenauthfromcookie/main.go b/examples/middlewares/tokenauthfromcookie/main.go index 1020c56..eb1bdc0 100644 --- a/examples/middlewares/tokenauthfromcookie/main.go +++ b/examples/middlewares/tokenauthfromcookie/main.go @@ -5,6 +5,7 @@ import ( "github.com/foomo/keel" "github.com/foomo/keel/net/http/middleware" + "github.com/foomo/keel/service" ) func main() { @@ -26,7 +27,7 @@ func main() { tokenProvider := middleware.CookieTokenProvider("keel-token") svr.AddService( - keel.NewServiceHTTP(l, "demo", "localhost:8080", svs, + service.NewHTTP(l, "demo", "localhost:8080", svs, middleware.TokenAuth( token, middleware.TokenAuthWithTokenProvider(tokenProvider), diff --git a/examples/middlewares/tokenauthfromheader/main.go b/examples/middlewares/tokenauthfromheader/main.go index 1e14572..b3e8717 100644 --- a/examples/middlewares/tokenauthfromheader/main.go +++ b/examples/middlewares/tokenauthfromheader/main.go @@ -5,6 +5,7 @@ import ( "github.com/foomo/keel" "github.com/foomo/keel/net/http/middleware" + "github.com/foomo/keel/service" ) func main() { @@ -29,7 +30,7 @@ func main() { ) svr.AddService( - keel.NewServiceHTTP(l, "demo", "localhost:8080", svs, + service.NewHTTP(l, "demo", "localhost:8080", svs, middleware.TokenAuth( token, middleware.TokenAuthWithTokenProvider(tokenProvider), diff --git a/examples/remoteconfig/main.go b/examples/remoteconfig/main.go index 6cdddc9..3e5b43b 100644 --- a/examples/remoteconfig/main.go +++ b/examples/remoteconfig/main.go @@ -6,6 +6,7 @@ import ( "github.com/foomo/keel" "github.com/foomo/keel/config" + "github.com/foomo/keel/service" ) func main() { @@ -42,7 +43,7 @@ func main() { // curl localhost:8080 svr.AddService( - keel.NewServiceHTTP(l, "demo", "localhost:8080", http.HandlerFunc( + service.NewHTTP(l, "demo", "localhost:8080", http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { fmt.Println("current foo:", fooFn()) //nolint:forbidigo }), diff --git a/examples/roundtripwares/logger/server.go b/examples/roundtripwares/logger/server.go index 676f4b2..5262ee0 100644 --- a/examples/roundtripwares/logger/server.go +++ b/examples/roundtripwares/logger/server.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/foomo/keel" + "github.com/foomo/keel/service" ) func server() { @@ -26,7 +27,7 @@ func server() { }) svr.AddService( - keel.NewServiceHTTP(l, "demo", "localhost:8080", svs), + service.NewHTTP(l, "demo", "localhost:8080", svs), ) svr.Run() diff --git a/examples/roundtripwares/requestid/main.go b/examples/roundtripwares/requestid/main.go index a928892..7fea6d8 100644 --- a/examples/roundtripwares/requestid/main.go +++ b/examples/roundtripwares/requestid/main.go @@ -8,6 +8,7 @@ import ( keelhttp "github.com/foomo/keel/net/http" "github.com/foomo/keel/net/http/middleware" "github.com/foomo/keel/net/http/roundtripware" + "github.com/foomo/keel/service" httputils "github.com/foomo/keel/utils/net/http" ) @@ -52,7 +53,7 @@ func main() { }) svr.AddService( - keel.NewServiceHTTP(l, "demo", "localhost:8080", svs, + service.NewHTTP(l, "demo", "localhost:8080", svs, // add middleware middleware.RequestID(), // add middleware diff --git a/examples/roundtripwares/retry/server.go b/examples/roundtripwares/retry/server.go index 70a270d..8133f94 100644 --- a/examples/roundtripwares/retry/server.go +++ b/examples/roundtripwares/retry/server.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/foomo/keel" + "github.com/foomo/keel/service" ) func server() { @@ -27,7 +28,7 @@ func server() { }) svr.AddService( - keel.NewServiceHTTP(l, "demo", "localhost:8080", svs), + service.NewHTTP(l, "demo", "localhost:8080", svs), ) svr.Run() diff --git a/examples/serviceenabler/main.go b/examples/serviceenabler/main.go index 289941e..592685d 100644 --- a/examples/serviceenabler/main.go +++ b/examples/serviceenabler/main.go @@ -5,6 +5,7 @@ import ( "github.com/foomo/keel" "github.com/foomo/keel/config" + "github.com/foomo/keel/service" ) func main() { @@ -23,7 +24,7 @@ func main() { }) svr.AddServices( - keel.NewServiceHTTP(l, "demo", "localhost:8080", + service.NewHTTP(l, "demo", "localhost:8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c.Set("service.enabled", !enabled()) w.WriteHeader(http.StatusOK) @@ -32,7 +33,7 @@ func main() { ), keel.NewServiceEnabler(l, "service-enabler", func() keel.Service { - return keel.NewServiceHTTP(l, "service", "localhost:8081", svs) + return service.NewHTTP(l, "service", "localhost:8081", svs) }, enabled, ), diff --git a/examples/services/main.go b/examples/services/main.go index 0e1e00e..6448678 100644 --- a/examples/services/main.go +++ b/examples/services/main.go @@ -1,7 +1,6 @@ package main import ( - "net/http" "os" "github.com/foomo/keel" @@ -29,24 +28,11 @@ func main() { keel.WithHTTPPProfService(false), ) - l := svr.Logger() - // alternatively you can add them manually // svr.AddServices(keel.NewDefaultServiceHTTPZap()) // svr.AddServices(keel.NewDefaultServiceHTTPViper()) // svr.AddServices(keel.NewDefaultServiceHTTPPProf()) // svr.AddServices(keel.NewDefaultServiceHTTPPrometheus()) - // create demo service - svs := http.NewServeMux() - svs.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte("OK")) - }) - - svr.AddService( - keel.NewServiceHTTP(l, "demo", ":8080", svs), - ) - svr.Run() } diff --git a/examples/stream/jetstream/main.go b/examples/stream/jetstream/main.go index 40e6e1f..b7f88a8 100644 --- a/examples/stream/jetstream/main.go +++ b/examples/stream/jetstream/main.go @@ -5,6 +5,7 @@ import ( "net/http" "time" + "github.com/foomo/keel/service" "github.com/nats-io/nats.go" "github.com/pkg/errors" "go.uber.org/zap" @@ -89,7 +90,7 @@ func main() { svr.AddClosers(subscription, stream) svr.AddService( - keel.NewServiceHTTP(l, "demo", "localhost:8080", svs), + service.NewHTTP(l, "demo", "localhost:8080", svs), ) svr.Run() diff --git a/examples/stream/jetstreamraw/main.go b/examples/stream/jetstreamraw/main.go index 710a89c..b46a635 100644 --- a/examples/stream/jetstreamraw/main.go +++ b/examples/stream/jetstreamraw/main.go @@ -4,6 +4,7 @@ import ( "net/http" "time" + "github.com/foomo/keel/service" "github.com/nats-io/nats.go" "github.com/foomo/keel" @@ -73,7 +74,7 @@ func main() { svr.AddClosers(subscription, stream.Conn()) svr.AddService( - keel.NewServiceHTTP(l, "demo", "localhost:8080", svs), + service.NewHTTP(l, "demo", "localhost:8080", svs), ) svr.Run() diff --git a/examples/telemetry/main.go b/examples/telemetry/main.go index fa4f37d..2b04bbf 100644 --- a/examples/telemetry/main.go +++ b/examples/telemetry/main.go @@ -4,6 +4,9 @@ import ( "math/rand" "net/http" + "github.com/foomo/keel/service" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/metric/instrument" @@ -58,6 +61,14 @@ func main() { }) } + promauto.NewCounter(prometheus.CounterOpts{ + Namespace: "foo", + Subsystem: "", + Name: "bar", + Help: "blubb", + ConstLabels: nil, + }) + { // up down upDown, err := meter.SyncInt64().UpDownCounter( "a.updown", @@ -92,7 +103,7 @@ func main() { } svr.AddService( - keel.NewServiceHTTP(l, "demo", "localhost:8080", svs, + service.NewHTTP(l, "demo", "localhost:8080", svs, middleware.Telemetry(), middleware.Recover(), ), diff --git a/go.mod b/go.mod index aa296e2..5f156ac 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.20 require ( github.com/avast/retry-go v3.0.0+incompatible github.com/davecgh/go-spew v1.1.1 + github.com/fbiville/markdown-table-formatter v0.3.0 github.com/foomo/gotsrpc/v2 v2.7.2 github.com/go-logr/logr v1.2.4 github.com/golang-jwt/jwt v3.2.2+incompatible diff --git a/go.sum b/go.sum index 18f8eef..4a0c14f 100644 --- a/go.sum +++ b/go.sum @@ -117,6 +117,8 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fbiville/markdown-table-formatter v0.3.0 h1:PIm1UNgJrFs8q1htGTw+wnnNYvwXQMMMIKNZop2SSho= +github.com/fbiville/markdown-table-formatter v0.3.0/go.mod h1:q89TDtSEVDdTaufgSbfHpNVdPU/bmfvqNkrC5HagmLY= github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o= github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/foomo/gotsrpc/v2 v2.7.2 h1:a94V/a8LSssq+aRN3Fv1lJPjWoyMilOvRq+yEaDTHVM= 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/healthz/docs.go b/healthz/docs.go new file mode 100644 index 0000000..59c42b6 --- /dev/null +++ b/healthz/docs.go @@ -0,0 +1 @@ +package healthz diff --git a/healthzer.go b/healthz/healthzer.go similarity index 87% rename from healthzer.go rename to healthz/healthzer.go index 3156353..5ed08cf 100644 --- a/healthzer.go +++ b/healthz/healthzer.go @@ -1,4 +1,4 @@ -package keel +package healthz import "context" @@ -16,6 +16,10 @@ func (h healther) Healthz(ctx context.Context) error { return h.handle(ctx) } +func (h healther) Close(ctx context.Context) error { + return h.handle(ctx) +} + // BoolHealthzer interface type BoolHealthzer interface { Healthz() bool diff --git a/healthztype.go b/healthz/type.go similarity index 70% rename from healthztype.go rename to healthz/type.go index 848edc0..e5c45ba 100644 --- a/healthztype.go +++ b/healthz/type.go @@ -1,31 +1,31 @@ -package keel +package healthz -// HealthzType type +// Type type // https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ -type HealthzType string +type Type string const ( - // HealthzTypeAlways will run on any checks - HealthzTypeAlways HealthzType = "always" - // HealthzTypeStartup will run on /healthz/startup checks + // TypeAlways will run on any checks + TypeAlways Type = "always" + // TypeStartup will run on /healthz/startup checks // > The kubelet uses startup probes to know when a container application has started. If such a probe is configured, // > it disables liveness and readiness checks until it succeeds, making sure those probes don't interfere with the // > application startup. This can be used to adopt liveness checks on slow starting containers, avoiding them getting // > killed by the kubelet before they are up and running. - HealthzTypeStartup HealthzType = "startup" - // HealthzTypeReadiness will run on /healthz/readiness checks + TypeStartup Type = "startup" + // TypeReadiness will run on /healthz/readiness checks // > The kubelet uses readiness probes to know when a container is ready to start accepting traffic. // > A Pod is considered ready when all of its containers are ready. One use of this signal is to control // > which Pods are used as backends for Services. When a Pod is not ready, it is removed from Service load balancers. - HealthzTypeReadiness HealthzType = "readiness" - // HealthzTypeLiveness will run on /healthz/liveness checks + TypeReadiness Type = "readiness" + // TypeLiveness will run on /healthz/liveness checks // > The kubelet uses liveness probes to know when to restart a container. For example, liveness probes could catch // > a deadlock, where an application is running, but unable to make progress. Restarting a container in such a state // > can help to make the application more available despite bugs. - HealthzTypeLiveness HealthzType = "liveness" + TypeLiveness Type = "liveness" ) // String interface -func (t HealthzType) String() string { +func (t Type) String() string { return string(t) } diff --git a/interfaces/closer.go b/interfaces/closer.go new file mode 100644 index 0000000..abbc719 --- /dev/null +++ b/interfaces/closer.go @@ -0,0 +1,25 @@ +package interfaces + +import ( + "context" +) + +// Closer interface +type Closer interface { + Close() +} + +// ErrorCloser interface +type ErrorCloser interface { + Close() error +} + +// CloserWithContext interface +type CloserWithContext interface { + Close(ctx context.Context) +} + +// ErrorCloserWithContext interface +type ErrorCloserWithContext interface { + Close(ctx context.Context) error +} diff --git a/interfaces/doc.go b/interfaces/doc.go new file mode 100644 index 0000000..08badf2 --- /dev/null +++ b/interfaces/doc.go @@ -0,0 +1 @@ +package interfaces 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/pinger.go b/interfaces/pinger.go similarity index 91% rename from pinger.go rename to interfaces/pinger.go index 5d97f09..7cd2b7c 100644 --- a/pinger.go +++ b/interfaces/pinger.go @@ -1,4 +1,4 @@ -package keel +package interfaces import "context" diff --git a/interfaces/readmer.go b/interfaces/readmer.go new file mode 100644 index 0000000..a16ba1f --- /dev/null +++ b/interfaces/readmer.go @@ -0,0 +1,20 @@ +package interfaces + +// Readmer interface +type Readmer interface { + Readme() string +} + +type ReadmeHandler struct { + Value func() string +} + +func (r ReadmeHandler) Readme() string { + return r.Value() +} + +func ReadmeFunc(v func() string) ReadmeHandler { + return ReadmeHandler{ + Value: v, + } +} diff --git a/shutdowner.go b/interfaces/shutdowner.go similarity index 95% rename from shutdowner.go rename to interfaces/shutdowner.go index 08538d7..b587b86 100644 --- a/shutdowner.go +++ b/interfaces/shutdowner.go @@ -1,4 +1,4 @@ -package keel +package interfaces import "context" diff --git a/stopper.go b/interfaces/stopper.go similarity index 94% rename from stopper.go rename to interfaces/stopper.go index 93b49f3..6ada3d7 100644 --- a/stopper.go +++ b/interfaces/stopper.go @@ -1,4 +1,4 @@ -package keel +package interfaces import "context" diff --git a/unsubscriber.go b/interfaces/unsubscriber.go similarity index 95% rename from unsubscriber.go rename to interfaces/unsubscriber.go index 2d235f8..6395664 100644 --- a/unsubscriber.go +++ b/interfaces/unsubscriber.go @@ -1,4 +1,4 @@ -package keel +package interfaces import "context" diff --git a/log/fields_keel.go b/log/fields_keel.go new file mode 100644 index 0000000..81352ee --- /dev/null +++ b/log/fields_keel.go @@ -0,0 +1,11 @@ +package log + +import ( + "go.opentelemetry.io/otel/attribute" +) + +const ( + KeelServiceTypeKey = attribute.Key("keel.service.type") + KeelServiceNameKey = attribute.Key("keel.service.name") + KeelServiceInstKey = attribute.Key("keel.service.inst") +) diff --git a/log/fields_service.go b/log/fields_service.go index 3c20e7b..0496e91 100644 --- a/log/fields_service.go +++ b/log/fields_service.go @@ -15,6 +15,8 @@ func FPeerService(name string) zap.Field { } const ( + ServiceTypeKey = "service_type" + // ServiceNameKey represents the NameKey of the service. ServiceNameKey = "service_name" @@ -35,6 +37,10 @@ const ( ServiceVersionKey = "service_version" ) +func FServiceType(name string) zap.Field { + return zap.String(ServiceTypeKey, name) +} + func FServiceName(name string) zap.Field { return zap.String(ServiceNameKey, name) } diff --git a/log/with.go b/log/with.go index ed236ef..cf23441 100644 --- a/log/with.go +++ b/log/with.go @@ -7,6 +7,7 @@ import ( "net/http" "strings" + "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "go.uber.org/zap" @@ -20,6 +21,17 @@ func With(l *zap.Logger, fields ...zap.Field) *zap.Logger { return l.With(fields...) } +func WithAttributes(l *zap.Logger, attrs ...attribute.KeyValue) *zap.Logger { + if l == nil { + l = Logger() + } + fields := make([]zap.Field, len(attrs)) + for i, attr := range attrs { + fields[i] = zap.Any(strings.ReplaceAll(string(attr.Key), ".", "_"), attr.Value.AsInterface()) + } + return l.With(fields...) +} + func WithError(l *zap.Logger, err error) *zap.Logger { return With(l, FErrorType(err), FError(err)) } diff --git a/markdown/markdown.go b/markdown/markdown.go new file mode 100644 index 0000000..f195964 --- /dev/null +++ b/markdown/markdown.go @@ -0,0 +1,40 @@ +package markdown + +import ( + "fmt" + + markdowntable "github.com/fbiville/markdown-table-formatter/pkg/markdown" +) + +// Markdown output helper +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) +} diff --git a/markdown/utils.go b/markdown/utils.go new file mode 100644 index 0000000..bea89ff --- /dev/null +++ b/markdown/utils.go @@ -0,0 +1,28 @@ +package markdown + +import ( + "fmt" +) + +func Code(v string) string { + if v == "" { + return "" + } + return "`" + v + "`" +} + +func Name(v any) string { + if i, ok := v.(interface { + Name() string + }); ok { + return i.Name() + } + return "" +} + +func String(v any) string { + if i, ok := v.(fmt.Stringer); ok { + return i.String() + } + return "" +} diff --git a/metrics/readme.go b/metrics/readme.go new file mode 100644 index 0000000..9273505 --- /dev/null +++ b/metrics/readme.go @@ -0,0 +1,32 @@ +package metrics + +import ( + "github.com/foomo/keel/markdown" + "github.com/prometheus/client_golang/prometheus" +) + +func Readme() string { + md := markdown.Markdown{} + var rows [][]string + + if gatherer, err := prometheus.DefaultGatherer.Gather(); err == nil { + for _, value := range gatherer { + rows = append(rows, []string{ + markdown.Code(value.GetName()), + value.GetType().String(), + value.GetHelp(), + }) + } + } + + 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/net/stream/jetstream/readme.go b/net/stream/jetstream/readme.go new file mode 100644 index 0000000..75140d9 --- /dev/null +++ b/net/stream/jetstream/readme.go @@ -0,0 +1,62 @@ +package jetstream + +import ( + "github.com/foomo/keel/markdown" +) + +type ( + publisher struct { + Namespace string + Stream string + Subject string + } + subscriber struct { + Namespace string + Stream string + Subject string + } +) + +var ( + publishers []publisher + subscribers []subscriber +) + +func Readme() string { + if len(publishers) == 0 && len(subscribers) == 0 { + return "" + } + + var rows [][]string + md := &markdown.Markdown{} + md.Println("### NATS") + md.Println("") + md.Println("List of all registered nats publishers & subscribers.") + md.Println("") + + if len(publishers) > 0 { + for _, value := range publishers { + rows = append(rows, []string{ + markdown.Code(value.Namespace), + markdown.Code(value.Stream), + markdown.Code(value.Subject), + markdown.Code("publish"), + }) + } + } + + if len(subscribers) > 0 { + for _, value := range subscribers { + rows = append(rows, []string{ + markdown.Code(value.Namespace), + markdown.Code(value.Stream), + markdown.Code(value.Subject), + markdown.Code("subscribe"), + }) + } + } + + md.Table([]string{"Namespace", "Stream", "Subject", "Type"}, rows) + + return md.String() +} diff --git a/net/stream/jetstream/stream.go b/net/stream/jetstream/stream.go index 37048fd..129d330 100644 --- a/net/stream/jetstream/stream.go +++ b/net/stream/jetstream/stream.go @@ -2,6 +2,7 @@ package jetstream import ( "encoding/json" + "slices" "time" "github.com/nats-io/nats.go" @@ -280,6 +281,20 @@ func (s *Stream) Publisher(subject string, opts ...PublisherOption) *Publisher { opt(pub) } } + + { // append to recoreded publishers + value := publisher{ + Stream: s.name, + Namespace: s.namespace, + Subject: subject, + } + if !slices.ContainsFunc(publishers, func(p publisher) bool { + return p.Stream == value.Stream && p.Namespace == value.Namespace && p.Subject == value.Subject + }) { + publishers = append(publishers, value) + } + } + return pub } @@ -295,6 +310,20 @@ func (s *Stream) Subscriber(subject string, opts ...SubscriberOption) *Subscribe opt(sub) } } + + { // append to recoreded publishers + value := subscriber{ + Stream: s.name, + Namespace: s.namespace, + Subject: subject, + } + if !slices.ContainsFunc(subscribers, func(p subscriber) bool { + return p.Stream == value.Stream && p.Namespace == value.Namespace && p.Subject == value.Subject + }) { + subscribers = append(subscribers, value) + } + } + return sub } diff --git a/option.go b/option.go index e519ea2..b00a486 100644 --- a/option.go +++ b/option.go @@ -5,6 +5,7 @@ import ( "os" "time" + "github.com/foomo/keel/service" "github.com/spf13/viper" "go.uber.org/zap" @@ -72,9 +73,9 @@ func WithShutdownTimeout(shutdownTimeout time.Duration) Option { func WithHTTPZapService(enabled bool) Option { return func(inst *Server) { if config.GetBool(inst.Config(), "service.zap.enabled", enabled)() { - service := NewDefaultServiceHTTPZap() - inst.initServices = append(inst.initServices, service) - inst.AddAlwaysHealthzers(service) + svs := service.NewDefaultHTTPZap(inst.Logger()) + inst.initServices = append(inst.initServices, svs) + inst.AddAlwaysHealthzers(svs) } } } @@ -83,9 +84,9 @@ func WithHTTPZapService(enabled bool) Option { func WithHTTPViperService(enabled bool) Option { return func(inst *Server) { if config.GetBool(inst.Config(), "service.viper.enabled", enabled)() { - service := NewDefaultServiceHTTPViper() - inst.initServices = append(inst.initServices, service) - inst.AddAlwaysHealthzers(service) + svs := service.NewDefaultHTTPViper(inst.Logger()) + inst.initServices = append(inst.initServices, svs) + inst.AddAlwaysHealthzers(svs) } } } @@ -149,9 +150,9 @@ func WithPrometheusMeter(enabled bool) Option { func WithHTTPPrometheusService(enabled bool) Option { return func(inst *Server) { if config.GetBool(inst.Config(), "service.prometheus.enabled", enabled)() { - service := NewDefaultServiceHTTPPrometheus() - inst.initServices = append(inst.initServices, service) - inst.AddAlwaysHealthzers(service) + svs := service.NewDefaultHTTPPrometheus(inst.Logger()) + inst.initServices = append(inst.initServices, svs) + inst.AddAlwaysHealthzers(svs) } } } @@ -160,9 +161,9 @@ func WithHTTPPrometheusService(enabled bool) Option { func WithHTTPPProfService(enabled bool) Option { return func(inst *Server) { if config.GetBool(inst.Config(), "service.pprof.enabled", enabled)() { - service := NewDefaultServiceHTTPPProf() - inst.initServices = append(inst.initServices, service) - inst.AddAlwaysHealthzers(service) + svs := service.NewDefaultHTTPPProf(inst.Logger()) + inst.initServices = append(inst.initServices, svs) + inst.AddAlwaysHealthzers(svs) } } } @@ -171,9 +172,20 @@ func WithHTTPPProfService(enabled bool) Option { func WithHTTPHealthzService(enabled bool) Option { return func(inst *Server) { if config.GetBool(inst.Config(), "service.healthz.enabled", enabled)() { - service := NewDefaultServiceHTTPProbes(inst.probes()) - inst.initServices = append(inst.initServices, service) - inst.AddAlwaysHealthzers(service) + svs := service.NewDefaultHTTPProbes(inst.Logger(), inst.probes()) + inst.initServices = append(inst.initServices, svs) + inst.AddAlwaysHealthzers(svs) + } + } +} + +// WithHTTPReadmeService option with default value +func WithHTTPReadmeService(enabled bool) Option { + return func(inst *Server) { + 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..89ae5f0 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" @@ -120,11 +121,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 != nil && 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..21a51f9 --- /dev/null +++ b/persistence/mongo/readme.go @@ -0,0 +1,42 @@ +package keelmongo + +import ( + "strings" + + "github.com/foomo/keel/markdown" +) + +var ( + dbs = map[string][]string{} + indices = map[string]map[string][]string{} +) + +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 df43165..b786b6a 100644 --- a/server.go +++ b/server.go @@ -6,11 +6,18 @@ import ( "net/http" "os" "os/signal" + "reflect" + "slices" "sync" "sync/atomic" "syscall" "time" + "github.com/foomo/keel/healthz" + "github.com/foomo/keel/interfaces" + "github.com/foomo/keel/markdown" + "github.com/foomo/keel/metrics" + "github.com/foomo/keel/service" "github.com/go-logr/logr" "github.com/pkg/errors" "github.com/spf13/viper" @@ -31,33 +38,36 @@ import ( // 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 - running atomic.Bool - syncClosers []interface{} - syncClosersLock sync.RWMutex - syncProbes map[HealthzType][]interface{} - syncProbesLock sync.RWMutex - ctx context.Context - ctxCancel context.Context - ctxCancelFn context.CancelFunc - g *errgroup.Group - gCtx context.Context - l *zap.Logger - c *viper.Viper + services []Service + initServices []Service + meter metric.Meter + meterProvider metric.MeterProvider + tracer trace.Tracer + traceProvider trace.TracerProvider + shutdownSignals []os.Signal + shutdownTimeout time.Duration + running atomic.Bool + syncClosers []interface{} + syncClosersLock sync.RWMutex + syncReadmers []interfaces.Readmer + syncReadmersLock sync.RWMutex + syncProbes map[healthz.Type][]interface{} + syncProbesLock sync.RWMutex + ctx context.Context + ctxCancel context.Context + ctxCancelFn context.CancelFunc + g *errgroup.Group + gCtx context.Context + l *zap.Logger + c *viper.Viper } func NewServer(opts ...Option) *Server { inst := &Server{ shutdownTimeout: 30 * time.Second, shutdownSignals: []os.Signal{os.Interrupt, syscall.SIGTERM}, - syncProbes: map[HealthzType][]interface{}{}, + syncReadmers: []interfaces.Readmer{}, + syncProbes: map[healthz.Type][]interface{}{}, ctx: context.Background(), c: config.Config(), l: log.Logger(), @@ -86,51 +96,51 @@ func NewServer(opts ...Option) *Server { for _, closer := range closers { l := inst.l.With(log.FName(fmt.Sprintf("%T", closer))) switch c := closer.(type) { - case Closer: + case interfaces.Closer: c.Close() - case ErrorCloser: + case interfaces.ErrorCloser: if err := c.Close(); err != nil { log.WithError(l, err).Error("failed to gracefully stop ErrorCloser") } - case CloserWithContext: + case interfaces.CloserWithContext: c.Close(timeoutCtx) - case ErrorCloserWithContext: + case interfaces.ErrorCloserWithContext: if err := c.Close(timeoutCtx); err != nil { log.WithError(l, err).Error("failed to gracefully stop ErrorCloserWithContext") } - case Shutdowner: + case interfaces.Shutdowner: c.Shutdown() - case ErrorShutdowner: + case interfaces.ErrorShutdowner: if err := c.Shutdown(); err != nil { log.WithError(l, err).Error("failed to gracefully stop ErrorShutdowner") } - case ShutdownerWithContext: + case interfaces.ShutdownerWithContext: c.Shutdown(timeoutCtx) - case ErrorShutdownerWithContext: + case interfaces.ErrorShutdownerWithContext: if err := c.Shutdown(timeoutCtx); err != nil { log.WithError(l, err).Error("failed to gracefully stop ErrorShutdownerWithContext") } - case Stopper: + case interfaces.Stopper: c.Stop() - case ErrorStopper: + case interfaces.ErrorStopper: if err := c.Stop(); err != nil { log.WithError(l, err).Error("failed to gracefully stop ErrorStopper") } - case StopperWithContext: + case interfaces.StopperWithContext: c.Stop(timeoutCtx) - case ErrorStopperWithContext: + case interfaces.ErrorStopperWithContext: if err := c.Stop(timeoutCtx); err != nil { log.WithError(l, err).Error("failed to gracefully stop ErrorStopperWithContext") } - case Unsubscriber: + case interfaces.Unsubscriber: c.Unsubscribe() - case ErrorUnsubscriber: + case interfaces.ErrorUnsubscriber: if err := c.Unsubscribe(); err != nil { log.WithError(l, err).Error("failed to gracefully stop ErrorUnsubscriber") } - case UnsubscriberWithContext: + case interfaces.UnsubscriberWithContext: c.Unsubscribe(timeoutCtx) - case ErrorUnsubscriberWithContext: + case interfaces.ErrorUnsubscriberWithContext: if err := c.Unsubscribe(timeoutCtx); err != nil { log.WithError(l, err).Error("failed to gracefully stop ErrorUnsubscriberWithContext") } @@ -168,6 +178,12 @@ func NewServer(opts ...Option) *Server { // add probe inst.AddAlwaysHealthzers(inst) + inst.AddReadmers( + interfaces.ReadmeFunc(env.Readme), + interfaces.ReadmeFunc(config.Readme), + inst, + interfaces.ReadmeFunc(metrics.Readme), + ) // start init services inst.startService(inst.initServices...) @@ -207,20 +223,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) } } @@ -231,25 +244,9 @@ func (s *Server) AddCloser(closer interface{}) { return } } - switch closer.(type) { - case Closer, - ErrorCloser, - CloserWithContext, - ErrorCloserWithContext, - Shutdowner, - ErrorShutdowner, - ShutdownerWithContext, - ErrorShutdownerWithContext, - Stopper, - ErrorStopper, - StopperWithContext, - ErrorStopperWithContext, - Unsubscriber, - ErrorUnsubscriber, - UnsubscriberWithContext, - ErrorUnsubscriberWithContext: + if IsCloser(closer) { s.addClosers(closer) - default: + } else { s.l.Warn("unable to add closer", log.FValue(fmt.Sprintf("%T", closer))) } } @@ -261,23 +258,29 @@ func (s *Server) AddClosers(closers ...interface{}) { } } +// AddReadmer adds a readmer to be added to the exposed readme +func (s *Server) AddReadmer(readmer interfaces.Readmer) { + s.addReadmers(readmer) +} + +// AddReadmers adds readmers to be added to the exposed readme +func (s *Server) AddReadmers(readmers ...interfaces.Readmer) { + for _, readmer := range readmers { + s.AddReadmer(readmer) + } +} + // AddHealthzer adds a probe to be called on healthz checks -func (s *Server) AddHealthzer(typ HealthzType, probe interface{}) { - switch probe.(type) { - case BoolHealthzer, - BoolHealthzerWithContext, - ErrorHealthzer, - ErrorHealthzWithContext, - ErrorPinger, - ErrorPingerWithContext: +func (s *Server) AddHealthzer(typ healthz.Type, probe interface{}) { + if IsHealthz(probe) { s.addProbes(typ, probe) - default: + } else { 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 HealthzType, probes ...interface{}) { +func (s *Server) AddHealthzers(typ healthz.Type, probes ...interface{}) { for _, probe := range probes { s.AddHealthzer(typ, probe) } @@ -285,22 +288,22 @@ func (s *Server) AddHealthzers(typ HealthzType, probes ...interface{}) { // AddAlwaysHealthzers adds the probes to be called on any healthz checks func (s *Server) AddAlwaysHealthzers(probes ...interface{}) { - s.AddHealthzers(HealthzTypeAlways, probes...) + s.AddHealthzers(healthz.TypeAlways, probes...) } // AddStartupHealthzers adds the startup probes to be called on healthz checks func (s *Server) AddStartupHealthzers(probes ...interface{}) { - s.AddHealthzers(HealthzTypeStartup, probes...) + s.AddHealthzers(healthz.TypeStartup, probes...) } // AddLivenessHealthzers adds the liveness probes to be called on healthz checks func (s *Server) AddLivenessHealthzers(probes ...interface{}) { - s.AddHealthzers(HealthzTypeLiveness, probes...) + s.AddHealthzers(healthz.TypeLiveness, probes...) } // AddReadinessHealthzers adds the readiness probes to be called on healthz checks func (s *Server) AddReadinessHealthzers(probes ...interface{}) { - s.AddHealthzers(HealthzTypeReadiness, probes...) + s.AddHealthzers(healthz.TypeReadiness, probes...) } // IsCanceled returns true if the internal errgroup has been canceled @@ -360,24 +363,51 @@ func (s *Server) addClosers(v ...interface{}) { s.syncClosers = append(s.syncClosers, v...) } -func (s *Server) probes() map[HealthzType][]interface{} { +func (s *Server) readmers() []interfaces.Readmer { + s.syncReadmersLock.RLock() + defer s.syncReadmersLock.RUnlock() + return s.syncReadmers +} + +func (s *Server) addReadmers(v ...interfaces.Readmer) { + s.syncReadmersLock.Lock() + defer s.syncReadmersLock.Unlock() + s.syncReadmers = append(s.syncReadmers, v...) +} + +func (s *Server) probes() map[healthz.Type][]interface{} { s.syncProbesLock.RLock() defer s.syncProbesLock.RUnlock() return s.syncProbes } -func (s *Server) addProbes(typ HealthzType, v ...interface{}) { +func (s *Server) addProbes(typ healthz.Type, v ...interface{}) { s.syncProbesLock.Lock() defer s.syncProbesLock.Unlock() s.syncProbes[typ] = append(s.syncProbes[typ], v...) } +// Readme returns the self-documenting string +func (s *Server) Readme() string { + md := &markdown.Markdown{} + + md.Println(s.readmeServices()) + md.Println(s.readmeHealthz()) + md.Print(s.readmeCloser()) + + 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") @@ -387,3 +417,137 @@ func (s *Server) startService(services ...Service) { }) } } + +func (s *Server) readmeCloser() string { + md := &markdown.Markdown{} + closers := s.closers() + rows := make([][]string, 0, len(closers)) + for _, value := range 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("") + } + + 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(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 runtime.") + md.Println("") + md.Table([]string{"Name", "Probe", "Type", "Description"}, rows) + } + + return md.String() +} + +func (s *Server) readmeServices() string { + md := &markdown.Markdown{} + + { + 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 { + t := reflect.TypeOf(value) + rows = append(rows, []string{ + markdown.Code(value.Name()), + markdown.Code(t.String()), + markdown.String(value), + }) + } + if len(rows) > 0 { + md.Println("### Runtime Services") + md.Println("") + md.Println("List of all registered services that are being started.") + md.Println("") + md.Table([]string{"Name", "Type", "Description"}, rows) + } + } + + return md.String() +} diff --git a/server_test.go b/server_test.go index c998dbe..55d48b4 100644 --- a/server_test.go +++ b/server_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + "github.com/foomo/keel/service" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "go.uber.org/zap" @@ -74,7 +75,7 @@ func (s *KeelTestSuite) TearDownSuite() {} func (s *KeelTestSuite) TestServiceHTTP() { s.svr.AddServices( - keel.NewServiceHTTP(s.l, "test", ":55000", s.mux), + service.NewHTTP(s.l, "test", ":55000", s.mux), ) s.runServer() @@ -86,8 +87,8 @@ func (s *KeelTestSuite) TestServiceHTTP() { func (s *KeelTestSuite) TestServiceHTTPZap() { s.svr.AddServices( - keel.NewServiceHTTPZap(s.l, "zap", ":9100", "/log"), - keel.NewServiceHTTP(s.l, "test", ":55000", s.mux), + service.NewHTTPZap(s.l, "zap", ":9100", "/log"), + service.NewHTTP(s.l, "test", ":55000", s.mux), ) s.runServer() @@ -141,7 +142,7 @@ func (s *KeelTestSuite) TestServiceHTTPZap() { func (s *KeelTestSuite) TestGraceful() { s.svr.AddServices( - keel.NewServiceHTTP(s.l, "test", ":55000", s.mux), + service.NewHTTP(s.l, "test", ":55000", s.mux), ) s.runServer() diff --git a/service/errors.go b/service/errors.go new file mode 100644 index 0000000..53f93a8 --- /dev/null +++ b/service/errors.go @@ -0,0 +1,10 @@ +package service + +import ( + "github.com/pkg/errors" +) + +var ( + ErrServiceNotRunning = errors.New("service not running") + ErrServiceShutdown = errors.New("service shutdown") +) diff --git a/service/goroutine.go b/service/goroutine.go new file mode 100644 index 0000000..ef895b7 --- /dev/null +++ b/service/goroutine.go @@ -0,0 +1,110 @@ +package service + +import ( + "context" + "fmt" + "sync" + "sync/atomic" + + "go.uber.org/zap" + "golang.org/x/sync/errgroup" + + "github.com/foomo/keel/log" +) + +// GoRoutine struct +type ( + GoRoutine struct { + running atomic.Bool + handler GoRoutineFn + cancel context.CancelCauseFunc + cancelLock sync.Mutex + parallel int + name string + wg errgroup.Group + l *zap.Logger + } + GoRoutineOption func(*GoRoutine) + GoRoutineFn func(ctx context.Context, l *zap.Logger) error +) + +func NewGoRoutine(l *zap.Logger, name string, handler GoRoutineFn, opts ...GoRoutineOption) *GoRoutine { + if l == nil { + l = log.Logger() + } + // enrich the log + l = log.WithAttributes(l, + log.KeelServiceTypeKey.String("goroutine"), + log.KeelServiceNameKey.String(name), + ) + + inst := &GoRoutine{ + handler: handler, + name: name, + parallel: 1, + l: l, + } + + for _, opt := range opts { + opt(inst) + } + + return inst +} + +// ------------------------------------------------------------------------------------------------ +// ~ Options +// ------------------------------------------------------------------------------------------------ + +func GoRoutineWithPralllel(v int) GoRoutineOption { + return func(o *GoRoutine) { + o.parallel = v + } +} + +// ------------------------------------------------------------------------------------------------ +// ~ Public methods +// ------------------------------------------------------------------------------------------------ + +func (s *GoRoutine) Name() string { + return s.name +} + +func (s *GoRoutine) Healthz() error { + if !s.running.Load() { + return ErrServiceNotRunning + } + return nil +} + +func (s *GoRoutine) String() string { + return fmt.Sprintf("parallel: `%d`", s.parallel) +} + +func (s *GoRoutine) Start(ctx context.Context) error { + s.l.Info("starting keel service") + ctx, cancel := context.WithCancelCause(ctx) + s.cancelLock.Lock() + s.cancel = cancel + s.cancelLock.Unlock() + for i := 0; i < s.parallel; i++ { + i := i + l := log.WithAttributes(s.l, log.KeelServiceInstKey.Int(i)) + s.wg.Go(func() error { + return s.handler(ctx, l) + }) + } + s.running.Store(true) + defer func() { + s.running.Store(false) + }() + return s.wg.Wait() +} + +func (s *GoRoutine) Close(ctx context.Context) error { + s.l.Info("stopping keel service") + s.cancelLock.Lock() + s.cancel(ErrServiceShutdown) + s.cancelLock.Unlock() + return s.wg.Wait() +} diff --git a/service/goroutine_test.go b/service/goroutine_test.go new file mode 100644 index 0000000..6257789 --- /dev/null +++ b/service/goroutine_test.go @@ -0,0 +1,50 @@ +package service_test + +import ( + "context" + "sync" + "time" + + "github.com/foomo/keel" + "github.com/foomo/keel/service" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +func ExampleNewGoRoutine() { + var once sync.Once + + svr := keel.NewServer( + keel.WithLogger(zap.NewExample()), + ) + + svr.AddService( + service.NewGoRoutine(svr.Logger(), "demo", func(ctx context.Context, l *zap.Logger) error { + for { + // handle graceful shutdowns + if err := ctx.Err(); errors.Is(context.Cause(ctx), service.ErrServiceShutdown) { + l.Info("context has been canceled du to graceful shutdow") + return nil + } else if err != nil { + return errors.Wrap(err, "unexpected context error") + } + + l.Info("ping") + time.Sleep(time.Second) + once.Do(shutdown) + } + }), + ) + + svr.Run() + + // Output: + // {"level":"info","msg":"starting keel server"} + // {"level":"info","msg":"starting keel service","keel_service_type":"goroutine","keel_service_name":"demo"} + // {"level":"info","msg":"ping","keel_service_type":"goroutine","keel_service_name":"demo","keel_service_inst":0} + // {"level":"info","msg":"ping","keel_service_type":"goroutine","keel_service_name":"demo","keel_service_inst":0} + // {"level":"debug","msg":"keel graceful shutdown"} + // {"level":"info","msg":"stopping keel service","keel_service_type":"goroutine","keel_service_name":"demo"} + // {"level":"info","msg":"context has been canceled du to graceful shutdow","keel_service_type":"goroutine","keel_service_name":"demo","keel_service_inst":0} + // {"level":"info","msg":"keel server stopped"} +} diff --git a/service/helper_test.go b/service/helper_test.go new file mode 100644 index 0000000..32a00d9 --- /dev/null +++ b/service/helper_test.go @@ -0,0 +1,42 @@ +package service_test + +import ( + "io" + "net" + "net/http" + "syscall" + "time" +) + +// shutdown example after the given time +func shutdownAfter(duration time.Duration) { + go func() { + time.Sleep(duration) + shutdown() + }() +} + +func shutdown() { + if err := syscall.Kill(syscall.Getpid(), syscall.SIGINT); err != nil { + panic(err) + } +} + +func waitFor(addr string) { + if _, err := net.DialTimeout("tcp", addr, 10*time.Second); err != nil { + panic(err.Error()) + } +} + +func httpGet(url string) string { + resp, err := http.Get(url) //nolint:all + if err != nil { + panic(err.Error()) + } + defer resp.Body.Close() + b, err := io.ReadAll(resp.Body) + if err != nil { + panic(err.Error()) + } + return string(b) +} diff --git a/servicehttp.go b/service/http.go similarity index 52% rename from servicehttp.go rename to service/http.go index a1e3cc4..e955f6e 100644 --- a/servicehttp.go +++ b/service/http.go @@ -1,7 +1,8 @@ -package keel +package service import ( "context" + "fmt" "net" "net/http" "strings" @@ -14,22 +15,25 @@ import ( "github.com/foomo/keel/net/http/middleware" ) -// ServiceHTTP struct -type ServiceHTTP struct { +// HTTP struct +type HTTP struct { running atomic.Bool server *http.Server name string l *zap.Logger } -func NewServiceHTTP(l *zap.Logger, name, addr string, handler http.Handler, middlewares ...middleware.Middleware) *ServiceHTTP { +func NewHTTP(l *zap.Logger, name, addr string, handler http.Handler, middlewares ...middleware.Middleware) *HTTP { if l == nil { l = log.Logger() } // enrich the log - l = log.WithHTTPServerName(l, name) + l = log.WithAttributes(l, + log.KeelServiceTypeKey.String("http"), + log.KeelServiceNameKey.String(name), + ) - return &ServiceHTTP{ + return &HTTP{ server: &http.Server{ Addr: addr, ErrorLog: zap.NewStdLog(l), @@ -40,18 +44,22 @@ func NewServiceHTTP(l *zap.Logger, name, addr string, handler http.Handler, midd } } -func (s *ServiceHTTP) Name() string { +func (s *HTTP) Name() string { return s.name } -func (s *ServiceHTTP) Healthz() error { +func (s *HTTP) Healthz() error { if !s.running.Load() { return ErrServiceNotRunning } return nil } -func (s *ServiceHTTP) Start(ctx context.Context) error { +func (s *HTTP) String() string { + return fmt.Sprintf("`%T` on `%s`", s.server.Handler, s.server.Addr) +} + +func (s *HTTP) Start(ctx context.Context) error { var fields []zap.Field if value := strings.Split(s.server.Addr, ":"); len(value) == 2 { ip, port := value[0], value[1] @@ -60,20 +68,24 @@ func (s *ServiceHTTP) Start(ctx context.Context) error { } fields = append(fields, log.FNetHostIP(ip), log.FNetHostPort(port)) } - s.l.Info("starting http service", fields...) + s.l.Info("starting keel service", fields...) s.server.BaseContext = func(_ net.Listener) context.Context { return ctx } s.server.RegisterOnShutdown(func() { s.running.Store(false) }) s.running.Store(true) - if err := s.server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { - log.WithError(s.l, err).Error("service error") - return err + if err := s.server.ListenAndServe(); errors.Is(err, http.ErrServerClosed) { + return nil + } else if err != nil { + return errors.Wrap(err, "failed to start service") } return nil } -func (s *ServiceHTTP) Close(ctx context.Context) error { - s.l.Info("stopping http service") - return s.server.Shutdown(ctx) +func (s *HTTP) Close(ctx context.Context) error { + s.l.Info("stopping keel service") + if err := s.server.Shutdown(ctx); err != nil { + return errors.Wrap(err, "failed to stop service") + } + return nil } diff --git a/service/http_test.go b/service/http_test.go new file mode 100644 index 0000000..e899744 --- /dev/null +++ b/service/http_test.go @@ -0,0 +1,40 @@ +package service_test + +import ( + "net/http" + + "github.com/foomo/keel" + "github.com/foomo/keel/service" + "go.uber.org/zap" +) + +func ExampleNewHTTP() { + svr := keel.NewServer( + keel.WithLogger(zap.NewExample()), + ) + + l := svr.Logger() + + svr.AddService( + service.NewHTTP(l, "demo", "localhost:8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("OK")) + })), + ) + + go func() { + waitFor("localhost:8080") + l.Info(httpGet("http://localhost:8080")) + shutdown() + }() + + svr.Run() + + // Output: + // {"level":"info","msg":"starting keel server"} + // {"level":"info","msg":"starting keel service","keel_service_type":"http","keel_service_name":"demo","net_host_ip":"localhost","net_host_port":"8080"} + // {"level":"info","msg":"OK"} + // {"level":"debug","msg":"keel graceful shutdown"} + // {"level":"info","msg":"stopping keel service","keel_service_type":"http","keel_service_name":"demo"} + // {"level":"info","msg":"keel server stopped"} +} diff --git a/servicehttphealthz.go b/service/httphealthz.go similarity index 67% rename from servicehttphealthz.go rename to service/httphealthz.go index 1bdfe96..09c5606 100644 --- a/servicehttphealthz.go +++ b/service/httphealthz.go @@ -1,19 +1,21 @@ -package keel +package service import ( "context" "errors" "net/http" + "github.com/foomo/keel/healthz" + "github.com/foomo/keel/interfaces" "go.uber.org/zap" "github.com/foomo/keel/log" ) const ( - DefaultServiceHTTPHealthzName = "healthz" - DefaultServiceHTTPHealthzAddr = ":9400" - DefaultServiceHTTPHealthzPath = "/healthz" + DefaultHTTPHealthzName = "healthz" + DefaultHTTPHealthzAddr = ":9400" + DefaultHTTPHealthzPath = "/healthz" ) var ( @@ -24,7 +26,7 @@ var ( ErrStartupProbeFailed = errors.New("startup probe failed") ) -func NewServiceHTTPHealthz(l *zap.Logger, name, addr, path string, probes map[HealthzType][]interface{}) *ServiceHTTP { +func NewHealthz(l *zap.Logger, name, addr, path string, probes map[healthz.Type][]interface{}) *HTTP { handler := http.NewServeMux() unavailable := func(l *zap.Logger, w http.ResponseWriter, r *http.Request, err error) { @@ -36,17 +38,17 @@ func NewServiceHTTPHealthz(l *zap.Logger, name, addr, path string, probes map[He call := func(ctx context.Context, probe interface{}) (bool, error) { switch h := probe.(type) { - case BoolHealthzer: + case healthz.BoolHealthzer: return h.Healthz(), nil - case BoolHealthzerWithContext: + case healthz.BoolHealthzerWithContext: return h.Healthz(ctx), nil - case ErrorHealthzer: + case healthz.ErrorHealthzer: return true, h.Healthz() - case ErrorHealthzWithContext: + case healthz.ErrorHealthzWithContext: return true, h.Healthz(ctx) - case ErrorPinger: + case interfaces.ErrorPinger: return true, h.Ping() - case ErrorPingerWithContext: + case interfaces.ErrorPingerWithContext: return true, h.Ping(ctx) default: return false, ErrUnhandledHealthzProbe @@ -55,7 +57,7 @@ func NewServiceHTTPHealthz(l *zap.Logger, name, addr, path string, probes map[He handler.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { for typ, values := range probes { - if typ == HealthzTypeStartup { + if typ == healthz.TypeStartup { continue } for _, p := range values { @@ -72,12 +74,12 @@ func NewServiceHTTPHealthz(l *zap.Logger, name, addr, path string, probes map[He _, _ = w.Write([]byte("OK")) }) - handler.HandleFunc(path+"/"+HealthzTypeLiveness.String(), func(w http.ResponseWriter, r *http.Request) { + handler.HandleFunc(path+"/"+healthz.TypeLiveness.String(), func(w http.ResponseWriter, r *http.Request) { var ps []interface{} - if p, ok := probes[HealthzTypeAlways]; ok { + if p, ok := probes[healthz.TypeAlways]; ok { ps = append(ps, p...) } - if p, ok := probes[HealthzTypeLiveness]; ok { + if p, ok := probes[healthz.TypeLiveness]; ok { ps = append(ps, p...) } for _, p := range ps { @@ -93,12 +95,12 @@ func NewServiceHTTPHealthz(l *zap.Logger, name, addr, path string, probes map[He _, _ = w.Write([]byte("OK")) }) - handler.HandleFunc(path+"/"+HealthzTypeReadiness.String(), func(w http.ResponseWriter, r *http.Request) { + handler.HandleFunc(path+"/"+healthz.TypeReadiness.String(), func(w http.ResponseWriter, r *http.Request) { var ps []interface{} - if p, ok := probes[HealthzTypeAlways]; ok { + if p, ok := probes[healthz.TypeAlways]; ok { ps = append(ps, p...) } - if p, ok := probes[HealthzTypeReadiness]; ok { + if p, ok := probes[healthz.TypeReadiness]; ok { ps = append(ps, p...) } for _, p := range ps { @@ -114,12 +116,12 @@ func NewServiceHTTPHealthz(l *zap.Logger, name, addr, path string, probes map[He _, _ = w.Write([]byte("OK")) }) - handler.HandleFunc(path+"/"+HealthzTypeStartup.String(), func(w http.ResponseWriter, r *http.Request) { + handler.HandleFunc(path+"/"+healthz.TypeStartup.String(), func(w http.ResponseWriter, r *http.Request) { var ps []interface{} - if p, ok := probes[HealthzTypeAlways]; ok { + if p, ok := probes[healthz.TypeAlways]; ok { ps = append(ps, p...) } - if p, ok := probes[HealthzTypeStartup]; ok { + if p, ok := probes[healthz.TypeStartup]; ok { ps = append(ps, p...) } for _, p := range ps { @@ -134,15 +136,15 @@ func NewServiceHTTPHealthz(l *zap.Logger, name, addr, path string, probes map[He w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("OK")) }) - return NewServiceHTTP(l, name, addr, handler) + return NewHTTP(l, name, addr, handler) } -func NewDefaultServiceHTTPProbes(probes map[HealthzType][]interface{}) *ServiceHTTP { - return NewServiceHTTPHealthz( - log.Logger(), - DefaultServiceHTTPHealthzName, - DefaultServiceHTTPHealthzAddr, - DefaultServiceHTTPHealthzPath, +func NewDefaultHTTPProbes(l *zap.Logger, probes map[healthz.Type][]interface{}) *HTTP { + return NewHealthz( + l, + DefaultHTTPHealthzName, + DefaultHTTPHealthzAddr, + DefaultHTTPHealthzPath, probes, ) } diff --git a/servicehttppprof.go b/service/httppprof.go similarity index 52% rename from servicehttppprof.go rename to service/httppprof.go index cb10cc3..2ff264f 100644 --- a/servicehttppprof.go +++ b/service/httppprof.go @@ -1,21 +1,20 @@ //go:build !pprof -package keel +package service import ( "net/http" - "github.com/foomo/keel/log" "go.uber.org/zap" ) const ( - DefaultServiceHTTPPProfName = "pprof" - DefaultServiceHTTPPProfAddr = "localhost:6060" - DefaultServiceHTTPPProfPath = "/debug/pprof" + DefaultHTTPPProfName = "pprof" + DefaultHTTPPProfAddr = "localhost:6060" + DefaultHTTPPProfPath = "/debug/pprof" ) -func NewServiceHTTPPProf(l *zap.Logger, name, addr, path string) *ServiceHTTP { +func NewHTTPPProf(l *zap.Logger, name, addr, path string) *HTTP { route := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotImplemented) _, _ = w.Write([]byte("To enable pprof, you need to build your binary with the `-tags=pprof` flag")) @@ -26,14 +25,14 @@ func NewServiceHTTPPProf(l *zap.Logger, name, addr, path string) *ServiceHTTP { handler.HandleFunc(path+"/profile", route) handler.HandleFunc(path+"/symbol", route) handler.HandleFunc(path+"/trace", route) - return NewServiceHTTP(l, name, addr, handler) + return NewHTTP(l, name, addr, handler) } -func NewDefaultServiceHTTPPProf() *ServiceHTTP { - return NewServiceHTTPPProf( - log.Logger(), - DefaultServiceHTTPPProfName, - DefaultServiceHTTPPProfAddr, - DefaultServiceHTTPPProfPath, +func NewDefaultHTTPPProf(l *zap.Logger) *HTTP { + return NewHTTPPProf( + l, + DefaultHTTPPProfName, + DefaultHTTPPProfAddr, + DefaultHTTPPProfPath, ) } diff --git a/servicehttppprof_pprof.go b/service/httppprof_pprof.go similarity index 54% rename from servicehttppprof_pprof.go rename to service/httppprof_pprof.go index 2092e63..ca020c4 100644 --- a/servicehttppprof_pprof.go +++ b/service/httppprof_pprof.go @@ -1,7 +1,7 @@ //go:build pprof // +build pprof -package keel +package service import ( "net/http" @@ -12,12 +12,12 @@ import ( ) const ( - DefaultServiceHTTPPProfName = "pprof" - DefaultServiceHTTPPProfAddr = "localhost:6060" - DefaultServiceHTTPPProfPath = "/debug/pprof" + DefaultHTTPPProfName = "pprof" + DefaultHTTPPProfAddr = "localhost:6060" + DefaultHTTPPProfPath = "/debug/pprof" ) -func NewServiceHTTPPProf(l *zap.Logger, name, addr, path string) *ServiceHTTP { +func NewHTTPPProf(l *zap.Logger, name, addr, path string) *ServiceHTTP { handler := http.NewServeMux() handler.HandleFunc(path+"/", pprof.Index) handler.HandleFunc(path+"/cmdline", pprof.Cmdline) @@ -27,11 +27,11 @@ func NewServiceHTTPPProf(l *zap.Logger, name, addr, path string) *ServiceHTTP { return NewServiceHTTP(l, name, addr, handler) } -func NewDefaultServiceHTTPPProf() *ServiceHTTP { - return NewServiceHTTPPProf( +func NewDefaultHTTPPProf() *ServiceHTTP { + return NewHTTPPProf( log.Logger(), - DefaultServiceHTTPPProfName, - DefaultServiceHTTPPProfAddr, - DefaultServiceHTTPPProfPath, + DefaultHTTPPProfName, + DefaultHTTPPProfAddr, + DefaultHTTPPProfPath, ) } diff --git a/service/httpprometheus.go b/service/httpprometheus.go new file mode 100644 index 0000000..53000b5 --- /dev/null +++ b/service/httpprometheus.go @@ -0,0 +1,35 @@ +package service + +import ( + "net/http" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + "go.uber.org/zap" +) + +const ( + DefaultHTTPPrometheusName = "prometheus" + DefaultHTTPPrometheusAddr = ":9200" + DefaultHTTPPrometheusPath = "/metrics" +) + +func NewHTTPPrometheus(l *zap.Logger, name, addr, path string) *HTTP { + handler := http.NewServeMux() + handler.Handle(path, promhttp.HandlerFor( + prometheus.DefaultGatherer, + promhttp.HandlerOpts{ + EnableOpenMetrics: true, + }, + )) + return NewHTTP(l, name, addr, handler) +} + +func NewDefaultHTTPPrometheus(l *zap.Logger) *HTTP { + return NewHTTPPrometheus( + l, + DefaultHTTPPrometheusName, + DefaultHTTPPrometheusAddr, + DefaultHTTPPrometheusPath, + ) +} diff --git a/service/httpreadme.go b/service/httpreadme.go new file mode 100644 index 0000000..967173a --- /dev/null +++ b/service/httpreadme.go @@ -0,0 +1,44 @@ +package service + +import ( + "net/http" + + "github.com/foomo/keel/interfaces" + "github.com/foomo/keel/markdown" + "go.uber.org/zap" +) + +const ( + DefaultHTTPReadmeName = "readme" + DefaultHTTPReadmeAddr = "localhost:9001" + DefaultHTTPReadmePath = "/readme" +) + +func NewHTTPReadme(l *zap.Logger, name, addr, path string, readmers func() []interfaces.Readmer) *HTTP { + handler := http.NewServeMux() + handler.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.Header().Add("Content-Type", "text/markdown") + w.WriteHeader(http.StatusOK) + md := &markdown.Markdown{} + for _, readmer := range readmers() { + md.Print(readmer.Readme()) + } + _, _ = w.Write([]byte(md.String())) + default: + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + } + }) + return NewHTTP(l, name, addr, handler) +} + +func NewDefaultHTTPReadme(l *zap.Logger, readmers func() []interfaces.Readmer) *HTTP { + return NewHTTPReadme( + l, + DefaultHTTPReadmeName, + DefaultHTTPReadmeAddr, + DefaultHTTPReadmePath, + readmers, + ) +} diff --git a/service/httpreadme_test.go b/service/httpreadme_test.go new file mode 100644 index 0000000..5ebb6f7 --- /dev/null +++ b/service/httpreadme_test.go @@ -0,0 +1,191 @@ +package service_test + +import ( + "context" + "fmt" + "net/http" + "os" + + "github.com/foomo/keel" + "github.com/foomo/keel/config" + "github.com/foomo/keel/env" + "github.com/foomo/keel/service" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "go.opentelemetry.io/otel/metric/instrument" + "go.uber.org/zap" +) + +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.WithPrometheusMeter(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() + // 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") + + m := svr.Meter() + + // add metrics + fooBarCounter := promauto.NewCounter(prometheus.CounterOpts{ + Name: "foo_bar_total", + Help: "Foo bar metrics", + }) + fooBazCounter, _ := m.SyncInt64().Counter("foo_baz_total", instrument.WithDescription("Foo baz metrics")) + + fooBarCounter.Add(1) + fooBazCounter.Add(svr.Context(), 1) + + // 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() { + waitFor("localhost:9001") + fmt.Print(httpGet("http://localhost:9001/readme")) + shutdown() + }() + + svr.Run() + + // Output: + // ### 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_SERVICE_NAME` | `string` | | `service` | + // + // ### Config + // + // List of all registered config variables with their defaults. + // + // | Key | Type | Required | Default | + // | ------------------------- | -------- | -------- | ---------- | + // | `example.bool` | `bool` | | `false` | + // | `example.required.bool` | `bool` | `true` | | + // | `example.required.string` | `string` | `true` | | + // | `example.string` | `string` | | `fallback` | + // | `otel.enabled` | `bool` | | `true` | + // | `service.readme.enabled` | `bool` | | `true` | + // + // ### Init Services + // + // List of all registered init services that are being immediately started. + // + // | Name | Type | Address | + // | -------- | --------------- | ------------------------------------ | + // | `readme` | `*service.HTTP` | `*http.ServeMux` on `localhost:9001` | + // + // ### Runtime Services + // + // List of all registered services that are being started. + // + // | Name | Type | Description | + // | ---------------- | -------------------- | -------------------------------------- | + // | `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 runtime. + // + // | Name | Probe | Type | Description | + // | ---------------- | -------- | -------------------- | -------------------------------------- | + // | | `always` | `*keel.Server` | | + // | `demo-goroutine` | `always` | `*service.GoRoutine` | parallel: `1` | + // | `demp-http` | `always` | `*service.HTTP` | `http.HandlerFunc` on `localhost:8080` | + // | `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` | + // | `readme` | `*service.HTTP` | `ErrorCloserWithContext` | `*http.ServeMux` on `localhost:9001` | + // + // ### Metrics + // + // List of all registered metrics than are being exposed. + // + // | Name | Type | Description | + // | ---------------------------------- | ------- | ------------------------------------------------------------------ | + // | `foo_bar_total` | COUNTER | Foo bar metrics | + // | `foo_baz_total` | COUNTER | Foo baz metrics | + // | `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. | + // | `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. | + // | `process_resident_memory_bytes` | GAUGE | Resident memory size in bytes. | + // | `process_start_time_seconds` | GAUGE | Start time of the process since unix epoch in seconds. | + // | `process_virtual_memory_bytes` | GAUGE | Virtual memory size in bytes. | + // | `process_virtual_memory_max_bytes` | GAUGE | Maximum amount of virtual memory available in bytes. | +} diff --git a/servicehttpviper.go b/service/httpviper.go similarity index 64% rename from servicehttpviper.go rename to service/httpviper.go index 2e34025..3959371 100644 --- a/servicehttpviper.go +++ b/service/httpviper.go @@ -1,4 +1,4 @@ -package keel +package service import ( "encoding/json" @@ -8,16 +8,15 @@ import ( "go.uber.org/zap" "github.com/foomo/keel/config" - "github.com/foomo/keel/log" ) const ( - DefaultServiceHTTPViperName = "viper" - DefaultServiceHTTPViperAddr = "localhost:9300" - DefaultServiceHTTPViperPath = "/config" + DefaultHTTPViperName = "viper" + DefaultHTTPViperAddr = "localhost:9300" + DefaultHTTPViperPath = "/config" ) -func NewServiceHTTPViper(l *zap.Logger, c *viper.Viper, name, addr, path string) *ServiceHTTP { +func NewHTTPViper(l *zap.Logger, c *viper.Viper, name, addr, path string) *HTTP { handler := http.NewServeMux() handler.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { type payload struct { @@ -44,15 +43,15 @@ func NewServiceHTTPViper(l *zap.Logger, c *viper.Viper, name, addr, path string) http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) } }) - return NewServiceHTTP(l, name, addr, handler) + return NewHTTP(l, name, addr, handler) } -func NewDefaultServiceHTTPViper() *ServiceHTTP { - return NewServiceHTTPViper( - log.Logger(), +func NewDefaultHTTPViper(l *zap.Logger) *HTTP { + return NewHTTPViper( + l, config.Config(), - DefaultServiceHTTPViperName, - DefaultServiceHTTPViperAddr, - DefaultServiceHTTPViperPath, + DefaultHTTPViperName, + DefaultHTTPViperAddr, + DefaultHTTPViperPath, ) } diff --git a/servicehttpzap.go b/service/httpzap.go similarity index 84% rename from servicehttpzap.go rename to service/httpzap.go index 384189a..f3caca2 100644 --- a/servicehttpzap.go +++ b/service/httpzap.go @@ -1,4 +1,4 @@ -package keel +package service import ( "encoding/json" @@ -12,12 +12,12 @@ import ( ) const ( - DefaultServiceHTTPZapName = "zap" - DefaultServiceHTTPZapAddr = "localhost:9100" - DefaultServiceHTTPZapPath = "/log" + DefaultHTTPZapName = "zap" + DefaultHTTPZapAddr = "localhost:9100" + DefaultHTTPZapPath = "/log" ) -func NewServiceHTTPZap(l *zap.Logger, name, addr, path string) *ServiceHTTP { +func NewHTTPZap(l *zap.Logger, name, addr, path string) *HTTP { handler := http.NewServeMux() handler.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { type errorResponse struct { @@ -91,14 +91,14 @@ func NewServiceHTTPZap(l *zap.Logger, name, addr, path string) *ServiceHTTP { }) } }) - return NewServiceHTTP(l, name, addr, handler) + return NewHTTP(l, name, addr, handler) } -func NewDefaultServiceHTTPZap() *ServiceHTTP { - return NewServiceHTTPZap( - log.Logger(), - DefaultServiceHTTPZapName, - DefaultServiceHTTPZapAddr, - DefaultServiceHTTPZapPath, +func NewDefaultHTTPZap(l *zap.Logger) *HTTP { + return NewHTTPZap( + l, + DefaultHTTPZapName, + DefaultHTTPZapAddr, + DefaultHTTPZapPath, ) } diff --git a/servicehttpprometheus.go b/servicehttpprometheus.go deleted file mode 100644 index 9636205..0000000 --- a/servicehttpprometheus.go +++ /dev/null @@ -1,37 +0,0 @@ -package keel - -import ( - "net/http" - - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promhttp" - "go.uber.org/zap" - - "github.com/foomo/keel/log" -) - -const ( - DefaultServiceHTTPPrometheusName = "prometheus" - DefaultServiceHTTPPrometheusAddr = ":9200" - DefaultServiceHTTPPrometheusPath = "/metrics" -) - -func NewServiceHTTPPrometheus(l *zap.Logger, name, addr, path string) *ServiceHTTP { - handler := http.NewServeMux() - handler.Handle(path, promhttp.HandlerFor( - prometheus.DefaultGatherer, - promhttp.HandlerOpts{ - EnableOpenMetrics: true, - }, - )) - return NewServiceHTTP(l, name, addr, handler) -} - -func NewDefaultServiceHTTPPrometheus() *ServiceHTTP { - return NewServiceHTTPPrometheus( - log.Logger(), - DefaultServiceHTTPPrometheusName, - DefaultServiceHTTPPrometheusAddr, - DefaultServiceHTTPPrometheusPath, - ) -}