Merge pull request #193 from foomo/service-goroutine

Add goroutine service
This commit is contained in:
Kevin Franklin Kim 2023-10-18 17:15:13 +02:00 committed by GitHub
commit bbd90886cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
83 changed files with 1640 additions and 456 deletions

View File

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

View File

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

3
.gitignore vendored
View File

@ -1,5 +1,6 @@
.*
*.log
*.out
!.github/
!.husky/
!.editorconfig
@ -7,6 +8,4 @@
!.golangci.yml
!.goreleaser.yml
!.husky.yaml
/coverage.out
/coverage.html
/tmp/

View File

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

View File

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

View File

@ -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<type>\w+)(\((?P<scope>[\w/.-]+)\))?(?P<breaking>!)?:( +)?(?P<header>.+)'

View File

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

View File

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

View File

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

View File

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

71
config/readme.go Normal file
View File

@ -0,0 +1,71 @@
package config
import (
"fmt"
"github.com/foomo/keel/markdown"
)
func Readme() string {
var configRows [][]string
var remoteRows [][]string
c := Config()
md := &markdown.Markdown{}
{
keys := c.AllKeys()
for _, key := range keys {
var fallback interface{}
if v, ok := defaults[key]; ok {
fallback = v
}
configRows = append(configRows, []string{
markdown.Code(key),
markdown.Code(TypeOf(key)),
"",
markdown.Code(fmt.Sprintf("%v", fallback)),
})
}
for _, key := range requiredKeys {
configRows = append(configRows, []string{
markdown.Code(key),
markdown.Code(TypeOf(key)),
markdown.Code("true"),
"",
})
}
}
{
for _, remote := range remotes {
remoteRows = append(remoteRows, []string{
markdown.Code(remote.provider),
markdown.Code(remote.path),
})
}
}
if len(configRows) > 0 || len(remoteRows) > 0 {
md.Println("### Config")
md.Println("")
}
if len(configRows) > 0 {
md.Println("List of all registered config 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()
}

View File

@ -6,6 +6,12 @@ import (
_ "github.com/spf13/viper/remote"
)
var remotes []struct {
provider string
endpoint string
path string
}
func WithRemoteConfig(c *viper.Viper, provider, endpoint string, path string) error {
if err := c.AddRemoteProvider(provider, endpoint, path); err != nil {
return err
@ -19,5 +25,11 @@ func WithRemoteConfig(c *viper.Viper, provider, endpoint string, path string) er
return errors.Wrap(err, "failed to watch remote config")
}
remotes = append(remotes, struct {
provider string
endpoint string
path string
}{provider: provider, endpoint: endpoint, path: path})
return nil
}

55
env/env.go vendored
View File

@ -3,10 +3,17 @@ package env
import (
"fmt"
"os"
"slices"
"strconv"
"strings"
)
var (
defaults = map[string]interface{}{}
requiredKeys []string
types = map[string]string{}
)
// Exists return true if env var is defined
func Exists(key string) bool {
_, ok := os.LookupEnv(key)
@ -15,13 +22,20 @@ func Exists(key string) bool {
// MustExists panics if not exists
func MustExists(key string) {
if _, ok := os.LookupEnv(key); !ok {
panic(fmt.Sprintf("required environment variable %s does not exist", key))
if !Exists(key) {
panic(fmt.Sprintf("required environment variable `%s` does not exist", key))
}
if !slices.Contains(requiredKeys, key) {
requiredKeys = append(requiredKeys, key)
}
}
// Get env var or fallback
func Get(key, fallback string) string {
defaults[key] = fallback
if _, ok := types[key]; !ok {
types[key] = "string"
}
if v, ok := os.LookupEnv(key); ok {
return v
}
@ -36,6 +50,9 @@ func MustGet(key string) string {
// GetInt env var or fallback as int
func GetInt(key string, fallback int) int {
if _, ok := types[key]; !ok {
types[key] = "int"
}
if value, err := strconv.Atoi(Get(key, "")); err == nil {
return value
}
@ -50,6 +67,9 @@ func MustGetInt(key string) int {
// GetInt64 env var or fallback as int64
func GetInt64(key string, fallback int64) int64 {
if _, ok := types[key]; !ok {
types[key] = "int64"
}
if value, err := strconv.ParseInt(Get(key, ""), 10, 64); err == nil {
return value
}
@ -64,6 +84,9 @@ func MustGetInt64(key string) int64 {
// GetFloat64 env var or fallback as float64
func GetFloat64(key string, fallback float64) float64 {
if _, ok := types[key]; !ok {
types[key] = "float64"
}
if value, err := strconv.ParseFloat(Get(key, ""), 64); err == nil {
return value
}
@ -78,6 +101,9 @@ func MustGetFloat64(key string) float64 {
// GetBool env var or fallback as bool
func GetBool(key string, fallback bool) bool {
if _, ok := types[key]; !ok {
types[key] = "bool"
}
if val, err := strconv.ParseBool(Get(key, "")); err == nil {
return val
}
@ -92,6 +118,9 @@ func MustGetBool(key string) bool {
// GetStringSlice env var or fallback as []string
func GetStringSlice(key string, fallback []string) []string {
if _, ok := types[key]; !ok {
types[key] = "[]string"
}
if v := Get(key, ""); v != "" {
return strings.Split(v, ",")
}
@ -106,6 +135,9 @@ func MustGetStringSlice(key string) []string {
// GetIntSlice env var or fallback as []string
func GetIntSlice(key string, fallback []int) []int {
if _, ok := types[key]; !ok {
types[key] = "[]int"
}
if v := Get(key, ""); v != "" {
elements := strings.Split(v, ",")
ret := make([]int, len(elements))
@ -125,3 +157,22 @@ func MustGetGetIntSlice(key string) []int {
MustExists(key)
return GetIntSlice(key, nil)
}
func RequiredKeys() []string {
return requiredKeys
}
func Defaults() map[string]interface{} {
return defaults
}
func Types() map[string]string {
return types
}
func TypeOf(key string) string {
if v, ok := types[key]; ok {
return v
}
return ""
}

43
env/readme.go vendored Normal file
View File

@ -0,0 +1,43 @@
package env
import (
"fmt"
"github.com/foomo/keel/markdown"
)
func Readme() string {
var rows [][]string
md := &markdown.Markdown{}
{
for key, fallback := range defaults {
rows = append(rows, []string{
markdown.Code(key),
markdown.Code(TypeOf(key)),
"",
markdown.Code(fmt.Sprintf("%v", fallback)),
})
}
for _, key := range requiredKeys {
rows = append(rows, []string{
markdown.Code(key),
markdown.Code(TypeOf(key)),
markdown.Code("true"),
"",
})
}
}
if len(rows) > 0 {
md.Println("### Env")
md.Println("")
md.Println("List of all accessed environment variables.")
md.Println("")
md.Table([]string{"Key", "Type", "Required", "Default"}, rows)
md.Println("")
}
return md.String()
}

View File

@ -5,6 +5,5 @@ import (
)
var (
ErrServerNotRunning = errors.New("server not running")
ErrServiceNotRunning = errors.New("service not running")
ErrServerNotRunning = errors.New("server not running")
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

1
go.mod
View File

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

2
go.sum
View File

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

20
healthz.go Normal file
View File

@ -0,0 +1,20 @@
package keel
import (
"github.com/foomo/keel/healthz"
"github.com/foomo/keel/interfaces"
)
func IsHealthz(v any) bool {
switch v.(type) {
case healthz.BoolHealthzer,
healthz.BoolHealthzerWithContext,
healthz.ErrorHealthzer,
healthz.ErrorHealthzWithContext,
interfaces.ErrorPinger,
interfaces.ErrorPingerWithContext:
return true
default:
return false
}
}

1
healthz/docs.go Normal file
View File

@ -0,0 +1 @@
package healthz

View File

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

View File

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

25
interfaces/closer.go Normal file
View File

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

1
interfaces/doc.go Normal file
View File

@ -0,0 +1 @@
package interfaces

6
interfaces/namer.go Normal file
View File

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

View File

@ -1,4 +1,4 @@
package keel
package interfaces
import "context"

20
interfaces/readmer.go Normal file
View File

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

View File

@ -1,4 +1,4 @@
package keel
package interfaces
import "context"

View File

@ -1,4 +1,4 @@
package keel
package interfaces
import "context"

View File

@ -1,4 +1,4 @@
package keel
package interfaces
import "context"

11
log/fields_keel.go Normal file
View File

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

View File

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

View File

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

40
markdown/markdown.go Normal file
View File

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

28
markdown/utils.go Normal file
View File

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

32
metrics/readme.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

330
server.go
View File

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

View File

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

10
service/errors.go Normal file
View File

@ -0,0 +1,10 @@
package service
import (
"github.com/pkg/errors"
)
var (
ErrServiceNotRunning = errors.New("service not running")
ErrServiceShutdown = errors.New("service shutdown")
)

110
service/goroutine.go Normal file
View File

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

50
service/goroutine_test.go Normal file
View File

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

42
service/helper_test.go Normal file
View File

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

View File

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

40
service/http_test.go Normal file
View File

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

View File

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

View File

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

View File

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

35
service/httpprometheus.go Normal file
View File

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

44
service/httpreadme.go Normal file
View File

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

191
service/httpreadme_test.go Normal file
View File

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

View File

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

View File

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

View File

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