From 07f0c394d542ca4637c5b58a099a845d18ee9b14 Mon Sep 17 00:00:00 2001 From: Kevin Franklin Kim Date: Fri, 8 Sep 2023 12:14:56 +0200 Subject: [PATCH] feat: add GoRoutine service moves all services into the service package --- README.md | 3 +- errors.go | 3 +- examples/config/main.go | 3 +- examples/graceful/main.go | 3 +- examples/healthz/main.go | 6 +- examples/logging/main.go | 3 +- examples/middlewares/basicauth/main.go | 3 +- examples/middlewares/cors/main.go | 3 +- examples/middlewares/jwtfromcookie/main.go | 3 +- examples/middlewares/jwtfromtoken/main.go | 3 +- examples/middlewares/logger/main.go | 3 +- examples/middlewares/recover/main.go | 3 +- examples/middlewares/requestid/main.go | 3 +- examples/middlewares/responsetime/main.go | 3 +- examples/middlewares/sessionid/main.go | 3 +- examples/middlewares/skip/main.go | 5 +- examples/middlewares/telemetry/main.go | 3 +- .../middlewares/tokenauthfromcookie/main.go | 3 +- .../middlewares/tokenauthfromheader/main.go | 3 +- examples/remoteconfig/main.go | 3 +- examples/roundtripwares/logger/server.go | 3 +- examples/roundtripwares/requestid/main.go | 3 +- examples/roundtripwares/retry/server.go | 3 +- examples/serviceenabler/main.go | 5 +- examples/services/main.go | 14 - examples/stream/jetstream/main.go | 3 +- examples/stream/jetstreamraw/main.go | 3 +- examples/telemetry/main.go | 13 +- go.mod | 1 + go.sum | 2 + healthz/docs.go | 1 + healthzer.go => healthz/healthzer.go | 6 +- healthztype.go => healthz/type.go | 24 +- closer.go => interfaces/closer.go | 10 +- interfaces/doc.go | 1 + pinger.go => interfaces/pinger.go | 2 +- shutdowner.go => interfaces/shutdowner.go | 2 +- stopper.go => interfaces/stopper.go | 2 +- unsubscriber.go => interfaces/unsubscriber.go | 2 +- log/fields_keel.go | 11 + log/fields_service.go | 6 + log/with.go | 12 + option.go | 31 +- server.go | 97 ++-- server_docs.go | 435 ++++++++++++++++++ server_test.go | 9 +- service/errors.go | 10 + service/goroutine.go | 94 ++++ service/goroutine_test.go | 47 ++ service/helper_test.go | 14 + servicehttp.go => service/http.go | 44 +- service/http_test.go | 39 ++ .../httphealthz.go | 56 +-- servicehttppprof.go => service/httppprof.go | 22 +- .../httppprof_pprof.go | 20 +- service/httpprometheus.go | 37 ++ servicehttpviper.go => service/httpviper.go | 22 +- servicehttpzap.go => service/httpzap.go | 22 +- servicehttpprometheus.go | 37 -- telemetry/nonrecording/instruments.go | 162 +++++++ telemetry/nonrecording/meter.go | 50 ++ 61 files changed, 1186 insertions(+), 256 deletions(-) create mode 100644 healthz/docs.go rename healthzer.go => healthz/healthzer.go (87%) rename healthztype.go => healthz/type.go (70%) rename closer.go => interfaces/closer.go (82%) create mode 100644 interfaces/doc.go rename pinger.go => interfaces/pinger.go (91%) rename shutdowner.go => interfaces/shutdowner.go (95%) rename stopper.go => interfaces/stopper.go (94%) rename unsubscriber.go => interfaces/unsubscriber.go (95%) create mode 100644 log/fields_keel.go create mode 100644 server_docs.go create mode 100644 service/errors.go create mode 100644 service/goroutine.go create mode 100644 service/goroutine_test.go create mode 100644 service/helper_test.go rename servicehttp.go => service/http.go (52%) create mode 100644 service/http_test.go rename servicehttphealthz.go => service/httphealthz.go (67%) rename servicehttppprof.go => service/httppprof.go (56%) rename servicehttppprof_pprof.go => service/httppprof_pprof.go (54%) create mode 100644 service/httpprometheus.go rename servicehttpviper.go => service/httpviper.go (67%) rename servicehttpzap.go => service/httpzap.go (84%) delete mode 100644 servicehttpprometheus.go create mode 100644 telemetry/nonrecording/instruments.go create mode 100644 telemetry/nonrecording/meter.go diff --git a/README.md b/README.md index 02690fe..f69b1a9 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ import ( "net/http" "github.com/foomo/keel" + "github.com/foomo/keel/service" ) func main() { @@ -39,7 +40,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/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/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/closer.go b/interfaces/closer.go similarity index 82% rename from closer.go rename to interfaces/closer.go index 9de714d..fe62a5b 100644 --- a/closer.go +++ b/interfaces/closer.go @@ -1,6 +1,8 @@ -package keel +package interfaces -import "context" +import ( + "context" +) type closer struct { handle func(context.Context) error @@ -12,10 +14,6 @@ func NewCloserFn(handle func(context.Context) error) closer { } } -func (h healther) Close(ctx context.Context) error { - return h.handle(ctx) -} - // Closer interface type Closer interface { Close() 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/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/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/option.go b/option.go index e6b82a1..0d634e3 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.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.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.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.initServices = append(inst.initServices, svs) + inst.AddAlwaysHealthzers(svs) } } } @@ -171,9 +172,9 @@ 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.probes) + inst.initServices = append(inst.initServices, svs) + inst.AddAlwaysHealthzers(svs) } } } diff --git a/server.go b/server.go index b38c095..b0cd2fb 100644 --- a/server.go +++ b/server.go @@ -1,3 +1,6 @@ +//go:build !docs +// +build !docs + package keel import ( @@ -10,6 +13,8 @@ import ( "syscall" "time" + "github.com/foomo/keel/healthz" + "github.com/foomo/keel/interfaces" "github.com/go-logr/logr" "github.com/pkg/errors" "github.com/spf13/viper" @@ -40,7 +45,7 @@ type Server struct { shutdownTimeout time.Duration running atomic.Bool closers []interface{} - probes map[HealthzType][]interface{} + probes map[healthz.Type][]interface{} ctx context.Context ctxCancel context.Context ctxCancelFn context.CancelFunc @@ -54,7 +59,7 @@ func NewServer(opts ...Option) *Server { inst := &Server{ shutdownTimeout: 30 * time.Second, shutdownSignals: []os.Signal{os.Interrupt, syscall.SIGTERM}, - probes: map[HealthzType][]interface{}{}, + probes: map[healthz.Type][]interface{}{}, ctx: context.Background(), c: config.Config(), l: log.Logger(), @@ -83,51 +88,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") } @@ -229,22 +234,22 @@ func (s *Server) AddCloser(closer interface{}) { } } switch closer.(type) { - case Closer, - ErrorCloser, - CloserWithContext, - ErrorCloserWithContext, - Shutdowner, - ErrorShutdowner, - ShutdownerWithContext, - ErrorShutdownerWithContext, - Stopper, - ErrorStopper, - StopperWithContext, - ErrorStopperWithContext, - Unsubscriber, - ErrorUnsubscriber, - UnsubscriberWithContext, - ErrorUnsubscriberWithContext: + case interfaces.Closer, + interfaces.ErrorCloser, + interfaces.CloserWithContext, + interfaces.ErrorCloserWithContext, + interfaces.Shutdowner, + interfaces.ErrorShutdowner, + interfaces.ShutdownerWithContext, + interfaces.ErrorShutdownerWithContext, + interfaces.Stopper, + interfaces.ErrorStopper, + interfaces.StopperWithContext, + interfaces.ErrorStopperWithContext, + interfaces.Unsubscriber, + interfaces.ErrorUnsubscriber, + interfaces.UnsubscriberWithContext, + interfaces.ErrorUnsubscriberWithContext: s.closers = append(s.closers, closer) default: s.l.Warn("unable to add closer", log.FValue(fmt.Sprintf("%T", closer))) @@ -259,14 +264,14 @@ func (s *Server) AddClosers(closers ...interface{}) { } // AddHealthzer adds a probe to be called on healthz checks -func (s *Server) AddHealthzer(typ HealthzType, probe interface{}) { +func (s *Server) AddHealthzer(typ healthz.Type, probe interface{}) { switch probe.(type) { - case BoolHealthzer, - BoolHealthzerWithContext, - ErrorHealthzer, - ErrorHealthzWithContext, - ErrorPinger, - ErrorPingerWithContext: + case healthz.BoolHealthzer, + healthz.BoolHealthzerWithContext, + healthz.ErrorHealthzer, + healthz.ErrorHealthzWithContext, + interfaces.ErrorPinger, + interfaces.ErrorPingerWithContext: s.probes[typ] = append(s.probes[typ], probe) default: s.l.Debug("not a healthz probe", log.FValue(fmt.Sprintf("%T", probe))) @@ -274,7 +279,7 @@ func (s *Server) AddHealthzer(typ HealthzType, probe interface{}) { } // 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) } @@ -282,22 +287,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 diff --git a/server_docs.go b/server_docs.go new file mode 100644 index 0000000..0fba72c --- /dev/null +++ b/server_docs.go @@ -0,0 +1,435 @@ +//go:build docs +// +build docs + +package keel + +import ( + "context" + "fmt" + "os" + "reflect" + "sort" + "time" + + markdowntable "github.com/fbiville/markdown-table-formatter/pkg/markdown" + "github.com/foomo/keel/config" + "github.com/foomo/keel/healthz" + "github.com/foomo/keel/interfaces" + "github.com/foomo/keel/log" + "github.com/foomo/keel/service" + "github.com/foomo/keel/telemetry/nonrecording" + "github.com/pkg/errors" + "github.com/prometheus/client_golang/prometheus" + "github.com/spf13/viper" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/metric" + otelglobal "go.opentelemetry.io/otel/metric/global" + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" +) + +// Server struct +type Server struct { + services []Service + initServices []Service + meter metric.Meter + meterProvider metric.MeterProvider + tracer trace.Tracer + traceProvider trace.TracerProvider + shutdownSignals []os.Signal + shutdownTimeout time.Duration + closers []interface{} + probes map[healthz.Type][]interface{} + ctx context.Context + gCtx context.Context + l *zap.Logger + c *viper.Viper +} + +func NewServer(opts ...Option) *Server { + inst := &Server{ + probes: map[healthz.Type][]interface{}{}, + meterProvider: nonrecording.NewNoopMeterProvider(), + traceProvider: trace.NewNoopTracerProvider(), + ctx: context.Background(), + c: config.Config(), + l: log.Logger(), + } + + inst.meter = inst.meterProvider.Meter("") + otelglobal.SetMeterProvider(inst.meterProvider) + inst.tracer = inst.traceProvider.Tracer("") + otel.SetTracerProvider(inst.traceProvider) + + // add probe + inst.AddAlwaysHealthzers(inst) + + return inst +} + +// Logger returns server logger +func (s *Server) Logger() *zap.Logger { + return s.l +} + +// Meter returns the implementation meter +func (s *Server) Meter() metric.Meter { + return s.meter +} + +// Tracer returns the implementation tracer +func (s *Server) Tracer() trace.Tracer { + return s.tracer +} + +// Config returns server config +func (s *Server) Config() *viper.Viper { + return s.c +} + +// Context returns server context +func (s *Server) Context() context.Context { + return s.ctx +} + +// CancelContext returns server's cancel context +func (s *Server) CancelContext() context.Context { + return s.ctx +} + +// AddService add a single service +func (s *Server) AddService(service Service) { + for _, value := range s.services { + if value == service { + return + } + } + s.services = append(s.services, service) + s.AddAlwaysHealthzers(service) + s.AddCloser(service) +} + +// AddServices adds multiple service +func (s *Server) AddServices(services ...Service) { + for _, service := range services { + s.AddService(service) + } +} + +// AddCloser adds a closer to be called on shutdown +func (s *Server) AddCloser(closer interface{}) { + for _, value := range s.closers { + if value == closer { + return + } + } + switch closer.(type) { + case interfaces.Closer, + interfaces.ErrorCloser, + interfaces.CloserWithContext, + interfaces.ErrorCloserWithContext, + interfaces.Shutdowner, + interfaces.ErrorShutdowner, + interfaces.ShutdownerWithContext, + interfaces.ErrorShutdownerWithContext, + interfaces.Stopper, + interfaces.ErrorStopper, + interfaces.StopperWithContext, + interfaces.ErrorStopperWithContext, + interfaces.Unsubscriber, + interfaces.ErrorUnsubscriber, + interfaces.UnsubscriberWithContext, + interfaces.ErrorUnsubscriberWithContext: + s.closers = append(s.closers, closer) + default: + s.l.Warn("unable to add closer", log.FValue(fmt.Sprintf("%T", closer))) + } +} + +// AddClosers adds the given closers to be called on shutdown +func (s *Server) AddClosers(closers ...interface{}) { + for _, closer := range closers { + s.AddCloser(closer) + } +} + +// AddHealthzer adds a probe to be called on healthz checks +func (s *Server) AddHealthzer(typ healthz.Type, probe interface{}) { + switch probe.(type) { + case healthz.BoolHealthzer, + healthz.BoolHealthzerWithContext, + healthz.ErrorHealthzer, + healthz.ErrorHealthzWithContext, + interfaces.ErrorPinger, + interfaces.ErrorPingerWithContext: + s.probes[typ] = append(s.probes[typ], probe) + default: + s.l.Debug("not a healthz probe", log.FValue(fmt.Sprintf("%T", probe))) + } +} + +// AddHealthzers adds the given probes to be called on healthz checks +func (s *Server) AddHealthzers(typ healthz.Type, probes ...interface{}) { + for _, probe := range probes { + s.AddHealthzer(typ, probe) + } +} + +// AddAlwaysHealthzers adds the probes to be called on any healthz checks +func (s *Server) AddAlwaysHealthzers(probes ...interface{}) { + s.AddHealthzers(healthz.TypeAlways, probes...) +} + +// AddStartupHealthzers adds the startup probes to be called on healthz checks +func (s *Server) AddStartupHealthzers(probes ...interface{}) { + s.AddHealthzers(healthz.TypeStartup, probes...) +} + +// AddLivenessHealthzers adds the liveness probes to be called on healthz checks +func (s *Server) AddLivenessHealthzers(probes ...interface{}) { + s.AddHealthzers(healthz.TypeLiveness, probes...) +} + +// AddReadinessHealthzers adds the readiness probes to be called on healthz checks +func (s *Server) AddReadinessHealthzers(probes ...interface{}) { + s.AddHealthzers(healthz.TypeReadiness, probes...) +} + +// IsCanceled returns true if the internal errgroup has been canceled +func (s *Server) IsCanceled() bool { + return errors.Is(s.gCtx.Err(), context.Canceled) +} + +// Healthz returns true if the server is running +func (s *Server) Healthz() error { + return nil +} + +// Run runs the server +func (s *Server) Run() { + // add init services to closers + for _, initService := range s.initServices { + s.AddClosers(initService) + } + + md := &MD{} + + { + var rows [][]string + for _, key := range s.Config().AllKeys() { + rows = append(rows, []string{ + code(key), + code(s.Config().GetString(key)), + }) + } + if len(rows) > 0 { + md.Println("## Config") + md.Println("") + md.Println("List of all registered config variabled with their defaults.") + md.Println("") + md.Table([]string{"Key", "Default"}, rows) + md.Println("") + } + } + + { + var rows [][]string + for _, value := range s.initServices { + if v, ok := value.(*service.HTTP); ok { + t := reflect.TypeOf(v) + rows = append(rows, []string{ + code(v.Name()), + code(t.String()), + stringer(v), + }) + } + } + if len(rows) > 0 { + md.Println("## Init Services") + md.Println("") + md.Println("List of all registerd init services that are being immediately started.") + md.Println("") + md.Table([]string{"Name", "Type", "Address"}, rows) + md.Println("") + } + } + + { + var rows [][]string + for _, value := range s.services { + if v, ok := value.(*service.HTTP); ok { + t := reflect.TypeOf(v) + rows = append(rows, []string{ + code(v.Name()), + code(t.String()), + stringer(v), + }) + } + } + if len(rows) > 0 { + md.Println("## Services") + md.Println("") + md.Println("List of all registered services that are being started.") + md.Println("") + md.Table([]string{"Name", "Type", "Description"}, rows) + md.Println("") + } + } + + { + var rows [][]string + for k, probes := range s.probes { + for _, probe := range probes { + t := reflect.TypeOf(probe) + rows = append(rows, []string{ + code(k.String()), + code(t.String()), + }) + } + } + if len(rows) > 0 { + md.Println("## Health probes") + md.Println("") + md.Println("List of all registered healthz probes that are being called during startup and runntime.") + md.Println("") + md.Table([]string{"Name", "Type"}, rows) + md.Println("") + } + } + + { + var rows [][]string + for _, value := range s.closers { + t := reflect.TypeOf(value) + var closer string + switch value.(type) { + case interfaces.Closer: + closer = "Closer" + case interfaces.ErrorCloser: + closer = "ErrorCloser" + case interfaces.CloserWithContext: + closer = "CloserWithContext" + case interfaces.ErrorCloserWithContext: + closer = "ErrorCloserWithContext" + case interfaces.Shutdowner: + closer = "Shutdowner" + case interfaces.ErrorShutdowner: + closer = "ErrorShutdowner" + case interfaces.ShutdownerWithContext: + closer = "ShutdownerWithContext" + case interfaces.ErrorShutdownerWithContext: + closer = "ErrorShutdownerWithContext" + case interfaces.Stopper: + closer = "Stopper" + case interfaces.ErrorStopper: + closer = "ErrorStopper" + case interfaces.StopperWithContext: + closer = "StopperWithContext" + case interfaces.ErrorStopperWithContext: + closer = "ErrorStopperWithContext" + case interfaces.Unsubscriber: + closer = "Unsubscriber" + case interfaces.ErrorUnsubscriber: + closer = "ErrorUnsubscriber" + case interfaces.UnsubscriberWithContext: + closer = "UnsubscriberWithContext" + case interfaces.ErrorUnsubscriberWithContext: + closer = "ErrorUnsubscriberWithContext" + } + rows = append(rows, []string{ + code(t.String()), + code(closer), + }) + } + if len(rows) > 0 { + md.Println("## Closers") + md.Println("") + md.Println("List of all registered closers that are being called during graceful shutdown.") + md.Println("") + md.Table([]string{"Name", "Type"}, rows) + md.Println("") + } + } + + { + var rows [][]string + s.meter.AsyncFloat64() + + var names []string + values := map[string]nonrecording.Metric{} + for _, value := range nonrecording.Metrics() { + names = append(names, value.Name) + values[value.Name] = value + } + + gatherer, _ := prometheus.DefaultRegisterer.(*prometheus.Registry).Gather() + for _, value := range gatherer { + names = append(names, value.GetName()) + values[value.GetName()] = nonrecording.Metric{ + Name: value.GetName(), + Type: value.GetType().String(), + Help: value.GetHelp(), + } + } + sort.Strings(names) + for _, name := range names { + value := values[name] + rows = append(rows, []string{ + code(value.Name), + value.Type, + value.Help, + }) + } + if len(rows) > 0 { + md.Println("## Metrics") + md.Println("") + md.Println("List of all registered metrics than are being exposed.") + md.Println("") + md.Table([]string{"Name", "Type", "Description"}, rows) + md.Println("") + } + } + + fmt.Print(md.String()) +} + +type MD struct { + value string +} + +func (s *MD) Println(a ...any) { + s.value += fmt.Sprintln(a...) +} + +func (s *MD) Print(a ...any) { + s.value += fmt.Sprint(a...) +} + +func (s *MD) String() string { + return s.value +} + +func (s *MD) Table(headers []string, rows [][]string) { + table, err := markdowntable.NewTableFormatterBuilder(). + WithPrettyPrint(). + Build(headers...). + Format(rows) + if err != nil { + panic(err) + } + s.Print(table) +} + +func code(v string) string { + if v == "" { + return "" + } + return "`" + v + "`" +} + +func stringer(v any) string { + if i, ok := v.(fmt.Stringer); ok { + return i.String() + } + return "" +} 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..b4c9b78 --- /dev/null +++ b/service/goroutine.go @@ -0,0 +1,94 @@ +package service + +import ( + "context" + "fmt" + "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 + 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) *GoRoutine { + if l == nil { + l = log.Logger() + } + // enrich the log + l = log.WithAttributes(l, + log.KeelServiceTypeKey.String("goroutine"), + log.KeelServiceNameKey.String(name), + ) + + return &GoRoutine{ + handler: handler, + name: name, + parallel: 1, + l: l, + } +} + +// ------------------------------------------------------------------------------------------------ +// ~ 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.cancel = cancel + 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) + }) + } + return s.wg.Wait() +} + +func (s *GoRoutine) Close(ctx context.Context) error { + s.l.Info("stopping keel service") + s.cancel(ErrServiceShutdown) + return s.wg.Wait() +} diff --git a/service/goroutine_test.go b/service/goroutine_test.go new file mode 100644 index 0000000..29df2f9 --- /dev/null +++ b/service/goroutine_test.go @@ -0,0 +1,47 @@ +package service_test + +import ( + "context" + "time" + + "github.com/foomo/keel" + "github.com/foomo/keel/service" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +func ExampleNewGoRoutine() { + shutdown(3 * time.Second) + + svr := keel.NewServer( + keel.WithLogger(zap.NewExample()), + ) + + svr.AddService( + service.NewGoRoutine(svr.Logger(), "demo", func(ctx context.Context, l *zap.Logger) error { + for { + 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) + } + }), + ) + + 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":"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..5e93805 --- /dev/null +++ b/service/helper_test.go @@ -0,0 +1,14 @@ +package service_test + +import ( + "syscall" + "time" +) + +// shutdown example after the given time +func shutdown(duration time.Duration) { + go func() { + time.Sleep(duration) + _ = syscall.Kill(syscall.Getpid(), syscall.SIGINT) + }() +} diff --git a/servicehttp.go b/service/http.go similarity index 52% rename from servicehttp.go rename to service/http.go index a1e3cc4..62f6fe5 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("address: `%s`", 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..9f7e01c --- /dev/null +++ b/service/http_test.go @@ -0,0 +1,39 @@ +package service_test + +import ( + "net/http" + "time" + + "github.com/foomo/keel" + "github.com/foomo/keel/service" + "go.uber.org/zap" +) + +func ExampleNewHTTP() { + shutdown(3 * time.Second) + + svr := keel.NewServer( + keel.WithLogger(zap.NewExample()), + ) + + l := svr.Logger() + // 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( + service.NewHTTP(l, "demo", "localhost:8080", svs), + ) + + 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":"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..6b3629e 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( +func NewDefaultHTTPProbes(probes map[healthz.Type][]interface{}) *HTTP { + return NewHealthz( log.Logger(), - DefaultServiceHTTPHealthzName, - DefaultServiceHTTPHealthzAddr, - DefaultServiceHTTPHealthzPath, + DefaultHTTPHealthzName, + DefaultHTTPHealthzAddr, + DefaultHTTPHealthzPath, probes, ) } diff --git a/servicehttppprof.go b/service/httppprof.go similarity index 56% rename from servicehttppprof.go rename to service/httppprof.go index cb10cc3..7855d96 100644 --- a/servicehttppprof.go +++ b/service/httppprof.go @@ -1,6 +1,6 @@ //go:build !pprof -package keel +package service import ( "net/http" @@ -10,12 +10,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) *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 +26,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( +func NewDefaultHTTPPProf() *HTTP { + return NewHTTPPProf( log.Logger(), - DefaultServiceHTTPPProfName, - DefaultServiceHTTPPProfAddr, - DefaultServiceHTTPPProfPath, + 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..94be252 --- /dev/null +++ b/service/httpprometheus.go @@ -0,0 +1,37 @@ +package service + +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 ( + 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() *HTTP { + return NewHTTPPrometheus( + log.Logger(), + DefaultHTTPPrometheusName, + DefaultHTTPPrometheusAddr, + DefaultHTTPPrometheusPath, + ) +} diff --git a/servicehttpviper.go b/service/httpviper.go similarity index 67% rename from servicehttpviper.go rename to service/httpviper.go index 2e34025..47639d0 100644 --- a/servicehttpviper.go +++ b/service/httpviper.go @@ -1,4 +1,4 @@ -package keel +package service import ( "encoding/json" @@ -12,12 +12,12 @@ import ( ) 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 +44,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( +func NewDefaultHTTPViper() *HTTP { + return NewHTTPViper( log.Logger(), 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..4969682 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( +func NewDefaultHTTPZap() *HTTP { + return NewHTTPZap( log.Logger(), - DefaultServiceHTTPZapName, - DefaultServiceHTTPZapAddr, - DefaultServiceHTTPZapPath, + 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, - ) -} diff --git a/telemetry/nonrecording/instruments.go b/telemetry/nonrecording/instruments.go new file mode 100644 index 0000000..d11c927 --- /dev/null +++ b/telemetry/nonrecording/instruments.go @@ -0,0 +1,162 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package nonrecording + +import ( + "context" + "strings" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric/instrument" + "go.opentelemetry.io/otel/metric/instrument/asyncfloat64" + "go.opentelemetry.io/otel/metric/instrument/asyncint64" + "go.opentelemetry.io/otel/metric/instrument/syncfloat64" + "go.opentelemetry.io/otel/metric/instrument/syncint64" +) + +type Metric struct { + Name string + Type string + Help string +} + +var metrics []Metric + +func Metrics() []Metric { + return metrics +} + +type nonrecordingAsyncFloat64Instrument struct { + instrument.Asynchronous +} + +var ( + _ asyncfloat64.InstrumentProvider = nonrecordingAsyncFloat64Instrument{} + _ asyncfloat64.Counter = nonrecordingAsyncFloat64Instrument{} + _ asyncfloat64.UpDownCounter = nonrecordingAsyncFloat64Instrument{} + _ asyncfloat64.Gauge = nonrecordingAsyncFloat64Instrument{} +) + +func (n nonrecordingAsyncFloat64Instrument) Counter(name string, opts ...instrument.Option) (asyncfloat64.Counter, error) { + metrics = append(metrics, Metric{Name: strings.ReplaceAll(name, ".", "_"), Type: "COUNTER", Help: instrument.NewConfig(opts...).Description()}) + return n, nil +} + +func (n nonrecordingAsyncFloat64Instrument) UpDownCounter(name string, opts ...instrument.Option) (asyncfloat64.UpDownCounter, error) { + metrics = append(metrics, Metric{Name: strings.ReplaceAll(name, ".", "_"), Type: "UPDOWNCOUNTER", Help: instrument.NewConfig(opts...).Description()}) + return n, nil +} + +func (n nonrecordingAsyncFloat64Instrument) Gauge(name string, opts ...instrument.Option) (asyncfloat64.Gauge, error) { + metrics = append(metrics, Metric{Name: strings.ReplaceAll(name, ".", "_"), Type: "GAUGE", Help: instrument.NewConfig(opts...).Description()}) + return n, nil +} + +func (nonrecordingAsyncFloat64Instrument) Observe(context.Context, float64, ...attribute.KeyValue) { +} + +type nonrecordingAsyncInt64Instrument struct { + instrument.Asynchronous +} + +var ( + _ asyncint64.InstrumentProvider = nonrecordingAsyncInt64Instrument{} + _ asyncint64.Counter = nonrecordingAsyncInt64Instrument{} + _ asyncint64.UpDownCounter = nonrecordingAsyncInt64Instrument{} + _ asyncint64.Gauge = nonrecordingAsyncInt64Instrument{} +) + +func (n nonrecordingAsyncInt64Instrument) Counter(name string, opts ...instrument.Option) (asyncint64.Counter, error) { + metrics = append(metrics, Metric{Name: strings.ReplaceAll(name, ".", "_"), Type: "COUNTER", Help: instrument.NewConfig(opts...).Description()}) + return n, nil +} + +func (n nonrecordingAsyncInt64Instrument) UpDownCounter(name string, opts ...instrument.Option) (asyncint64.UpDownCounter, error) { + metrics = append(metrics, Metric{Name: strings.ReplaceAll(name, ".", "_"), Type: "UPDOWNCOUNTER", Help: instrument.NewConfig(opts...).Description()}) + return n, nil +} + +func (n nonrecordingAsyncInt64Instrument) Gauge(name string, opts ...instrument.Option) (asyncint64.Gauge, error) { + metrics = append(metrics, Metric{Name: strings.ReplaceAll(name, ".", "_"), Type: "GAUGE", Help: instrument.NewConfig(opts...).Description()}) + return n, nil +} + +func (nonrecordingAsyncInt64Instrument) Observe(context.Context, int64, ...attribute.KeyValue) { +} + +type nonrecordingSyncFloat64Instrument struct { + instrument.Synchronous +} + +var ( + _ syncfloat64.InstrumentProvider = nonrecordingSyncFloat64Instrument{} + _ syncfloat64.Counter = nonrecordingSyncFloat64Instrument{} + _ syncfloat64.UpDownCounter = nonrecordingSyncFloat64Instrument{} + _ syncfloat64.Histogram = nonrecordingSyncFloat64Instrument{} +) + +func (n nonrecordingSyncFloat64Instrument) Counter(name string, opts ...instrument.Option) (syncfloat64.Counter, error) { + metrics = append(metrics, Metric{Name: strings.ReplaceAll(name, ".", "_"), Type: "COUNTER", Help: instrument.NewConfig(opts...).Description()}) + return n, nil +} + +func (n nonrecordingSyncFloat64Instrument) UpDownCounter(name string, opts ...instrument.Option) (syncfloat64.UpDownCounter, error) { + metrics = append(metrics, Metric{Name: strings.ReplaceAll(name, ".", "_"), Type: "UPDOWNCOUNTER", Help: instrument.NewConfig(opts...).Description()}) + return n, nil +} + +func (n nonrecordingSyncFloat64Instrument) Histogram(name string, opts ...instrument.Option) (syncfloat64.Histogram, error) { + metrics = append(metrics, Metric{Name: strings.ReplaceAll(name, ".", "_"), Type: "HISTOGRAM", Help: instrument.NewConfig(opts...).Description()}) + return n, nil +} + +func (nonrecordingSyncFloat64Instrument) Add(context.Context, float64, ...attribute.KeyValue) { + +} + +func (nonrecordingSyncFloat64Instrument) Record(context.Context, float64, ...attribute.KeyValue) { + +} + +type nonrecordingSyncInt64Instrument struct { + instrument.Synchronous +} + +var ( + _ syncint64.InstrumentProvider = nonrecordingSyncInt64Instrument{} + _ syncint64.Counter = nonrecordingSyncInt64Instrument{} + _ syncint64.UpDownCounter = nonrecordingSyncInt64Instrument{} + _ syncint64.Histogram = nonrecordingSyncInt64Instrument{} +) + +func (n nonrecordingSyncInt64Instrument) Counter(name string, opts ...instrument.Option) (syncint64.Counter, error) { + metrics = append(metrics, Metric{Name: strings.ReplaceAll(name, ".", "_"), Type: "COUNTER", Help: instrument.NewConfig(opts...).Description()}) + return n, nil +} + +func (n nonrecordingSyncInt64Instrument) UpDownCounter(name string, opts ...instrument.Option) (syncint64.UpDownCounter, error) { + metrics = append(metrics, Metric{Name: strings.ReplaceAll(name, ".", "_"), Type: "UPDOWNCOUNTER", Help: instrument.NewConfig(opts...).Description()}) + return n, nil +} + +func (n nonrecordingSyncInt64Instrument) Histogram(name string, opts ...instrument.Option) (syncint64.Histogram, error) { + metrics = append(metrics, Metric{Name: strings.ReplaceAll(name, ".", "_"), Type: "HISTOGRAM", Help: instrument.NewConfig(opts...).Description()}) + return n, nil +} + +func (nonrecordingSyncInt64Instrument) Add(context.Context, int64, ...attribute.KeyValue) { +} +func (nonrecordingSyncInt64Instrument) Record(context.Context, int64, ...attribute.KeyValue) { +} diff --git a/telemetry/nonrecording/meter.go b/telemetry/nonrecording/meter.go new file mode 100644 index 0000000..15c9582 --- /dev/null +++ b/telemetry/nonrecording/meter.go @@ -0,0 +1,50 @@ +package nonrecording + +import ( + "context" + + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/metric/instrument" + "go.opentelemetry.io/otel/metric/instrument/asyncfloat64" + "go.opentelemetry.io/otel/metric/instrument/asyncint64" + "go.opentelemetry.io/otel/metric/instrument/syncfloat64" + "go.opentelemetry.io/otel/metric/instrument/syncint64" +) + +// NewNoopMeterProvider creates a MeterProvider that does not record any metrics. +func NewNoopMeterProvider() metric.MeterProvider { + return noopMeterProvider{} +} + +type noopMeterProvider struct{} + +var _ metric.MeterProvider = noopMeterProvider{} + +func (noopMeterProvider) Meter(instrumentationName string, opts ...metric.MeterOption) metric.Meter { + return noopMeter{} +} + +// NewNoopMeter creates a Meter that does not record any metrics. +func NewNoopMeter() metric.Meter { + return noopMeter{} +} + +type noopMeter struct{} + +var _ metric.Meter = noopMeter{} + +func (noopMeter) AsyncInt64() asyncint64.InstrumentProvider { + return nonrecordingAsyncInt64Instrument{} +} +func (noopMeter) AsyncFloat64() asyncfloat64.InstrumentProvider { + return nonrecordingAsyncFloat64Instrument{} +} +func (noopMeter) SyncInt64() syncint64.InstrumentProvider { + return nonrecordingSyncInt64Instrument{} +} +func (noopMeter) SyncFloat64() syncfloat64.InstrumentProvider { + return nonrecordingSyncFloat64Instrument{} +} +func (noopMeter) RegisterCallback([]instrument.Asynchronous, func(context.Context)) error { + return nil +}