From 0447cf0cfc01b0b3f539a8b86f4feb9e47bd3e4e Mon Sep 17 00:00:00 2001 From: Cem Basaranoglu Date: Sun, 23 May 2021 03:32:43 +0300 Subject: [PATCH] i added building blocks, components and other functionalities --- .DS_Store | Bin 0 -> 6148 bytes .idea/.gitignore | 8 + .idea/kenobi.iml | 9 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + README.md | 536 ++++++++++- dist/.DS_Store | Bin 0 -> 6148 bytes dist/images/arch.png | Bin 0 -> 238054 bytes dist/images/logo.jpg | Bin 0 -> 27320 bytes examples/todo-service/cmd/main.go | 21 + .../todo-service/pkg/api/todo_controller.go | 63 ++ .../pkg/application/base_handler.go | 16 + .../create_todo_command_handler.go | 34 + .../delete_todo_by_id_command_handler.go | 26 + .../find_todo_by_id_query_handler.go | 34 + .../repository/interfaces/todo_repository.go | 9 + examples/todo-service/pkg/domain/todo.go | 21 + .../repository/todo_repository.go | 36 + go.mod | 31 + go.sum | 888 ++++++++++++++++++ pkg/.DS_Store | Bin 0 -> 6148 bytes .../interfaces/distributed_caching_source.go | 13 + .../redis/redis_server_caching_source.go | 30 + pkg/caching/hybrid/hybrid_caching_source.go | 63 ++ .../interfaces/hybrid_caching_source.go | 11 + .../inmemory/inmemory_caching_source.go | 43 + .../interfaces/inmemory_caching_source.go | 9 + .../interfaces/configuration_source.go | 17 + .../local_file_configuration_source.go | 69 ++ ...local_file_configuration_source_options.go | 79 ++ ..._file_configuration_source_options_test.go | 88 ++ .../local_file_configuration_source_test.go | 17 + .../consul/consul_configuration_source.go | 82 ++ .../remote/etcd/etcd_configuration_source.go | 82 ++ .../firestore_configuration_source.go | 82 ++ .../remote_configuration_source_options.go | 86 ++ ...emote_configuration_source_options_test.go | 97 ++ pkg/controller/http_controller.go | 11 + pkg/controller/interfaces/controller_base.go | 6 + .../middlewares/healthcheck_middleware.go | 18 + pkg/http/middlewares/logging_middleware.go | 70 ++ pkg/http_client/.DS_Store | Bin 0 -> 6148 bytes pkg/http_client/http_client.go | 694 ++++++++++++++ pkg/http_client/http_client_behaviors.go | 212 +++++ pkg/http_client/middleware.go | 525 +++++++++++ pkg/http_client/redirect.go | 75 ++ pkg/http_client/request.go | 516 ++++++++++ pkg/http_client/response.go | 134 +++ pkg/http_client/resty.go | 29 + pkg/http_client/trace.go | 108 +++ pkg/http_client/transport.go | 29 + pkg/http_client/util.go | 292 ++++++ pkg/hub/hub.go | 100 ++ pkg/hub/matching.go | 43 + pkg/hub/matching_crities.go | 596 ++++++++++++ pkg/hub/message.go | 40 + pkg/hub/subscriber.go | 81 ++ pkg/logging/enumeration/encode_caller.go | 9 + pkg/logging/enumeration/encode_duration.go | 10 + pkg/logging/enumeration/encode_level.go | 9 + pkg/logging/enumeration/encode_time.go | 12 + pkg/logging/interfaces/logger.go | 10 + pkg/logging/logger.go | 60 ++ pkg/logging/logger_test.go | 158 ++++ pkg/logging/options/logger_options.go | 168 ++++ pkg/logging/options/logger_options_test.go | 359 +++++++ pkg/logging/providers/uber_zap.go | 65 ++ pkg/logging/providers/uber_zap_test.go | 46 + pkg/logging/utilities/logger_utility.go | 121 +++ pkg/logging/utilities/logger_utility_test.go | 282 ++++++ pkg/marshalling/interfaces/marshaller.go | 7 + .../json/default_json_marshaller.go | 48 + pkg/mediator/interfaces.go | 25 + pkg/mediator/mediator.go | 43 + pkg/mediator/pipeline.go | 54 ++ pkg/metrics/.DS_Store | Bin 0 -> 6148 bytes pkg/metrics/const_unix.go | 12 + pkg/metrics/const_windows.go | 13 + pkg/metrics/inmem.go | 337 +++++++ pkg/metrics/inmem_endpoint.go | 131 +++ pkg/metrics/inmem_signal.go | 117 +++ pkg/metrics/metrics.go | 293 ++++++ pkg/metrics/sink.go | 108 +++ pkg/metrics/sinks/.DS_Store | Bin 0 -> 6148 bytes pkg/metrics/sinks/circonus/circonus.go | 119 +++ pkg/metrics/sinks/datadog/dogstatsd.go | 140 +++ pkg/metrics/sinks/prometheus/prometheus.go | 437 +++++++++ pkg/metrics/start.go | 146 +++ pkg/metrics/statsd.go | 184 ++++ pkg/metrics/statsite.go | 172 ++++ pkg/mongodb/interfaces/mongodb_database.go | 18 + pkg/mongodb/mongodb_database.go | 128 +++ .../interfaces/postgresql_database.go | 18 + .../options/postgresql_server_options.go | 9 + .../standalone_postgresql_database.go | 104 ++ pkg/rabbitmq/.DS_Store | Bin 0 -> 6148 bytes pkg/rabbitmq/consumer.go | 51 + pkg/rabbitmq/consumer/connection.go | 22 + pkg/rabbitmq/consumer/consumer.go | 490 ++++++++++ pkg/rabbitmq/consumer/handler.go | 34 + pkg/rabbitmq/consumer/middleware/ack_nack.go | 45 + pkg/rabbitmq/consumer/middleware/expire.go | 43 + .../consumer/middleware/has_correlation_id.go | 18 + .../consumer/middleware/has_reply_to.go | 17 + .../consumer/middleware/middleware.go | 32 + pkg/rabbitmq/consumer/middleware/recover.go | 20 + pkg/rabbitmq/consumer/worker.go | 74 ++ pkg/rabbitmq/declare.go | 56 ++ pkg/rabbitmq/dialer.go | 450 +++++++++ pkg/rabbitmq/publisher.go | 54 ++ pkg/rabbitmq/publisher/connection.go | 22 + pkg/rabbitmq/publisher/publisher.go | 520 ++++++++++ pkg/redis/clustered/clustered_redis_server.go | 68 ++ .../clustered_redis_server_options.go | 8 + pkg/redis/failover/failover_redis_server.go | 69 ++ .../failover/failover_redis_server_options.go | 10 + pkg/redis/interfaces/redis_server.go | 12 + pkg/redis/options/redis_server_options.go | 8 + .../standalone/standalone_redis_server.go | 67 ++ .../standalone_redis_server_options.go | 8 + pkg/server/kenobi_server.go | 221 +++++ pkg/server/options/kenobi_server_options.go | 26 + pkg/utilities/http_utility.go | 36 + pkg/utilities/os_utility.go | 11 + pkg/utilities/string_slice_utility.go | 30 + pkg/utilities/string_utility.go | 18 + 126 files changed, 12703 insertions(+), 2 deletions(-) create mode 100644 .DS_Store create mode 100644 .idea/.gitignore create mode 100644 .idea/kenobi.iml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 dist/.DS_Store create mode 100644 dist/images/arch.png create mode 100644 dist/images/logo.jpg create mode 100644 examples/todo-service/cmd/main.go create mode 100644 examples/todo-service/pkg/api/todo_controller.go create mode 100644 examples/todo-service/pkg/application/base_handler.go create mode 100644 examples/todo-service/pkg/application/create_todo_command_handler.go create mode 100644 examples/todo-service/pkg/application/delete_todo_by_id_command_handler.go create mode 100644 examples/todo-service/pkg/application/find_todo_by_id_query_handler.go create mode 100644 examples/todo-service/pkg/domain/repository/interfaces/todo_repository.go create mode 100644 examples/todo-service/pkg/domain/todo.go create mode 100644 examples/todo-service/pkg/infrastructure/repository/todo_repository.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 pkg/.DS_Store create mode 100644 pkg/caching/distributed/interfaces/distributed_caching_source.go create mode 100644 pkg/caching/distributed/redis/redis_server_caching_source.go create mode 100644 pkg/caching/hybrid/hybrid_caching_source.go create mode 100644 pkg/caching/hybrid/interfaces/hybrid_caching_source.go create mode 100644 pkg/caching/inmemory/inmemory_caching_source.go create mode 100644 pkg/caching/inmemory/interfaces/inmemory_caching_source.go create mode 100644 pkg/configuration/interfaces/configuration_source.go create mode 100644 pkg/configuration/local_file/local_file_configuration_source.go create mode 100644 pkg/configuration/local_file/local_file_configuration_source_options.go create mode 100644 pkg/configuration/local_file/local_file_configuration_source_options_test.go create mode 100644 pkg/configuration/local_file/local_file_configuration_source_test.go create mode 100644 pkg/configuration/remote/consul/consul_configuration_source.go create mode 100644 pkg/configuration/remote/etcd/etcd_configuration_source.go create mode 100644 pkg/configuration/remote/firestore/firestore_configuration_source.go create mode 100644 pkg/configuration/remote/options/remote_configuration_source_options.go create mode 100644 pkg/configuration/remote/options/remote_configuration_source_options_test.go create mode 100644 pkg/controller/http_controller.go create mode 100644 pkg/controller/interfaces/controller_base.go create mode 100644 pkg/http/middlewares/healthcheck_middleware.go create mode 100644 pkg/http/middlewares/logging_middleware.go create mode 100644 pkg/http_client/.DS_Store create mode 100644 pkg/http_client/http_client.go create mode 100644 pkg/http_client/http_client_behaviors.go create mode 100644 pkg/http_client/middleware.go create mode 100644 pkg/http_client/redirect.go create mode 100644 pkg/http_client/request.go create mode 100644 pkg/http_client/response.go create mode 100644 pkg/http_client/resty.go create mode 100644 pkg/http_client/trace.go create mode 100644 pkg/http_client/transport.go create mode 100644 pkg/http_client/util.go create mode 100644 pkg/hub/hub.go create mode 100644 pkg/hub/matching.go create mode 100644 pkg/hub/matching_crities.go create mode 100644 pkg/hub/message.go create mode 100644 pkg/hub/subscriber.go create mode 100644 pkg/logging/enumeration/encode_caller.go create mode 100644 pkg/logging/enumeration/encode_duration.go create mode 100644 pkg/logging/enumeration/encode_level.go create mode 100644 pkg/logging/enumeration/encode_time.go create mode 100644 pkg/logging/interfaces/logger.go create mode 100644 pkg/logging/logger.go create mode 100644 pkg/logging/logger_test.go create mode 100644 pkg/logging/options/logger_options.go create mode 100644 pkg/logging/options/logger_options_test.go create mode 100644 pkg/logging/providers/uber_zap.go create mode 100644 pkg/logging/providers/uber_zap_test.go create mode 100644 pkg/logging/utilities/logger_utility.go create mode 100644 pkg/logging/utilities/logger_utility_test.go create mode 100644 pkg/marshalling/interfaces/marshaller.go create mode 100644 pkg/marshalling/json/default_json_marshaller.go create mode 100644 pkg/mediator/interfaces.go create mode 100644 pkg/mediator/mediator.go create mode 100644 pkg/mediator/pipeline.go create mode 100644 pkg/metrics/.DS_Store create mode 100644 pkg/metrics/const_unix.go create mode 100644 pkg/metrics/const_windows.go create mode 100644 pkg/metrics/inmem.go create mode 100644 pkg/metrics/inmem_endpoint.go create mode 100644 pkg/metrics/inmem_signal.go create mode 100644 pkg/metrics/metrics.go create mode 100644 pkg/metrics/sink.go create mode 100644 pkg/metrics/sinks/.DS_Store create mode 100644 pkg/metrics/sinks/circonus/circonus.go create mode 100644 pkg/metrics/sinks/datadog/dogstatsd.go create mode 100644 pkg/metrics/sinks/prometheus/prometheus.go create mode 100644 pkg/metrics/start.go create mode 100644 pkg/metrics/statsd.go create mode 100644 pkg/metrics/statsite.go create mode 100644 pkg/mongodb/interfaces/mongodb_database.go create mode 100644 pkg/mongodb/mongodb_database.go create mode 100644 pkg/postgresql/interfaces/postgresql_database.go create mode 100644 pkg/postgresql/options/postgresql_server_options.go create mode 100644 pkg/postgresql/standalone_postgresql_database.go create mode 100644 pkg/rabbitmq/.DS_Store create mode 100644 pkg/rabbitmq/consumer.go create mode 100644 pkg/rabbitmq/consumer/connection.go create mode 100644 pkg/rabbitmq/consumer/consumer.go create mode 100644 pkg/rabbitmq/consumer/handler.go create mode 100644 pkg/rabbitmq/consumer/middleware/ack_nack.go create mode 100644 pkg/rabbitmq/consumer/middleware/expire.go create mode 100644 pkg/rabbitmq/consumer/middleware/has_correlation_id.go create mode 100644 pkg/rabbitmq/consumer/middleware/has_reply_to.go create mode 100644 pkg/rabbitmq/consumer/middleware/middleware.go create mode 100644 pkg/rabbitmq/consumer/middleware/recover.go create mode 100644 pkg/rabbitmq/consumer/worker.go create mode 100644 pkg/rabbitmq/declare.go create mode 100644 pkg/rabbitmq/dialer.go create mode 100644 pkg/rabbitmq/publisher.go create mode 100644 pkg/rabbitmq/publisher/connection.go create mode 100644 pkg/rabbitmq/publisher/publisher.go create mode 100644 pkg/redis/clustered/clustered_redis_server.go create mode 100644 pkg/redis/clustered/clustered_redis_server_options.go create mode 100644 pkg/redis/failover/failover_redis_server.go create mode 100644 pkg/redis/failover/failover_redis_server_options.go create mode 100644 pkg/redis/interfaces/redis_server.go create mode 100644 pkg/redis/options/redis_server_options.go create mode 100644 pkg/redis/standalone/standalone_redis_server.go create mode 100644 pkg/redis/standalone/standalone_redis_server_options.go create mode 100644 pkg/server/kenobi_server.go create mode 100644 pkg/server/options/kenobi_server_options.go create mode 100644 pkg/utilities/http_utility.go create mode 100644 pkg/utilities/os_utility.go create mode 100644 pkg/utilities/string_slice_utility.go create mode 100644 pkg/utilities/string_utility.go diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..7a37296f7de610df618b448cd744999714567db9 GIT binary patch literal 6148 zcmeHK!AiqG5S?wSZYyFBf*$wct%o*ROAkV*dhjMh^q^uBQZ!hT(xeuxk^F}Kkze5N zIJ3Jg*6K-+qB}7AW@l$M%uCqK003(Z{2D+R05~dPu86}ILhGb6lGC07BBMRLVche& zk%#lwnP}sfM+WHK6~Ko8Vn{x|Kd;vf;-p%AW4U}`VR4D`5-+WkE}ZLV=p=43h+A&| zjAln}a_wpRIEuOx+x1R@&hWClwikto8-$&{Ob+}GhMb=Up&t!f(IE6=xsRh3I4|zlRR!}jg{!{{-6dRDA5`~an_Y8=B08j}L= zoPOAi!YlMqd>(pm16T7_c73}dYE6sEZdyMmTbK8#=rJ({4FiUO|IC0sGWqg83qgke~JNC z*mw3@SdzY5XNse{R-m>~NhmH;I7vZ6Ud0$oSMdg_6!Z%+5N(Z_LiC`R9|1{&84Lrz G%D_9E + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..6b37918 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 1a64a56..6b1fd1e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,534 @@ -# kenobi -The kenobi is the microservice framework based on golang-echo. It contains logging, marshalling, configuration, database, cache and third party providers. +# Welcome to Kenobi 👋 + +![Kenobi Logo](./dist/images/logo.jpg) + +Kenobi is a powerful, robust and clean microservice framework that we designed to rapidly develop services that include your business functionalities.Our main goal is to reduce the repetitive development cost while developing our functionalities, to enable us to focus on the solution of problems and to provide you with many basic capabilities while implementing these solutions. + +It takes its name from the obi-wan kenobi, who was the great Jedi master in the Star Wars universe. + +The whole structure is built on the [labstack-echo](https://echo.labstack.com/) framework. Our main motivation is to get rid of the cost of solving some problems in the native http implementation within the go. At this point, we do not plan to support "gin" or other microservice frameworks. + +## Table of Contents + +- [Background](#background) +- [Usage](#usage) + - [Kenobi Server](#kenobi-server) + - [Kenobi Http Controller](#kenobi-controller) + - [Kenobi Command/Event Handler](#kenobi-handler) + - [Logging](#logging) + - [Configuration](#configuration) + - [Caching](#caching) + - [Marshalling](#marshalling) + - [Mediator](#mediator) + - [Http Client](#http-client) + - [Metrics](#metrics) +- [Example](#example) +- [Maintainers](#maintainers) +- [Contributing](#contributing) +- [License](#license) + +## Background +![Kenobi Logo](./dist/images/arch.png) + +This kenobi contains three basic components, one of which is optional. + +You can create and run a http-server with the first of these three basic components. We named this component as kenobi-server. + +You can create your APIs and define your interfaces with the second component kenobi-controller. +For now, we only allow a single API structure or interface due to the functional decomposition principle. + +Finally, you can develop your own application structure, which includes your business functionalities or domain, with kenobi-handler and use it in a synchronous and / or asynchronous way. +> kenobi-handler is completely optional. + +However, it contains many common components like logging, configuration, caching, persistence, http-client etc. But these components are independent of the three basic components we mentioned above. + +The goals for this repository are: + +1. Gathering repeated components under the same framework +2. To create new microservices as rapidly as possible +3. Retaining a specific structure when creating a new service + +## Usage + +### Kenobi Server +You can create a server in a very simple and fast way. + +```go +kenobiServer := server.New("NAME_OF_YOUR_APP").UseHttp() +kenobiServer.Start() +``` + +You can start server with default values. Or, you can define your own settings. + +```go +kenobiServer := server.New("NAME_OF_YOUR_APP").UseHttp() +kenobiServer.StartWithOptions(&options.KenobiServerStartOptions{Port: YOUR_PORT}) +``` + +We support gracefully shutdown options. You can use this options. +```go +kenobiServer := server.New("NAME_OF_YOUR_APP").UseHttp() +kenobiServer.StartWithOptions(&options.KenobiServerStartOptions{Port: YOUR_PORT, GracefullyShutdown: true, GracefullyShutdownTimeoutPeriod: YOUR_PERIOD}) +``` +You can easily use the pre-defined middlewares. + +```go +kenobiServer := server.New("NAME_OF_YOUR_APP").UseHttp(). + WithRecoverMiddleware(). + WithRequestIDMiddleware(). + WithAllowAnyCORSMiddleware(). + WithGzipMiddleware(). + WithHealthCheckMiddleware("/ping", "pong!") +kenobiServer.Start() +``` + +Or you can apply your custom middlewares. +```go +kenobiServer := server.New("NAME_OF_YOUR_APP").UseHttp(). + WithCustomMiddlewares() +kenobiServer.Start() +``` + +You can use default logger, +```go +kenobiServer := server.New("sample_app").WithDefaultLogger().UseHttp() +kenobiServer.Start() +``` +or you can use your own logger. +```go +kenobiServer := server.New("sample_app").WithLogger(YOUR_LOGGER).UseHttp() +kenobiServer.Start() +``` +or you can specify your logger options +```go +kenobiServer := server.New("sample_app").WithDefaultLoggerWithOptions(&options.LoggerOptions{--OPTIONS--}).UseHttp() +kenobiServer.Start() +``` + +On the other hand, You can use the pre-defined http middlewares or you can use your own http middlewares. You can see the available http middlewares in the list below. + +* Recover Middleware +```go +kenobiServer := server.New("sample_app").UseHttp().WithRecoverMiddleware() +kenobiServer.Start() +``` + +* HealthCheck Middleware + +```go +kenobiServer := server.New("sample_app").UseHttp().WithHealthCheckMiddleware("YOUR_PATH","YOUR_RESPONSE") +kenobiServer.Start() +``` +* Timeout Middleware + +```go +kenobiServer := server.New("sample_app").UseHttp().WithTimeoutMiddleware("DURATION_OF_TIME_OUT") +kenobiServer.Start() +``` +* CORS Middleware + +```go +kenobiServer := server.New("sample_app").UseHttp().WithCORSMiddleware("ALLOWS_VERBS") +kenobiServer.Start() +``` +* RequestID Middleware + +```go +kenobiServer := server.New("sample_app").UseHttp().WithRequestIDMiddleware() +kenobiServer.Start() +``` +* Gzip Middleware + +```go +kenobiServer := server.New("sample_app").UseHttp().WithGzipMiddleware() +kenobiServer.Start() +``` +* Logging Middleware + +```go +kenobiServer := server.New("sample_app").WithDefaultLogger().UseHttp().WithLoggingMiddleware() +kenobiServer.Start() +``` +* Prometheus Middleware + +```go +kenobiServer := server.New("sample_app").UseHttp().UsePrometheus("YOUR_EXCLUDED_ENDPOINTS") +kenobiServer.Start() +``` +* Openstack Middleware + +```go +kenobiServer := server.New("sample_app").UseHttp().UseOpenTracing() +kenobiServer.Start() +``` +* Newrelic Middleware + +```go +kenobiServer := server.New("sample_app").UseHttp().WithNewRelicMiddleware("YOUR_LICENCE_KEY") +kenobiServer.Start() +``` + + +### Kenobi Controller + +You must define your own controller using with the controller.HttpController interface. +```go +type HelloWorldController struct {} + +func(h HelloWorldController) Name() string { + return "welcome" +} + +func(h HelloWorldController) Prefix() string { + return "" +} + +func(h HelloWorldController) Version() string { + return "" +} +func(h HelloWorldController) Endpoints() * map[string] map[string] echo.HandlerFunc { + return &map[string] map[string] echo.HandlerFunc { + "/say-hi": { + "GET": func(context echo.Context) error { + return context.JSON(200, "Hello!") + }, + }, + } +} + +func NewHelloWorldController() controller.HttpController { + return &HelloWorldController {} +} +``` +You can add your own controller to the kenobi-server after defining your controller +```go +kenobiServer := server.New("sample_app"). + WithDefaultLogger(). + UseHttp(). + WithController(NewHelloWorldController()) + kenobiServer.Start() +``` +The server will automatically detect your endpoints while it is being created. + +### Kenobi Handler + +You can use your own command or event handlers. +```go + +type HelloWorldCommand struct { + Name string +} + +func (*HelloWorldCommand) Key() string { return "HelloWorldCommand" } + +type HelloWorldCommandHandler struct{ } + + +func NewHelloWorldCommandHandler() HelloWorldCommandHandler { + return HelloWorldCommandHandler{} +} + +func (h HelloWorldCommandHandler) Handle(_ context.Context, command mediator.Message) (interface{}, error) { + cmd := command.(*HelloWorldCommand) + return fmt.Sprintf("Hello, %s", cmd.Name), nil +} + +``` +If you want to use these handlers, you have to make some minor changes to your controller. **Remember, our main motivation is to let developers have their own playground. That's why you have to apply the components you need yourself.** +```go + +type HelloWorldController struct { + mediator *mediator.Mediator +} + +func (h HelloWorldController) Name() string { + return "hello-world" +} + +func (h HelloWorldController) Prefix() string { + return "hello-world" +} + +func (h HelloWorldController) Version() string { + return "v1" +} + +func (h HelloWorldController) Endpoints() *map[string]map[string]echo.HandlerFunc { + return &map[string]map[string]echo.HandlerFunc{ + "/say-hi": { + "GET": func(echoContext echo.Context) error { + result, _ := h.mediator.Send(context.Background(), &HelloWorldCommand{Name: echoContext.QueryParam("name")}) + return echoContext.JSON(200, result) + }, + }, + } +} + +func NewHelloWorldController() controller.HttpController { + m, _ := mediator.NewContext(). + RegisterHandler(&HelloWorldCommand{}, NewHelloWorldCommandHandler(baseHandler)). + Build() + + return &HelloWorldController{ + mediator: m, + } +} + +``` +Also you can use **Publish()** to your async processes. On the other hand, You can build your own pipelines and add your own pipes to mediator. + +### Logging + +We preferred to use uber's zap to logging but you can use your own logging provider if you want. You just need to apply logging.Logger interface. + +```go + if defaultLogger, err: = logging.New(); + err != nil { + panic(err) + } else { + defaultLogger.Debug("HELLO,WORLD!") + } +``` + +Also, you can customize logger with options. + + +```go + if defaultLogger, err: = logging.NewWithOptions( & options.LoggerOptions { + DefaultParameters: nil, + Development: false, + Encoding: "", + OutputPaths: nil, + ErrorOutputPaths: nil, + Level: "", + TimeKey: "", + LevelKey: "", + NameKey: "", + CallerKey: "", + StackTraceKey: "", + MessageKey: "", + FunctionKey: "", + EncodeLevel: nil, + EncodeTime: nil, + EncodeDuration: nil, + EncodeCaller: nil, + }); + err != nil { + panic(err) + } else { + defaultLogger.Debug("HELLO,WORLD!") + } +``` +**We do not planned support sinks for logging.** We strongly recommend that you use the pull-based logging strategy also known as log shippers like fluent-bit, fluent-d or others. + +### Configuration +We only support three different configuration source. +* You can use your local files as a configuration source. +* You can use your remote files as a configuration source. +* You can use your own configuration structure + +##### Your local files as a configuration source +You can use your yaml, json, ini and env files as a configuration source. +```go + if localConfigurationSource, err: = local_file.NewWithOptions("sample_config", "yaml", "./configs/"); + err != nil { + panic(err) + } else { + fmt.Println(fmt.Sprintf("your value is %s", localConfigurationSource.GetStringValueByKey("sample"))) + } +``` + +Also, you can use consul, etcd and firestore as an configuration source +```go + watcherDuration, _: = time.ParseDuration("10s") + if consulConfigurationSource, err: = consul.NewWithOptions("YOUR_CONSUL_ENDPOINT", "yaml", "development", watcherDuration); + err != nil { + panic(err) + } else { + fmt.Println(fmt.Sprintf("your value is %s", consulConfigurationSource.GetStringValueByKey("sample"))) + } +``` +If you want to use remote configuration source, you must specify the duration of watcher. We will be watching the changes in the configuration source in the time interval you have specified. + + +### Caching +In fact, the caching is a very complex problem in computer science.We support caching with three different methods. +* Inmemory caching +```go +if inmemoryCachingSource, err := inmemory.New(); err != nil{ + panic(err) + }else{ + if err := inmemoryCachingSource.SetValue("sample", "sample"); err != nil{ + panic(err) + }else{ + if valueFromCache, err := inmemoryCachingSource.GetValueByKey("sample"); err != nil{ + panic(err) + }else{ + fmt.Println(to.String(valueFromCache)) + } + } + } +``` +* Distributed Caching + For now, we support three different redis server of standalone, failover and clustered as distributed caching source. However, you can use the distributed caching source you want by applying interfaces.DistributedCachingSource. +```go +defaultLogger, _: = logging.New() +defaultMarshaller: = json.New() + +redisServer: = standalone.New(defaultLogger, defaultMarshaller, & standalone.StandaloneRedisServerOptions { + Address: "YOUR_REDIS_SERVER_ADDRESS", + RedisServerOptions: & options.RedisServerOptions { + Username: "YOUR_REDIS_SERVER_USER_NAME", + Password: "YOUR_REDIS_SERVER_PASSWORD", + Database: 1, + }, +}); + +distributedCachingSource: = redis.New(redisServer) +expiration, _: = time.ParseDuration("10s") +if err: = distributedCachingSource.SetValue(context.Background(), "sample", "sample", expiration); +err != nil { + panic(err) +} else { + var value string + if err: = distributedCachingSource.GetValueByKey(context.Background(), "sample", value); + err != nil { + panic(err) + } else { + fmt.Println(value) + } +} +``` +* Hybrid Caching + You can use both inmemory and distributed caching source at the same time. This method is to implement the "cache-aside" pattern. +```go + +func getMyValueFromDatabase() (interface{}, error){ + return "Hello,World", nil +} + +``` +```go +defaultLogger, _: = logging.New() +defaultMarshaller: = json.New() + +redisServer: = standalone.New(defaultLogger, defaultMarshaller, & standalone.StandaloneRedisServerOptions { + Address: "YOUR_REDIS_SERVER_ADDRESS", + RedisServerOptions: & options.RedisServerOptions { + Username: "YOUR_REDIS_SERVER_USER_NAME", + Password: "YOUR_REDIS_SERVER_PASSWORD", + Database: 1, + }, +}); + +distributedCachingSource: = redis.New(redisServer) +inmemoryCachingSource, _: = inmemory.New() + +hybridCachingSource: = hybrid.New(inmemoryCachingSource, distributedCachingSource) + +expiration, _: = time.ParseDuration("10s") +if val, err: = hybridCachingSource.GetOrSetValueByKey(context.Background(), "sample", expiration, getMyValueFromDatabase); +err != nil { + panic(err) +} else { + fmt.Println(val) +} +``` +Also, you can develop your own caching providers with using the following interfaces: +InMemoryCachingSource, DistributedCachingSource or HybridCachingSource + +### Marshalling +We did not use the native json marshaller due to performance problems. Instead, we used json-iterator by default. You can use our default marshaller or you can use your own choose. +```go +type Marshaller interface { + Marshall(v interface{}) ([]byte, error) + Unmarshall(data []byte, v interface{}) error + MarshallString(v interface{}) (string, error) + UnmarshallString(data string, v interface{}) error +} + +``` + +### Mediator +We use the mediator pattern because of its many functionality. For this reason, we applied the mediator as a behavior in the kenobi. If you want, you can develop and use your own implementation of mediator pattern using the our Mediator interface. If you need the pipeline behavior, you can use the Pipeline interface or customize it. +```go + + +type ( + Sender interface { + Send(context.Context, Message) (interface{}, error) + } + Builder interface { + RegisterHandler(request Message, handler RequestHandler) Builder + UseBehaviour(PipelineBehaviour) Builder + Use(fn func(context.Context, Message, Next) (interface{}, error)) Builder + Build() (*Mediator, error) + } + RequestHandler interface { + Handle(context.Context, Message) (interface{}, error) + } + PipelineBehaviour interface { + Process(context.Context, Message, Next) (interface{}, error) + } + Message interface { + Key() string + } +) +``` + + +### Http Client +If you want to communicate with a different servers at the http layer, you can use the http-client we developed. +```go + httClient := http_client.New() + httClient.UseBaseUrl("BASE_URL").UseTimeout("YOUR_CLIENT_TIMEOUT") + var responseModel SampleResponseModel + if rawResponse, err := httClient.NewRequest().UseResponse(&responseModel).Get("YOUR_SERVER_URL"); err != nil{ + fmt.Println(err) + }else{ + fmt.Println(rawResponse) + fmt.Println(responseModel) + } +``` +Also you can customize QoS. + +```go +httClient.UseRetryCount(3).UseRetryWaitTime(RETRY_DURATION) +``` + +## Metrics +We support statsd, statsite, prometheus, datadog, circois, in-memory and empty sinks, but you can apply and use the sink interface for any third-party product. + +```go + type MetricSink interface { + SetGauge(key []string, val float32) + SetGaugeWithLabels(key []string, val float32, labels []Label) + EmitKey(key []string, val float32) + IncrCounter(key []string, val float32) + IncrCounterWithLabels(key []string, val float32, labels []Label) + AddSample(key []string, val float32) + AddSampleWithLabels(key []string, val float32, labels []Label) +} +``` + +## Example + + +## Maintainers + +[@cem-basaranoglu](https://github.com/cem-basaranoglu). + +## Contributing + +Feel free to dive in! [Open an issue](https://github.com/ereb-or-od/kenobi/issues/new) or submit PRs. +Kenobi follows the [Contributor Covenant](http://contributor-covenant.org/version/1/3/0/) Code of Conduct. + +### Contributors + +This project exists thanks to all the people who contribute. + + + +## License + +[MIT](LICENSE) © Cem Basaranoglu diff --git a/dist/.DS_Store b/dist/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..f2f2acfa90b3e32a300033edf2e738b401d3f46d GIT binary patch literal 6148 zcmeHK!A=4(5N!dqi!pLAkz+63IDioFVzNOzcr#g}2Q{!82y6nogk9VyV)i%mkNg6E z$C`MvzTGYAtWcH?Q( zbSJmiyL97$XY8vm94>9wyY_oYyKrz82C?f0y@^WpyFCcGyYYi=m^8y_(2dkQjvf%g z5>~rVoXw6K^^&aCPUa;!Yc$FwS*zCPb4wf^RZcHDqsOP&^ZeyiGt1#8RI+by0dHWe zZ1TaG1j8`sgMX2;h$SQjhyh|?0~jzzpV-}iz0wwm0b<~14B+`7K@n|(sYZ2lK!evu z9M2)5fQ@enL}}1Am}-O(5Ux`Jbt*Sc46f6`FHM|nFx9Bj8CNUAJZ5EXUMO6x4t}Y^ z8Mif3OAHVLs|;jyw}AEk{QLL+Y7+H`0b<}^F~D;zr`3clncBLxIjpr3v;&HQajC{- k3K*&sLoAl!BB&DZOEds&gQ-UFfY3!i(LfC`@T&}b0_DC^TmS$7 literal 0 HcmV?d00001 diff --git a/dist/images/arch.png b/dist/images/arch.png new file mode 100644 index 0000000000000000000000000000000000000000..8c1dfe88a1fe8a2c5f3e7179e7a38d90864ed94a GIT binary patch literal 238054 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 z zhrCuN=F)^HVrPnxE$a|;XP92ex&OKzC1$2~bbOYqZUYsy|9$;iR&XbE{!eCN#bfUw zBX`O57OBn$a*vaiG~B!K{u!_HzR982DHa4?ee2gg0uC1LlLn9i;Fs{jw_cX~0*|8X z<(xkgGJJUQ56!qIdwRrMGRS7Ro*yiuS;oi|&oeA7y*az5IRynX&Ip(*k-zp$J|KUk z*7%Som8qA=rB(X9!o!$f;A-k~is1VSH$KqXFhgYv?6#xn)9z=T2B=!gSFEaDl-cT< zPrE;+(g@{f!{ybpW=*06V5BZfnHs5$Dk)snIz)tN_|Xhwgke${>sG5UP?$is#TaR! zqPusnKdV8epqG2orQd4Jaz$xDVMYq5efer!I;C|r#2h~F1 zw8s9!UH{W~+i@FJkb`U(^C7W~w8i74s&#JU)1S$^?(22=qRJJp2+~Bp*Kuoc-R18f z>9dgQi({Kv@15*;d`9V7eV0dnO(yXN@%U9Sz>`5cL_YnPUWLyu*t8sud=ga2QG5k_M-2gFoS0$Ukvgy#5I zJD54X9bKKa)3o~JR~9@Q(yIVXo1gW6*hjop`V@Q`_==u8+xB-WR@uk+aZ=6tUq`Rw zgaR)e$NfA8?d(+}WHQG#&?iR56Phqn)ivhnMtp9=Mj<6$D`wvt2SWpcWiRLKm2yqi z%CpCmPR39mSGp}VOf#qoxD`(F{g#E1TnkjKClBjQ2J}oSyyL^_8=~}vg{A4vbM*RN zi=-9_yS;+1@nd<_XPhico#mLyhqd+*Q_M#iyh#uR86t5P478JBW!kBQ$Sgv7Q4vJc zhrsm$tJGfX%^i<<>w5WXD6Lkxd(`wYD*EqJrt7vwSq7{A#gX|JRdCWa0F*8QXta3C$e_$f{vCW$R; zw)^~Wc-Bt?#H@Lgw+saQw_*)vN2CuAODQG1WFPr&-_f zmx!iUMy77GV2aK|D{MKdu8-jUo$Kalt*4zRL~HJ+S$>l%Ta<$8c>0A)UF!%J{jNaB zK<;yds(ikz_|X;i3kLZ650nMd1=58X4__-~%+O`;Nu_R;4!v>q$8AEvhQoci5O zdc>T4*$!oL(k6UQh67$J2cN-^UC(oemI|Sj;(}8yOn%n7Xs?TBYkf~5Y_%JKy3z>l z=RZwngKCp*zj0Td+wj+7^c26$x9u}Obt`h}7uIck1StCmlckBJo-a90ZuUf#7{y9N z<=Z2)(4EdaNEe(v0t{AeMLu*#9nGDbOtwd;<) z-|~tIfBK@WjIF&y{*i)idc)C<{a=1{zvmA~1uZA`wSog)r(X>}v_Br&^67z$8xS21 zngto8r1;Y97nD55*Sbl`n0+o&QF-9@EXt?9-Z6Osns@HZ<{Te&bn=WMo7a<;r#>_f zT36qdI895)*oSaTJ~iXoL?q#C%`gYmLgV`)=YKhFx=5D@B>oiAG5 z>ogrXH(EQFmEM$+5VCM?2FDVzHm``2%`6JWCPwV_EzDtCs1eZAGdS7I+X{kb;Qf{P zyp9h}Ywn#di)OoyBQ87p`zA{!t&Ft}BnJB>W+H=r2F~1OJ)Ze%-2IYR-xaWZ6Pm($ z;E_6NQ2-m^)~+XH@Hpo`)QL1_&rd32@&gMIuHki)sdNx`_{i)A( zrv>0yr`s|q`vk@(k=E1s2c>TO=)iEg@RL2IN5l?4dD==0yP)Aof^K}G#VZzHXw0=c z=vRNh6Ol*}-F|rMCi^gB>7w$5xv3&qvpTMgyol8h*jq&JSlJ^*qUJJUy~n$Amn7kh zLVa@hV?=4&)y=1;S#|7<;xV5+E9|GM_pcw4O=|@cHK$EZJPsR2yLNhuy#y%1w+D(5jBv>~&*wl>MA9Jn6h-RA^bn8F8@e-c# z6uxH{Lb=tx$J~1s*rHiOnHRq+s#uP3{h*n7z(?@}AlAO<2ui+wleqp#yj?L#7j@Ny z`ZS-VE`QVU4`K8Y3&uz}e5+v9R!T8BTiDFLI~K<0d-xHLsLYEt;+Q||U>j?dbgGMs zI$)tACg(_{rIidvIsIaC3J1BCy1^H@K&$;=g-0gaVt(C+n@C$v%D%JMXwOfLGY^6P zc8YvJMo3VUR{aPWiuS{z_5_r02?B?c+1T}co_Tj!gz{=a*3Lz~_zPE`1|QZ0A#BbP z6{8Tgu*ANTwjri&8Sk=gp>~=MBZfepINCUa5anA+lg5Fc8XE%8!p*kd{(T zl$>*YTy>20U&3nj0xyZ`CXP&4;@FRO?IE30xiGBR{%%PYraZON-c2~ zUF7Ff-fweGHpy03{=OUNkDQ{+pSKk#C44P9#B|L$sVs|}!#r}!9VJ6zm1jMKP=X5) zJd2Uk@o*B2pn?}6+sm{qsJFO(QR?|$ZC@0Mkc|=oY~$7;U1b<^sS>}KX8pyPebUwDPhYeU|@Ehg!`R>=z<;HC$gZ!>48A$|g zadajJ4@UTmgc{b^BbdLGODcZdhlm+uP8`hMa?u#_Kp8VQBnd$TSjO)ww^)i_}3aOH!6zOlT$gb{nmO4tjh)nK8 zoX8qhA`6MJ0aAyxY{am0&-2b&pm6(zk5j@BnIR1m@gdD8Hqbgi9MN zyG+hhWQN%`VMWziQGic~)cH@f#V=!RkAHl^gOW`MIS<>7 zmr_)cNPc^H0pE!gJt3*m#Do`B4Z5u38?_VeBHvg?H2q0 z`Xa&{c_<3#^^3_r=GvY80ztM{gqX9Ug5vyfzt_QR@ZQj-QBNlZOr-c9)!ZMsBMK2l zeQmnaRkj+E7tVAdZLg2ji2JS2Jq=W+GD01ox|Wq&)6gFsmTr&z8I+Z>j2bLsS}emj zh;jXTG%^Lj5sAD{!HQu#+PHFUKCwzXJt}O``D-fug3_D&#ba0lB$EE-ZR^H24jRqD z)sIbD_@S)qM3&V`l9N`Q9EoGu!X_%hx}wUm}HZ( z^WV`5C422yF?0jtB%OXf{L&4PHh%+yT*Xelnpxw;e$&wA`9!F)EH>vY-5-(3nZfCJ zG)4I)rh}U?J3G7WmD7^g-5c@%HcFKf_HBWp#w3aO;L+S{Hms7R59wq4_Sm(KOP)`w z$$+7CYz*C$!nt)0NF&vIRJfps58w<>{1oQAVDPN@82J2x#{6}Qde{RUa*qTr22 z)X4hcHd+pIzA=jh`SfSUk=3LP%aJZ)RXrM4pQ2?*!6@zB_<5-0oda3>n1=gwYl@+p zUYf%b+G*&r+w-Y zR&YoM2PtB`u&swyC(RL(MhMUIUYO(cg2tU;W*TEknnyw!yFJ`Nl^ZAqhEfq^WiwgA z8iFplHykv>HJLGw^?5Vj%-w~$%5?q~`_)lAvDvf!tBry;ad9GKcTR#e^GBLqc+-6y zo5D&8XurA@*OZjuTEwpHDKR@=+2y}$Qi?V!uq^yEv8kyE+W0LB7mm0?%y6faw3W3O zeoE=;xqD%{>*MX2N7+<$Snn;EftjRB`;{3Wrzn^ysx|0E9$#%a{$kGggJUEJdZLfh z*;52vR637_U-!efe%htu_CFk!%i}p58t{$c`5lnjzKx&EPw@8abWQo`K~Az^avCZZ zdMkoKQZbgc_<8~vWZs@`SBTrI8_jFPYS52y5%r@0!0F&k?zc&4dyIyB5hi9DwT0gW zI?vVd2%fQ+DP;!w_jzC*AIW)ri{uEg7n%FYF>@7Tbk(w;LTuGbwZIQap(AVwvP4}3 zYe32*`&Ax>IZc=OI0yvi*bCN*=(kWVJt4~rqxU1Oc{EIVFieA6x3`Un!#s7%%1#kb zQ_@C%Ug^Sg?uwe+6tpvE34$w$WaFcDNPGHQPd!er4Wis#_BK5&9CA#%L#}W0s85Fc z`S-83v_dSGUsoRfwrkvT>3tZeV7(&OaxYzwobFq=9VutXH_z~*3=1NNxPd$C6 zFRy(HQ>0^2yb@0FpG+i4a>}oab?@?)p~pU7zO1}s=fXdC^=_kp?x-KuO7 z>iZcT!vAVl%q!~ivITZruo{w!u!(KUuKXeoFz-Ts-8ziT|690v&=8Gf|3EBGWxBHt zTmOZ-oJDlxn5WqfL}~pRYTRkT3-XdPf>z6x^5qAbX`%bsn^vc*+2W^qA7acbW5h#; z)866&hhO(qt>C;n-Q^T+`nSDxm*<&MOle#e(84rqW~#j-);2n-wCnNO&lZfACQ-k} zsPyxuc&``KW~z-z^1g&)TsqNFq*KL!bEegX>&mZ1<>@{0@}{?j>F&6hhjgf`ozB?qC|~DF3bw*;&Dc>rDuVjKKjI(`jb<7sErvyKU`l^H zw*4~ybmVMRVueQ2D1AR>KHuA9rH((+SOOKeIbKOp{iI&vVTc=pS;_mhw&~J7=V748 zc0P9!^ak~(ht@bmDFj)6uemXn)XVDsWJB_lp`Hgf6Zm;L^)| zbhZ)C^pJJT6-0ZCT?$+Y+}}(O!IVW}2!9)=k~r_;&4WKmP&X9m0G8P28DZB5D?uZ1 zBMyv^qVgs!-Y*Rb&WRmlFRPIGn6nz39m6OGJ+4=!M9N^AW|4?>nO7cV)URaZTKSf} zHN1sw0=;TES5oCNG&+0fRx&1nXWma86N}#K$X?x@VolltOs}OxyqBVFzaWI7)?1kp>8R(~ z7Wd&}0ue?4ym&?-JEALPz8t@TlnWKY#bZn45d8`Y_^wDNBp&P4as%MFH4c@l!cB7R zdO#MO$k(L?-fnXdzHSn>!jbea`XaXX<_eY!bk>hU?Rj&ZD1}XXkt;M7Pv=5~cZGrp ziF?uF6+b~0=%fy6#?o>&-+Iv)`>EeW$dQP)bVT|Ez=6y~w`Wzzb<8TxMTxx_ z))U?*ozlLG3AtL{+BR{cBm=GNE~7k!P!hXVYJ>@MJvr;i zyVGb@UH{h)DTdGPB|bOZ`lF^~*3)IVW*_jCl=66HngYY?kk~gV@f4_fqFS&n4>#;B zhbGkHI=XWiA;sQjznV;Pg$DG7&fFyh-ZX~Hg^FQQU52PyzW9%%lZ}>~h|bsMmzn$i zxT$QP>>Cm|JccuL{ld+8Z6eMHs||JM!!FdpKHPK1s`%-cbuG7ntAz#c9Y8|cG`FDVWviUxMD!vHByNyzbS2CKf~Lo` ziHTiw9GUCE-a1xhu!)$D|5l(mii%f5XbJ@tm|v?$6l$ zc7D!n3Fk9{^fG9V3ME)gJghjv~-fV40oxKDsbh?>&~f) zz^tB!{U#FfN%qqO2r4~a4{B!RjuQ8e9FLduVsRSMqJ*MquBz0Wy9lV1jYp`DD+;24zSor-a3!yH zpJ@ojI(3voo7f?)z(B#%iZ%B|cf-|zUr{u7O`^kBiICG!So)Dq4IT7;wO+ohEmWSn zbZeAN=M9>&?*(NbB#<@y=|>7qtsaD%gesKU=V{4?Oem&1;G&2z9=gz6bNsHvPOJor znV5vDHPY?!h891m%U@jJ4!{w+|mhPXLr|cFcMHE&(Uv6MEKyvNfmnuQp@1k(G9&BXQb7NT62>dsm zixaAUfPdXv2V2^$?fg2&pwY`bXfa0J^`O%5dPrY9|79ZdIJ3@{q!kpt@~wH%#)k+A zh%7CYg%*WG;m;7^rTUABBNcHix;Yz+tBtotX0z5oZMbOss({gdPQ4`LT;+&2r z?#gck2synujX}1C96%nMQ*zq2bM*6dCE` zhAq+@?%K#>?|GV4h4?G));7<-n_o)QX@4+fD`*j_AUXXk6HzAf zkd3Ki(@-|&E+|g0#BTc4BrFx}hHCbQivEnJO1Cx5YHywZg5n5^4-co{ zbz{wkM_t$N;$!Xqx&!cm4S}ISQLt80tM#i)35v*EN@+xik`;*aTaH7R<&T)PY8XI{ zoS;zr4rK_0^T~W7=y9yo@{5Yn!0f2HERxHv9pM5r>B#aFRhjMmkmU0%YOlTex>!gCnG*hVepxwsDO31msgnFvX{|Ig=qMzPs5c|UzBqEyh~{lI@<>-3vR`3rE0Q(FQUt&S6UGx$5ES#KT2u88 zlS1kJ+aGwN0p5A(u^QbAId%$q+l);so+5zG^4f|~MCBN}_l2N!o5U4icM!(JnkA1I zR9_+`R=l(Xh)cXn{NZTQt>rkhLb|iXeKo-!BMIE9w%&Joc7IS(vCdq4{WW+R!zAEuluoGU^96If%BP2ekCW^ zJ`6zqw(C|kRVV?n5w-D@3}P&JM#V_0Py?;^<0db7d{Al9B-O46XZ zh(4BD{0*{?@?_wQ+C3#qK3t$_d9x0jMI#-pZY`a8Q|N<6E9Fee(SDw;m5}e!i;r+v z*W;UlVAO{bB)dvv`jzu~!KS4e(3Kxg(Gds`75*Trs_q#3I^fyWD)jZ2ex7kEo!;ZA2<7z$ zV3H_09VuG%!uvIp&c-w>CsgrH(RBj_#|hUtYOX= z#e8$^B8%j5h3K3#e7meWZ{-4u?et4APW>-PNyu85HC8+5BnXgRquu6?&D zEU#m>0#4MgCe1Piu%22HXP;mohC`)=DiE6lELk7?b zO0G`SHI%ib@)$sd?i_lAZ}86dA@^_Zcta9AM*GHK_LzZHol1oLmAuc-C&!lQ_=;h#B4A(sLz0Lw|D!sME{Osa3 znHs#o(H)MFN}3sen$KcW%q+WJH34?H$+51oF7WMxdSL4SZLCKC7NVxr9yQ;|<<+c22vdP?C^w6A zwu72)H@|HleFp($rzoMe4k&N^g8425B1P+CZOBn%kuuQ;LH>JavgeDQB&bMRml?wE zNjL%c?*_k0139^%?n-iba~tmC*nF{E0rv&+cVGPv=SrvQ0m`VI;IFa}A1Kl5(qBspSechU#a^`!BS4nA3n6YJyjV5D&<8zt8eaHhQj$T&?Cq1wTR;D{R5 z$^c~mpBR7Y*Ku*`b8vr;<|i}Gg01n&)2mLs4W}3YSX!26K5@h{YfeJ;uq{g%kR=Yu zt4%9D*`bCg0$8EHFuYStIC(VeU(d+wI$M?+0B!JeLg}%qavInxNYEIDCIXD`D#))s z^0m51(}(MDL&7iAt(o?gR?b9*O=>Ap(#(cA_2gFTj5GJV0Vb^9T>I}EDAz1zlE-9) zbPD$0mz|no;v4vvwg2mi;)jn2`glg+m_JO&DkGwWst^MxOFTN$ulyYI)4(VRmqLJX zI|<;FO}iUq57E&QPLB%Cr&4VR;0KpbxGCe#pssfe5LRh2H)TK0TIt|H*Oc|LY|}yy z&3^J5gN8gnSwA&`*P)cjs{1d6c>YfsTW)TCdW^CTzampOq9xE|*)mjpI5U1H?gWSh zT_jj`W3X1halLgSQHiwAb$->!BZ^!s(Lnns8dLSQ z77xbfhA{kMXTb9ht&VeuJJAnza1U!RSwUJ{n0xe zd=vaf!tY}bu1Y-qz5|y(P=Rg>IVwJBi=fs8N@mK2N*>%qfy+I5L}@t^v9QhytOb>#s)*BNF3zKnI+y4oTJRh;&=bK_ArkQa+T1T zG$GQgH8dz>+g@#Y9t{%ibHJsSly1~qLyJn^OIvYDblFO2Ssy{PkCM)VdFsT*T(~U1 zk-I;F5F$|CPe5PAVg?h+2;_+9Z!!K}J;=`DRG`(93^kSz$T?cr(xZ)U3mFe0MC1-6 zOGGIoAAU$pG=5$~X|6R|L-sPUaIqW%n|=G%L-4E#44psyM&Ei$M&BXx2+&c5*9lm$ zzB-U*plA&Ov%-P^2Ts>5%oEi9g85v5?85g!=K;;vrms5@{Y1+1Q&^@3iWndj$}R!! zObzHIBF2qm%n{I{jT8d33`)X64VgDPl3C1l1y5VtNgjAuvEk$>3V8!xba2h62d2L< z#nr77jD~cTG`Io`K)z7zvZ7Uk>^^zq=DdrbD-vIIo83JU((X=!>i-(ZN18i55IP zBu+FyE9fMDjysNMG>m=h!{zgtA&mejd<%k`|Fbl5NlSQ)HILVJB9j*^rnO%#vqT~oFoDl;xBps^QC z343_WZLb!x3V&*gLl74eUX7NkB%FCz z#Zqnkv0$&)4C}=aO+rfilQ~6p8d1HQ*2N>{^(M?VZ>!hCcb#<`l;~LA4Jv5VwJy(m z{NOTdR*;f0#!O@o%|mxeN1}@jrD39v+@Sz5!$yHO@BFpXlA=NY93t7f2W1lrS|!&B zq%7)pPqTmD#55`a6)D!jUq%(CSjHQg@Qm_pM+FY8V1NNL;4zb~axYe(oozbPWji!(KrFrAY$*y9-*3k%Ti;2ZFdacDQNrcJvYM_7Xxp0NvYmoSuK znxZijp-ejPL^wS;f%?w}%{gR$P>`$x<-qf_x_myeJf@Khhdo&bhSwvsELxpR^0JX6C^Sbf8 zVF;5CRS*4=R}A(9M{uT=J*Y*vRQr41RAFl{8+>RFg-)@A;nI?Nr2nvy6Qrs5ChY-p zs{GBvqv;UIBh8D+876%wQsyDy5Q-NIMOuZM6SYvvqgy=~JbbW9d8eTF)%1HKJ_EL7 zhDVqV8xC80{X!lDW%SaM$G#!h<5)=%7rugPiPrpG?d#KTx8A=lM;wb9;$Qt7EqJ~< zpWrZYm+hBJ`-M@67Da?aSpn+r;1ur;3(o5G@U!}=K zm{2+{lquG4sNPUoTQG^z2x2__5@%K*_}yn68<~DC!X(lzLN@E{`O(heapYf`ARIfH zBuu_j1Y5d%#$o1o)Z?uTpA9Fr%Z;{aNIcP8FWMH6`FHHR38zYMfl#3sRyc_~*0UF1>hu!zSU%>A0LdWY9*j#9JnzJFMBipM@(Ntj?8 z(?Xl_4!sFlt+!#{LQ*hC+xWPVa|(P{B1&3oP~;IH%_@sFQufQ13<&@$2Mz!W>`Sox zWvdbafuDPL?Dd14AS%hg5H0O6j)wkems-H-$eQ!~V+r|B-P1WiY$f~0czz8%ilk+Z zipH<6W&A$Q0zH84J1)!VO*0hj-g`U}) z1?n<_b`?f?_c(bKi*0QacSJ?@3S9I+O=)Z zy%V8D@4cC&t#?q%4Rzqvp*MwCT+LvwG`3#+2xr+du2iEZ{BC%@P8=t!?rK4w6;Y#R znHf{!5A@#=9ehQPTLu+BRe#D@0cJtUkD8=9RaeO$DPfURiAwtv;h}?0B1CdiFeTyn z{USig)T_=~=(xx!gCD~#YbY83zj5H5lYS1J^%r&T+$iKVtj=N;FNg-NM>zy~wiS`2 z1CABVH{wPx;So0|mxv@1&r^}fS3oW1SQYJ*&;9L>fozJWGtMY%}XDXD3nlMAXA?{Sd7X~v%;^nv% zr4=8iqRH_h*C}N^pYQFM>>4@fxtbBO>F(%MzY(fVxrs2w@dda^MW$Ic;m=oLG4;If z;Y~Ne=?v$fMit`>lWYu1m2uz?$Ii457o^+~lB|WkPq;KctLrp$pxG-rp$ZC>VV~p= zt)UfXK(!|C<3E#Lty50X%_IKxOKm-x7eSX=fVqO_fuqmBJMV$o5{?|Ku_!pD{>>y< zz26u)H(W_dXo9xC_Y~oH@OiT#_lFqjj56*_uV4N(x`dZDMg)U|hpuceQg%JXvGEB2>r4-U)2OYC# z12~7!bUpU~6-hNp{|li1uLUja@6A_JF#{OczD=yngN-81AMQuhiR75OQtQ`arvsqj z8wuL3m@_Ct$PV?H!jB}#byI;N<2%-Qpchk6;dNA08ja}cl-c(TQL;S8vT}q1#r7Cp zIp*!QHqSe)sm%c=PS*?~2yrQZ%?87Nr?7^~uJyOVJRLRNSK2R9XoJc+_BT&%OpSgu zUw#Aza~D4i3lk{w>PK8Hw54Wrd$%Gx*bSm%e!ex&KdlnQ;{5Wt!A(p3v(n=#M<=qs z!3y&<34q39klxQ^ofogJocZ=cJ0uqPQ`$s_ZW|4ghNug4V3-IF zaP=PwU<2zNr2bhKx0lDNf?daA7037 z5(Oym*N$H7xhrVqrLF`cgZrzTQYHW#_av>ggo zV2lp#A(KA_$;TvvYgqux*YJwh>2*#^}rZOSMPEkX`};k|?`4h7b>^y*wodQW&@RS-^~;2XKw zE>lq^?7i(GHhi?=t$h%KIJxhxtw-K<%pQeE_GMhAxZj)tY0QuI-^GRa{v&1Do9w-a zn7BISdxZ1`XcOqM;ELJ|vA}g9*S**ksAw60;H&?wrJs>=w0b~MdlziJsRHz8ygaAYstOeZ7IOA1@eceI3YsxQPy_f47lu~>htjMR-fNzp4|hXF zcNcQuY~8%s6iBF32%N$&8)LI0QDGO<4#=OIU=P?if8RZ&KCAWr5Oo$@aYfsf#w|zzg#<0!-QA&(AceaH zcXxMpclW|Q5Zv885Ht`7!GZ+RyxX^5k8%FN8SCt|=bYa>uxx`o5jJiVkR=E{lXF%7 zhe7f=f>6=Wujht-=VKu6&mj?qAXklLBd}(-PTjo!8~k+8BhB?_1jHkt={tZ&dQFYx z{o0_szO;g4*a2NBh#|PUjLo|%F_55*Fu}$FGn7&g3LB$|@k3bqm3df>toQ%ErwTiQ zAkSEidA5S+_o7;v$Bn4z%t)oPLn1S6r-?ti~HxR*Nt z6Z(vK{GG$Ok3J7R-3*#X(V|L3W1wi&Q%X-^!C@q@};`T*a&sW@g~D0 ze;7)o41*8b7v(eHE2pL?-uJCpGcwJ~qLyXYyv}Z1OZ}2u)-BNMN^GvJcoyh36F|v&_7w<2gSm zcyXH^{K=Q_hgP)EeBJ(yGM*gT_P07WOZngCr|2?7z99iKD&f_weM}LBjh_d=w)e8f?}C5~5zveNQ!9r(2aEW;-!J{COmyU7ALWDNED$}qxTn+Q1T-S6<#^v(3;;R;g za}(QckEDJjG9S7eGT$uChK}Utj)XYtfURTRsBu zvZ$TQudWHhAQ-IoLZl}xerj)&DM~S;af!{`6^M@f;zSg!Y~f;bB30RqV+me{&DfYV!`_rVOMET?W8x`#~ML54ljhXW!-%h z(WZj@VooH436|f($Qt1#E`v5)Ra|w3K!Rxw5c{iTH&LIyPcnq2ox3aTZB5}BeURQU z&Wm&lL(gy7=<(I>D^XlUVofu|A)2dGe!rOC-Z36e8x@MWm8DFc&_nF^YXRf>wgU*Z zm^==++ag8VYn2Lj2aS*!2y-u4^7F<6QUJ)VFM~b`aVclX%T3;eJ=`mJ6qk{|zcFCP zw$)Zq4;%@8w3jQ{#_(vxJqUf~LKu6;_{=aGm!WHD@@&(l#&{pjk~COGD2YO`>yFut z5TF5`%uAmEa&s>j=8aLsBj?IV!kbFHc3)70x`Ow#utm?}?4k(o`>A5v8s5ds60_0W z)u@d~*+H!le1wFbQv+%(xuVl8;oOc@h8nnW^ttV+tOJNCco> zIdhEAEh3$YCiT9@4res#b{s=u&6jgXitCEUHVEQ8L-nUFYMT1bYqc$o+IG3%Kb_Yz zAqA|%t)I+;7+IS5$s25Z&-t`g_eQQH$3X5AzC^N<1-fs$1z&y0Pp~2~zcD4QGlhs%S=QMtoX8-0qUd@Im@enG%CZsJv^gYZjpK9s~OgHmzf|ntk~nzIv3$ zbhNS3_OGf`R9oXt^`+F{$qc*Le77*eVp;{%hMA@BD`e4n#wwf_GK+uaD1sRsvj(mQ z;t@Y?6X%Mr{tg8jrv+mx!|zb*-BVvhUUU;(ML<9?L^u|;pAhO0e08ax#`t1E>ae7U zqK8-i&`9uh+OfvvJYm5I(HW6IjWpR*b+%H`ND{6-Xe{`3p(A050q34??{^|)M#X7t zd*c_*9p1}4CuJVQnwXXRS-xMJwSVzKWLy3y@7KI<=z~b>DvC0;rZ}TfhpXT9 zyQEjF9MnS5BCTPB6QtKqZZq=t}x75RL>bBcXbF{0+B#(GyXp za5{3)EJ6ulrHPSU#sA{bA5Gs($|N(W`zuMOp}HO(w?+d+CR(YhHoLW!5BRIZwg4}I z;r#h5=`nU>;^a?Tx=?8@372L^S2VvPGv6$sN4|AW|L;I*8r7@j5kE~ygZ6`P3PX0t zf#$#xWRxaEpJd)1p)xFn~4V#MlEzDMH*joVQe=R%j~h*he<6O#>mmk}nf zbvW1G_c$gvM(a9Vja*q3dQ zb>UbeWS1)+m)0Q^-L~6e&f$@8`*d4&ygpgrHs`x54}TXJp(YVpsnBieGI8IBCb&0b zYZwZUkeh}d8hW@LOz6^MGYL7K#SIilLdm`Dw7DhiOTUrwvWcQBBVVBy@KYRt<0oZo zU2H(&4gIl>Zw!Xf76sTc&WfNr61H{4b~QhwkV|#Oy|}U?zfzBC5#B+R`r@RDjqnR#~ZVdp`s<>wku1uQht+m?= zr_uSWt!qxzb1J`i#|H0`&yRQB#F+x_9*-}|K-U9giQLk)RK5m~_&Bp=Vg|g1+#dH5 zX|lj^4unq4I|0%809ynXQeC)b=2;OEsUlyMKd-~h&cA+kPMeJH;;rjmW$b_hN{!T3 zMMmxT&A<**buNSURNZ4O`?qn^+{Pm@9XgK4aKGt{2xBycyqCW1Eg;kmDMsH3Zi5qw zWf4MSLJe(gj*jEYnxG^I<*seu%amk^09?JabbKu~N^s^fMd-j(OK&swH{gt1K?d{Q zgy~W${rfY>s31TS!wrB4=-75`NyeM%k5!4ccs&0&Ae&?w)#x1U3(*OkkN|DV3J<0< zl}5UWOUgOS<$Lqk#?w1Q6cf3GuOy;4VmlqpOpa=75~@`hA~mV4N9=bQ7}9;vQVWwx z1ngj;2s8(MM4>`bJ>x@u>mt&@u1!>zQL=9M6(pu}m|A?sW~Nate(-kYq8uJ9>8oVH zQ+g)-LlA{pt*sM?``stNT;4MCJTbIW7!|AHaD-7@kPnTOYD8x7^9UB|=WexM;qFyB zn{t?ifRsBq^xq-)>(#(0Kzwynn0eJ!=>(f2K0-lD#Qvyfv>1+gbqS6l$LYFm>_?91 zTu}iYMK{`wAKi(;@51O0aU9C*4+TS`g7bhlfDj`K2{TT@4bK6;0|})owG1OrGD&ZR zU2-5*J<_7noQK*#3^+KNlX`K!Uzl*1|`>eA(o$}@1|$R3GW zt$Q~m4vdygeNSK|Kk;u+E2N&w#x>g+;N~u4j1bu}*XVkqqQ6=Gr$UrH0#(W9pyJ&7 zsbLAVdYOZ_hnZBwO16vFH?9_^nhXNPmmc_f!5a&tf%|*+{dZg6)t*yr?qQ~eXqUfe>hxh3xjao82-818@=gNQ|fdd1SA{j<5yb< zEeVsQ-X1NBgfKL|M+~VBSzHZ-X-klrLCDYLT}KtsE|xnLnie{ed6W&B+SAA9MZhYm z2PQcG=ZZH(Sf)9o@RDgKk|dfM7bOi+5)yJR#2Rqcxi{TZv(&*#Efeye1>G!V>&-LM zp2Wyjnh(Gy0XTmxx!gKI!5#5g4=;W!+fmR`Jx8k%E9x$$%b_;ol)}MY$kjsFzx5#A z!nY(WEV%+~15rC$G20dmVlzBweiV)O=Vr8KR1UaiRIkxJMs~$n91>}unW(|Noprgr zOMf2Jn7_nZxroJ__))dj!eSL+N>VDY;CH`8k*s9;gpE-bxEeXlP+>-b1#mwtlDzOk)ZL3JQs;0C4kSf+td4>;EhW6;1NPYgET`w6?_rtS0u74rTH!8Wi zEMG_{>xUAG$$0gZQon9Mh?WCUIv}S}UsOf90huSr)y3|7T9`@=IV4Ipmg11vsjKR} zY$RK@^3%GXkyq#L8#Rimk{1(NKVy@n&$ph?VX%Z+iFT&Aqu6ihaSm(`NHOI&oO0s( zF7B(#3^WaMPeu8Ar&qoN>N%3nwfX)Cnyp0FtNDR*MS%FqvxDukX^9rWI9mOCG3i9) zkYh;|%&_Rm)2%4TW28~s(K*`TS^)Sd8-Tq-UNKzar5}ieEdOIJ2G5k*Uiy+K20faO zS&oE9#2K-f^j}Q%fxkFF+{I}+2WJSobTmWTbB^ng(srT`C*&?)fB~(bMNAQGx_J;y zoI9VOoM83XA6!u#Aw^9AH(`ofn)z-ZuTjA=x5gK2lK+WB!_;0@_|B7EYr<+i&-3X< zRJh~q-dNp(r<|TO9xS8U$T0?){yU??Yle-v!H0aUnKGkOTpPwjv!YHrsz0h)w$vuKV!e`94G-CkZJ0Km|3q3 z@*6{gZMUX7k7cc^s}buS?|3s$WS9rLQ;0pluGjuqW6}dJ-DyCMKinj=2-W5JsxP79 zK)w$cA>pM>Qn3-Jq6}}b)e-~x9_>4&>G8JDN3xj|Q z>GFBdQu0u!TWAld)1s|p(>E2Sm3`~Ugx_zkHD!WEgCFRY-4udq*Njc|ZHJlDlZVYc zlaeztMrqNAocMY(9eW%1$9{FGx10^&Y%a@gk!Aq{Al#XDAR^<8kDFJ~vVgw)fw}c$XMDrt_aG!jU0iiFMJ$_DRXtZRf@Q*VzX;TmIiTdE(Yx z%N)@S0P-#RQR{#nu-}#x99Y?E`sMl&ON{UnxmwDO>8AvpL9mU|3j0(8Lnm*I8}NEp zk-@P$6KmSCk%j_k5d{_tJGLAAVLWL>5rL$`T;Tkx_cvAM`}`LX{m~F?duQ#%Y$i8q z!WKrn1iYfcfEtEO!;+}e8g;i9c-$Yu;~uA+S6N|G+(OS1CbPF$&Aa~NaTHUO;Nvmx z-MwN2YH4P5kg&Fd=#YF`E9=K2|U2Pp`VsYLqD0&<4Ah3dpcZ8IE_J zt`QfXiCl)l9PnLLVghAc;iI(InEDe1oOS`98JM5b-4O-NA)JbX!h;^p_uO`Wf<8o|gOSiNLFa~FFKi*HezU8)(r4Zq|ywZIx z4sJvNSNCd1_gJ3tQ=UYv|2o_SP%^%_HaJB>3cU2{40&Bwk z1<-S2E@e62jMR#nk=5zfxQ!4KM)V%Juv{>Q6dToK;f!Z3V@9FzIJr`Zpm(b9YXe}9 zAEKW$9fSD^l`@bot17z*qBoeF#`fkCq2XDHawg1IVcX>?9?w-_Z< zjovgR^o?NP9O1~jbx=y1k{C10V8DL-LbD^9K_uZm{fI3o`DJRHfgF3+jE54HISqkj zgdFX5E{}l6$Q9ebGY&`#g->LIz-eETSqDB%r?jM#3Q-_s+=n^me>!{RQ0)mnyT$DL zs)M-vzw^%+0+nQ!k<)o!`e8D@jo%-5hj~QO;U$d5{BDETbgb_>FGF!@f1_xcG~ieKjzBp~i=lXtu2cZg^8v^GC~Jux%;}wn4m*uga|7ix zsXte52Xevt8|6|5==_F5Q^-lAE0FcwV7@=MEHP3}nlMS%s?m6(6%-ZWLsbIppcKU7U&nDVLYhCCaX6!n|06!%5u5btC^TGXx)q}^MFHuX~g6jtJ zR8F_=BNGS2pQGzETBIRpE@1sL>0vemS>cAb==l6Fa-sD=CP1u{x$Gy!q(;@!!x3&o z71=_?zg>!_;6q>3YEy5A*O$_oAyFeknGTR?C?(`P83&VcJ%( zdw}DIJ_^eC><=UL5f~DZa>d>9W=%xQ=c;PF2`CkN>7l zWrt#h1G%BFp(^cX%=28B;ca|2Z^;kgy|hUw>o#Bd!J!|Z<+UKsl1#}{5&+IeFQ=C| zw(((7nL)j!5df|0;I}WAH45MKw0|{vv`aso=v_w3=4z=cU`v|u2XhzIjVI)?<+j#q zxI@EhvFQ7tj3*#hPAIc9hnNfbakqcjh>F&X;L{naU*h!N^6Q)Nbk+>w((O3~T=Snz zp?;oW2@2dJ47|M0EeO+BFIg4ebL1SIRN-9Tt&3{y;3 zei8P)7pQG#`26kw*bNQu{(Gy*KnP z{-_uBYPHJJGJu_aYzuWEBUZYHuoU@+x<^6*jMxa)4vj@z>7ZF~@Ys$u(~pgCAsfipv0IWj zL6Q@BTy!Ulug7Cc7YE`@-7<@Yb9hzhJ3{T5g;%uAQf)DgefPVUQq!;)ga+T9l`NiMKut5-Faf?gbBncCc%8As+0) zZ}vjXxfp}){b9=NiUj}?IPOf2O7OTy0;-yXR?eR&3n?^#9l=(zVv5ej~ucYMV) zDj8t3As2udI|$iES%gVp6biUnUz)@H#6rrfJ(TIn@+!+Lu%1E9d2lU`FlEDk7w3G# zCiYRiAu9wxjz2Z&GKOfbCZ4nI7IvYDBUUYucHw_b#0re<;3OOKY|R({l2{xuY?;@R z&AOgXIX3gbf>~OsxTGJ|{5whQebB}}vKMMX_7i3!3M9}%^3~nG`Q@DhUiNDh z+nJQRjQQ|uo06n-qkim#e=nrRbD?E@`50l*YRVZ2kBYHh{VZEhBF-Hu_wyT`c9$00 ztkWnXq}=A7*R~i`shUr)#x|=`@V(zhe}}UiLSg$kB?d2Ar;wy1{J{)R5zzLqLvc> zaBDq=k^1?azg{bocr?3gZ?*>~!<`^~>|RHGTjSfQkDpk-1PYkYnxY5b&k#+l>MRm( z9kQ>gi->$Ghj6@OIA0nH*pGe>|GeYG;#&j;pnHgng?9SGa9}jgd0s`za17-E4cZz+ zxWm5u;Tj1ug-%RfAqE1rMM#W~)BD(E`}TAG27gzsB0!oEH{ugdp|epnk^;|hS#R@P z2uxNFCBi(KH8yQE&yuY5z7HweQc1dzv}(~P>T~hG>eE4Q#c_$17tq<*$6?;`z6Ip> zwBa8>(T-$vDi=K;M2>!o*h8%?=%T)L$VI)3ziuhqbvJ(_5?CIqA0q19AibOS6NW)3 zk#Zbo(-wtZgvC3KZU~cQ1hL{0#LG?>+Bt@!2xPLJpSF_|Oc@ba8aXe&1Od*rEY=}i z5$*+t**%W4js7&N+RlBF8V7$}xA04g-g!1b=CAROI6vghsXFnmNb*_BZ_*PeySmb9 z;bcyE&htUMXa0hr_>}m{g|Xs859})Xjybl^v3KmY2=at+uQbrn+pcuNthYvLk@|$V zs1+{TqkXhGe1l5Xc!XTRD>~_1TXU#_j=2R?uL57Cekh`viUXaZDa5<9dAali)0}oT z(N`L0LQv9Zlg}dvd|I1j4wc;@(5-$vcrwR@Kqo$PcbOj&&UYm)%`jk z;T+{gE2Mr_)ca3}UZj%9phXOdqUG`w_s2ss_T>o1B*zR+=G_y}&*at4RfoP{t7Luy z>*}f$SuimAhJW;awV74qWlYbDM`Ibk9<(^uq!+!K`N6q{ zUS?>PXWS8-TO|*Ln=~as;$6)@I_!v{d@2%hp+~4T>Kx4>NQ%nNP>aynT`|DtsG=wgxAR`6HHPUTCfa$hxf@;|%W#U#uLgtjUAFdv*(Uwm;%u~8H_om=9 z#fpKpjPujY;CXOY4Nn;`nhnop{bXVIH23Jd>RbST&o=&dSEd%jx_rZam~uRd&86&X zr_FwdIPxV=1KF~V$Cd-RMzM99ca&$IG+9c{2$+DEtD~2H;T0Xrq1`Cf z*m|=f8RJ^6)Lgre;QH$*qtLs+GX?=otBcZ8M2W$e+xB7SC3?x%lj#KYGyx{9Kt#LF zauJHdZR}rxCwXA0mrHD=?Z~|RX*VzFYb!t#&_$o$Yudeq=SEDoD0Hm)Js@@45C@1o z?aWGHvIw62E-cR^tzl>^`B}5POq|_DtzLuYUj~7DTA( zcRFep2we#CuDKw>(Z@4wbMzAMp!*VJtl8cpM^DDM*`H(78TCZ*H^Lv4E_8)Xti&Xp zfT&v;^vNsqhpIqSGvuF-hKl&=@CaiGIqey?sH}0C7^1;28Uxy(p z-&O0ZW)xjSl>C&su=03URVJ62j2RM>D~uw`?{=n+aq8yz<*0~?huP@8K}8@LiLBj*)pvL^+{`_h$jsS16jLA)NX?LZnNt!q&Y&d3PSIVaE_8xUuCr!x&I z`xnRqoI8UOa%uzUQbHpE%^g~(Q-iw&Sf8vJwmTrKwra_|ytV%3y4L5a`-~+K3&jJ% zjc|8tR*4lDYqr%7VC)rqeY|gMy&;la^003v_sP2A}Xuk4blMP*jYCEp#v=Ce5&3 zv9qaI)uL$l@~Ijp`cmWaQv)-!l_5x~hZ2H+2&8ZKZUGoI0dFAM9A8AP6~WYWHPzEO}_? z{P9^OS7RZO78q$8p2jOByl7Cc+SD@Bhn7jP5I}cXxBl4*_z=2z-YoEaSHREN;EhvV zF*wMbNwr4K3QC@MNGkd@r~NRR;L*A9P$@t7uQRtd&pLxlOZ>Ya6_IA1=z8UC<6t|4 zKaO{;gL}zE=iGNSBaWtfJ-M}Z3$Ouj7j<@b*^Nzay5jk)55<$5AG_d9FV%4V?md&k zcd2Wyc!{4_V5rR>O5ZOex`;_@&}tiie?H@H+-~;v{dJgwgoK%k8pjeFC*&&gC70qx zV>})vqU#R7V%~xx9uu14%`L~uM#9bYem5Kio&#qMWEgCH8{Hg?R-R7|XC+AMz&Ww7 z|8W*QrEv_#c7rl8<`Q5uLx_0f!8Xyo5A=0F8_fkBxxcCdiapZxU9L;=KJ#-_%BT>5 z>S1gfyl6rcd^7`d%{+q(>8sR}q@eus3O`ywP=NFh3;Xck=u-(RC{ykIF}qoI#Qv!0sIRkH@WjJ*rxgpC9{c_=0;(d(<@G|w<;m|y@E zwWikVfes36SBP%}!VW#fwg2O=J#S6BCN%*C%X(;xWc$jz+P`|&VSdW$Q-OS@XiV;I z1G5T}j^V*FxB)f03irRD!f;~QA=R#EXJc?%9aLJHuer@jMk8j0bcEzNV_{*;6hXy+ z*P=|7*+!(hm`f!}ykefuBcEJQ*~zZfgx!rU7Bx$`ID5Kcn9hm3#=g9whMyhd1n4ZQ zS`&*G%@Iu%?GSANRIIc}p$%N@to8`YrpwaS0OJH`)BX5vH> z>A2DnvsoTgWc*TX8Bl@)HEp|d6-BUoeq8Bl4;cl2j9tDLjfI^DDqmfOjT5e~3Z^ym z*u9&-`oCaO_5hG-A;Gt5W8lWQDan^(Qe2(oSM&fO@+M`f

slIIkz1sAG^3Q7YJRkbqw&LEtr=B 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

^U48jZ%mXUD}+1wLILxAKQCTJR1x9$?qnY*MX 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 Km*p=Y0sjXuX#?E= literal 0 HcmV?d00001 diff --git a/dist/images/logo.jpg b/dist/images/logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ca552daf449b9c5896ec3c35c3a1d40f701cb57d GIT binary patch literal 27320 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 500: + logger.Error("[server-http-log]", err, logParameters) + case n >= 400: + logger.Warn("[server-http-log]", logParameters) + case n >= 300: + logger.Warn("[server-http-log]", logParameters) + default: + logger.Info("[server-http-log]", logParameters) + } + return + } + } +} diff --git a/pkg/http_client/.DS_Store b/pkg/http_client/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..1ce6777bca8ca8ce7c44d878ac108cdf2efbf150 GIT binary patch literal 6148 zcmeHK%}xR_5S}7R!5BH1=&=`X901XTgUJT*;LT)>9@M~YAhHSW8g>y8G5Z?&Mm~YB z<4jwOiXOZeLuQicH=Un0{dUtI01%B~rvgv}0EtSN$z$`4P@Hs5GS)*V^cVvO;Lab# zH-3LEn;rj=0s3}1=)o9bNZ`Zwi+qf{_&5C^PRiw1ky*+vujHgG$iiCT((OefH}R5T z-0%kH)I0W)o^S1wDC)o4o_`v2N3G(D*|JJF~S4Z}{X$8k+S z%Dl|Cid&OOeYaLpm3nnrQj>bMTvD~2z3DVBH@7SMNA2tJ{p4Z#_++>h_#0|jw>X1m zG`=kJ)*XcXD7?bRtZJ6c$P6$8%)o*(V7EQFy5Q^N*&7&#{;qH02=l%s7snS=bkf(6ggo z>2wf|L2j7=X5cdeOQu_-^Z)SY_y6Z2?lA+*z(O$~vQ4+yz$MwUb#8HV)+*FCDhcJK m2H#82&_^-m(owvHss;Uu3`ECbY7jjrd=bzzaKjAzDFbi+-cwHi literal 0 HcmV?d00001 diff --git a/pkg/http_client/http_client.go b/pkg/http_client/http_client.go new file mode 100644 index 0000000..0e7bc22 --- /dev/null +++ b/pkg/http_client/http_client.go @@ -0,0 +1,694 @@ +package http_client + +import ( + "bytes" + "compress/gzip" + "crypto/tls" + "crypto/x509" + marshallers "github.com/ereb-or-od/kenobi/pkg/marshalling/interfaces" + "github.com/ereb-or-od/kenobi/pkg/marshalling/json" + + "errors" + "fmt" + "github.com/ereb-or-od/kenobi/pkg/logging" + logger "github.com/ereb-or-od/kenobi/pkg/logging/interfaces" + "io" + "io/ioutil" + "math" + "net/http" + "net/url" + "reflect" + "regexp" + "strings" + "sync" + "time" +) + +const ( + // MethodGet HTTP method + MethodGet = "GET" + + // MethodPost HTTP method + MethodPost = "POST" + + // MethodPut HTTP method + MethodPut = "PUT" + + // MethodDelete HTTP method + MethodDelete = "DELETE" + + // MethodPatch HTTP method + MethodPatch = "PATCH" + + // MethodHead HTTP method + MethodHead = "HEAD" + + // MethodOptions HTTP method + MethodOptions = "OPTIONS" +) + +var ( + hdrUserAgentKey = http.CanonicalHeaderKey("User-Agent") + hdrAcceptKey = http.CanonicalHeaderKey("Accept") + hdrContentTypeKey = http.CanonicalHeaderKey("Content-Type") + hdrContentLengthKey = http.CanonicalHeaderKey("Content-Length") + hdrContentEncodingKey = http.CanonicalHeaderKey("Content-Encoding") + + plainTextType = "text/plain; charset=utf-8" + jsonContentType = "application/json" + formContentType = "application/x-www-form-urlencoded" + + jsonCheck = regexp.MustCompile(`(?i:(application|text)/(json|.*\+json|json\-.*)(;|$))`) + xmlCheck = regexp.MustCompile(`(?i:(application|text)/(xml|.*\+xml)(;|$))`) + + bufPool = &sync.Pool{New: func() interface{} { return &bytes.Buffer{} }} +) + +type ( + // RequestMiddleware type is for request middleware, called before a request is sent + RequestMiddleware func(*HttpClient, *Request) error + + // ResponseMiddleware type is for response middleware, called after a response has been received + ResponseMiddleware func(*HttpClient, *Response) error + + // PreRequestHook type is for the request hook, called right before the request is sent + PreRequestHook func(*HttpClient, *http.Request) error + + // RequestLogCallback type is for request logs, called before the request is logged + RequestLogCallback func(*RequestLog) error + + // ResponseLogCallback type is for response logs, called before the response is logged + ResponseLogCallback func(*ResponseLog) error + + // ErrorHook type is for reacting to request errors, called after all retries were attempted + ErrorHook func(*Request, error) +) + +type HttpClient struct { + BaseUrl string + QueryParam url.Values + FormData url.Values + Header http.Header + UserInfo *User + Token string + AuthScheme string + Cookies []*http.Cookie + Error reflect.Type + Debug bool + DisableWarn bool + AllowGetMethodPayload bool + RetryCount int + RetryWaitTime time.Duration + RetryMaxWaitTime time.Duration + RetryConditions []RetryConditionFunc + RetryHooks []OnRetryFunc + RetryAfter RetryAfterFunc + Marshaller marshallers.Marshaller + + HeaderAuthorizationKey string + + jsonEscapeHTML bool + setContentLength bool + closeConnection bool + notParseResponse bool + trace bool + debugBodySizeLimit int64 + outputDirectory string + scheme string + pathParams map[string]string + log logger.Logger + httpClient *http.Client + proxyURL *url.URL + beforeRequest []RequestMiddleware + udBeforeRequest []RequestMiddleware + preReqHook PreRequestHook + afterResponse []ResponseMiddleware + requestLog RequestLogCallback + responseLog ResponseLogCallback + errorHooks []ErrorHook +} + +type User struct { + Username, Password string +} + +func (c *HttpClient) UseBaseUrl(url string) *HttpClient { + c.BaseUrl = strings.TrimRight(url, "/") + return c +} + +func (c *HttpClient) UseHeader(header, value string) *HttpClient { + c.Header.Set(header, value) + return c +} + +func (c *HttpClient) UseHeaders(headers map[string]string) *HttpClient { + for h, v := range headers { + c.Header.Set(h, v) + } + return c +} + +func (c *HttpClient) UseHeaderVerbatim(header, value string) *HttpClient { + c.Header[header] = []string{value} + return c +} + +func (c *HttpClient) UseCookieJar(jar http.CookieJar) *HttpClient { + c.httpClient.Jar = jar + return c +} + +func (c *HttpClient) UseCookie(hc *http.Cookie) *HttpClient { + c.Cookies = append(c.Cookies, hc) + return c +} + +func (c *HttpClient) UseCookies(cs []*http.Cookie) *HttpClient { + c.Cookies = append(c.Cookies, cs...) + return c +} + +func (c *HttpClient) UseQueryParam(param, value string) *HttpClient { + c.QueryParam.Set(param, value) + return c +} + +func (c *HttpClient) UseQueryParams(params map[string]string) *HttpClient { + for p, v := range params { + c.UseQueryParam(p, v) + } + return c +} + +func (c *HttpClient) UseFormData(data map[string]string) *HttpClient { + for k, v := range data { + c.FormData.Set(k, v) + } + return c +} + +func (c *HttpClient) UseBasicAuthentication(username, password string) *HttpClient { + c.UserInfo = &User{Username: username, Password: password} + return c +} + +func (c *HttpClient) UseAuthenticationToken(token string) *HttpClient { + c.Token = token + return c +} + +func (c *HttpClient) UseAuthenticationSchema(scheme string) *HttpClient { + c.AuthScheme = scheme + return c +} + +func (c *HttpClient) NewRequest() *Request { + r := &Request{ + QueryParam: url.Values{}, + FormData: url.Values{}, + Header: http.Header{}, + Cookies: make([]*http.Cookie, 0), + + client: c, + multipartFiles: []*File{}, + multipartFields: []*MultipartField{}, + pathParams: map[string]string{}, + jsonEscapeHTML: true, + } + return r +} + +func (c *HttpClient) new() *Request { + return c.NewRequest() +} + +func (c *HttpClient) OnBeforeRequest(m RequestMiddleware) *HttpClient { + c.udBeforeRequest = append(c.udBeforeRequest, m) + return c +} + +func (c *HttpClient) OnAfterResponse(m ResponseMiddleware) *HttpClient { + c.afterResponse = append(c.afterResponse, m) + return c +} + +func (c *HttpClient) OnError(h ErrorHook) *HttpClient { + c.errorHooks = append(c.errorHooks, h) + return c +} + +func (c *HttpClient) UsePreRequestHook(h PreRequestHook) *HttpClient { + if c.preReqHook != nil { + c.log.Warn(fmt.Sprintf("Overwriting an existing pre-request hook: %s", functionName(h))) + } + c.preReqHook = h + return c +} + +func (c *HttpClient) UseDebug(d bool) *HttpClient { + c.Debug = d + return c +} + +func (c *HttpClient) UseDebugBodyLimit(sl int64) *HttpClient { + c.debugBodySizeLimit = sl + return c +} + +func (c *HttpClient) OnRequestLog(rl RequestLogCallback) *HttpClient { + if c.requestLog != nil { + c.log.Warn(fmt.Sprintf("Overwriting an existing on-request-log callback from=%s to=%s", + functionName(c.requestLog), functionName(rl))) + } + c.requestLog = rl + return c +} + +func (c *HttpClient) OnResponseLog(rl ResponseLogCallback) *HttpClient { + if c.responseLog != nil { + c.log.Warn(fmt.Sprintf("Overwriting an existing on-response-log callback from=%s to=%s", + functionName(c.responseLog), functionName(rl))) + } + c.responseLog = rl + return c +} + +func (c *HttpClient) UseDisableWarnings(d bool) *HttpClient { + c.DisableWarn = d + return c +} + +func (c *HttpClient) UseAllowGetMethodPayload(a bool) *HttpClient { + c.AllowGetMethodPayload = a + return c +} + +func (c *HttpClient) UseLogger(l logger.Logger) *HttpClient { + c.log = l + return c +} + +func (c *HttpClient) UseContentLength(l bool) *HttpClient { + c.setContentLength = l + return c +} + +func (c *HttpClient) UseTimeout(timeout time.Duration) *HttpClient { + c.httpClient.Timeout = timeout + return c +} + +func (c *HttpClient) UseError(err interface{}) *HttpClient { + c.Error = typeOf(err) + return c +} + +func (c *HttpClient) UseRedirectPolicy(policies ...interface{}) *HttpClient { + for _, p := range policies { + if _, ok := p.(RedirectPolicy); !ok { + c.log.Warn(fmt.Sprintf("%v does not implemented.RedirectPolicy (missing Apply method)", + functionName(p))) + } + } + + c.httpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { + for _, p := range policies { + if err := p.(RedirectPolicy).Apply(req, via); err != nil { + return err + } + } + return nil // looks good, go ahead + } + + return c +} + +func (c *HttpClient) UseRetryCount(count int) *HttpClient { + c.RetryCount = count + return c +} + +func (c *HttpClient) UseRetryWaitTime(waitTime time.Duration) *HttpClient { + c.RetryWaitTime = waitTime + return c +} + +func (c *HttpClient) UseRetryMaxWaitTime(maxWaitTime time.Duration) *HttpClient { + c.RetryMaxWaitTime = maxWaitTime + return c +} + +func (c *HttpClient) UseRetryAfter(callback RetryAfterFunc) *HttpClient { + c.RetryAfter = callback + return c +} + +func (c *HttpClient) AddRetryCondition(condition RetryConditionFunc) *HttpClient { + c.RetryConditions = append(c.RetryConditions, condition) + return c +} + +func (c *HttpClient) AddRetryAfterErrorCondition() *HttpClient { + c.AddRetryCondition(func(response *Response, err error) bool { + return response.IsError() + }) + return c +} + +func (c *HttpClient) AddRetryHook(hook OnRetryFunc) *HttpClient { + c.RetryHooks = append(c.RetryHooks, hook) + return c +} + +func (c *HttpClient) UseTLSClientConfiguration(config *tls.Config) *HttpClient { + transport, err := c.transport() + if err != nil { + c.log.Error("an unexpected error occurred", err) + return c + } + transport.TLSClientConfig = config + return c +} + +func (c *HttpClient) UseProxy(proxyURL string) *HttpClient { + transport, err := c.transport() + if err != nil { + c.log.Error("an unexpected error occurred", err) + return c + } + + pURL, err := url.Parse(proxyURL) + if err != nil { + c.log.Error("an unexpected error occurred", err) + return c + } + + c.proxyURL = pURL + transport.Proxy = http.ProxyURL(c.proxyURL) + return c +} + +func (c *HttpClient) RemoveProxy() *HttpClient { + transport, err := c.transport() + if err != nil { + c.log.Error("an unexpected error occurred", err) + return c + } + c.proxyURL = nil + transport.Proxy = nil + return c +} +func (c *HttpClient) UseCertificates(certs ...tls.Certificate) *HttpClient { + config, err := c.tlsConfig() + if err != nil { + c.log.Error("an unexpected error occurred", err) + return c + } + config.Certificates = append(config.Certificates, certs...) + return c +} + +func (c *HttpClient) UseRootCertificate(pemFilePath string) *HttpClient { + rootPemData, err := ioutil.ReadFile(pemFilePath) + if err != nil { + c.log.Error("an unexpected error occurred", err) + return c + } + + config, err := c.tlsConfig() + if err != nil { + c.log.Error("an unexpected error occurred", err) + return c + } + if config.RootCAs == nil { + config.RootCAs = x509.NewCertPool() + } + + config.RootCAs.AppendCertsFromPEM(rootPemData) + return c +} + +func (c *HttpClient) UseRootCertificateFromString(pemContent string) *HttpClient { + config, err := c.tlsConfig() + if err != nil { + c.log.Error("an unexpected error occurred", err) + return c + } + if config.RootCAs == nil { + config.RootCAs = x509.NewCertPool() + } + + config.RootCAs.AppendCertsFromPEM([]byte(pemContent)) + return c +} +func (c *HttpClient) UseOutputDirectory(dirPath string) *HttpClient { + c.outputDirectory = dirPath + return c +} + +func (c *HttpClient) UseTransport(transport http.RoundTripper) *HttpClient { + if transport != nil { + c.httpClient.Transport = transport + } + return c +} + +func (c *HttpClient) UseSchema(scheme string) *HttpClient { + if !IsStringEmpty(scheme) { + c.scheme = scheme + } + return c +} + +func (c *HttpClient) UseCloseConnection(close bool) *HttpClient { + c.closeConnection = close + return c +} + +func (c *HttpClient) UseDoNotParseResponse(parse bool) *HttpClient { + c.notParseResponse = parse + return c +} + +func (c *HttpClient) UsePathParam(param, value string) *HttpClient { + c.pathParams[param] = value + return c +} + +func (c *HttpClient) UsePathParams(params map[string]string) *HttpClient { + for p, v := range params { + c.UsePathParam(p, v) + } + return c +} + +func (c *HttpClient) UseJSONEscapeHTML(b bool) *HttpClient { + c.jsonEscapeHTML = b + return c +} + +func (c *HttpClient) EnableTrace() *HttpClient { + c.trace = true + return c +} + +func (c *HttpClient) DisableTrace() *HttpClient { + c.trace = false + return c +} + +func (c *HttpClient) IsProxySet() bool { + return c.proxyURL != nil +} + +func (c *HttpClient) GetClient() *http.Client { + return c.httpClient +} + +func (c *HttpClient) execute(req *Request) (*Response, error) { + defer releaseBuffer(req.bodyBuf) + // Apply Request middleware + var err error + + // user defined on before request methods + for _, f := range c.udBeforeRequest { + if err = f(c, req); err != nil { + return nil, wrapNoRetryErr(err) + } + } + + for _, f := range c.beforeRequest { + if err = f(c, req); err != nil { + return nil, wrapNoRetryErr(err) + } + } + + if hostHeader := req.Header.Get("Host"); hostHeader != "" { + req.RawRequest.Host = hostHeader + } + + // call pre-request if defined + if c.preReqHook != nil { + if err = c.preReqHook(c, req.RawRequest); err != nil { + return nil, wrapNoRetryErr(err) + } + } + + if err = requestLogger(c, req); err != nil { + return nil, wrapNoRetryErr(err) + } + + req.Time = time.Now() + resp, err := c.httpClient.Do(req.RawRequest) + + response := &Response{ + Request: req, + RawResponse: resp, + } + + if err != nil || req.notParseResponse || c.notParseResponse { + response.setReceivedAt() + return response, err + } + + if !req.isSaveResponse { + defer closeq(resp.Body) + body := resp.Body + + // GitHub #142 & #187 + if strings.EqualFold(resp.Header.Get(hdrContentEncodingKey), "gzip") && resp.ContentLength != 0 { + if _, ok := body.(*gzip.Reader); !ok { + body, err = gzip.NewReader(body) + if err != nil { + response.setReceivedAt() + return response, err + } + defer closeq(body) + } + } + + if response.body, err = ioutil.ReadAll(body); err != nil { + response.setReceivedAt() + return response, err + } + + response.size = int64(len(response.body)) + } + + response.setReceivedAt() // after we read the body + + // Apply Response middleware + for _, f := range c.afterResponse { + if err = f(c, response); err != nil { + break + } + } + + return response, wrapNoRetryErr(err) +} + +func (c *HttpClient) tlsConfig() (*tls.Config, error) { + transport, err := c.transport() + if err != nil { + return nil, err + } + if transport.TLSClientConfig == nil { + transport.TLSClientConfig = &tls.Config{} + } + return transport.TLSClientConfig, nil +} + +func (c *HttpClient) transport() (*http.Transport, error) { + if transport, ok := c.httpClient.Transport.(*http.Transport); ok { + return transport, nil + } + return nil, errors.New("current transport is not an *http.Transport instance") +} + +type ResponseError struct { + Response *Response + Err error +} + +func (e *ResponseError) Error() string { + return e.Err.Error() +} + +func (e *ResponseError) Unwrap() error { + return e.Err +} + +func (c *HttpClient) onErrorHooks(req *Request, resp *Response, err error) { + if err != nil { + if resp != nil { // wrap with ResponseError + err = &ResponseError{Response: resp, Err: err} + } + for _, h := range c.errorHooks { + h(req, err) + } + } +} + +type File struct { + Name string + ParamName string + io.Reader +} + +func (f *File) String() string { + return fmt.Sprintf("ParamName: %v; FileName: %v", f.ParamName, f.Name) +} + +type MultipartField struct { + Param string + FileName string + ContentType string + io.Reader +} + +func createClient(hc *http.Client) *HttpClient { + if hc.Transport == nil { + hc.Transport = createTransport(nil) + } + + c := &HttpClient{ // not setting lang default values + QueryParam: url.Values{}, + FormData: url.Values{}, + Header: http.Header{}, + Cookies: make([]*http.Cookie, 0), + RetryWaitTime: defaultWaitTime, + RetryMaxWaitTime: defaultMaxWaitTime, + Marshaller: json.New(), + HeaderAuthorizationKey: http.CanonicalHeaderKey("Authorization"), + jsonEscapeHTML: true, + httpClient: hc, + debugBodySizeLimit: math.MaxInt32, + pathParams: make(map[string]string), + } + + // Logger + defaultLogger, _ := logging.New() + c.UseLogger(defaultLogger) + + // default before request middlewares + c.beforeRequest = []RequestMiddleware{ + parseRequestURL, + parseRequestHeader, + parseRequestBody, + createHTTPRequest, + addCredentials, + } + + // user defined request middlewares + c.udBeforeRequest = []RequestMiddleware{} + + // default after response middlewares + c.afterResponse = []ResponseMiddleware{ + responseLogger, + parseResponseBody, + saveResponseIntoFile, + } + + return c +} diff --git a/pkg/http_client/http_client_behaviors.go b/pkg/http_client/http_client_behaviors.go new file mode 100644 index 0000000..74b2f09 --- /dev/null +++ b/pkg/http_client/http_client_behaviors.go @@ -0,0 +1,212 @@ +package http_client + + +import ( + "context" + "math" + "math/rand" + "sync" + "time" +) + +const ( + defaultMaxRetries = 3 + defaultWaitTime = time.Duration(100) * time.Millisecond + defaultMaxWaitTime = time.Duration(2000) * time.Millisecond +) + +type ( + // Option is to create convenient retry options like wait time, max retries, etc. + Option func(*Options) + + // RetryConditionFunc type is for retry condition function + // input: non-nil Response OR request execution error + RetryConditionFunc func(*Response, error) bool + + // OnRetryFunc is for side-effecting functions triggered on retry + OnRetryFunc func(*Response, error) + + // RetryAfterFunc returns time to wait before retry + // For example, it can parse HTTP Retry-After header + // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html + // Non-nil error is returned if it is found that request is not retryable + // (0, nil) is a special result means 'use default algorithm' + RetryAfterFunc func(*HttpClient, *Response) (time.Duration, error) + + // Options struct is used to hold retry settings. + Options struct { + maxRetries int + waitTime time.Duration + maxWaitTime time.Duration + retryConditions []RetryConditionFunc + retryHooks []OnRetryFunc + } +) + +// Retries sets the max number of retries +func Retries(value int) Option { + return func(o *Options) { + o.maxRetries = value + } +} + +// WaitTime sets the default wait time to sleep between requests +func WaitTime(value time.Duration) Option { + return func(o *Options) { + o.waitTime = value + } +} + +// MaxWaitTime sets the max wait time to sleep between requests +func MaxWaitTime(value time.Duration) Option { + return func(o *Options) { + o.maxWaitTime = value + } +} + +// RetryConditions sets the conditions that will be checked for retry. +func RetryConditions(conditions []RetryConditionFunc) Option { + return func(o *Options) { + o.retryConditions = conditions + } +} + +// RetryHooks sets the hooks that will be executed after each retry +func RetryHooks(hooks []OnRetryFunc) Option { + return func(o *Options) { + o.retryHooks = hooks + } +} + +// Backoff retries with increasing timeout duration up until X amount of retries +// (Default is 3 attempts, Override with option Retries(n)) +func Backoff(operation func() (*Response, error), options ...Option) error { + // Defaults + opts := Options{ + maxRetries: defaultMaxRetries, + waitTime: defaultWaitTime, + maxWaitTime: defaultMaxWaitTime, + retryConditions: []RetryConditionFunc{}, + } + + for _, o := range options { + o(&opts) + } + + var ( + resp *Response + err error + ) + + for attempt := 0; attempt <= opts.maxRetries; attempt++ { + resp, err = operation() + ctx := context.Background() + if resp != nil && resp.Request.ctx != nil { + ctx = resp.Request.ctx + } + if ctx.Err() != nil { + return err + } + + err1 := unwrapNoRetryErr(err) // raw error, it used for return users callback. + needsRetry := err != nil && err == err1 // retry on a few operation errors by default + + for _, condition := range opts.retryConditions { + needsRetry = condition(resp, err1) + if needsRetry { + break + } + } + + if !needsRetry { + return err + } + + for _, hook := range opts.retryHooks { + hook(resp, err) + } + + waitTime, err2 := sleepDuration(resp, opts.waitTime, opts.maxWaitTime, attempt) + if err2 != nil { + if err == nil { + err = err2 + } + return err + } + + select { + case <-time.After(waitTime): + case <-ctx.Done(): + return ctx.Err() + } + } + + return err +} + +func sleepDuration(resp *Response, min, max time.Duration, attempt int) (time.Duration, error) { + const maxInt = 1<<31 - 1 // max int for arch 386 + if max < 0 { + max = maxInt + } + if resp == nil { + return jitterBackoff(min, max, attempt), nil + } + + retryAfterFunc := resp.Request.client.RetryAfter + + // Check for custom callback + if retryAfterFunc == nil { + return jitterBackoff(min, max, attempt), nil + } + + result, err := retryAfterFunc(resp.Request.client, resp) + if err != nil { + return 0, err // i.e. 'API quota exceeded' + } + if result == 0 { + return jitterBackoff(min, max, attempt), nil + } + if result < 0 || max < result { + result = max + } + if result < min { + result = min + } + return result, nil +} + +// Return capped exponential backoff with jitter +// http://www.awsarchitectureblog.com/2015/03/backoff.html +func jitterBackoff(min, max time.Duration, attempt int) time.Duration { + base := float64(min) + capLevel := float64(max) + + temp := math.Min(capLevel, base*math.Exp2(float64(attempt))) + ri := time.Duration(temp / 2) + result := randDuration(ri) + + if result < min { + result = min + } + + return result +} + +var rnd = newRnd() +var rndMu sync.Mutex + +func randDuration(center time.Duration) time.Duration { + rndMu.Lock() + defer rndMu.Unlock() + + var ri = int64(center) + var jitter = rnd.Int63n(ri) + return time.Duration(math.Abs(float64(ri + jitter))) +} + +func newRnd() *rand.Rand { + var seed = time.Now().UnixNano() + var src = rand.NewSource(seed) + return rand.New(src) +} diff --git a/pkg/http_client/middleware.go b/pkg/http_client/middleware.go new file mode 100644 index 0000000..b5000c8 --- /dev/null +++ b/pkg/http_client/middleware.go @@ -0,0 +1,525 @@ + +package http_client + +import ( + "bytes" + "encoding/xml" + "errors" + "fmt" + "io" + "io/ioutil" + "mime/multipart" + "net/http" + "net/url" + "os" + "path/filepath" + "reflect" + "strings" + "time" +) + +const debugRequestLogKey = "__HttpClientDebugRequestLog" + +func parseRequestURL(c *HttpClient, r *Request) error { + // GitHub #103 Path Params + if len(r.pathParams) > 0 { + for p, v := range r.pathParams { + r.URL = strings.Replace(r.URL, "{"+p+"}", url.PathEscape(v), -1) + } + } + if len(c.pathParams) > 0 { + for p, v := range c.pathParams { + r.URL = strings.Replace(r.URL, "{"+p+"}", url.PathEscape(v), -1) + } + } + + // Parsing request URL + reqURL, err := url.Parse(r.URL) + if err != nil { + return err + } + + // If Request.URL is relative path then added c.BaseUrl into + // the request URL otherwise Request.URL will be used as-is + if !reqURL.IsAbs() { + r.URL = reqURL.String() + if len(r.URL) > 0 && r.URL[0] != '/' { + r.URL = "/" + r.URL + } + + reqURL, err = url.Parse(c.BaseUrl + r.URL) + if err != nil { + return err + } + } + + // Adding Query Param + query := make(url.Values) + for k, v := range c.QueryParam { + for _, iv := range v { + query.Add(k, iv) + } + } + + for k, v := range r.QueryParam { + // remove query param from client level by key + // since overrides happens for that key in the request + query.Del(k) + + for _, iv := range v { + query.Add(k, iv) + } + } + if len(query) > 0 { + if IsStringEmpty(reqURL.RawQuery) { + reqURL.RawQuery = query.Encode() + } else { + reqURL.RawQuery = reqURL.RawQuery + "&" + query.Encode() + } + } + + r.URL = reqURL.String() + + return nil +} + +func parseRequestHeader(c *HttpClient, r *Request) error { + hdr := make(http.Header) + for k := range c.Header { + hdr[k] = append(hdr[k], c.Header[k]...) + } + + for k := range r.Header { + hdr.Del(k) + hdr[k] = append(hdr[k], r.Header[k]...) + } + + + ct := hdr.Get(hdrContentTypeKey) + if IsStringEmpty(hdr.Get(hdrAcceptKey)) && !IsStringEmpty(ct) && + (IsJSONType(ct) || IsXMLType(ct)) { + hdr.Set(hdrAcceptKey, hdr.Get(hdrContentTypeKey)) + } + + r.Header = hdr + + return nil +} + +func parseRequestBody(c *HttpClient, r *Request) (err error) { + if isPayloadSupported(r.Method, c.AllowGetMethodPayload) { + // Handling Multipart + if r.isMultiPart && !(r.Method == MethodPatch) { + if err = handleMultipart(c, r); err != nil { + return + } + + goto CL + } + + // Handling Form Data + if len(c.FormData) > 0 || len(r.FormData) > 0 { + handleFormData(c, r) + + goto CL + } + + // Handling Request body + if r.Body != nil { + handleContentType(c, r) + + if err = handleRequestBody(c, r); err != nil { + return + } + } + } + +CL: + if (c.setContentLength || r.setContentLength) && r.bodyBuf != nil { + r.Header.Set(hdrContentLengthKey, fmt.Sprintf("%d", r.bodyBuf.Len())) + } + + return +} + +func createHTTPRequest(c *HttpClient, r *Request) (err error) { + if r.bodyBuf == nil { + if reader, ok := r.Body.(io.Reader); ok { + r.RawRequest, err = http.NewRequest(r.Method, r.URL, reader) + } else if c.setContentLength || r.setContentLength { + r.RawRequest, err = http.NewRequest(r.Method, r.URL, http.NoBody) + } else { + r.RawRequest, err = http.NewRequest(r.Method, r.URL, nil) + } + } else { + r.RawRequest, err = http.NewRequest(r.Method, r.URL, r.bodyBuf) + } + + if err != nil { + return + } + + // Assign close connection option + r.RawRequest.Close = c.closeConnection + + // Add headers into http request + r.RawRequest.Header = r.Header + + // Add cookies from client instance into http request + for _, cookie := range c.Cookies { + r.RawRequest.AddCookie(cookie) + } + + // Add cookies from request instance into http request + for _, cookie := range r.Cookies { + r.RawRequest.AddCookie(cookie) + } + + // it's for non-http scheme option + if r.RawRequest.URL != nil && r.RawRequest.URL.Scheme == "" { + r.RawRequest.URL.Scheme = c.scheme + r.RawRequest.URL.Host = r.URL + } + + // Enable trace + if c.trace || r.trace { + r.clientTrace = &clientTrace{} + r.ctx = r.clientTrace.createContext(r.Context()) + } + + // Use context if it was specified + if r.ctx != nil { + r.RawRequest = r.RawRequest.WithContext(r.ctx) + } + + bodyCopy, err := getBodyCopy(r) + if err != nil { + return err + } + + // assign get body func for the underlying raw request instance + r.RawRequest.GetBody = func() (io.ReadCloser, error) { + if bodyCopy != nil { + return ioutil.NopCloser(bytes.NewReader(bodyCopy.Bytes())), nil + } + return nil, nil + } + + return +} + +func addCredentials(c *HttpClient, r *Request) error { + var isBasicAuth bool + // Basic Auth + if r.UserInfo != nil { // takes precedence + r.RawRequest.SetBasicAuth(r.UserInfo.Username, r.UserInfo.Password) + isBasicAuth = true + } else if c.UserInfo != nil { + r.RawRequest.SetBasicAuth(c.UserInfo.Username, c.UserInfo.Password) + isBasicAuth = true + } + + if !c.DisableWarn { + if isBasicAuth && !strings.HasPrefix(r.URL, "https") { + c.log.Warn("Using Basic Auth in HTTP mode is not secure, use HTTPS") + } + } + + // Set the Authorization ExtractHeader Scheme + var authScheme string + if !IsStringEmpty(r.AuthScheme) { + authScheme = r.AuthScheme + } else if !IsStringEmpty(c.AuthScheme) { + authScheme = c.AuthScheme + } else { + authScheme = "Bearer" + } + + // Build the Token Auth header + if !IsStringEmpty(r.Token) { // takes precedence + r.RawRequest.Header.Set(c.HeaderAuthorizationKey, authScheme+" "+r.Token) + } else if !IsStringEmpty(c.Token) { + r.RawRequest.Header.Set(c.HeaderAuthorizationKey, authScheme+" "+c.Token) + } + + return nil +} + +func requestLogger(c *HttpClient, r *Request) error { + if c.Debug { + rr := r.RawRequest + rl := &RequestLog{Header: copyHeaders(rr.Header), Body: r.fmtBodyString(c.debugBodySizeLimit)} + if c.requestLog != nil { + if err := c.requestLog(rl); err != nil { + return err + } + } + // fmt.Sprintf("COOKIES:\n%s\n", composeCookies(c.GetClient().Jar, *rr.URL)) + + + reqLog := "\n==============================================================================\n" + + "~~~ REQUEST ~~~\n" + + fmt.Sprintf("%s %s %s\n", r.Method, rr.URL.RequestURI(), rr.Proto) + + fmt.Sprintf("HOST : %s\n", rr.URL.Host) + + fmt.Sprintf("HEADERS:\n%s\n", composeHeaders(c, r, rl.Header)) + + fmt.Sprintf("BODY :\n%v\n", rl.Body) + + "------------------------------------------------------------------------------\n" + + r.initValuesMap() + r.values[debugRequestLogKey] = reqLog + } + + return nil +} + + +func responseLogger(c *HttpClient, res *Response) error { + if c.Debug { + rl := &ResponseLog{Header: copyHeaders(res.ExtractHeader()), Body: res.fmtBodyString(c.debugBodySizeLimit)} + if c.responseLog != nil { + if err := c.responseLog(rl); err != nil { + return err + } + } + + debugLog := res.Request.values[debugRequestLogKey].(string) + debugLog += "~~~ RESPONSE ~~~\n" + + fmt.Sprintf("STATUS : %s\n", res.ExtractStatus()) + + fmt.Sprintf("PROTO : %s\n", res.RawResponse.Proto) + + fmt.Sprintf("RECEIVED AT : %v\n", res.ReceivedAt().Format(time.RFC3339Nano)) + + fmt.Sprintf("TIME DURATION: %v\n", res.CalculateDuration()) + + "HEADERS :\n" + + composeHeaders(c, res.Request, rl.Header) + "\n" + if res.Request.isSaveResponse { + debugLog += "BODY :\n***** RESPONSE WRITTEN INTO FILE *****\n" + } else { + debugLog += fmt.Sprintf("BODY :\n%v\n", rl.Body) + } + debugLog += "==============================================================================\n" + + c.log.Debug(fmt.Sprintf("%s", debugLog)) + } + + return nil +} + +func parseResponseBody(c *HttpClient, res *Response) (err error) { + if res.ExtractStatusCode() == http.StatusNoContent { + return + } + // Handles only JSON or XML content type + ct := firstNonEmpty(res.Request.forceContentType, res.ExtractHeader().Get(hdrContentTypeKey), res.Request.fallbackContentType) + if IsJSONType(ct) || IsXMLType(ct) { + // HTTP status code > 199 and < 300, considered as ExtractResult + if res.IsSuccess() { + res.Request.Error = nil + if res.Request.Result != nil { + err = Unmarshalc(c, ct, res.body, res.Request.Result) + return + } + } + + // HTTP status code > 399, considered as ExtractError + if res.IsError() { + // global error interface + if res.Request.Error == nil && c.Error != nil { + res.Request.Error = reflect.New(c.Error).Interface() + } + + if res.Request.Error != nil { + err = Unmarshalc(c, ct, res.body, res.Request.Error) + } + } + } + + return +} + +func handleMultipart(c *HttpClient, r *Request) (err error) { + r.bodyBuf = acquireBuffer() + w := multipart.NewWriter(r.bodyBuf) + + for k, v := range c.FormData { + for _, iv := range v { + if err = w.WriteField(k, iv); err != nil { + return err + } + } + } + + for k, v := range r.FormData { + for _, iv := range v { + if strings.HasPrefix(k, "@") { // file + err = addFile(w, k[1:], iv) + if err != nil { + return + } + } else { // form value + if err = w.WriteField(k, iv); err != nil { + return err + } + } + } + } + + // #21 - adding io.Reader support + if len(r.multipartFiles) > 0 { + for _, f := range r.multipartFiles { + err = addFileReader(w, f) + if err != nil { + return + } + } + } + + // GitHub #130 adding multipart field support with content type + if len(r.multipartFields) > 0 { + for _, mf := range r.multipartFields { + if err = addMultipartFormField(w, mf); err != nil { + return + } + } + } + + r.Header.Set(hdrContentTypeKey, w.FormDataContentType()) + err = w.Close() + + return +} + +func handleFormData(c *HttpClient, r *Request) { + formData := url.Values{} + + for k, v := range c.FormData { + for _, iv := range v { + formData.Add(k, iv) + } + } + + for k, v := range r.FormData { + // remove form data field from client level by key + // since overrides happens for that key in the request + formData.Del(k) + + for _, iv := range v { + formData.Add(k, iv) + } + } + + r.bodyBuf = bytes.NewBuffer([]byte(formData.Encode())) + r.Header.Set(hdrContentTypeKey, formContentType) + r.isFormData = true +} + +func handleContentType(c *HttpClient, r *Request) { + contentType := r.Header.Get(hdrContentTypeKey) + if IsStringEmpty(contentType) { + contentType = DetectContentType(r.Body) + r.Header.Set(hdrContentTypeKey, contentType) + } +} + +func handleRequestBody(c *HttpClient, r *Request) (err error) { + var bodyBytes []byte + contentType := r.Header.Get(hdrContentTypeKey) + kind := kindOf(r.Body) + r.bodyBuf = nil + + if reader, ok := r.Body.(io.Reader); ok { + if c.setContentLength || r.setContentLength { // keep backward compatibility + r.bodyBuf = acquireBuffer() + _, err = r.bodyBuf.ReadFrom(reader) + r.Body = nil + } else { + // Otherwise buffer less processing for `io.Reader`, sounds good. + return + } + } else if b, ok := r.Body.([]byte); ok { + bodyBytes = b + } else if s, ok := r.Body.(string); ok { + bodyBytes = []byte(s) + } else if IsJSONType(contentType) && + (kind == reflect.Struct || kind == reflect.Map || kind == reflect.Slice) { + bodyBytes, err = jsonMarshal(c, r, r.Body) + if err != nil { + return + } + } else if IsXMLType(contentType) && (kind == reflect.Struct) { + bodyBytes, err = xml.Marshal(r.Body) + if err != nil { + return + } + } + + if bodyBytes == nil && r.bodyBuf == nil { + err = errors.New("unsupported 'ExtractBody' type/value") + } + + // if any errors during body bytes handling, return it + if err != nil { + return + } + + // []byte into Buffer + if bodyBytes != nil && r.bodyBuf == nil { + r.bodyBuf = acquireBuffer() + _, _ = r.bodyBuf.Write(bodyBytes) + } + + return +} + +func saveResponseIntoFile(c *HttpClient, res *Response) error { + if res.Request.isSaveResponse { + file := "" + + if len(c.outputDirectory) > 0 && !filepath.IsAbs(res.Request.outputFile) { + file += c.outputDirectory + string(filepath.Separator) + } + + file = filepath.Clean(file + res.Request.outputFile) + if err := createDirectory(filepath.Dir(file)); err != nil { + return err + } + + outFile, err := os.Create(file) + if err != nil { + return err + } + defer closeq(outFile) + + // io.Copy reads maximum 32kb size, it is perfect for large file download too + defer closeq(res.RawResponse.Body) + + written, err := io.Copy(outFile, res.RawResponse.Body) + if err != nil { + return err + } + + res.size = written + } + + return nil +} + +func getBodyCopy(r *Request) (*bytes.Buffer, error) { + // If r.bodyBuf present, return the copy + if r.bodyBuf != nil { + return bytes.NewBuffer(r.bodyBuf.Bytes()), nil + } + + // Maybe body is `io.Reader`. + if r.RawRequest.Body != nil { + b, err := ioutil.ReadAll(r.RawRequest.Body) + if err != nil { + return nil, err + } + + // Restore the ExtractBody + closeq(r.RawRequest.Body) + r.RawRequest.Body = ioutil.NopCloser(bytes.NewBuffer(b)) + + // Return the ExtractBody bytes + return bytes.NewBuffer(b), nil + } + return nil, nil +} diff --git a/pkg/http_client/redirect.go b/pkg/http_client/redirect.go new file mode 100644 index 0000000..5963650 --- /dev/null +++ b/pkg/http_client/redirect.go @@ -0,0 +1,75 @@ + +package http_client + +import ( + "errors" + "fmt" + "net" + "net/http" + "strings" +) + +type ( + + RedirectPolicy interface { + Apply(req *http.Request, via []*http.Request) error + } + + RedirectPolicyFunc func(*http.Request, []*http.Request) error +) + +func (f RedirectPolicyFunc) Apply(req *http.Request, via []*http.Request) error { + return f(req, via) +} + +func NoRedirectPolicy() RedirectPolicy { + return RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error { + return errors.New("auto redirect is disabled") + }) +} + +func FlexibleRedirectPolicy(noOfRedirect int) RedirectPolicy { + return RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error { + if len(via) >= noOfRedirect { + return fmt.Errorf("stopped after %d redirects", noOfRedirect) + } + checkHostAndAddHeaders(req, via[0]) + return nil + }) +} + +func DomainCheckRedirectPolicy(hostnames ...string) RedirectPolicy { + hosts := make(map[string]bool) + for _, h := range hostnames { + hosts[strings.ToLower(h)] = true + } + + fn := RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error { + if ok := hosts[getHostname(req.URL.Host)]; !ok { + return errors.New("redirect is not allowed as per DomainCheckRedirectPolicy") + } + + return nil + }) + + return fn +} + + +func getHostname(host string) (hostname string) { + if strings.Index(host, ":") > 0 { + host, _, _ = net.SplitHostPort(host) + } + hostname = strings.ToLower(host) + return +} + +func checkHostAndAddHeaders(cur *http.Request, pre *http.Request) { + curHostname := getHostname(cur.URL.Host) + preHostname := getHostname(pre.URL.Host) + if strings.EqualFold(curHostname, preHostname) { + for key, val := range pre.Header { + cur.Header[key] = val + } + } +} diff --git a/pkg/http_client/request.go b/pkg/http_client/request.go new file mode 100644 index 0000000..b64e1e6 --- /dev/null +++ b/pkg/http_client/request.go @@ -0,0 +1,516 @@ + +package http_client + +import ( + "bytes" + "context" + "encoding/json" + "encoding/xml" + "fmt" + "io" + "net" + "net/http" + "net/url" + "reflect" + "strings" + "time" +) + + +type Request struct { + URL string + Method string + Token string + AuthScheme string + QueryParam url.Values + FormData url.Values + Header http.Header + Time time.Time + Body interface{} + Result interface{} + Error interface{} + RawRequest *http.Request + SRV *SRVRecord + UserInfo *User + Cookies []*http.Cookie + + Attempt int + + isMultiPart bool + isFormData bool + setContentLength bool + isSaveResponse bool + notParseResponse bool + jsonEscapeHTML bool + trace bool + outputFile string + fallbackContentType string + forceContentType string + ctx context.Context + pathParams map[string]string + values map[string]interface{} + client *HttpClient + bodyBuf *bytes.Buffer + clientTrace *clientTrace + multipartFiles []*File + multipartFields []*MultipartField +} + +func (r *Request) Context() context.Context { + if r.ctx == nil { + return context.Background() + } + return r.ctx +} + +func (r *Request) UseContext(ctx context.Context) *Request { + r.ctx = ctx + return r +} + +func (r *Request) UseHeader(header, value string) *Request { + r.Header.Set(header, value) + return r +} + +func (r *Request) UseHeaders(headers map[string]string) *Request { + for h, v := range headers { + r.UseHeader(h, v) + } + return r +} + +func (r *Request) UseHeaderVerbatim(header, value string) *Request { + r.Header[header] = []string{value} + return r +} + +func (r *Request) UseQueryParam(param, value string) *Request { + r.QueryParam.Set(param, value) + return r +} + +func (r *Request) UseQueryParams(params map[string]string) *Request { + for p, v := range params { + r.UseQueryParam(p, v) + } + return r +} + +func (r *Request) UseQueryParamsFromValues(params url.Values) *Request { + for p, v := range params { + for _, pv := range v { + r.QueryParam.Add(p, pv) + } + } + return r +} + +func (r *Request) UseQueryString(query string) *Request { + params, err := url.ParseQuery(strings.TrimSpace(query)) + if err == nil { + for p, v := range params { + for _, pv := range v { + r.QueryParam.Add(p, pv) + } + } + } else { + r.client.log.Error("an error occurred", err) + } + return r +} + +func (r *Request) UseFormData(data map[string]string) *Request { + for k, v := range data { + r.FormData.Set(k, v) + } + return r +} +func (r *Request) UseFormDataFromValues(data url.Values) *Request { + for k, v := range data { + for _, kv := range v { + r.FormData.Add(k, kv) + } + } + return r +} + +func (r *Request) UseBody(body interface{}) *Request { + r.Body = body + return r +} +func (r *Request) UseResponse(res interface{}) *Request { + r.Result = getPointer(res) + return r +} + +func (r *Request) UseError(err interface{}) *Request { + r.Error = getPointer(err) + return r +} + +func (r *Request) UseFile(param, filePath string) *Request { + r.isMultiPart = true + r.FormData.Set("@"+param, filePath) + return r +} + +func (r *Request) UseFiles(files map[string]string) *Request { + r.isMultiPart = true + for f, fp := range files { + r.FormData.Set("@"+f, fp) + } + return r +} + +func (r *Request) UseFileReader(param, fileName string, reader io.Reader) *Request { + r.isMultiPart = true + r.multipartFiles = append(r.multipartFiles, &File{ + Name: fileName, + ParamName: param, + Reader: reader, + }) + return r +} + +func (r *Request) UseMultipartFormData(data map[string]string) *Request { + for k, v := range data { + r = r.UseMultipartField(k, "", "", strings.NewReader(v)) + } + + return r +} + +func (r *Request) UseMultipartField(param, fileName, contentType string, reader io.Reader) *Request { + r.isMultiPart = true + r.multipartFields = append(r.multipartFields, &MultipartField{ + Param: param, + FileName: fileName, + ContentType: contentType, + Reader: reader, + }) + return r +} + +func (r *Request) UseMultipartFields(fields ...*MultipartField) *Request { + r.isMultiPart = true + r.multipartFields = append(r.multipartFields, fields...) + return r +} + +func (r *Request) UseContentLength(l bool) *Request { + r.setContentLength = l + return r +} + +func (r *Request) UseBasicAuthentication(username, password string) *Request { + r.UserInfo = &User{Username: username, Password: password} + return r +} + +func (r *Request) UseAuthenticationToken(token string) *Request { + r.Token = token + return r +} + +func (r *Request) UseAuthenticationSchema(scheme string) *Request { + r.AuthScheme = scheme + return r +} + +func (r *Request) UseOutputFile(file string) *Request { + r.outputFile = file + r.isSaveResponse = true + return r +} + +func (r *Request) UseSRV(srv *SRVRecord) *Request { + r.SRV = srv + return r +} + +func (r *Request) UseDoNotParseResponse(parse bool) *Request { + r.notParseResponse = parse + return r +} + +func (r *Request) UsePathParam(param, value string) *Request { + r.pathParams[param] = value + return r +} + +func (r *Request) UsePathParams(params map[string]string) *Request { + for p, v := range params { + r.UsePathParam(p, v) + } + return r +} + +func (r *Request) ExpectContentType(contentType string) *Request { + r.fallbackContentType = contentType + return r +} + +func (r *Request) ForceContentType(contentType string) *Request { + r.forceContentType = contentType + return r +} + +func (r *Request) UseJSONEscapeHTML(b bool) *Request { + r.jsonEscapeHTML = b + return r +} + +func (r *Request) UseCookie(hc *http.Cookie) *Request { + r.Cookies = append(r.Cookies, hc) + return r +} + +func (r *Request) UseCookies(rs []*http.Cookie) *Request { + r.Cookies = append(r.Cookies, rs...) + return r +} + +func (r *Request) EnableTrace() *Request { + r.trace = true + return r +} + +func (r *Request) TraceInfo() TraceInfo { + ct := r.clientTrace + + if ct == nil { + return TraceInfo{} + } + + ti := TraceInfo{ + DNSLookup: ct.dnsDone.Sub(ct.dnsStart), + TLSHandshake: ct.tlsHandshakeDone.Sub(ct.tlsHandshakeStart), + ServerTime: ct.gotFirstResponseByte.Sub(ct.gotConn), + IsConnReused: ct.gotConnInfo.Reused, + IsConnWasIdle: ct.gotConnInfo.WasIdle, + ConnIdleTime: ct.gotConnInfo.IdleTime, + RequestAttempt: r.Attempt, + } + + // Calculate the total time accordingly, + // when connection is reused + if ct.gotConnInfo.Reused { + ti.TotalTime = ct.endTime.Sub(ct.getConn) + } else { + ti.TotalTime = ct.endTime.Sub(ct.dnsStart) + } + + // Only calculate on successful connections + if !ct.connectDone.IsZero() { + ti.TCPConnTime = ct.connectDone.Sub(ct.dnsDone) + } + + // Only calculate on successful connections + if !ct.gotConn.IsZero() { + ti.ConnTime = ct.gotConn.Sub(ct.getConn) + } + + // Only calculate on successful connections + if !ct.gotFirstResponseByte.IsZero() { + ti.ResponseTime = ct.endTime.Sub(ct.gotFirstResponseByte) + } + + // Capture remote address info when connection is non-nil + if ct.gotConnInfo.Conn != nil { + ti.RemoteAddr = ct.gotConnInfo.Conn.RemoteAddr() + } + + return ti +} + + +// Get method does GET HTTP request. It's defined in section 4.3.1 of RFC7231. +func (r *Request) Get(url string) (*Response, error) { + return r.Execute(MethodGet, url) +} + +// Head method does HEAD HTTP request. It's defined in section 4.3.2 of RFC7231. +func (r *Request) Head(url string) (*Response, error) { + return r.Execute(MethodHead, url) +} + +// Post method does POST HTTP request. It's defined in section 4.3.3 of RFC7231. +func (r *Request) Post(url string) (*Response, error) { + return r.Execute(MethodPost, url) +} + +// Put method does PUT HTTP request. It's defined in section 4.3.4 of RFC7231. +func (r *Request) Put(url string) (*Response, error) { + return r.Execute(MethodPut, url) +} + +// Delete method does DELETE HTTP request. It's defined in section 4.3.5 of RFC7231. +func (r *Request) Delete(url string) (*Response, error) { + return r.Execute(MethodDelete, url) +} + +// Options method does OPTIONS HTTP request. It's defined in section 4.3.7 of RFC7231. +func (r *Request) Options(url string) (*Response, error) { + return r.Execute(MethodOptions, url) +} + +// Patch method does PATCH HTTP request. It's defined in section 2 of RFC5789. +func (r *Request) Patch(url string) (*Response, error) { + return r.Execute(MethodPatch, url) +} + +func (r *Request) Send() (*Response, error) { + return r.Execute(r.Method, r.URL) +} + +func (r *Request) Execute(method, url string) (*Response, error) { + var addrs []*net.SRV + var resp *Response + var err error + + if r.isMultiPart && !(method == MethodPost || method == MethodPut || method == MethodPatch) { + // No OnError hook here since this is a request validation error + return nil, fmt.Errorf("multipart content is not allowed in HTTP verb [%v]", method) + } + + if r.SRV != nil { + _, addrs, err = net.LookupSRV(r.SRV.Service, "tcp", r.SRV.Domain) + if err != nil { + r.client.onErrorHooks(r, nil, err) + return nil, err + } + } + + r.Method = method + r.URL = r.selectAddr(addrs, url, 0) + + if r.client.RetryCount == 0 { + r.Attempt = 1 + resp, err = r.client.execute(r) + r.client.onErrorHooks(r, resp, unwrapNoRetryErr(err)) + return resp, unwrapNoRetryErr(err) + } + + err = Backoff( + func() (*Response, error) { + r.Attempt++ + + r.URL = r.selectAddr(addrs, url, r.Attempt) + + resp, err = r.client.execute(r) + if err != nil { + r.client.log.Error(fmt.Sprintf("%s, Attempt %d", err, r.Attempt), err) + } + + return resp, err + }, + Retries(r.client.RetryCount), + WaitTime(r.client.RetryWaitTime), + MaxWaitTime(r.client.RetryMaxWaitTime), + RetryConditions(r.client.RetryConditions), + RetryHooks(r.client.RetryHooks), + ) + + r.client.onErrorHooks(r, resp, unwrapNoRetryErr(err)) + + return resp, unwrapNoRetryErr(err) +} + +type SRVRecord struct { + Service string + Domain string +} + + +func (r *Request) fmtBodyString(sl int64) (body string) { + body = "***** NO CONTENT *****" + if !isPayloadSupported(r.Method, r.client.AllowGetMethodPayload) { + return + } + + if _, ok := r.Body.(io.Reader); ok { + body = "***** BODY IS io.Reader *****" + return + } + + // multipart or form-data + if r.isMultiPart || r.isFormData { + bodySize := int64(r.bodyBuf.Len()) + if bodySize > sl { + body = fmt.Sprintf("***** REQUEST TOO LARGE (size - %d) *****", bodySize) + return + } + body = r.bodyBuf.String() + return + } + + // request body data + if r.Body == nil { + return + } + var prtBodyBytes []byte + var err error + + contentType := r.Header.Get(hdrContentTypeKey) + kind := kindOf(r.Body) + if canJSONMarshal(contentType, kind) { + prtBodyBytes, err = json.MarshalIndent(&r.Body, "", " ") + } else if IsXMLType(contentType) && (kind == reflect.Struct) { + prtBodyBytes, err = xml.MarshalIndent(&r.Body, "", " ") + } else if b, ok := r.Body.(string); ok { + if IsJSONType(contentType) { + bodyBytes := []byte(b) + out := acquireBuffer() + defer releaseBuffer(out) + if err = json.Indent(out, bodyBytes, "", " "); err == nil { + prtBodyBytes = out.Bytes() + } + } else { + body = b + } + } else if b, ok := r.Body.([]byte); ok { + body = fmt.Sprintf("***** BODY IS byte(s) (size - %d) *****", len(b)) + return + } + + if prtBodyBytes != nil && err == nil { + body = string(prtBodyBytes) + } + + if len(body) > 0 { + bodySize := int64(len([]byte(body))) + if bodySize > sl { + body = fmt.Sprintf("***** REQUEST TOO LARGE (size - %d) *****", bodySize) + } + } + + return +} + +func (r *Request) selectAddr(addrs []*net.SRV, path string, attempt int) string { + if addrs == nil { + return path + } + + idx := attempt % len(addrs) + domain := strings.TrimRight(addrs[idx].Target, ".") + path = strings.TrimLeft(path, "/") + + return fmt.Sprintf("%s://%s:%d/%s", r.client.scheme, domain, addrs[idx].Port, path) +} + +func (r *Request) initValuesMap() { + if r.values == nil { + r.values = make(map[string]interface{}) + } +} + +var noescapeJSONMarshal = func(v interface{}) ([]byte, error) { + buf := acquireBuffer() + defer releaseBuffer(buf) + encoder := json.NewEncoder(buf) + encoder.SetEscapeHTML(false) + err := encoder.Encode(v) + return buf.Bytes(), err +} diff --git a/pkg/http_client/response.go b/pkg/http_client/response.go new file mode 100644 index 0000000..9db825d --- /dev/null +++ b/pkg/http_client/response.go @@ -0,0 +1,134 @@ + +package http_client + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" +) + +type Response struct { + Request *Request + RawResponse *http.Response + + body []byte + size int64 + receivedAt time.Time +} + +func (r *Response) ExtractBody() []byte { + if r.RawResponse == nil { + return []byte{} + } + return r.body +} + +func (r *Response) ExtractStatus() string { + if r.RawResponse == nil { + return "" + } + return r.RawResponse.Status +} + +func (r *Response) ExtractStatusCode() int { + if r.RawResponse == nil { + return 0 + } + return r.RawResponse.StatusCode +} + +func (r *Response) ExtractProto() string { + if r.RawResponse == nil { + return "" + } + return r.RawResponse.Proto +} + +func (r *Response) ExtractResult() interface{} { + return r.Request.Result +} + +func (r *Response) ExtractError() interface{} { + return r.Request.Error +} + +func (r *Response) ExtractHeader() http.Header { + if r.RawResponse == nil { + return http.Header{} + } + return r.RawResponse.Header +} + +func (r *Response) ExtractCookies() []*http.Cookie { + if r.RawResponse == nil { + return make([]*http.Cookie, 0) + } + return r.RawResponse.Cookies() +} + +func (r *Response) ExtractBodyAsString() string { + if r.body == nil { + return "" + } + return strings.TrimSpace(string(r.body)) +} + +func (r *Response) CalculateDuration() time.Duration { + if r.Request.clientTrace != nil { + return r.Request.TraceInfo().TotalTime + } + return r.receivedAt.Sub(r.Request.Time) +} + +func (r *Response) ReceivedAt() time.Time { + return r.receivedAt +} + +func (r *Response) Size() int64 { + return r.size +} + +func (r *Response) ExtractRawBody() io.ReadCloser { + if r.RawResponse == nil { + return nil + } + return r.RawResponse.Body +} + +func (r *Response) IsSuccess() bool { + return r.ExtractStatusCode() > 199 && r.ExtractStatusCode() < 300 +} + +func (r *Response) IsError() bool { + return r.ExtractStatusCode() > 399 +} + +func (r *Response) setReceivedAt() { + r.receivedAt = time.Now() + if r.Request.clientTrace != nil { + r.Request.clientTrace.endTime = r.receivedAt + } +} +func (r *Response) fmtBodyString(sl int64) string { + if r.body != nil { + if int64(len(r.body)) > sl { + return fmt.Sprintf("***** RESPONSE TOO LARGE (size - %d) *****", len(r.body)) + } + ct := r.ExtractHeader().Get(hdrContentTypeKey) + if IsJSONType(ct) { + out := acquireBuffer() + defer releaseBuffer(out) + err := json.Indent(out, r.body, "", " ") + if err != nil { + return fmt.Sprintf("*** ExtractError: Unable to format response body - \"%s\" ***\n\nLog ExtractBody as-is:\n%s", err, r.ExtractBodyAsString()) + } + return out.String() + } + return r.ExtractBodyAsString() + } + + return "***** NO CONTENT *****" +} diff --git a/pkg/http_client/resty.go b/pkg/http_client/resty.go new file mode 100644 index 0000000..5650be0 --- /dev/null +++ b/pkg/http_client/resty.go @@ -0,0 +1,29 @@ + +package http_client + +import ( + "net" + "net/http" + "net/http/cookiejar" + + "golang.org/x/net/publicsuffix" +) + +func New() *HttpClient { + cookieJar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) + return createClient(&http.Client{ + Jar: cookieJar, + }) +} + +func NewWithClient(hc *http.Client) *HttpClient { + return createClient(hc) +} + +func NewWithLocalAddr(localAddr net.Addr) *HttpClient { + cookieJar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) + return createClient(&http.Client{ + Jar: cookieJar, + Transport: createTransport(localAddr), + }) +} diff --git a/pkg/http_client/trace.go b/pkg/http_client/trace.go new file mode 100644 index 0000000..1755b2b --- /dev/null +++ b/pkg/http_client/trace.go @@ -0,0 +1,108 @@ +package http_client + +import ( +"context" +"crypto/tls" +"net" +"net/http/httptrace" +"time" +) + +type TraceInfo struct { + // DNSLookup is a duration that transport took to perform + // DNS lookup. + DNSLookup time.Duration + + // ConnTime is a duration that took to obtain a successful connection. + ConnTime time.Duration + + // TCPConnTime is a duration that took to obtain the TCP connection. + TCPConnTime time.Duration + + // TLSHandshake is a duration that TLS handshake took place. + TLSHandshake time.Duration + + // ServerTime is a duration that server took to respond first byte. + ServerTime time.Duration + + // ResponseTime is a duration since first response byte from server to + // request completion. + ResponseTime time.Duration + + // TotalTime is a duration that total request took end-to-end. + TotalTime time.Duration + + // IsConnReused is whether this connection has been previously + // used for another HTTP request. + IsConnReused bool + + // IsConnWasIdle is whether this connection was obtained from an + // idle pool. + IsConnWasIdle bool + + // ConnIdleTime is a duration how long the connection was previously + // idle, if IsConnWasIdle is true. + ConnIdleTime time.Duration + + // RequestAttempt is to represent the request attempt made during a client + // request execution flow, including retry count. + RequestAttempt int + + // RemoteAddr returns the remote network address. + RemoteAddr net.Addr +} + +type clientTrace struct { + getConn time.Time + dnsStart time.Time + dnsDone time.Time + connectDone time.Time + tlsHandshakeStart time.Time + tlsHandshakeDone time.Time + gotConn time.Time + gotFirstResponseByte time.Time + endTime time.Time + gotConnInfo httptrace.GotConnInfo +} + + +func (t *clientTrace) createContext(ctx context.Context) context.Context { + return httptrace.WithClientTrace( + ctx, + &httptrace.ClientTrace{ + DNSStart: func(_ httptrace.DNSStartInfo) { + t.dnsStart = time.Now() + }, + DNSDone: func(_ httptrace.DNSDoneInfo) { + t.dnsDone = time.Now() + }, + ConnectStart: func(_, _ string) { + if t.dnsDone.IsZero() { + t.dnsDone = time.Now() + } + if t.dnsStart.IsZero() { + t.dnsStart = t.dnsDone + } + }, + ConnectDone: func(net, addr string, err error) { + t.connectDone = time.Now() + }, + GetConn: func(_ string) { + t.getConn = time.Now() + }, + GotConn: func(ci httptrace.GotConnInfo) { + t.gotConn = time.Now() + t.gotConnInfo = ci + }, + GotFirstResponseByte: func() { + t.gotFirstResponseByte = time.Now() + }, + TLSHandshakeStart: func() { + t.tlsHandshakeStart = time.Now() + }, + TLSHandshakeDone: func(_ tls.ConnectionState, _ error) { + t.tlsHandshakeDone = time.Now() + }, + }, + ) +} diff --git a/pkg/http_client/transport.go b/pkg/http_client/transport.go new file mode 100644 index 0000000..a6699bd --- /dev/null +++ b/pkg/http_client/transport.go @@ -0,0 +1,29 @@ + +package http_client + +import ( + "net" + "net/http" + "runtime" + "time" +) + +func createTransport(localAddr net.Addr) *http.Transport { + dialer := &net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + } + if localAddr != nil { + dialer.LocalAddr = localAddr + } + return &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: dialer.DialContext, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + MaxIdleConnsPerHost: runtime.GOMAXPROCS(0) + 1, + } +} diff --git a/pkg/http_client/util.go b/pkg/http_client/util.go new file mode 100644 index 0000000..50d666f --- /dev/null +++ b/pkg/http_client/util.go @@ -0,0 +1,292 @@ + +package http_client + +import ( + "bytes" + "encoding/xml" + "fmt" + "io" + "mime/multipart" + "net/http" + "net/textproto" + "os" + "path/filepath" + "reflect" + "runtime" + "sort" + "strings" +) + +func IsStringEmpty(str string) bool { + return len(strings.TrimSpace(str)) == 0 +} + +// DetectContentType method is used to figure out `Request.Body` content type for request header +func DetectContentType(body interface{}) string { + contentType := plainTextType + kind := kindOf(body) + switch kind { + case reflect.Struct, reflect.Map: + contentType = jsonContentType + case reflect.String: + contentType = plainTextType + default: + if b, ok := body.([]byte); ok { + contentType = http.DetectContentType(b) + } else if kind == reflect.Slice { + contentType = jsonContentType + } + } + + return contentType +} + +// IsJSONType method is to check JSON content type or not +func IsJSONType(ct string) bool { + return jsonCheck.MatchString(ct) +} + +// IsXMLType method is to check XML content type or not +func IsXMLType(ct string) bool { + return xmlCheck.MatchString(ct) +} + +// Unmarshalc content into object from JSON or XML +func Unmarshalc(c *HttpClient, ct string, b []byte, d interface{}) (err error) { + if IsJSONType(ct) { + err = c.Marshaller.Unmarshall(b, d) + } else if IsXMLType(ct) { + err = xml.Unmarshal(b, d) + } + + return +} + +type RequestLog struct { + Header http.Header + Body string +} + +type ResponseLog struct { + Header http.Header + Body string +} + + +// way to disable the HTML escape as opt-in +func jsonMarshal(c *HttpClient, r *Request, d interface{}) ([]byte, error) { + if !r.jsonEscapeHTML { + return noescapeJSONMarshal(d) + } else if !c.jsonEscapeHTML { + return noescapeJSONMarshal(d) + } + return c.Marshaller.Marshall(d) +} + +func firstNonEmpty(v ...string) string { + for _, s := range v { + if !IsStringEmpty(s) { + return s + } + } + return "" +} + +var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") + +func escapeQuotes(s string) string { + return quoteEscaper.Replace(s) +} + +func createMultipartHeader(param, fileName, contentType string) textproto.MIMEHeader { + hdr := make(textproto.MIMEHeader) + + var contentDispositionValue string + if IsStringEmpty(fileName) { + contentDispositionValue = fmt.Sprintf(`form-data; name="%s"`, param) + } else { + contentDispositionValue = fmt.Sprintf(`form-data; name="%s"; filename="%s"`, + param, escapeQuotes(fileName)) + } + hdr.Set("Content-Disposition", contentDispositionValue) + + if !IsStringEmpty(contentType) { + hdr.Set(hdrContentTypeKey, contentType) + } + return hdr +} + +func addMultipartFormField(w *multipart.Writer, mf *MultipartField) error { + partWriter, err := w.CreatePart(createMultipartHeader(mf.Param, mf.FileName, mf.ContentType)) + if err != nil { + return err + } + + _, err = io.Copy(partWriter, mf.Reader) + return err +} + +func writeMultipartFormFile(w *multipart.Writer, fieldName, fileName string, r io.Reader) error { + // Auto detect actual multipart content type + cbuf := make([]byte, 512) + size, err := r.Read(cbuf) + if err != nil { + return err + } + + partWriter, err := w.CreatePart(createMultipartHeader(fieldName, fileName, http.DetectContentType(cbuf))) + if err != nil { + return err + } + + if _, err = partWriter.Write(cbuf[:size]); err != nil { + return err + } + + _, err = io.Copy(partWriter, r) + return err +} + +func addFile(w *multipart.Writer, fieldName, path string) error { + file, err := os.Open(path) + if err != nil { + return err + } + defer closeq(file) + return writeMultipartFormFile(w, fieldName, filepath.Base(path), file) +} + +func addFileReader(w *multipart.Writer, f *File) error { + return writeMultipartFormFile(w, f.ParamName, f.Name, f.Reader) +} + +func getPointer(v interface{}) interface{} { + vv := valueOf(v) + if vv.Kind() == reflect.Ptr { + return v + } + return reflect.New(vv.Type()).Interface() +} + +func isPayloadSupported(m string, allowMethodGet bool) bool { + return !(m == MethodHead || m == MethodOptions || (m == MethodGet && !allowMethodGet)) +} + +func typeOf(i interface{}) reflect.Type { + return indirect(valueOf(i)).Type() +} + +func valueOf(i interface{}) reflect.Value { + return reflect.ValueOf(i) +} + +func indirect(v reflect.Value) reflect.Value { + return reflect.Indirect(v) +} + +func kindOf(v interface{}) reflect.Kind { + return typeOf(v).Kind() +} + +func createDirectory(dir string) (err error) { + if _, err = os.Stat(dir); err != nil { + if os.IsNotExist(err) { + if err = os.MkdirAll(dir, 0755); err != nil { + return + } + } + } + return +} + +func canJSONMarshal(contentType string, kind reflect.Kind) bool { + return IsJSONType(contentType) && (kind == reflect.Struct || kind == reflect.Map || kind == reflect.Slice) +} + +func functionName(i interface{}) string { + return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() +} + +func acquireBuffer() *bytes.Buffer { + return bufPool.Get().(*bytes.Buffer) +} + +func releaseBuffer(buf *bytes.Buffer) { + if buf != nil { + buf.Reset() + bufPool.Put(buf) + } +} + +func closeq(v interface{}) { + if c, ok := v.(io.Closer); ok { + silently(c.Close()) + } +} + +func silently(_ ...interface{}) {} + +func composeHeaders(c *HttpClient, r *Request, hdrs http.Header) string { + str := make([]string, 0, len(hdrs)) + for _, k := range sortHeaderKeys(hdrs) { + var v string + if k == "Cookie" { + cv := strings.TrimSpace(strings.Join(hdrs[k], ", ")) + if c.GetClient().Jar != nil { + for _, c := range c.GetClient().Jar.Cookies(r.RawRequest.URL) { + if cv != "" { + cv = cv + "; " + c.String() + } else { + cv = c.String() + } + } + } + v = strings.TrimSpace(fmt.Sprintf("%25s: %s", k, cv)) + } else { + v = strings.TrimSpace(fmt.Sprintf("%25s: %s", k, strings.Join(hdrs[k], ", "))) + } + if v != "" { + str = append(str, "\t"+v) + } + } + return strings.Join(str, "\n") +} + +func sortHeaderKeys(hdrs http.Header) []string { + keys := make([]string, 0, len(hdrs)) + for key := range hdrs { + keys = append(keys, key) + } + sort.Strings(keys) + return keys +} + +func copyHeaders(hdrs http.Header) http.Header { + nh := http.Header{} + for k, v := range hdrs { + nh[k] = v + } + return nh +} + +type noRetryErr struct { + err error +} + +func (e *noRetryErr) Error() string { + return e.err.Error() +} + +func wrapNoRetryErr(err error) error { + if err != nil { + err = &noRetryErr{err: err} + } + return err +} + +func unwrapNoRetryErr(err error) error { + if e, ok := err.(*noRetryErr); ok { + err = e.err + } + return err +} diff --git a/pkg/hub/hub.go b/pkg/hub/hub.go new file mode 100644 index 0000000..99b2fa1 --- /dev/null +++ b/pkg/hub/hub.go @@ -0,0 +1,100 @@ +package hub + +// AlertTopic is used to notify when a nonblocking subscriber loose one message +// You can subscribe on this topic and log or send metrics. +const AlertTopic = "hub.subscription.messageslost" + +type ( + //Hub is a component that provides publish and subscribe capabilities for messages. + // Every message has a Name used to route them to subscribers and this can be used like RabbitMQ topics exchanges. + // Where every word is separated by dots `.` and you can use `*` as a wildcard. + Hub struct { + matcher matcher + fields Fields + } +) + +// New create and return a new empty hub. +func New() *Hub { + return &Hub{ + matcher: newCSTrieMatcher(), + fields: Fields{}, + } +} + +// Publish will send an event to all the subscribers matching the event name. +func (h *Hub) Publish(m Message) { + for k, v := range h.fields { + m.Fields[k] = v + } + + for _, sub := range h.matcher.Lookup(m.Topic()) { + sub.Set(m) + } +} + +// With creates a child Hub with the fields added to it. +// When someone call Publish, this Fields will be added automatically into the message. +func (h *Hub) With(f Fields) *Hub { + hub := Hub{ + matcher: h.matcher, + fields: Fields{}, + } + for k, v := range h.fields { + hub.fields[k] = v + } + + for k, v := range f { + hub.fields[k] = v + } + + return &hub +} + +// Subscribe create a blocking subscription to receive events for a given topic. +// The cap param is used inside the subscriber and in this case used to create a channel. +// cap(1) = unbuffered channel. +func (h *Hub) Subscribe(cap int, topics ...string) Subscription { + return h.matcher.Subscribe(topics, newBlockingSubscriber(cap)) +} + +// NonBlockingSubscribe create a nonblocking subscription to receive events for a given topic. +// This subscriber will loose messages if the buffer reaches the max capability. +func (h *Hub) NonBlockingSubscribe(cap int, topics ...string) Subscription { + return h.matcher.Subscribe( + topics, + newNonBlockingSubscriber( + cap, + alertFunc(func(missed int) { + h.alert(missed, topics) + }), + )) +} + +// Unsubscribe remove and close the Subscription. +func (h *Hub) Unsubscribe(sub Subscription) { + h.matcher.Unsubscribe(sub) + sub.subscriber.Close() +} + +// Close will unsubscribe all the subscriptions and close them all. +func (h *Hub) Close() { + subs := h.matcher.Subscriptions() + for _, s := range subs { + h.matcher.Unsubscribe(s) + } + + for _, s := range subs { + s.subscriber.Close() + } +} + +func (h *Hub) alert(missed int, topics []string) { + h.Publish(Message{ + Name: AlertTopic, + Fields: Fields{ + "missed": missed, + "topic": topics, + }, + }) +} diff --git a/pkg/hub/matching.go b/pkg/hub/matching.go new file mode 100644 index 0000000..4784952 --- /dev/null +++ b/pkg/hub/matching.go @@ -0,0 +1,43 @@ +package hub + +const ( + delimiter = "." + wildcard = "*" +) + +type ( + // Subscription represents a topic subscription. + Subscription struct { + Topics []string + Receiver <-chan Message + subscriber subscriber + } + + // subscriber is the interface used internally to send values and get the channel used by subscribers. + // This is used to override the behavior of channel and support nonBlocking operations + subscriber interface { + // Set send the given Event to be processed by the subscriber + Set(Message) + // Ch return the channel used to consume messages inside the subscription. + // This func MUST always return the same channel. + Ch() <-chan Message + // Close will close the internal state and the subscriber will not receive more messages + // WARN: This function can be executed more than one time so the code MUST take care of this situation and + // avoid problems like close a closed channel. + Close() + } +) + +// matcher contains topic subscriptions and performs matches on them. +type matcher interface { + // Subscribe adds the Subscriber to the topics and returns a Subscription. + Subscribe(topics []string, sub subscriber) Subscription + + // Unsubscribe removes the Subscription. + Unsubscribe(sub Subscription) + + // Lookup returns the subscribers for the given topic. + Lookup(topic string) []subscriber + + Subscriptions() []Subscription +} diff --git a/pkg/hub/matching_crities.go b/pkg/hub/matching_crities.go new file mode 100644 index 0000000..aa393ba --- /dev/null +++ b/pkg/hub/matching_crities.go @@ -0,0 +1,596 @@ +package hub + + +import ( + "strings" + "sync/atomic" + "unsafe" +) + +type iNode struct { + main *mainNode +} + +type mainNode struct { + cNode *cNode + tNode *tNode +} + +type cNode struct { + branches map[string]*branch +} + +// newCNode creates a new C-node with the given subscription path. +func newCNode(words []string, sub subscriber) *cNode { + if len(words) == 1 { + return &cNode{ + branches: map[string]*branch{ + words[0]: {subs: map[subscriber]struct{}{sub: {}}}, + }, + } + } + + nin := &iNode{main: &mainNode{cNode: newCNode(words[1:], sub)}} + + return &cNode{ + branches: map[string]*branch{ + words[0]: {subs: map[subscriber]struct{}{}, iNode: nin}, + }, + } +} + +// inserted returns a copy of this C-node with the specified subscriber +// inserted. +func (c *cNode) inserted(words []string, sub subscriber) *cNode { + branches := make(map[string]*branch, len(c.branches)+1) + for key, branch := range c.branches { + branches[key] = branch + } + + var br *branch + if len(words) == 1 { + br = &branch{subs: map[subscriber]struct{}{sub: {}}} + } else { + br = &branch{ + subs: make(map[subscriber]struct{}), + iNode: &iNode{main: &mainNode{cNode: newCNode(words[1:], sub)}}, + } + } + + branches[words[0]] = br + + return &cNode{branches: branches} +} + +// updated returns a copy of this C-node with the specified branch updated. +func (c *cNode) updated(word string, sub subscriber) *cNode { + branches := make(map[string]*branch, len(c.branches)) + for word, branch := range c.branches { + branches[word] = branch + } + + newBranch := &branch{subs: map[subscriber]struct{}{sub: {}}} + br, ok := branches[word] + + if ok { + for id, sub := range br.subs { + newBranch.subs[id] = sub + } + + newBranch.iNode = br.iNode + } + + branches[word] = newBranch + + return &cNode{branches: branches} +} + +// updatedBranch returns a copy of this C-node with the specified branch +// updated. +func (c *cNode) updatedBranch(word string, in *iNode, br *branch) *cNode { + branches := make(map[string]*branch, len(c.branches)) + for key, branch := range c.branches { + branches[key] = branch + } + + branches[word] = br.updated(in) + + return &cNode{branches: branches} +} + +// removed returns a copy of this C-node with the subscriber removed from the +// corresponding branch. +func (c *cNode) removed(word string, sub subscriber) *cNode { + branches := make(map[string]*branch, len(c.branches)) + for word, branch := range c.branches { + branches[word] = branch + } + + br, ok := branches[word] + if ok { + br = br.removed(sub) + if len(br.subs) == 0 && br.iNode == nil { + // Remove the branch if it contains no subscribers and doesn't + // point anywhere. + delete(branches, word) + } else { + branches[word] = br + } + } + + return &cNode{branches: branches} +} + +// getBranches returns the branches for the given word. There are two possible +// branches: exact match and single wildcard. +func (c *cNode) getBranches(word string) (*branch, *branch) { + return c.branches[word], c.branches[wildcard] +} + +type branch struct { + iNode *iNode + subs map[subscriber]struct{} +} + +// updated returns a copy of this branch updated with the given I-node. +func (b *branch) updated(in *iNode) *branch { + subs := make(map[subscriber]struct{}, len(b.subs)) + for id, sub := range b.subs { + subs[id] = sub + } + + return &branch{subs: subs, iNode: in} +} + +// removed returns a copy of this branch with the given subscriber removed. +func (b *branch) removed(sub subscriber) *branch { + subs := make(map[subscriber]struct{}, len(b.subs)) + for id, sub := range b.subs { + subs[id] = sub + } + + delete(subs, sub) + + return &branch{subs: subs, iNode: b.iNode} +} + +// subscribers returns the Subscribers for this branch. +func (b *branch) subscribers() []subscriber { + subs := make([]subscriber, len(b.subs)) + i := 0 + + for sub := range b.subs { + subs[i] = sub + i++ + } + + return subs +} + +type tNode struct{} + +type csTrieMatcher struct { + root *iNode +} + +func newCSTrieMatcher() matcher { + root := &iNode{main: &mainNode{cNode: &cNode{}}} + return &csTrieMatcher{root: root} +} + +// Subscribe adds the subscriber to the topic and returns a Subscription. +func (c *csTrieMatcher) Subscribe(topics []string, sub subscriber) Subscription { + var ( + rootPtr = (*unsafe.Pointer)(unsafe.Pointer(&c.root)) + root = (*iNode)(atomic.LoadPointer(rootPtr)) + ) + + for _, topic := range topics { + words := strings.Split(topic, delimiter) + if !c.iinsert(root, nil, words, sub) { + return c.Subscribe(topics, sub) + } + } + + return Subscription{Topics: topics, Receiver: sub.Ch(), subscriber: sub} +} + +func (c *csTrieMatcher) iinsert(i, parent *iNode, words []string, sub subscriber) bool { + // Linearization point. + mainPtr := (*unsafe.Pointer)(unsafe.Pointer(&i.main)) + main := (*mainNode)(atomic.LoadPointer(mainPtr)) + + switch { + case main.cNode != nil: + cn := main.cNode + br := cn.branches[words[0]] + + if br == nil { + // If the relevant branch is not in the map, a copy of the C-node + // with the new entry is created. The linearization point is a + // successful CAS. + ncn := &mainNode{cNode: cn.inserted(words, sub)} + return atomic.CompareAndSwapPointer( + mainPtr, unsafe.Pointer(main), unsafe.Pointer(ncn)) + } + // If the relevant key is present in the map, its corresponding + // branch is read. + if len(words) > 1 { + // If more than 1 word is present in the path, the tree must be + // traversed deeper. + if br.iNode != nil { + // If the branch has an I-node, iinsert is called + // recursively. + return c.iinsert(br.iNode, i, words[1:], sub) + } + // Otherwise, an I-node which points to a new C-node must be + // added. The linearization point is a successful CAS. + nin := &iNode{main: &mainNode{cNode: newCNode(words[1:], sub)}} + ncn := &mainNode{cNode: cn.updatedBranch(words[0], nin, br)} + + return atomic.CompareAndSwapPointer( + mainPtr, unsafe.Pointer(main), unsafe.Pointer(ncn)) + } + + if _, ok := br.subs[sub]; ok { + // Already subscribed. + return true + } + // Insert the subscriber by copying the C-node and updating the + // respective branch. The linearization point is a successful CAS. + ncn := &mainNode{cNode: cn.updated(words[0], sub)} + + return atomic.CompareAndSwapPointer(mainPtr, unsafe.Pointer(main), unsafe.Pointer(ncn)) + case main.tNode != nil: + clean(parent) + return false + default: + panic("csTrie is in an invalid state") + } +} + +// Unsubscribe removes the Subscription. +func (c *csTrieMatcher) Unsubscribe(sub Subscription) { + var ( + rootPtr = (*unsafe.Pointer)(unsafe.Pointer(&c.root)) + root = (*iNode)(atomic.LoadPointer(rootPtr)) + ) + + for _, topic := range sub.Topics { + words := strings.Split(topic, delimiter) + if !c.iremove(root, nil, nil, words, 0, sub.subscriber) { + c.Unsubscribe(sub) + } + } +} + +func (c *csTrieMatcher) iremove(i, parent, parentsParent *iNode, words []string, wordIdx int, sub subscriber) bool { + // Linearization point. + mainPtr := (*unsafe.Pointer)(unsafe.Pointer(&i.main)) + main := (*mainNode)(atomic.LoadPointer(mainPtr)) + + switch { + case main.cNode != nil: + cn := main.cNode + br := cn.branches[words[wordIdx]] + + if br == nil { + // If the relevant word is not in the map, the subscription doesn't + // exist. + return true + } + // If the relevant word is present in the map, its corresponding + // branch is read. + if wordIdx+1 < len(words) { + // If more than 1 word is present in the path, the tree must be + // traversed deeper. + if br.iNode != nil { + // If the branch has an I-node, iremove is called + // recursively. + return c.iremove(br.iNode, i, parent, words, wordIdx+1, sub) + } + // Otherwise, the subscription doesn't exist. + return true + } + + if _, ok := br.subs[sub]; !ok { + // Not subscribed. + return true + } + // Remove the subscriber by copying the C-node without it. A + // contraction of the copy is then created. A successful CAS will + // substitute the old C-node with the copied C-node, thus removing + // the subscriber from the trie - this is the linearization point. + ncn := cn.removed(words[wordIdx], sub) + cntr := c.toContracted(ncn, i) + + if atomic.CompareAndSwapPointer( + mainPtr, unsafe.Pointer(main), unsafe.Pointer(cntr)) { + if parent != nil { + mainPtr = (*unsafe.Pointer)(unsafe.Pointer(&i.main)) + main = (*mainNode)(atomic.LoadPointer(mainPtr)) + + if main.tNode != nil { + cleanParent(i, parent, parentsParent, c, words[wordIdx-1]) + } + } + + return true + } + + return false + case main.tNode != nil: + clean(parent) + return false + default: + panic("csTrie is in an invalid state") + } +} + +// Lookup returns the Subscribers for the given topic. +func (c *csTrieMatcher) Lookup(topic string) []subscriber { + var ( + words = strings.Split(topic, delimiter) + rootPtr = (*unsafe.Pointer)(unsafe.Pointer(&c.root)) + root = (*iNode)(atomic.LoadPointer(rootPtr)) + ) + + result, ok := c.ilookup(root, nil, words) + if !ok { + return c.Lookup(topic) + } + + return result +} + +// ilookup attempts to retrieve the Subscribers for the word path. True is +// returned if the Subscribers were retrieved, false if the operation needs to +// be retried. +func (c *csTrieMatcher) ilookup(i, parent *iNode, words []string) ([]subscriber, bool) { + // Linearization point. + mainPtr := (*unsafe.Pointer)(unsafe.Pointer(&i.main)) + main := (*mainNode)(atomic.LoadPointer(mainPtr)) + + switch { + case main.cNode != nil: + // Traverse exact-match branch and single-word-wildcard branch. + exact, singleWC := main.cNode.getBranches(words[0]) + subs := make(map[subscriber]struct{}) + + if exact != nil { + s, ok := c.bLookup(i, exact, words) + if !ok { + return nil, false + } + + for _, sub := range s { + subs[sub] = struct{}{} + } + } + + if singleWC != nil { + s, ok := c.bLookup(i, singleWC, words) + if !ok { + return nil, false + } + + for _, sub := range s { + subs[sub] = struct{}{} + } + } + + s := make([]subscriber, len(subs)) + i := 0 + + for sub := range subs { + s[i] = sub + i++ + } + + return s, true + case main.tNode != nil: + clean(parent) + return nil, false + default: + panic("csTrie is in an invalid state") + } +} + +// bLookup attempts to retrieve the Subscribers from the word path along the +// given branch. True is returned if the Subscribers were retrieved, false if +// the operation needs to be retried. +func (c *csTrieMatcher) bLookup(i *iNode, b *branch, words []string) ([]subscriber, bool) { + if len(words) > 1 { + // If more than 1 key is present in the path, the tree must be + // traversed deeper. + if b.iNode == nil { + // If the branch doesn't point to an I-node, no subscribers + // exist. + return make([]subscriber, 0), true + } + // If the branch has an I-node, ilookup is called recursively. + return c.ilookup(b.iNode, i, words[1:]) + } + + // Retrieve the subscribers from the branch. + return b.subscribers(), true +} + +// Subscriptions return all the subscriptions inside the cstrie. +func (c *csTrieMatcher) Subscriptions() []Subscription { + var ( + rootPtr = (*unsafe.Pointer)(unsafe.Pointer(&c.root)) + root = (*iNode)(atomic.LoadPointer(rootPtr)) + ) + + result, ok := c.isubscriptions(root, nil, []string{}) + if !ok { + return c.Subscriptions() + } + + return result +} + +func (c *csTrieMatcher) isubscriptions(i, parent *iNode, words []string) ([]Subscription, bool) { + // Linearization point. + mainPtr := (*unsafe.Pointer)(unsafe.Pointer(&i.main)) + main := (*mainNode)(atomic.LoadPointer(mainPtr)) + subs := []Subscription{} + + switch { + case main.cNode != nil: + // Traverse all branches. + for word, br := range main.cNode.branches { + cwords := append([]string{}, words...) + cwords = append(cwords, word) + + if br.iNode != nil { + // If the branch has an I-node, isubscriptions is called recursively. + s, ok := c.isubscriptions(br.iNode, i, cwords) + if !ok { + return nil, false + } + + subs = append(subs, s...) + } + + for s := range br.subs { + subs = append(subs, Subscription{ + Topics: []string{strings.Join(cwords, delimiter)}, + subscriber: s, + Receiver: s.Ch(), + }) + } + } + + return subs, true + case main.tNode != nil: + clean(parent) + return nil, false + default: + panic("csTrie is in an invalid state") + } +} + +// toContracted ensures that every I-node except the root points to a C-node +// with at least one branch or a T-node. If a given C-node has no branches and +// is not at the root level, a T-node is returned. +func (c *csTrieMatcher) toContracted(cn *cNode, parent *iNode) *mainNode { + if c.root != parent && len(cn.branches) == 0 { + return &mainNode{tNode: &tNode{}} + } + + return &mainNode{cNode: cn} +} + +// clean replaces an I-node's C-node with a copy that has any tombed I-nodes +// resurrected. +func clean(i *iNode) { + mainPtr := (*unsafe.Pointer)(unsafe.Pointer(&i.main)) + main := (*mainNode)(atomic.LoadPointer(mainPtr)) + + if main.cNode != nil { + atomic.CompareAndSwapPointer(mainPtr, + unsafe.Pointer(main), unsafe.Pointer(toCompressed(main.cNode))) + } +} + +// cleanParent reads the main node of the parent I-node p and the current +// I-node i and checks if the T-node below i is reachable from p. If i is no +// longer reachable, some other thread has already completed the contraction. +// If it is reachable, the C-node below p is replaced with its contraction. +func cleanParent(i, parent, parentsParent *iNode, c *csTrieMatcher, word string) { + var ( + mainPtr = (*unsafe.Pointer)(unsafe.Pointer(&i.main)) + main = (*mainNode)(atomic.LoadPointer(mainPtr)) + pMainPtr = (*unsafe.Pointer)(unsafe.Pointer(&parent.main)) + pMain = (*mainNode)(atomic.LoadPointer(pMainPtr)) + ) + + if pMain.cNode != nil { + if br, ok := pMain.cNode.branches[word]; ok { + if br.iNode != i { + return + } + + if main.tNode != nil { + if !contract(parentsParent, parent, c, pMain) { + cleanParent(parentsParent, parent, i, c, word) + } + } + } + } +} + +// contract performs a contraction of the parent's C-node if possible. Returns +// true if the contraction succeeded, false if it needs to be retried. +func contract(parentsParent, parent *iNode, c *csTrieMatcher, pMain *mainNode) bool { + ncn := toCompressed(pMain.cNode) + if len(ncn.cNode.branches) == 0 && parentsParent != nil { + // If the compressed C-node has no branches, it and the I-node above it + // should be removed. To do this, a CAS must occur on the parent I-node + // of the parent to update the respective branch of the C-node below it + // to point to nil. + ppMainPtr := (*unsafe.Pointer)(unsafe.Pointer(&parentsParent.main)) + ppMain := (*mainNode)(atomic.LoadPointer(ppMainPtr)) + + for pKey, pBranch := range ppMain.cNode.branches { + // Find the branch pointing to the parent. + if pBranch.iNode == parent { + // Update the branch to point to nil. + updated := ppMain.cNode.updatedBranch(pKey, nil, pBranch) + + if len(pBranch.subs) == 0 { + // If the branch has no subscribers, simply prune it. + delete(updated.branches, pKey) + } + + // Replace the main node of the parent's parent. + return atomic.CompareAndSwapPointer(ppMainPtr, + unsafe.Pointer(ppMain), unsafe.Pointer(toCompressed(updated))) + } + } + } else { + // Otherwise, perform a simple contraction to a T-node. + cntr := c.toContracted(ncn.cNode, parent) + pMainPtr := (*unsafe.Pointer)(unsafe.Pointer(&parent.main)) + pMain := (*mainNode)(atomic.LoadPointer(pMainPtr)) + if !atomic.CompareAndSwapPointer(pMainPtr, unsafe.Pointer(pMain), + unsafe.Pointer(cntr)) { + return false + } + } + + return true +} + +// toCompressed prunes any branches to tombed I-nodes and returns the +// compressed main node. +func toCompressed(cn *cNode) *mainNode { + branches := make(map[string]*branch, len(cn.branches)) + for key, br := range cn.branches { + if !prunable(br) { + branches[key] = br + } + } + + return &mainNode{cNode: &cNode{branches: branches}} +} + +// prunable indicates if the branch can be pruned. A branch can be pruned if +// it has no subscribers and points to nowhere or it has no subscribers and +// points to a tombed I-node. +func prunable(br *branch) bool { + if len(br.subs) > 0 { + return false + } + + if br.iNode == nil { + return true + } + + mainPtr := (*unsafe.Pointer)(unsafe.Pointer(&br.iNode.main)) + main := (*mainNode)(atomic.LoadPointer(mainPtr)) + + return main.tNode != nil +} diff --git a/pkg/hub/message.go b/pkg/hub/message.go new file mode 100644 index 0000000..ce5ded0 --- /dev/null +++ b/pkg/hub/message.go @@ -0,0 +1,40 @@ +package hub + +import ( + "fmt" + "sort" + "strings" +) + +type ( + // Fields is a [key]value storage for Messages values. + Fields map[string]interface{} + + // Message represent some message/event passed into the hub + // It also contain some helper functions to convert the fields to primitive types. + Message struct { + Name string + Body []byte + Fields Fields + } +) + +// Topic return the message topic used when the message was sended. +func (m *Message) Topic() string { + return m.Name +} + +func (f Fields) String() string { + if len(f) == 0 { + return "Fields()" + } + + fields := make([]string, 0, len(f)) + for k, v := range f { + fields = append(fields, fmt.Sprintf("[%s]%v", k, v)) + } + + sort.Strings(fields) + + return "Fields( " + strings.Join(fields, ", ") + " )" +} diff --git a/pkg/hub/subscriber.go b/pkg/hub/subscriber.go new file mode 100644 index 0000000..4370223 --- /dev/null +++ b/pkg/hub/subscriber.go @@ -0,0 +1,81 @@ +package hub + +import "sync" + +type ( + alertFunc func(missed int) + + nonBlockingSubscriber struct { + ch chan Message + alert alertFunc + onceClose sync.Once + } + // blockingSubscriber uses an channel to receive events. + blockingSubscriber struct { + ch chan Message + onceClose sync.Once + } +) + +// newNonBlockingSubscriber returns a new nonBlockingSubscriber +// this subscriber will never block when sending an message, if the capacity is full +// we will ignore the message and call the Alert function from the Alerter. +func newNonBlockingSubscriber(cap int, alerter alertFunc) *nonBlockingSubscriber { + if cap <= 0 { + cap = 10 + } + + return &nonBlockingSubscriber{ + ch: make(chan Message, cap), + alert: alerter, + } +} + +// Set inserts the given Event into the diode. +func (s *nonBlockingSubscriber) Set(msg Message) { + select { + case s.ch <- msg: + default: + s.alert(1) + } +} + +// Ch return the channel used by subscriptions to consume messages. +func (s *nonBlockingSubscriber) Ch() <-chan Message { + return s.ch +} + +// Close will close the internal channel and stop receiving messages. +func (s *nonBlockingSubscriber) Close() { + s.onceClose.Do(func() { + close(s.ch) + }) +} + +// newBlockingSubscriber returns a blocking subscriber using chanels imternally. +func newBlockingSubscriber(cap int) *blockingSubscriber { + if cap < 0 { + cap = 0 + } + + return &blockingSubscriber{ + ch: make(chan Message, cap), + } +} + +// Set will send the message using the channel. +func (s *blockingSubscriber) Set(msg Message) { + s.ch <- msg +} + +// Ch return the channel used by subscriptions to consume messages. +func (s *blockingSubscriber) Ch() <-chan Message { + return s.ch +} + +// Close will close the internal channel and stop receiving messages. +func (s *blockingSubscriber) Close() { + s.onceClose.Do(func() { + close(s.ch) + }) +} diff --git a/pkg/logging/enumeration/encode_caller.go b/pkg/logging/enumeration/encode_caller.go new file mode 100644 index 0000000..9acaa08 --- /dev/null +++ b/pkg/logging/enumeration/encode_caller.go @@ -0,0 +1,9 @@ +package enumeration + +type EncodeCaller int + +var ( + DefaultCallerEncoder EncodeCaller = 0 + ShortestFunctionName EncodeCaller = 1 + LongestFunctionName EncodeCaller = 2 +) diff --git a/pkg/logging/enumeration/encode_duration.go b/pkg/logging/enumeration/encode_duration.go new file mode 100644 index 0000000..d2a80fe --- /dev/null +++ b/pkg/logging/enumeration/encode_duration.go @@ -0,0 +1,10 @@ +package enumeration + +type EncodeDuration int + +var ( + DefaultDurationEncoder EncodeDuration = 0 + StringDuration EncodeDuration = 1 + MillisecondDuration EncodeDuration = 2 + NanosecondDuration EncodeDuration = 3 +) diff --git a/pkg/logging/enumeration/encode_level.go b/pkg/logging/enumeration/encode_level.go new file mode 100644 index 0000000..b0793d2 --- /dev/null +++ b/pkg/logging/enumeration/encode_level.go @@ -0,0 +1,9 @@ +package enumeration + +type EncodeLevel int + +var ( + DefaultLevelEncoder EncodeLevel = 0 + Lowercase EncodeLevel = 1 + Camelcase EncodeLevel = 2 +) diff --git a/pkg/logging/enumeration/encode_time.go b/pkg/logging/enumeration/encode_time.go new file mode 100644 index 0000000..f68ea88 --- /dev/null +++ b/pkg/logging/enumeration/encode_time.go @@ -0,0 +1,12 @@ +package enumeration + +type EncodeTime int + +var ( + DefaultTimeEncoder EncodeTime = 0 + RFC3339Nano EncodeTime = 1 + RFC3339 EncodeTime = 2 + ISO8601 EncodeTime = 3 + Milliseconds EncodeTime = 4 + Nanoseconds EncodeTime = 5 +) \ No newline at end of file diff --git a/pkg/logging/interfaces/logger.go b/pkg/logging/interfaces/logger.go new file mode 100644 index 0000000..ba7e5a3 --- /dev/null +++ b/pkg/logging/interfaces/logger.go @@ -0,0 +1,10 @@ +package interfaces + +type Logger interface{ + Debug(msg string, parameters ...map[string]interface{}) + Info(msg string, parameters ...map[string]interface{}) + Warn(msg string, parameters ...map[string]interface{}) + Error(msg string, err error, parameters ...map[string]interface{}) + Fatal(msg string, err error, parameters ...map[string]interface{}) +} + diff --git a/pkg/logging/logger.go b/pkg/logging/logger.go new file mode 100644 index 0000000..4c85677 --- /dev/null +++ b/pkg/logging/logger.go @@ -0,0 +1,60 @@ +package logging + +import ( + "errors" + "github.com/ereb-or-od/kenobi/pkg/logging/interfaces" + "github.com/ereb-or-od/kenobi/pkg/logging/options" + "github.com/ereb-or-od/kenobi/pkg/logging/providers" + "github.com/ereb-or-od/kenobi/pkg/logging/utilities" + "go.uber.org/zap" +) + +type logger struct { + logger *zap.Logger +} + +func (l logger) Debug(msg string, parameters ...map[string]interface{}) { + l.logger.Debug(msg, *utilities.ToZapFields(parameters...)...) +} + +func (l logger) Warn(msg string, parameters ...map[string]interface{}) { + l.logger.Warn(msg, *utilities.ToZapFields(parameters...)...) +} + +func (l logger) Error(msg string, err error, parameters ...map[string]interface{}) { + if err == nil { + err = errors.New("an unrecognized error") + } + zapFields := *utilities.ToZapFields(parameters...) + if zapFields == nil || len(zapFields) == 0 { + zapFields = make([]zap.Field, 0) + zapFields = append(zapFields, zap.Error(err)) + } + l.logger.Error(msg, zapFields...) +} + +func (l logger) Fatal(msg string, err error, parameters ...map[string]interface{}) { + if err == nil { + err = errors.New("an unrecognized error") + } + zapFields := *utilities.ToZapFields(parameters...) + if zapFields == nil || len(zapFields) == 0 { + zapFields = make([]zap.Field, 0) + zapFields = append(zapFields, zap.Error(err)) + } + l.logger.Fatal(msg, zapFields...) +} + +func (l logger) Info(msg string, parameters ...map[string]interface{}) { + l.logger.Info(msg, *utilities.ToZapFields(parameters...)...) +} + +func New(loggerOptions ...*options.LoggerOptions) (interfaces.Logger, error) { + zapLogger, err := providers.New(loggerOptions...) + if err != nil { + return nil, err + } + return &logger{ + logger: zapLogger, + }, nil +} diff --git a/pkg/logging/logger_test.go b/pkg/logging/logger_test.go new file mode 100644 index 0000000..2695d2c --- /dev/null +++ b/pkg/logging/logger_test.go @@ -0,0 +1,158 @@ +package logging + +import ( + "errors" + "github.com/ereb-or-od/kenobi/pkg/logging/options" + "io/ioutil" + "os" + "testing" +) + +func TestNewLoggerShouldReturnLoggerWithDefaultOptionsWhenOptionsDoesNotSelected(t *testing.T) { + defaultLogger, err := New() + if err != nil { + t.Error("error does not expected when default-logger initialized") + } + + if defaultLogger == nil { + t.Errorf("default-logger must be initialized") + } + +} + +func TestNewLoggerWithOptionsShouldReturnLoggerWithSelectedOptionsWhenOptionsSelected(t *testing.T) { + defaultLogger, err := NewWithOptions(options.NewDefaultLoggerOptions()) + if err != nil { + t.Error("error does not expected when default-logger initialized") + } + + if defaultLogger == nil { + t.Errorf("default-logger must be initialized") + } +} + +func TestInfoShouldBuildInfoLog(t *testing.T) { + rescueStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + defaultLogger, _ := NewWithOptions(options.NewDefaultLoggerOptions()) + defaultLogger.Info("sample") + w.Close() + out, _ := ioutil.ReadAll(r) + os.Stdout = rescueStdout + if len(out) == 0 { + t.Error("info log could not be written") + } +} + +func TestInfoShouldBuildInfoLogWithParameters(t *testing.T) { + rescueStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + defaultLogger, _ := NewWithOptions(options.NewDefaultLoggerOptions()) + defaultLogger.Info("sample", map[string]interface{}{"sample": "foo"}) + w.Close() + out, _ := ioutil.ReadAll(r) + os.Stdout = rescueStdout + if len(out) == 0 { + t.Error("info log could not be written") + } +} + +func TestDebugShouldBuildDebugLog(t *testing.T) { + rescueStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + defaultLogger, _ := NewWithOptions(options.NewDefaultLoggerOptions()) + defaultLogger.Debug("sample") + w.Close() + out, _ := ioutil.ReadAll(r) + os.Stdout = rescueStdout + if len(out) == 0 { + t.Error("Debug log could not be written") + } +} + +func TestDebugShouldBuildDebugLogWithParameters(t *testing.T) { + rescueStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + defaultLogger, _ := NewWithOptions(options.NewDefaultLoggerOptions()) + defaultLogger.Debug("sample", map[string]interface{}{"sample": "foo"}) + w.Close() + out, _ := ioutil.ReadAll(r) + os.Stdout = rescueStdout + if len(out) == 0 { + t.Error("Debug log could not be written") + } +} + +func TestWarnShouldBuildWarnLog(t *testing.T) { + rescueStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + defaultLogger, _ := NewWithOptions(options.NewDefaultLoggerOptions()) + defaultLogger.Warn("sample") + w.Close() + out, _ := ioutil.ReadAll(r) + os.Stdout = rescueStdout + if len(out) == 0 { + t.Error("Warn log could not be written") + } +} + +func TestWarnShouldBuildWarnLogWithParameters(t *testing.T) { + rescueStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + defaultLogger, _ := NewWithOptions(options.NewDefaultLoggerOptions()) + defaultLogger.Warn("sample", map[string]interface{}{"sample": "foo"}) + w.Close() + out, _ := ioutil.ReadAll(r) + os.Stdout = rescueStdout + if len(out) == 0 { + t.Error("Warn log could not be written") + } +} + +func TestErrorShouldBuildErrorLog(t *testing.T) { + rescueStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + defaultLogger, _ := NewWithOptions(options.NewDefaultLoggerOptions()) + defaultLogger.Error("sample", errors.New("sample")) + w.Close() + out, _ := ioutil.ReadAll(r) + os.Stdout = rescueStdout + if len(out) == 0 { + t.Error("ExtractError log could not be written") + } +} + +func TestErrorWithParametersShouldBuildErrorLog(t *testing.T) { + rescueStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + defaultLogger, _ := NewWithOptions(options.NewDefaultLoggerOptions()) + defaultLogger.Error("sample", errors.New("sample"), map[string]interface{}{"sample": "foo"}) + w.Close() + out, _ := ioutil.ReadAll(r) + os.Stdout = rescueStdout + if len(out) == 0 { + t.Error("ExtractError log could not be written") + } +} + +func TestErrorWithParametersShouldBuildErrorLogWhenErrorIsNil(t *testing.T) { + rescueStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + defaultLogger, _ := NewWithOptions(options.NewDefaultLoggerOptions()) + defaultLogger.Error("sample", nil) + w.Close() + out, _ := ioutil.ReadAll(r) + os.Stdout = rescueStdout + if len(out) == 0 { + t.Error("ExtractError log could not be written") + } +} diff --git a/pkg/logging/options/logger_options.go b/pkg/logging/options/logger_options.go new file mode 100644 index 0000000..3e7f58f --- /dev/null +++ b/pkg/logging/options/logger_options.go @@ -0,0 +1,168 @@ +package options + +import ( + "github.com/ereb-or-od/kenobi/pkg/logging/enumeration" +) + +var ( + defaultEncoding = "json" + defaultOutputPaths = []string{"stdout"} + defaultErrorOutputPaths = []string{"stdout"} + defaultLoggingLevel = "DEBUG" + defaultTimeKey = "timestamp" + defaultLevelKey = "severity" + defaultNameKey = "default-logger" + defaultCallerKey = "caller" + defaultMessageKey = "message" + defaultStackTraceKey = "stacktrace" + defaultFunctionKey = "method" + defaultEncodeLevel = enumeration.Lowercase + defaultEncodeTime = enumeration.RFC3339 + defaultEncodeDuration = enumeration.MillisecondDuration + defaultEncodeCaller = enumeration.ShortestFunctionName +) + +type LoggerOptions struct { + DefaultParameters map[string]interface{} + Development bool + Encoding string + OutputPaths []string + ErrorOutputPaths []string + Level string + TimeKey string + LevelKey string + NameKey string + CallerKey string + StackTraceKey string + MessageKey string + FunctionKey string + EncodeLevel *enumeration.EncodeLevel + EncodeTime *enumeration.EncodeTime + EncodeDuration *enumeration.EncodeDuration + EncodeCaller *enumeration.EncodeCaller +} + +func New() *LoggerOptions { + return &LoggerOptions{ + DefaultParameters: nil, + Development: false, + Encoding: defaultEncoding, + OutputPaths: defaultOutputPaths, + ErrorOutputPaths: defaultErrorOutputPaths, + Level: defaultLoggingLevel, + TimeKey: defaultTimeKey, + LevelKey: defaultLevelKey, + NameKey: defaultNameKey, + CallerKey: defaultCallerKey, + StackTraceKey: defaultStackTraceKey, + MessageKey: defaultMessageKey, + EncodeLevel: &defaultEncodeLevel, + EncodeTime: &defaultEncodeTime, + EncodeDuration: &defaultEncodeDuration, + EncodeCaller: &defaultEncodeCaller, + } +} + +func (c *LoggerOptions) UseDefaultEncodingIfNotSpecified() *LoggerOptions { + if len(c.Encoding) == 0 { + c.Encoding = defaultEncoding + } + return c +} + +func (c *LoggerOptions) UseDefaultOutputPathsIfNotSpecified() *LoggerOptions { + if c.OutputPaths == nil || len(c.OutputPaths) == 0 { + c.OutputPaths = defaultOutputPaths + } + return c +} + +func (c *LoggerOptions) UseDefaultErrorOutputPathsIfNotSpecified() *LoggerOptions { + if c.ErrorOutputPaths == nil || len(c.ErrorOutputPaths) == 0 { + c.ErrorOutputPaths = defaultErrorOutputPaths + } + return c +} + +func (c *LoggerOptions) UseDefaultLoggingLevelIfNotSpecified() *LoggerOptions { + if len(c.Level) == 0 { + c.Level = defaultLoggingLevel + } + return c +} + +func (c *LoggerOptions) UseDefaultTimeKeyIfNotSpecified() *LoggerOptions { + if len(c.TimeKey) == 0 { + c.TimeKey = defaultTimeKey + } + return c +} + +func (c *LoggerOptions) UseDefaultLevelKeyIfNotSpecified() *LoggerOptions { + if len(c.LevelKey) == 0 { + c.LevelKey = defaultLevelKey + } + return c +} + +func (c *LoggerOptions) UseDefaultNameKeyIfNotSpecified() *LoggerOptions { + if len(c.NameKey) == 0 { + c.NameKey = defaultNameKey + } + return c +} + +func (c *LoggerOptions) UseDefaultCallerKeyIfNotSpecified() *LoggerOptions { + if len(c.CallerKey) == 0 { + c.CallerKey = defaultCallerKey + } + return c +} + +func (c *LoggerOptions) UseDefaultMessageKeyIfNotSpecified() *LoggerOptions { + if len(c.MessageKey) == 0 { + c.MessageKey = defaultMessageKey + } + return c +} +func (c *LoggerOptions) UseDefaultFunctionKeyIfNotSpecified() *LoggerOptions { + if len(c.FunctionKey) == 0 { + c.FunctionKey = defaultFunctionKey + } + return c +} + +func (c *LoggerOptions) UseDefaultStackTraceKeyIfNotSpecified() *LoggerOptions { + if len(c.StackTraceKey) == 0 { + c.StackTraceKey = defaultStackTraceKey + } + return c +} + +func (c *LoggerOptions) UseDefaultEncodeLevelIfNotSpecified() *LoggerOptions { + if c.EncodeLevel == nil { + c.EncodeLevel = &defaultEncodeLevel + } + return c +} + +func (c *LoggerOptions) UseDefaultEncodeTimeIfNotSpecified() *LoggerOptions { + if c.EncodeTime == nil { + c.EncodeTime = &defaultEncodeTime + } + return c +} + +func (c *LoggerOptions) UseDefaultEncodeDurationIfNotSpecified() *LoggerOptions { + if c.EncodeDuration == nil { + c.EncodeDuration = &defaultEncodeDuration + } + return c +} + +func (c *LoggerOptions) UseDefaultEncodeCallerIfNotSpecified() *LoggerOptions { + if c.EncodeCaller == nil { + c.EncodeCaller = &defaultEncodeCaller + } + return c +} diff --git a/pkg/logging/options/logger_options_test.go b/pkg/logging/options/logger_options_test.go new file mode 100644 index 0000000..583152a --- /dev/null +++ b/pkg/logging/options/logger_options_test.go @@ -0,0 +1,359 @@ +package options + +import ( + "github.com/ereb-or-od/kenobi/pkg/utilities" + "testing" +) + +func TestNewDefaultLoggerOptionsShouldReturnDefaultLoggerOptions(t *testing.T) { + defaultOptions := NewDefaultLoggerOptions() + if defaultOptions == nil { + t.Error("default option could not be nil") + } +} + +func TestNewDefaultLoggerOptionsShouldDevelopmentModeReturnFalseWhenDefaultOptionsSelected(t *testing.T) { + defaultOptions := NewDefaultLoggerOptions() + if defaultOptions.Development { + t.Error("development mode could not be selected as a default option") + } +} + +func TestNewDefaultLoggerOptionsShouldEncodingMustEqualsToDefaultEncodingWhenDefaultOptionsSelected(t *testing.T) { + defaultOptions := NewDefaultLoggerOptions() + if len(defaultOptions.Encoding) == 0 { + t.Error("Encoding could not be nil or empty") + } + + if defaultOptions.Encoding != defaultEncoding { + t.Errorf("%s must be a default encoding", defaultEncoding) + } +} +func TestNewDefaultLoggerOptionsShouldOutputPathsMustEqualsToDefaultOutputPathsWhenDefaultOptionsSelected(t *testing.T) { + defaultOptions := NewDefaultLoggerOptions() + if defaultOptions.OutputPaths == nil || len(defaultOptions.OutputPaths) == 0 { + t.Error("OutputPaths could not be nil or empty") + } + + if !utilities.CompareSlices(defaultOptions.OutputPaths, defaultOutputPaths) { + t.Error("OutputPaths must be equals default OutputPaths") + } +} + +func TestNewDefaultLoggerOptionsShouldErrorOutputPathsMustEqualsToDefaultErrorOutputPathsWhenDefaultOptionsSelected(t *testing.T) { + defaultOptions := NewDefaultLoggerOptions() + if defaultOptions.ErrorOutputPaths == nil || len(defaultOptions.ErrorOutputPaths) == 0 { + t.Error("ErrorOutputPaths could not be nil or empty") + } + + if !utilities.CompareSlices(defaultOptions.ErrorOutputPaths, defaultErrorOutputPaths) { + t.Error("ErrorOutputPaths must be equals default ErrorOutputPaths") + } +} + +func TestNewDefaultLoggerOptionsShouldLoggingLevelMustEqualsToDefaultLoggingLevelWhenDefaultOptionsSelected(t *testing.T) { + defaultOptions := NewDefaultLoggerOptions() + if len(defaultOptions.Level) == 0 { + t.Error("Level could not be nil or empty") + } + + if defaultOptions.Level != defaultLoggingLevel { + t.Errorf("%s must be specified as a default Level", defaultLoggingLevel) + } +} + +func TestNewDefaultLoggerOptionsShouldTimeKeyMustEqualsToDefaultTimeKeyWhenDefaultOptionsSelected(t *testing.T) { + defaultOptions := NewDefaultLoggerOptions() + if len(defaultOptions.TimeKey) == 0 { + t.Error("TimeKey could not be nil or empty") + } + + if defaultOptions.TimeKey != defaultTimeKey { + t.Errorf("%s must be specified as a default TimeKey", defaultTimeKey) + } +} + +func TestNewDefaultLoggerOptionsShouldLevelKeyMustEqualsToDefaultLevelKeyWhenDefaultOptionsSelected(t *testing.T) { + defaultOptions := NewDefaultLoggerOptions() + if len(defaultOptions.LevelKey) == 0 { + t.Error("LevelKey could not be nil or empty") + } + + if defaultOptions.LevelKey != defaultLevelKey { + t.Errorf("%s must be specified as a default LevelKey", defaultLevelKey) + } +} + +func TestNewDefaultLoggerOptionsShouldNameKeyMustEqualsToDefaultNameKeyWhenDefaultOptionsSelected(t *testing.T) { + defaultOptions := NewDefaultLoggerOptions() + if len(defaultOptions.NameKey) == 0 { + t.Error("NameKey could not be nil or empty") + } + + if defaultOptions.NameKey != defaultNameKey { + t.Errorf("%s must be specified as a default NameKey", defaultNameKey) + } +} + +func TestNewDefaultLoggerOptionsShouldCallerKeyMustEqualsToDefaultCallerKeyWhenDefaultOptionsSelected(t *testing.T) { + defaultOptions := NewDefaultLoggerOptions() + if len(defaultOptions.CallerKey) == 0 { + t.Error("CallerKey could not be nil or empty") + } + + if defaultOptions.CallerKey != defaultCallerKey { + t.Errorf("%s must be specified as a default CallerKey", defaultCallerKey) + } +} + +func TestNewDefaultLoggerOptionsShouldMessageKeyMustEqualsToDefaultMessageKeyWhenDefaultOptionsSelected(t *testing.T) { + defaultOptions := NewDefaultLoggerOptions() + if len(defaultOptions.MessageKey) == 0 { + t.Error("MessageKey could not be nil or empty") + } + + if defaultOptions.MessageKey != defaultMessageKey { + t.Errorf("%s must be specified as a default MessageKey", defaultMessageKey) + } +} + +func TestNewDefaultLoggerOptionsShouldStackTraceKeyMustEqualsToDefaultStackTraceWhenDefaultOptionsSelected(t *testing.T) { + defaultOptions := NewDefaultLoggerOptions() + if len(defaultOptions.StackTraceKey) == 0 { + t.Error("StackTraceKey could not be nil or empty") + } + + if defaultOptions.StackTraceKey != defaultStackTraceKey { + t.Errorf("%s must be specified as a default StackTraceKey", defaultStackTraceKey) + } +} + +func TestNewDefaultLoggerOptionsShouldEncodeLevelMustEqualsToDefaultEncodeLevelWhenDefaultOptionsSelected(t *testing.T) { + defaultOptions := NewDefaultLoggerOptions() + if defaultOptions.EncodeLevel == nil { + t.Error("EncodeLevel could not be nil or empty") + } + + if defaultOptions.EncodeLevel != &defaultEncodeLevel { + t.Errorf("%d must be specified as a default EncodeLevel", &defaultEncodeLevel) + } +} + +func TestNewDefaultLoggerOptionsShouldEncodeTimeMustEqualsToDefaultEncodeTimeWhenDefaultOptionsSelected(t *testing.T) { + defaultOptions := NewDefaultLoggerOptions() + if defaultOptions.EncodeTime == nil { + t.Error("EncodeTime could not be nil or empty") + } + + if defaultOptions.EncodeTime != &defaultEncodeTime { + t.Errorf("%d must be specified as a default EncodeTime", &defaultEncodeTime) + } +} + +func TestNewDefaultLoggerOptionsShouldEncodeCallerMustEqualsToDefaultEncodeCallerWhenDefaultOptionsSelected(t *testing.T) { + defaultOptions := NewDefaultLoggerOptions() + if defaultOptions.EncodeCaller == nil { + t.Error("EncodeCaller could not be nil or empty") + } + + if defaultOptions.EncodeCaller != &defaultEncodeCaller { + t.Errorf("%d must be specified as a default EncodeCaller", &defaultEncodeCaller) + } +} + +func TestNewDefaultLoggerOptionsShouldEncodeDurationMustEqualsToDefaultEncodeDurationWhenDefaultOptionsSelected(t *testing.T) { + defaultOptions := NewDefaultLoggerOptions() + if defaultOptions.EncodeDuration == nil { + t.Error("EncodeDuration could not be nil or empty") + } + + if defaultOptions.EncodeDuration != &defaultEncodeDuration { + t.Errorf("%d must be specified as a default EncodeDuration", &defaultEncodeDuration) + } +} + +func TestUseDefaultEncodingIfNotSpecifiedShouldReturnDefaultEncodingWhenEncodingDoesNotSelected(t *testing.T) { + options := &LoggerOptions{} + options.UseDefaultEncodingIfNotSpecified() + + if len(options.Encoding) == 0 { + t.Error("Encoding could not be nil or empty") + } + + if options.Encoding != defaultEncoding { + t.Errorf("%s must be specified as a default Encoding", defaultEncoding) + } + +} + +func TestUseDefaultOutputPathsIfNotSpecifiedShouldReturnDefaultOutputPathsWhenOutputPathsDoesNotSelected(t *testing.T) { + options := &LoggerOptions{} + options.UseDefaultOutputPathsIfNotSpecified() + + if options.OutputPaths == nil || len(options.OutputPaths) == 0 { + t.Error("OutputPaths could not be nil or empty") + } + + if !utilities.CompareSlices(options.OutputPaths, defaultOutputPaths) { + t.Error("OutputPaths must be equals default OutputPaths") + } + +} + +func TestUseDefaultErrorOutputPathsIfNotSpecifiedShouldReturnDefaultErrorOutputPathsWhenErrorOutputPathsDoesNotSelected(t *testing.T) { + options := &LoggerOptions{} + options.UseDefaultErrorOutputPathsIfNotSpecified() + + if options.ErrorOutputPaths == nil || len(options.ErrorOutputPaths) == 0 { + t.Error("ErrorOutputPaths could not be nil or empty") + } + + if !utilities.CompareSlices(options.ErrorOutputPaths, defaultErrorOutputPaths) { + t.Error("ErrorOutputPaths must be equals default ErrorOutputPaths") + } + +} + +func TestUseDefaultLoggingLevelIfNotSpecifiedShouldReturnDefaultLoggingLevelWhenLoggingLevelDoesNotSelected(t *testing.T) { + options := &LoggerOptions{} + options.UseDefaultLoggingLevelIfNotSpecified() + + if len(options.Level) == 0 { + t.Error("Level could not be nil or empty") + } + + if options.Level != defaultLoggingLevel { + t.Errorf("%s must be specified as a default Level", defaultLoggingLevel) + } + +} + +func TestUseDefaultTimeKeyIfNotSpecifiedShouldReturnDefaultTimeKeyWhenTimeKeyDoesNotSelected(t *testing.T) { + options := &LoggerOptions{} + options.UseDefaultTimeKeyIfNotSpecified() + + if len(options.TimeKey) == 0 { + t.Error("TimeKey could not be nil or empty") + } + + if options.TimeKey != defaultTimeKey { + t.Errorf("%s must be specified as a default TimeKey", defaultTimeKey) + } +} + +func TestUseDefaultLevelKeyIfNotSpecifiedShouldReturnDefaultLevelKeyWhenTimeKeyDoesNotSelected(t *testing.T) { + options := &LoggerOptions{} + options.UseDefaultLevelKeyIfNotSpecified() + + if len(options.LevelKey) == 0 { + t.Error("LevelKey could not be nil or empty") + } + + if options.LevelKey != defaultLevelKey { + t.Errorf("%s must be specified as a default LevelKey", defaultLevelKey) + } +} + +func TestUseDefaultNameKeyIfNotSpecifiedShouldReturnDefaultNameKeyWhenNameKeyDoesNotSelected(t *testing.T) { + options := &LoggerOptions{} + options.UseDefaultNameKeyIfNotSpecified() + + if len(options.NameKey) == 0 { + t.Error("TimeKey could not be nil or empty") + } + + if options.NameKey != defaultNameKey { + t.Errorf("%s must be specified as a default NameKey", defaultNameKey) + } +} + +func TestUseDefaultCallerKeyIfNotSpecifiedShouldReturnDefaultNameKeyWhenCallerKeyDoesNotSelected(t *testing.T) { + options := &LoggerOptions{} + options.UseDefaultCallerKeyIfNotSpecified() + + if len(options.CallerKey) == 0 { + t.Error("CallerKey could not be nil or empty") + } + + if options.CallerKey != defaultCallerKey { + t.Errorf("%s must be specified as a default CallerKey", defaultCallerKey) + } +} + +func TestUseDefaultMessageKeyIfNotSpecifiedShouldReturnDefaultNameKeyWhenMessageKeyDoesNotSelected(t *testing.T) { + options := &LoggerOptions{} + options.UseDefaultMessageKeyIfNotSpecified() + + if len(options.MessageKey) == 0 { + t.Error("MessageKey could not be nil or empty") + } + + if options.MessageKey != defaultMessageKey { + t.Errorf("%s must be specified as a default MessageKey", defaultMessageKey) + } +} + +func TestUseDefaultStackTraceKeyIfNotSpecifiedShouldReturnDefaultNameKeyWhenStackTraceKeyDoesNotSelected(t *testing.T) { + options := &LoggerOptions{} + options.UseDefaultStackTraceKeyIfNotSpecified() + + if len(options.StackTraceKey) == 0 { + t.Error("StackTraceKey could not be nil or empty") + } + + if options.StackTraceKey != defaultStackTraceKey { + t.Errorf("%s must be specified as a default StackTraceKey", defaultStackTraceKey) + } +} + +func TestUseDefaultEncodeLevelIfNotSpecifiedShouldReturnDefaultEncodeLevelWhenStackTraceKeyDoesNotSelected(t *testing.T) { + options := &LoggerOptions{} + options.UseDefaultEncodeLevelIfNotSpecified() + + if options.EncodeLevel == nil { + t.Error("EncodeLevel could not be nil or empty") + } + + if options.EncodeLevel != &defaultEncodeLevel { + t.Errorf("%d must be specified as a default EncodeLevel", &defaultEncodeLevel) + } +} + +func TestUseDefaultEncodeTimeIfNotSpecifiedShouldReturnDefaultEncodeTimeWhenStackTraceKeyDoesNotSelected(t *testing.T) { + options := &LoggerOptions{} + options.UseDefaultEncodeTimeIfNotSpecified() + + if options.EncodeTime == nil { + t.Error("EncodeTime could not be nil or empty") + } + + if options.EncodeTime != &defaultEncodeTime { + t.Errorf("%d must be specified as a default EncodeTime", &defaultEncodeTime) + } +} + +func TestUseDefaultEncodeDurationIfNotSpecifiedShouldReturnDefaultEncodeDurationWhenStackTraceKeyDoesNotSelected(t *testing.T) { + options := &LoggerOptions{} + options.UseDefaultEncodeDurationIfNotSpecified() + + if options.EncodeDuration == nil { + t.Error("EncodeDuration could not be nil or empty") + } + + if options.EncodeDuration != &defaultEncodeDuration { + t.Errorf("%d must be specified as a default EncodeDuration", &defaultEncodeDuration) + } +} + +func TestUseDefaultEncodeCallerIfNotSpecifiedShouldReturnDefaultEncodeCallerWhenStackTraceKeyDoesNotSelected(t *testing.T) { + options := &LoggerOptions{} + options.UseDefaultEncodeCallerIfNotSpecified() + + if options.EncodeCaller == nil { + t.Error("EncodeCaller could not be nil or empty") + } + + if options.EncodeCaller != &defaultEncodeCaller { + t.Errorf("%d must be specified as a default EncodeCaller", &defaultEncodeCaller) + } +} diff --git a/pkg/logging/providers/uber_zap.go b/pkg/logging/providers/uber_zap.go new file mode 100644 index 0000000..a0c6ac0 --- /dev/null +++ b/pkg/logging/providers/uber_zap.go @@ -0,0 +1,65 @@ +package providers + +import ( + "github.com/ereb-or-od/kenobi/pkg/logging/options" + "github.com/ereb-or-od/kenobi/pkg/logging/utilities" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +func New(configuration ...*options.LoggerOptions) (*zap.Logger, error) { + logger, err := newZapConfig(configuration...).Build() + if err != nil { + return nil, err + } + defer logger.Sync() + + return logger, nil +} + +func newZapConfig(loggerOptions ...*options.LoggerOptions) *zap.Config { + var loggerOption options.LoggerOptions + if loggerOptions == nil { + loggerOption = *options.New() + } else { + loggerOption = *loggerOptions[0] + } + loggerOption. + UseDefaultEncodingIfNotSpecified(). + UseDefaultOutputPathsIfNotSpecified(). + UseDefaultErrorOutputPathsIfNotSpecified(). + UseDefaultLoggingLevelIfNotSpecified(). + UseDefaultTimeKeyIfNotSpecified(). + UseDefaultLevelKeyIfNotSpecified(). + UseDefaultNameKeyIfNotSpecified(). + UseDefaultCallerKeyIfNotSpecified(). + UseDefaultMessageKeyIfNotSpecified(). + UseDefaultStackTraceKeyIfNotSpecified(). + UseDefaultEncodeLevelIfNotSpecified(). + UseDefaultEncodeTimeIfNotSpecified(). + UseDefaultEncodeDurationIfNotSpecified(). + UseDefaultEncodeCallerIfNotSpecified().UseDefaultFunctionKeyIfNotSpecified() + + return &zap.Config{ + Level: zap.NewAtomicLevelAt(utilities.ToZapLogLevel(loggerOption.Level)), + Development: loggerOption.Development, + Encoding: loggerOption.Encoding, + EncoderConfig: zapcore.EncoderConfig{ + TimeKey: loggerOption.TimeKey, + LevelKey: loggerOption.LevelKey, + NameKey: loggerOption.NameKey, + CallerKey: loggerOption.CallerKey, + FunctionKey: loggerOption.FunctionKey, + MessageKey: loggerOption.MessageKey, + StacktraceKey: loggerOption.StackTraceKey, + LineEnding: zapcore.DefaultLineEnding, + EncodeLevel: utilities.ToZapLevelEncoder(*loggerOption.EncodeLevel), + EncodeTime: utilities.ToZapTimeEncoder(*loggerOption.EncodeTime), + EncodeDuration: utilities.ToZapDurationEncoder(*loggerOption.EncodeDuration), + EncodeCaller: utilities.ToZapCallerEncoder(*loggerOption.EncodeCaller), + }, + InitialFields: loggerOption.DefaultParameters, + OutputPaths: loggerOption.OutputPaths, + ErrorOutputPaths: loggerOption.ErrorOutputPaths, + } +} diff --git a/pkg/logging/providers/uber_zap_test.go b/pkg/logging/providers/uber_zap_test.go new file mode 100644 index 0000000..3d49229 --- /dev/null +++ b/pkg/logging/providers/uber_zap_test.go @@ -0,0 +1,46 @@ +package providers + +import ( + "github.com/ereb-or-od/kenobi/pkg/logging/options" + "testing" +) + +func TestNewUberZapLoggerShouldReturnDefaultUberZapLoggerWhenConfigurationDoesNotSelected(t *testing.T) { + uberZapLogger, err := NewUberZapLogger() + if err != nil { + t.Error("error does not expected") + } + + if uberZapLogger == nil { + t.Error("default-logger must be initialized") + } +} + +func TestNewUberZapLoggerWithOptionsShouldReturnUberZapLoggerWhenConfigurationSelected(t *testing.T) { + uberZapLogger, err := NewUberZapLoggerWithOptions(options.NewDefaultLoggerOptions()) + if err != nil { + t.Error("error does not expected") + } + + if uberZapLogger == nil { + t.Error("default-logger must be initialized") + } +} + +func TestNewUberZapLoggerWithOptionsShouldReturnErrorWhenConfigurationIsNil(t *testing.T) { + uberZapLogger, err := newUberZapLogger(nil) + if err == nil { + t.Errorf("error should be expected as %s", configurationMustBeSpecifiedError) + } + + if uberZapLogger != nil { + t.Error("default-logger should not be initialized") + } +} + +func TestNewZapConfigShouldReturnDefaultZapConfigWhenConfigurationSelectedAsDefault(t *testing.T) { + config := newZapConfig(options.NewDefaultLoggerOptions()) + if config == nil { + t.Error("config should be initialized") + } +} diff --git a/pkg/logging/utilities/logger_utility.go b/pkg/logging/utilities/logger_utility.go new file mode 100644 index 0000000..21ce2ce --- /dev/null +++ b/pkg/logging/utilities/logger_utility.go @@ -0,0 +1,121 @@ +package utilities + +import ( + "errors" + "github.com/ereb-or-od/kenobi/pkg/logging/enumeration" + "github.com/xiam/to" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "time" +) + +func ToZapFields(parameters ...map[string]interface{}) *[]zap.Field { + var zapFields []zap.Field + if parameters != nil && len(parameters) > 0 { + for _, parameterItem := range parameters { + for key, value := range parameterItem { + if field, err := toZapField(key, value); err == nil { + if field != nil { + zapFields = append(zapFields, *field) + } + } + + } + } + return &zapFields + } + return &zapFields +} + +func toZapField(key string, value interface{}) (*zap.Field, error) { + if len(key) == 0 { + return nil, errors.New("key cannot be nil") + } + var zapField zap.Field + switch value.(type) { + case int: + zapField = zap.Int(key, to.Int(value)) + case float64: + zapField = zap.Float64(key, to.Float64(value)) + case string: + zapField = zap.String(key, to.String(value)) + case time.Time: + zapField = zap.Time(key, to.Time(value)) + case bool: + zapField = zap.Bool(key, to.Bool(value)) + default: + zapField = zap.String(key, to.String(value)) + } + return &zapField, nil +} + +func ToZapLogLevel(level string) zapcore.Level { + switch level { + case "DEBUG": + return zapcore.DebugLevel + case "INFO": + return zapcore.InfoLevel + case "WARN": + return zapcore.WarnLevel + case "ERROR": + return zapcore.ErrorLevel + case "PANIC": + return zapcore.PanicLevel + case "FATAL": + return zapcore.FatalLevel + default: + return zapcore.InfoLevel + } +} + +func ToZapLevelEncoder(encoder enumeration.EncodeLevel) zapcore.LevelEncoder { + switch encoder { + case enumeration.Lowercase: + return zapcore.LowercaseLevelEncoder + case enumeration.Camelcase: + return zapcore.CapitalLevelEncoder + default: + return zapcore.LowercaseLevelEncoder + } +} + +func ToZapTimeEncoder(encoder enumeration.EncodeTime) zapcore.TimeEncoder { + switch encoder { + case enumeration.RFC3339Nano: + return zapcore.RFC3339NanoTimeEncoder + case enumeration.RFC3339: + return zapcore.RFC3339TimeEncoder + case enumeration.ISO8601: + return zapcore.ISO8601TimeEncoder + case enumeration.Milliseconds: + return zapcore.EpochMillisTimeEncoder + case enumeration.Nanoseconds: + return zapcore.EpochNanosTimeEncoder + default: + return zapcore.EpochTimeEncoder + } +} + +func ToZapDurationEncoder(encoder enumeration.EncodeDuration) zapcore.DurationEncoder { + switch encoder { + case enumeration.StringDuration: + return zapcore.StringDurationEncoder + case enumeration.NanosecondDuration: + return zapcore.NanosDurationEncoder + case enumeration.MillisecondDuration: + return zapcore.NanosDurationEncoder + default: + return zapcore.SecondsDurationEncoder + } +} + +func ToZapCallerEncoder(encoder enumeration.EncodeCaller) zapcore.CallerEncoder { + switch encoder { + case enumeration.LongestFunctionName: + return zapcore.FullCallerEncoder + case enumeration.ShortestFunctionName: + return zapcore.ShortCallerEncoder + default: + return zapcore.FullCallerEncoder + } +} diff --git a/pkg/logging/utilities/logger_utility_test.go b/pkg/logging/utilities/logger_utility_test.go new file mode 100644 index 0000000..95ee6d9 --- /dev/null +++ b/pkg/logging/utilities/logger_utility_test.go @@ -0,0 +1,282 @@ +package utilities + +import ( + "github.com/ereb-or-od/kenobi/pkg/logging/enumeration" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "reflect" + "testing" +) + +func TestToZapFieldsShouldReturnEmptyZapFieldsWhenParametersIsNil(t *testing.T) { + zapFields := ToZapFields() + if zapFields == nil { + t.Error("ZapFields option could not be nil") + } +} + +func TestToZapFieldsShouldReturnEmptyZapFieldsWhenParametersIsEmpty(t *testing.T) { + var parameters map[string]interface{} + zapFields := ToZapFields(parameters) + if zapFields == nil { + t.Error("ZapFields option could not be nil") + } +} + +func TestToZapFieldsShouldReturnZapFieldsWhenParametersDoesNotEmpty(t *testing.T) { + parameters := map[string]interface{}{ + "sample": "1", + "sample_2": 1, + "sample_3": 1.1, + "sample_4": true, + "": true, + } + zapFields := ToZapFields(parameters) + if zapFields == nil { + t.Error("ZapFields option could not be nil") + } + + for _, zapField := range *zapFields { + switch zapField.Key { + case "sample": + if zapField.Type != zapcore.StringType { + t.Errorf("%s field could not be recognized. it must be StringType", zapField.Key) + } + case "sample_2": + if zapField.Type != zapcore.Int64Type { + t.Errorf("%s field could not be recognized. it must be Int64Type", zapField.Key) + } + case "sample_3": + if zapField.Type != zapcore.Float64Type { + t.Errorf("%s field could not be recognized. it must be Float64Type", zapField.Key) + } + case "sample_4": + if zapField.Type != zapcore.BoolType { + t.Errorf("%s field could not be recognized. it must be BoolType", zapField.Key) + } + } + } +} + +func TestToZapLogLevelShouldReturnDEBUGWhenSelectedLevelEqualsToDEBUG(t *testing.T) { + zapLevel := ToZapLogLevel("DEBUG") + if zapLevel != zapcore.DebugLevel { + t.Error("zapLevel could not be recognized") + } +} + +func TestToZapLogLevelShouldReturnINFOWhenSelectedLevelEqualsToINFO(t *testing.T) { + zapLevel := ToZapLogLevel("INFO") + if zapLevel != zapcore.InfoLevel { + t.Error("zapLevel could not be recognized") + } +} + +func TestToZapLogLevelShouldReturnWARNWhenSelectedLevelEqualsToWARN(t *testing.T) { + zapLevel := ToZapLogLevel("WARN") + if zapLevel != zapcore.WarnLevel { + t.Error("zapLevel could not be recognized") + } +} + +func TestToZapLogLevelShouldReturnERRORWhenSelectedLevelEqualsToERROR(t *testing.T) { + zapLevel := ToZapLogLevel("ERROR") + if zapLevel != zapcore.ErrorLevel { + t.Error("zapLevel could not be recognized") + } +} + +func TestToZapLogLevelShouldReturnPANICWhenSelectedLevelEqualsToPANIC(t *testing.T) { + zapLevel := ToZapLogLevel("PANIC") + if zapLevel != zapcore.PanicLevel { + t.Error("zapLevel could not be recognized") + } +} + +func TestToZapLogLevelShouldReturnFATALWhenSelectedLevelEqualsToFATAL(t *testing.T) { + zapLevel := ToZapLogLevel("FATAL") + if zapLevel != zapcore.FatalLevel { + t.Error("zapLevel could not be recognized") + } +} + +func TestToZapLogLevelShouldReturnDEBUGWhenSelectedLevelDoesNotEqualsDefinedLevels(t *testing.T) { + zapLevel := ToZapLogLevel("NOT_DEFINED_LEVEL") + if zapLevel != zapcore.InfoLevel { + t.Error("zapLevel could not be recognized") + } +} + +func TestToZapFieldShouldReturnIntWhenValueEqualsToInteger(t *testing.T) { + zapField, err := toZapField("sample", 1) + if err != nil { + t.Error("zapField must be valid") + } + if zapField == nil { + t.Error("zapField could not be nil") + } + if *zapField != zap.Int("sample", 1) { + t.Error("zapField must be Int") + } +} + +func TestToZapFieldShouldReturnFloatWhenValueEqualsToFloat(t *testing.T) { + zapField, err := toZapField("sample", 1.2) + if err != nil { + t.Error("zapField must be valid") + } + if zapField == nil { + t.Error("zapField could not be nil") + } + if *zapField != zap.Float64("sample", 1.2) { + t.Error("zapField must be Float64") + } +} + +func TestToZapFieldShouldReturnStringWhenValueEqualsToString(t *testing.T) { + zapField, err := toZapField("sample", "sample") + if err != nil { + t.Error("zapField must be valid") + } + if zapField == nil { + t.Error("zapField could not be nil") + } + if *zapField != zap.String("sample", "sample") { + t.Error("zapField must be ExtractBodyAsString") + } +} + +func TestToZapFieldShouldReturnBooleanWhenValueEqualsToBoolean(t *testing.T) { + zapField, err := toZapField("sample", true) + if err != nil { + t.Error("zapField must be valid") + } + if zapField == nil { + t.Error("zapField could not be nil") + } + if *zapField != zap.Bool("sample", true) { + t.Error("zapField must be Boolean") + } +} + +func TestToZapFieldShouldReturnErrorWhenKeyIsEmpty(t *testing.T) { + zapField, err := toZapField("", true) + if err == nil { + t.Error("zapField must have an error when key is nil") + } + if zapField != nil { + t.Error("zapField must be nil when key is nil") + } +} + +func TestToZapLevelEncoderShouldReturnLowercaseWhenLowercaseSelected(t *testing.T) { + levelEncoder := ToZapLevelEncoder(enumeration.Lowercase) + if reflect.TypeOf(levelEncoder).Size() != reflect.TypeOf(zapcore.LowercaseLevelEncoder).Size() { + t.Error("level encoder must be equals to LowercaseLevelEncoder") + } +} + +func TestToZapLevelEncoderShouldReturnCamelcaseWhenCamelcaseSelected(t *testing.T) { + levelEncoder := ToZapLevelEncoder(enumeration.Camelcase) + if reflect.TypeOf(levelEncoder).Size() != reflect.TypeOf(zapcore.CapitalLevelEncoder).Size() { + t.Error("level encoder must be equals to CapitalLevelEncoder") + } +} + +func TestToZapLevelEncoderShouldReturnLowercaseWhenDefaultSelected(t *testing.T) { + levelEncoder := ToZapLevelEncoder(enumeration.DefaultLevelEncoder) + if reflect.TypeOf(levelEncoder).Size() != reflect.TypeOf(zapcore.LowercaseLevelEncoder).Size() { + t.Error("level encoder must be equals to LowercaseLevelEncoder") + } +} + +func TestToZapTimeEncoderShouldReturnRFC3339NanoWhenRFC3339NanoSelected(t *testing.T) { + timeEncoder := ToZapTimeEncoder(enumeration.RFC3339Nano) + if reflect.TypeOf(timeEncoder).Size() != reflect.TypeOf(zapcore.RFC3339NanoTimeEncoder).Size() { + t.Error("time encoder must be equals to RFC3339NanoTimeEncoder") + } +} + +func TestToZapTimeEncoderShouldReturnRFC3339WhenRFC3339Selected(t *testing.T) { + timeEncoder := ToZapTimeEncoder(enumeration.RFC3339) + if reflect.TypeOf(timeEncoder).Size() != reflect.TypeOf(zapcore.RFC3339TimeEncoder).Size() { + t.Error("time encoder must be equals to RFC3339TimeEncoder") + } +} + +func TestToZapTimeEncoderShouldReturnISO8601WhenISO8601Selected(t *testing.T) { + timeEncoder := ToZapTimeEncoder(enumeration.ISO8601) + if reflect.TypeOf(timeEncoder).Size() != reflect.TypeOf(zapcore.ISO8601TimeEncoder).Size() { + t.Error("time encoder must be equals to ISO8601TimeEncoder") + } +} + +func TestToZapTimeEncoderShouldReturnMillisecondsWhenMillisecondsSelected(t *testing.T) { + timeEncoder := ToZapTimeEncoder(enumeration.Milliseconds) + if reflect.TypeOf(timeEncoder).Size() != reflect.TypeOf(zapcore.EpochMillisTimeEncoder).Size() { + t.Error("time encoder must be equals to EpochMillisTimeEncoder") + } +} + +func TestToZapTimeEncoderShouldReturnNanosecondsWhenNanosecondsSelected(t *testing.T) { + timeEncoder := ToZapTimeEncoder(enumeration.Nanoseconds) + if reflect.TypeOf(timeEncoder).Size() != reflect.TypeOf(zapcore.EpochNanosTimeEncoder).Size() { + t.Error("time encoder must be equals to EpochNanosTimeEncoder") + } +} + +func TestToZapTimeEncoderShouldReturnEpochTimeWhenDefaultSelected(t *testing.T) { + timeEncoder := ToZapTimeEncoder(enumeration.DefaultTimeEncoder) + if reflect.TypeOf(timeEncoder).Size() != reflect.TypeOf(zapcore.EpochTimeEncoder).Size() { + t.Error("time encoder must be equals to EpochTimeEncoder") + } +} + +func TestToZapDurationEncoderShouldReturnStringDurationWhenStringDurationSelected(t *testing.T) { + durationEncoder := ToZapDurationEncoder(enumeration.StringDuration) + if reflect.TypeOf(durationEncoder).Size() != reflect.TypeOf(zapcore.StringDurationEncoder).Size() { + t.Error("duration encoder must be equals to StringDurationEncoder") + } +} + +func TestToZapDurationEncoderShouldReturnMillisecondDurationWhenMillisecondDurationSelected(t *testing.T) { + durationEncoder := ToZapDurationEncoder(enumeration.MillisecondDuration) + if reflect.TypeOf(durationEncoder).Size() != reflect.TypeOf(zapcore.MillisDurationEncoder).Size() { + t.Error("time encoder must be equals to MillisDurationEncoder") + } +} + +func TestToZapDurationEncoderShouldReturnNanosecondDurationWhenNanosecondDurationSelected(t *testing.T) { + durationEncoder := ToZapDurationEncoder(enumeration.NanosecondDuration) + if reflect.TypeOf(durationEncoder).Size() != reflect.TypeOf(zapcore.NanosDurationEncoder).Size() { + t.Error("duration encoder must be equals to NanosDurationEncoder") + } +} + +func TestToZapDurationEncoderShouldReturnSecondsDurationWhenDefaultDurationSelected(t *testing.T) { + durationEncoder := ToZapDurationEncoder(enumeration.DefaultDurationEncoder) + if reflect.TypeOf(durationEncoder).Size() != reflect.TypeOf(zapcore.SecondsDurationEncoder).Size() { + t.Error("duration encoder must be equals to SecondsDurationEncoder") + } +} + +func TestToZapCallerEncoderShouldReturnLongestFunctionNameWhenLongestFunctionNameSelected(t *testing.T) { + callerEncoder := ToZapCallerEncoder(enumeration.LongestFunctionName) + if reflect.TypeOf(callerEncoder).Size() != reflect.TypeOf(zapcore.FullCallerEncoder).Size() { + t.Error("caller encoder must be equals to FullCallerEncoder") + } +} + +func TestToZapCallerEncoderShouldReturnShortestFunctionNameWhenShortestFunctionNameSelected(t *testing.T) { + callerEncoder := ToZapCallerEncoder(enumeration.ShortestFunctionName) + if reflect.TypeOf(callerEncoder).Size() != reflect.TypeOf(zapcore.ShortCallerEncoder).Size() { + t.Error("caller encoder must be equals to ShortCallerEncoder") + } +} + +func TestToZapCallerEncoderShouldReturnLongestFunctionNameWhenDefaultSelected(t *testing.T) { + callerEncoder := ToZapCallerEncoder(enumeration.DefaultCallerEncoder) + if reflect.TypeOf(callerEncoder).Size() != reflect.TypeOf(zapcore.FullCallerEncoder).Size() { + t.Error("caller encoder must be equals to FullCallerEncoder") + } +} diff --git a/pkg/marshalling/interfaces/marshaller.go b/pkg/marshalling/interfaces/marshaller.go new file mode 100644 index 0000000..7578ca0 --- /dev/null +++ b/pkg/marshalling/interfaces/marshaller.go @@ -0,0 +1,7 @@ +package interfaces +type Marshaller interface { + Marshall(v interface{}) ([]byte, error) + Unmarshall(data []byte, v interface{}) error + MarshallString(v interface{}) (string, error) + UnmarshallString(data string, v interface{}) error +} diff --git a/pkg/marshalling/json/default_json_marshaller.go b/pkg/marshalling/json/default_json_marshaller.go new file mode 100644 index 0000000..2f8cc9b --- /dev/null +++ b/pkg/marshalling/json/default_json_marshaller.go @@ -0,0 +1,48 @@ +package json + +import ( + "github.com/ereb-or-od/kenobi/pkg/marshalling/interfaces" + jsoniter "github.com/json-iterator/go" +) + +type defaultJsonMarshaller struct { + jsoniter jsoniter.API +} + +func (j defaultJsonMarshaller) Marshall(v interface{}) ([]byte, error) { + byteArray, err := j.jsoniter.Marshal(v) + if err != nil { + return nil, err + } + return byteArray, nil +} + +func (j defaultJsonMarshaller) Unmarshall(data []byte, v interface{}) error { + err := j.jsoniter.Unmarshal(data, &v) + if err != nil { + return err + } + return nil +} + +func (j defaultJsonMarshaller) MarshallString(v interface{}) (string, error) { + byteArray, err := j.jsoniter.MarshalToString(v) + if err != nil { + return "", err + } + return byteArray, nil +} + +func (j defaultJsonMarshaller) UnmarshallString(data string, v interface{}) error { + err := j.jsoniter.UnmarshalFromString(data, &v) + if err != nil { + return err + } + return nil +} + +func New() interfaces.Marshaller { + return &defaultJsonMarshaller{ + jsoniter: jsoniter.ConfigCompatibleWithStandardLibrary, + } +} diff --git a/pkg/mediator/interfaces.go b/pkg/mediator/interfaces.go new file mode 100644 index 0000000..b96c64e --- /dev/null +++ b/pkg/mediator/interfaces.go @@ -0,0 +1,25 @@ +package mediator + + +import "context" + +type ( + Sender interface { + Send(context.Context, Message) (interface{}, error) + } + Builder interface { + RegisterHandler(request Message, handler RequestHandler) Builder + UseBehaviour(PipelineBehaviour) Builder + Use(fn func(context.Context, Message, Next) (interface{}, error)) Builder + Build() (*Mediator, error) + } + RequestHandler interface { + Handle(context.Context, Message) (interface{}, error) + } + PipelineBehaviour interface { + Process(context.Context, Message, Next) (interface{}, error) + } + Message interface { + Key() string + } +) diff --git a/pkg/mediator/mediator.go b/pkg/mediator/mediator.go new file mode 100644 index 0000000..4fae91b --- /dev/null +++ b/pkg/mediator/mediator.go @@ -0,0 +1,43 @@ +package mediator + +import ( + "context" + "errors" +) + +type Mediator struct { + context PipelineContext +} + +func newMediator(ctx PipelineContext) *Mediator { + return &Mediator{ + context: ctx, + } +} + +func (m *Mediator) Send(ctx context.Context, req Message) (interface{}, error) { + if m.context.pipeline.empty() { + return m.send(ctx, req) + } + return m.context.pipeline(ctx, req) +} + +func (m *Mediator) send(ctx context.Context, req Message) (interface{}, error) { + key := req.Key() + handler, ok := m.context.handlers[key] + if !ok { + return nil, errors.New("handler could not be found") + } + return handler.Handle(ctx, req) +} + +func (m *Mediator) pipe(call Behaviour) { + if m.context.pipeline.empty() { + m.context.pipeline = m.send + } + seed := m.context.pipeline + + m.context.pipeline = func(ctx context.Context, msg Message) (interface{}, error) { + return call(ctx, msg, func(context.Context) (interface{}, error) { return seed(ctx, msg) }) + } +} \ No newline at end of file diff --git a/pkg/mediator/pipeline.go b/pkg/mediator/pipeline.go new file mode 100644 index 0000000..ff26e2f --- /dev/null +++ b/pkg/mediator/pipeline.go @@ -0,0 +1,54 @@ +package mediator + +import ( + "context" +) + +type Behaviour func(context.Context, Message, Next) (interface{}, error) + +type Next func(ctx context.Context) (interface{}, error) + +type Pipeline func(context.Context, Message) (interface{}, error) + +func (p Pipeline) empty() bool { return p == nil } + +type PipelineContext struct { + behaviours []Behaviour + pipeline Pipeline + handlers map[string]RequestHandler +} + +func NewContext() *PipelineContext { + return &PipelineContext{ + handlers: make(map[string]RequestHandler), + } +} + +func (p *PipelineContext) UseBehaviour(behaviour PipelineBehaviour) Builder { + return p.Use(behaviour.Process) +} + +func (p *PipelineContext) Use(call func(context.Context, Message, Next) (interface{}, error)) Builder { + p.behaviours = append(p.behaviours, call) + return p +} + +func (p *PipelineContext) RegisterHandler(req Message, h RequestHandler) Builder { + key := req.Key() + + p.handlers[key] = h + return p +} + +func (p *PipelineContext) Build() (*Mediator, error) { + m := newMediator(*p) + reverseApply(p.behaviours, m.pipe) + return m, nil +} + +func reverseApply(behaviours []Behaviour, + fn func(Behaviour)) { + for i := len(behaviours) - 1; i >= 0; i-- { + fn(behaviours[i]) + } +} \ No newline at end of file diff --git a/pkg/metrics/.DS_Store b/pkg/metrics/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..f883efd1ce596414ccc0215ab61f6c815ba07d80 GIT binary patch literal 6148 zcmeHK%}N6?5Kj7|*^1bMV2`!Gz$^dKy2J$MsV^q|u2(qb3uZs~3xd?7fFx+EFnLF8(V*dN7x zzZdySvE=xh4B)rRvzP@eWOr=-`@Q@9UJxgh$_LM6bNRJ(Aqt|fS-5e#(a24_WEeZ% z;0k+ZUefiAeI7-OSf2ZXSM z)hw3A?9-X%CAD_n0lb2V`EQeoE$(F$>yn!*d z#Rqo~_M-3>ywi+nBq1?C3=jh=!hkvc#Kwy3m$pd^5CcDF0M7>rif9{5HL9Zn8oWN@ zcn%Q-Y|Vt^P}WFV`%MXdiP-@pGClc+}w5Ci{;0iJ8P4F|SlYU|SGu+~b@7AOkFr5a}` jV5m|Iu~>>rpi00m(Ezj!rW(NmLKgu=12x3JuQKonpxRMZ literal 0 HcmV?d00001 diff --git a/pkg/metrics/const_unix.go b/pkg/metrics/const_unix.go new file mode 100644 index 0000000..31098dd --- /dev/null +++ b/pkg/metrics/const_unix.go @@ -0,0 +1,12 @@ +// +build !windows + +package metrics + +import ( + "syscall" +) + +const ( + // DefaultSignal is used with DefaultInmemSignal + DefaultSignal = syscall.SIGUSR1 +) diff --git a/pkg/metrics/const_windows.go b/pkg/metrics/const_windows.go new file mode 100644 index 0000000..38136af --- /dev/null +++ b/pkg/metrics/const_windows.go @@ -0,0 +1,13 @@ +// +build windows + +package metrics + +import ( + "syscall" +) + +const ( + // DefaultSignal is used with DefaultInmemSignal + // Windows has no SIGUSR1, use SIGBREAK + DefaultSignal = syscall.Signal(21) +) diff --git a/pkg/metrics/inmem.go b/pkg/metrics/inmem.go new file mode 100644 index 0000000..71deef1 --- /dev/null +++ b/pkg/metrics/inmem.go @@ -0,0 +1,337 @@ +package metrics + +import ( + "bytes" + "fmt" + "math" + "net/url" + "strings" + "sync" + "time" +) + +var spaceReplacer = strings.NewReplacer(" ", "_") + +// InmemSink provides a MetricSink that does in-memory aggregation +// without sending metrics over a network. It can be embedded within +// an application to provide profiling information. +type InmemSink struct { + // How long is each aggregation interval + interval time.Duration + + // Retain controls how many metrics interval we keep + retain time.Duration + + // maxIntervals is the maximum length of intervals. + // It is retain / interval. + maxIntervals int + + // intervals is a slice of the retained intervals + intervals []*IntervalMetrics + intervalLock sync.RWMutex + + rateDenom float64 +} + +// IntervalMetrics stores the aggregated metrics +// for a specific interval +type IntervalMetrics struct { + sync.RWMutex + + // The start time of the interval + Interval time.Time + + // Gauges maps the key to the last set value + Gauges map[string]GaugeValue + + // Points maps the string to the list of emitted values + // from EmitKey + Points map[string][]float32 + + // Counters maps the string key to a sum of the counter + // values + Counters map[string]SampledValue + + // Samples maps the key to an AggregateSample, + // which has the rolled up view of a sample + Samples map[string]SampledValue +} + +// NewIntervalMetrics creates a new IntervalMetrics for a given interval +func NewIntervalMetrics(intv time.Time) *IntervalMetrics { + return &IntervalMetrics{ + Interval: intv, + Gauges: make(map[string]GaugeValue), + Points: make(map[string][]float32), + Counters: make(map[string]SampledValue), + Samples: make(map[string]SampledValue), + } +} + +// AggregateSample is used to hold aggregate metrics +// about a sample +type AggregateSample struct { + Count int // The count of emitted pairs + Rate float64 // The values rate per time unit (usually 1 second) + Sum float64 // The sum of values + SumSq float64 `json:"-"` // The sum of squared values + Min float64 // Minimum value + Max float64 // Maximum value + LastUpdated time.Time `json:"-"` // When value was last updated +} + +// Computes a Stddev of the values +func (a *AggregateSample) Stddev() float64 { + num := (float64(a.Count) * a.SumSq) - math.Pow(a.Sum, 2) + div := float64(a.Count * (a.Count - 1)) + if div == 0 { + return 0 + } + return math.Sqrt(num / div) +} + +// Computes a mean of the values +func (a *AggregateSample) Mean() float64 { + if a.Count == 0 { + return 0 + } + return a.Sum / float64(a.Count) +} + +// Ingest is used to update a sample +func (a *AggregateSample) Ingest(v float64, rateDenom float64) { + a.Count++ + a.Sum += v + a.SumSq += (v * v) + if v < a.Min || a.Count == 1 { + a.Min = v + } + if v > a.Max || a.Count == 1 { + a.Max = v + } + a.Rate = float64(a.Sum) / rateDenom + a.LastUpdated = time.Now() +} + +func (a *AggregateSample) String() string { + if a.Count == 0 { + return "Count: 0" + } else if a.Stddev() == 0 { + return fmt.Sprintf("Count: %d Sum: %0.3f LastUpdated: %s", a.Count, a.Sum, a.LastUpdated) + } else { + return fmt.Sprintf("Count: %d Min: %0.3f Mean: %0.3f Max: %0.3f Stddev: %0.3f Sum: %0.3f LastUpdated: %s", + a.Count, a.Min, a.Mean(), a.Max, a.Stddev(), a.Sum, a.LastUpdated) + } +} + +// NewInmemSinkFromURL creates an InmemSink from a URL. It is used +// (and tested) from NewMetricSinkFromURL. +func NewInmemSinkFromURL(u *url.URL) (MetricSink, error) { + params := u.Query() + + interval, err := time.ParseDuration(params.Get("interval")) + if err != nil { + return nil, fmt.Errorf("Bad 'interval' param: %s", err) + } + + retain, err := time.ParseDuration(params.Get("retain")) + if err != nil { + return nil, fmt.Errorf("Bad 'retain' param: %s", err) + } + + return NewInmemSink(interval, retain), nil +} + +// NewInmemSink is used to construct a new in-memory sink. +// Uses an aggregation interval and maximum retention period. +func NewInmemSink(interval, retain time.Duration) *InmemSink { + rateTimeUnit := time.Second + i := &InmemSink{ + interval: interval, + retain: retain, + maxIntervals: int(retain / interval), + rateDenom: float64(interval.Nanoseconds()) / float64(rateTimeUnit.Nanoseconds()), + } + i.intervals = make([]*IntervalMetrics, 0, i.maxIntervals) + return i +} + +func (i *InmemSink) SetGauge(key []string, val float32) { + i.SetGaugeWithLabels(key, val, nil) +} + +func (i *InmemSink) SetGaugeWithLabels(key []string, val float32, labels []Label) { + k, name := i.flattenKeyLabels(key, labels) + intv := i.getInterval() + + intv.Lock() + defer intv.Unlock() + intv.Gauges[k] = GaugeValue{Name: name, Value: val, Labels: labels} +} + +func (i *InmemSink) EmitKey(key []string, val float32) { + k := i.flattenKey(key) + intv := i.getInterval() + + intv.Lock() + defer intv.Unlock() + vals := intv.Points[k] + intv.Points[k] = append(vals, val) +} + +func (i *InmemSink) IncrCounter(key []string, val float32) { + i.IncrCounterWithLabels(key, val, nil) +} + +func (i *InmemSink) IncrCounterWithLabels(key []string, val float32, labels []Label) { + k, name := i.flattenKeyLabels(key, labels) + intv := i.getInterval() + + intv.Lock() + defer intv.Unlock() + + agg, ok := intv.Counters[k] + if !ok { + agg = SampledValue{ + Name: name, + AggregateSample: &AggregateSample{}, + Labels: labels, + } + intv.Counters[k] = agg + } + agg.Ingest(float64(val), i.rateDenom) +} + +func (i *InmemSink) AddSample(key []string, val float32) { + i.AddSampleWithLabels(key, val, nil) +} + +func (i *InmemSink) AddSampleWithLabels(key []string, val float32, labels []Label) { + k, name := i.flattenKeyLabels(key, labels) + intv := i.getInterval() + + intv.Lock() + defer intv.Unlock() + + agg, ok := intv.Samples[k] + if !ok { + agg = SampledValue{ + Name: name, + AggregateSample: &AggregateSample{}, + Labels: labels, + } + intv.Samples[k] = agg + } + agg.Ingest(float64(val), i.rateDenom) +} + +// Data is used to retrieve all the aggregated metrics +// Intervals may be in use, and a read lock should be acquired +func (i *InmemSink) Data() []*IntervalMetrics { + // Get the current interval, forces creation + i.getInterval() + + i.intervalLock.RLock() + defer i.intervalLock.RUnlock() + + n := len(i.intervals) + intervals := make([]*IntervalMetrics, n) + + copy(intervals[:n-1], i.intervals[:n-1]) + current := i.intervals[n-1] + + // make its own copy for current interval + intervals[n-1] = &IntervalMetrics{} + copyCurrent := intervals[n-1] + current.RLock() + *copyCurrent = *current + // RWMutex is not safe to copy, so create a new instance on the copy + copyCurrent.RWMutex = sync.RWMutex{} + + copyCurrent.Gauges = make(map[string]GaugeValue, len(current.Gauges)) + for k, v := range current.Gauges { + copyCurrent.Gauges[k] = v + } + // saved values will be not change, just copy its link + copyCurrent.Points = make(map[string][]float32, len(current.Points)) + for k, v := range current.Points { + copyCurrent.Points[k] = v + } + copyCurrent.Counters = make(map[string]SampledValue, len(current.Counters)) + for k, v := range current.Counters { + copyCurrent.Counters[k] = v.deepCopy() + } + copyCurrent.Samples = make(map[string]SampledValue, len(current.Samples)) + for k, v := range current.Samples { + copyCurrent.Samples[k] = v.deepCopy() + } + current.RUnlock() + + return intervals +} + +func (i *InmemSink) getExistingInterval(intv time.Time) *IntervalMetrics { + i.intervalLock.RLock() + defer i.intervalLock.RUnlock() + + n := len(i.intervals) + if n > 0 && i.intervals[n-1].Interval == intv { + return i.intervals[n-1] + } + return nil +} + +func (i *InmemSink) createInterval(intv time.Time) *IntervalMetrics { + i.intervalLock.Lock() + defer i.intervalLock.Unlock() + + // Check for an existing interval + n := len(i.intervals) + if n > 0 && i.intervals[n-1].Interval == intv { + return i.intervals[n-1] + } + + // Add the current interval + current := NewIntervalMetrics(intv) + i.intervals = append(i.intervals, current) + n++ + + // Truncate the intervals if they are too long + if n >= i.maxIntervals { + copy(i.intervals[0:], i.intervals[n-i.maxIntervals:]) + i.intervals = i.intervals[:i.maxIntervals] + } + return current +} + +// getInterval returns the current interval to write to +func (i *InmemSink) getInterval() *IntervalMetrics { + intv := time.Now().Truncate(i.interval) + if m := i.getExistingInterval(intv); m != nil { + return m + } + return i.createInterval(intv) +} + +// Flattens the key for formatting, removes spaces +func (i *InmemSink) flattenKey(parts []string) string { + buf := &bytes.Buffer{} + + joined := strings.Join(parts, ".") + + spaceReplacer.WriteString(buf, joined) + + return buf.String() +} + +// Flattens the key for formatting along with its labels, removes spaces +func (i *InmemSink) flattenKeyLabels(parts []string, labels []Label) (string, string) { + key := i.flattenKey(parts) + buf := bytes.NewBufferString(key) + + for _, label := range labels { + spaceReplacer.WriteString(buf, fmt.Sprintf(";%s=%s", label.Name, label.Value)) + } + + return buf.String(), key +} diff --git a/pkg/metrics/inmem_endpoint.go b/pkg/metrics/inmem_endpoint.go new file mode 100644 index 0000000..5fac958 --- /dev/null +++ b/pkg/metrics/inmem_endpoint.go @@ -0,0 +1,131 @@ +package metrics + +import ( + "fmt" + "net/http" + "sort" + "time" +) + +// MetricsSummary holds a roll-up of metrics info for a given interval +type MetricsSummary struct { + Timestamp string + Gauges []GaugeValue + Points []PointValue + Counters []SampledValue + Samples []SampledValue +} + +type GaugeValue struct { + Name string + Hash string `json:"-"` + Value float32 + + Labels []Label `json:"-"` + DisplayLabels map[string]string `json:"Labels"` +} + +type PointValue struct { + Name string + Points []float32 +} + +type SampledValue struct { + Name string + Hash string `json:"-"` + *AggregateSample + Mean float64 + Stddev float64 + + Labels []Label `json:"-"` + DisplayLabels map[string]string `json:"Labels"` +} + +// deepCopy allocates a new instance of AggregateSample +func (source *SampledValue) deepCopy() SampledValue { + dest := *source + if source.AggregateSample != nil { + dest.AggregateSample = &AggregateSample{} + *dest.AggregateSample = *source.AggregateSample + } + return dest +} + +// DisplayMetrics returns a summary of the metrics from the most recent finished interval. +func (i *InmemSink) DisplayMetrics(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + data := i.Data() + + var interval *IntervalMetrics + n := len(data) + switch { + case n == 0: + return nil, fmt.Errorf("no metric intervals have been initialized yet") + case n == 1: + // Show the current interval if it's all we have + interval = data[0] + default: + // Show the most recent finished interval if we have one + interval = data[n-2] + } + + interval.RLock() + defer interval.RUnlock() + + summary := MetricsSummary{ + Timestamp: interval.Interval.Round(time.Second).UTC().String(), + Gauges: make([]GaugeValue, 0, len(interval.Gauges)), + Points: make([]PointValue, 0, len(interval.Points)), + } + + // Format and sort the output of each metric type, so it gets displayed in a + // deterministic order. + for name, points := range interval.Points { + summary.Points = append(summary.Points, PointValue{name, points}) + } + sort.Slice(summary.Points, func(i, j int) bool { + return summary.Points[i].Name < summary.Points[j].Name + }) + + for hash, value := range interval.Gauges { + value.Hash = hash + value.DisplayLabels = make(map[string]string) + for _, label := range value.Labels { + value.DisplayLabels[label.Name] = label.Value + } + value.Labels = nil + + summary.Gauges = append(summary.Gauges, value) + } + sort.Slice(summary.Gauges, func(i, j int) bool { + return summary.Gauges[i].Hash < summary.Gauges[j].Hash + }) + + summary.Counters = formatSamples(interval.Counters) + summary.Samples = formatSamples(interval.Samples) + + return summary, nil +} + +func formatSamples(source map[string]SampledValue) []SampledValue { + output := make([]SampledValue, 0, len(source)) + for hash, sample := range source { + displayLabels := make(map[string]string) + for _, label := range sample.Labels { + displayLabels[label.Name] = label.Value + } + + output = append(output, SampledValue{ + Name: sample.Name, + Hash: hash, + AggregateSample: sample.AggregateSample, + Mean: sample.AggregateSample.Mean(), + Stddev: sample.AggregateSample.Stddev(), + DisplayLabels: displayLabels, + }) + } + sort.Slice(output, func(i, j int) bool { + return output[i].Hash < output[j].Hash + }) + + return output +} diff --git a/pkg/metrics/inmem_signal.go b/pkg/metrics/inmem_signal.go new file mode 100644 index 0000000..0937f4a --- /dev/null +++ b/pkg/metrics/inmem_signal.go @@ -0,0 +1,117 @@ +package metrics + +import ( + "bytes" + "fmt" + "io" + "os" + "os/signal" + "strings" + "sync" + "syscall" +) + +// InmemSignal is used to listen for a given signal, and when received, +// to dump the current metrics from the InmemSink to an io.Writer +type InmemSignal struct { + signal syscall.Signal + inm *InmemSink + w io.Writer + sigCh chan os.Signal + + stop bool + stopCh chan struct{} + stopLock sync.Mutex +} + +// NewInmemSignal creates a new InmemSignal which listens for a given signal, +// and dumps the current metrics out to a writer +func NewInmemSignal(inmem *InmemSink, sig syscall.Signal, w io.Writer) *InmemSignal { + i := &InmemSignal{ + signal: sig, + inm: inmem, + w: w, + sigCh: make(chan os.Signal, 1), + stopCh: make(chan struct{}), + } + signal.Notify(i.sigCh, sig) + go i.run() + return i +} + +// DefaultInmemSignal returns a new InmemSignal that responds to SIGUSR1 +// and writes output to stderr. Windows uses SIGBREAK +func DefaultInmemSignal(inmem *InmemSink) *InmemSignal { + return NewInmemSignal(inmem, DefaultSignal, os.Stderr) +} + +// Stop is used to stop the InmemSignal from listening +func (i *InmemSignal) Stop() { + i.stopLock.Lock() + defer i.stopLock.Unlock() + + if i.stop { + return + } + i.stop = true + close(i.stopCh) + signal.Stop(i.sigCh) +} + +// run is a long running routine that handles signals +func (i *InmemSignal) run() { + for { + select { + case <-i.sigCh: + i.dumpStats() + case <-i.stopCh: + return + } + } +} + +// dumpStats is used to dump the data to output writer +func (i *InmemSignal) dumpStats() { + buf := bytes.NewBuffer(nil) + + data := i.inm.Data() + // Skip the last period which is still being aggregated + for j := 0; j < len(data)-1; j++ { + intv := data[j] + intv.RLock() + for _, val := range intv.Gauges { + name := i.flattenLabels(val.Name, val.Labels) + fmt.Fprintf(buf, "[%v][G] '%s': %0.3f\n", intv.Interval, name, val.Value) + } + for name, vals := range intv.Points { + for _, val := range vals { + fmt.Fprintf(buf, "[%v][P] '%s': %0.3f\n", intv.Interval, name, val) + } + } + for _, agg := range intv.Counters { + name := i.flattenLabels(agg.Name, agg.Labels) + fmt.Fprintf(buf, "[%v][C] '%s': %s\n", intv.Interval, name, agg.AggregateSample) + } + for _, agg := range intv.Samples { + name := i.flattenLabels(agg.Name, agg.Labels) + fmt.Fprintf(buf, "[%v][S] '%s': %s\n", intv.Interval, name, agg.AggregateSample) + } + intv.RUnlock() + } + + // Write out the bytes + i.w.Write(buf.Bytes()) +} + +// Flattens the key for formatting along with its labels, removes spaces +func (i *InmemSignal) flattenLabels(name string, labels []Label) string { + buf := bytes.NewBufferString(name) + replacer := strings.NewReplacer(" ", "_", ":", "_") + + for _, label := range labels { + replacer.WriteString(buf, ".") + replacer.WriteString(buf, label.Value) + } + + return buf.String() +} diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go new file mode 100644 index 0000000..92f571d --- /dev/null +++ b/pkg/metrics/metrics.go @@ -0,0 +1,293 @@ + package metrics + +import ( + "runtime" + "strings" + "time" + + "github.com/hashicorp/go-immutable-radix" +) + +type Label struct { + Name string + Value string +} + +func (m *Metrics) SetGauge(key []string, val float32) { + m.SetGaugeWithLabels(key, val, nil) +} + +func (m *Metrics) SetGaugeWithLabels(key []string, val float32, labels []Label) { + if m.HostName != "" { + if m.EnableHostnameLabel { + labels = append(labels, Label{"host", m.HostName}) + } else if m.EnableHostname { + key = insert(0, m.HostName, key) + } + } + if m.EnableTypePrefix { + key = insert(0, "gauge", key) + } + if m.ServiceName != "" { + if m.EnableServiceLabel { + labels = append(labels, Label{"service", m.ServiceName}) + } else { + key = insert(0, m.ServiceName, key) + } + } + allowed, labelsFiltered := m.allowMetric(key, labels) + if !allowed { + return + } + m.sink.SetGaugeWithLabels(key, val, labelsFiltered) +} + +func (m *Metrics) EmitKey(key []string, val float32) { + if m.EnableTypePrefix { + key = insert(0, "kv", key) + } + if m.ServiceName != "" { + key = insert(0, m.ServiceName, key) + } + allowed, _ := m.allowMetric(key, nil) + if !allowed { + return + } + m.sink.EmitKey(key, val) +} + +func (m *Metrics) IncrCounter(key []string, val float32) { + m.IncrCounterWithLabels(key, val, nil) +} + +func (m *Metrics) IncrCounterWithLabels(key []string, val float32, labels []Label) { + if m.HostName != "" && m.EnableHostnameLabel { + labels = append(labels, Label{"host", m.HostName}) + } + if m.EnableTypePrefix { + key = insert(0, "counter", key) + } + if m.ServiceName != "" { + if m.EnableServiceLabel { + labels = append(labels, Label{"service", m.ServiceName}) + } else { + key = insert(0, m.ServiceName, key) + } + } + allowed, labelsFiltered := m.allowMetric(key, labels) + if !allowed { + return + } + m.sink.IncrCounterWithLabels(key, val, labelsFiltered) +} + +func (m *Metrics) AddSample(key []string, val float32) { + m.AddSampleWithLabels(key, val, nil) +} + +func (m *Metrics) AddSampleWithLabels(key []string, val float32, labels []Label) { + if m.HostName != "" && m.EnableHostnameLabel { + labels = append(labels, Label{"host", m.HostName}) + } + if m.EnableTypePrefix { + key = insert(0, "sample", key) + } + if m.ServiceName != "" { + if m.EnableServiceLabel { + labels = append(labels, Label{"service", m.ServiceName}) + } else { + key = insert(0, m.ServiceName, key) + } + } + allowed, labelsFiltered := m.allowMetric(key, labels) + if !allowed { + return + } + m.sink.AddSampleWithLabels(key, val, labelsFiltered) +} + +func (m *Metrics) MeasureSince(key []string, start time.Time) { + m.MeasureSinceWithLabels(key, start, nil) +} + +func (m *Metrics) MeasureSinceWithLabels(key []string, start time.Time, labels []Label) { + if m.HostName != "" && m.EnableHostnameLabel { + labels = append(labels, Label{"host", m.HostName}) + } + if m.EnableTypePrefix { + key = insert(0, "timer", key) + } + if m.ServiceName != "" { + if m.EnableServiceLabel { + labels = append(labels, Label{"service", m.ServiceName}) + } else { + key = insert(0, m.ServiceName, key) + } + } + allowed, labelsFiltered := m.allowMetric(key, labels) + if !allowed { + return + } + now := time.Now() + elapsed := now.Sub(start) + msec := float32(elapsed.Nanoseconds()) / float32(m.TimerGranularity) + m.sink.AddSampleWithLabels(key, msec, labelsFiltered) +} + +// UpdateFilter overwrites the existing filter with the given rules. +func (m *Metrics) UpdateFilter(allow, block []string) { + m.UpdateFilterAndLabels(allow, block, m.AllowedLabels, m.BlockedLabels) +} + +// UpdateFilterAndLabels overwrites the existing filter with the given rules. +func (m *Metrics) UpdateFilterAndLabels(allow, block, allowedLabels, blockedLabels []string) { + m.filterLock.Lock() + defer m.filterLock.Unlock() + + m.AllowedPrefixes = allow + m.BlockedPrefixes = block + + if allowedLabels == nil { + // Having a white list means we take only elements from it + m.allowedLabels = nil + } else { + m.allowedLabels = make(map[string]bool) + for _, v := range allowedLabels { + m.allowedLabels[v] = true + } + } + m.blockedLabels = make(map[string]bool) + for _, v := range blockedLabels { + m.blockedLabels[v] = true + } + m.AllowedLabels = allowedLabels + m.BlockedLabels = blockedLabels + + m.filter = iradix.New() + for _, prefix := range m.AllowedPrefixes { + m.filter, _, _ = m.filter.Insert([]byte(prefix), true) + } + for _, prefix := range m.BlockedPrefixes { + m.filter, _, _ = m.filter.Insert([]byte(prefix), false) + } +} + +// labelIsAllowed return true if a should be included in metric +// the caller should lock m.filterLock while calling this method +func (m *Metrics) labelIsAllowed(label *Label) bool { + labelName := (*label).Name + if m.blockedLabels != nil { + _, ok := m.blockedLabels[labelName] + if ok { + // If present, let's remove this label + return false + } + } + if m.allowedLabels != nil { + _, ok := m.allowedLabels[labelName] + return ok + } + // Allow by default + return true +} + +// filterLabels return only allowed labels +// the caller should lock m.filterLock while calling this method +func (m *Metrics) filterLabels(labels []Label) []Label { + if labels == nil { + return nil + } + toReturn := []Label{} + for _, label := range labels { + if m.labelIsAllowed(&label) { + toReturn = append(toReturn, label) + } + } + return toReturn +} + +// Returns whether the metric should be allowed based on configured prefix filters +// Also return the applicable labels +func (m *Metrics) allowMetric(key []string, labels []Label) (bool, []Label) { + m.filterLock.RLock() + defer m.filterLock.RUnlock() + + if m.filter == nil || m.filter.Len() == 0 { + return m.Config.FilterDefault, m.filterLabels(labels) + } + + _, allowed, ok := m.filter.Root().LongestPrefix([]byte(strings.Join(key, "."))) + if !ok { + return m.Config.FilterDefault, m.filterLabels(labels) + } + + return allowed.(bool), m.filterLabels(labels) +} + +// Periodically collects runtime stats to publish +func (m *Metrics) collectStats() { + for { + time.Sleep(m.ProfileInterval) + m.EmitRuntimeStats() + } +} + +// Emits various runtime statsitics +func (m *Metrics) EmitRuntimeStats() { + // Export number of Goroutines + numRoutines := runtime.NumGoroutine() + m.SetGauge([]string{"runtime", "num_goroutines"}, float32(numRoutines)) + + // Export memory stats + var stats runtime.MemStats + runtime.ReadMemStats(&stats) + m.SetGauge([]string{"runtime", "alloc_bytes"}, float32(stats.Alloc)) + m.SetGauge([]string{"runtime", "sys_bytes"}, float32(stats.Sys)) + m.SetGauge([]string{"runtime", "malloc_count"}, float32(stats.Mallocs)) + m.SetGauge([]string{"runtime", "free_count"}, float32(stats.Frees)) + m.SetGauge([]string{"runtime", "heap_objects"}, float32(stats.HeapObjects)) + m.SetGauge([]string{"runtime", "total_gc_pause_ns"}, float32(stats.PauseTotalNs)) + m.SetGauge([]string{"runtime", "total_gc_runs"}, float32(stats.NumGC)) + + // Export info about the last few GC runs + num := stats.NumGC + + // Handle wrap around + if num < m.lastNumGC { + m.lastNumGC = 0 + } + + // Ensure we don't scan more than 256 + if num-m.lastNumGC >= 256 { + m.lastNumGC = num - 255 + } + + for i := m.lastNumGC; i < num; i++ { + pause := stats.PauseNs[i%256] + m.AddSample([]string{"runtime", "gc_pause_ns"}, float32(pause)) + } + m.lastNumGC = num +} + +// Creates a new slice with the provided string value as the first element +// and the provided slice values as the remaining values. +// Ordering of the values in the provided input slice is kept in tact in the output slice. +func insert(i int, v string, s []string) []string { + // Allocate new slice to avoid modifying the input slice + newS := make([]string, len(s)+1) + + // Copy s[0, i-1] into newS + for j := 0; j < i; j++ { + newS[j] = s[j] + } + + // Insert provided element at index i + newS[i] = v + + // Copy s[i, len(s)-1] into newS starting at newS[i+1] + for j := i; j < len(s); j++ { + newS[j+1] = s[j] + } + + return newS +} diff --git a/pkg/metrics/sink.go b/pkg/metrics/sink.go new file mode 100644 index 0000000..8202cf9 --- /dev/null +++ b/pkg/metrics/sink.go @@ -0,0 +1,108 @@ +package metrics + +import ( + "fmt" + "net/url" +) + +// The MetricSink interface is used to transmit metrics information +// to an external system +type MetricSink interface { + SetGauge(key []string, val float32) + SetGaugeWithLabels(key []string, val float32, labels []Label) + EmitKey(key []string, val float32) + IncrCounter(key []string, val float32) + IncrCounterWithLabels(key []string, val float32, labels []Label) + AddSample(key []string, val float32) + AddSampleWithLabels(key []string, val float32, labels []Label) +} + +// EmptySink is used to just blackhole messages +type EmptySink struct{} + +func (*EmptySink) SetGauge(key []string, val float32) {} +func (*EmptySink) SetGaugeWithLabels(key []string, val float32, labels []Label) {} +func (*EmptySink) EmitKey(key []string, val float32) {} +func (*EmptySink) IncrCounter(key []string, val float32) {} +func (*EmptySink) IncrCounterWithLabels(key []string, val float32, labels []Label) {} +func (*EmptySink) AddSample(key []string, val float32) {} +func (*EmptySink) AddSampleWithLabels(key []string, val float32, labels []Label) {} + +// FanoutSink is used to sink to fanout values to multiple sinks +type FanoutSink []MetricSink + +func (fh FanoutSink) SetGauge(key []string, val float32) { + fh.SetGaugeWithLabels(key, val, nil) +} + +func (fh FanoutSink) SetGaugeWithLabels(key []string, val float32, labels []Label) { + for _, s := range fh { + s.SetGaugeWithLabels(key, val, labels) + } +} + +func (fh FanoutSink) EmitKey(key []string, val float32) { + for _, s := range fh { + s.EmitKey(key, val) + } +} + +func (fh FanoutSink) IncrCounter(key []string, val float32) { + fh.IncrCounterWithLabels(key, val, nil) +} + +func (fh FanoutSink) IncrCounterWithLabels(key []string, val float32, labels []Label) { + for _, s := range fh { + s.IncrCounterWithLabels(key, val, labels) + } +} + +func (fh FanoutSink) AddSample(key []string, val float32) { + fh.AddSampleWithLabels(key, val, nil) +} + +func (fh FanoutSink) AddSampleWithLabels(key []string, val float32, labels []Label) { + for _, s := range fh { + s.AddSampleWithLabels(key, val, labels) + } +} + +// sinkURLFactoryFunc is an generic interface around the *SinkFromURL() function provided +// by each sink type +type sinkURLFactoryFunc func(*url.URL) (MetricSink, error) + +// sinkRegistry supports the generic NewMetricSink function by mapping URL +// schemes to metric sink factory functions +var sinkRegistry = map[string]sinkURLFactoryFunc{ + "statsd": NewStatsdSinkFromURL, + "statsite": NewStatsiteSinkFromURL, + "inmem": NewInmemSinkFromURL, +} + +// NewMetricSinkFromURL allows a generic URL input to configure any of the +// supported sinks. The scheme of the URL identifies the type of the sink, the +// and query parameters are used to set options. +// +// "statsd://" - Initializes a StatsdSink. The host and port are passed through +// as the "addr" of the sink +// +// "statsite://" - Initializes a StatsiteSink. The host and port become the +// "addr" of the sink +// +// "inmem://" - Initializes an InmemSink. The host and port are ignored. The +// "interval" and "duration" query parameters must be specified with valid +// durations, see NewInmemSink for details. +func NewMetricSinkFromURL(urlStr string) (MetricSink, error) { + u, err := url.Parse(urlStr) + if err != nil { + return nil, err + } + + sinkURLFactoryFunc := sinkRegistry[u.Scheme] + if sinkURLFactoryFunc == nil { + return nil, fmt.Errorf( + "cannot create metric sink, unrecognized sink name: %q", u.Scheme) + } + + return sinkURLFactoryFunc(u) +} diff --git a/pkg/metrics/sinks/.DS_Store b/pkg/metrics/sinks/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..f1922d645ce5f74a985eb325345000afc136a472 GIT binary patch literal 6148 zcmeHK%}xR_5N-jrV2m70Z#qA1`t7Daj4|FEcdLvQ8DjzzF_VYp8^Jj0l4Pt0k?S!O3)z@O zei99rvdQru8NhER*ewg#2-+{-&mSlsV(7x#T@pDQ((+n#?B^pbXQ=U9cY7lgf$E)KdqNV&cY!mdi1Y8-YWJ&tPv zLgYohT`Wx|je~kwRvWcxSxy?YN?F$T52w?-*xjohop$aYpC-@Kmsi6shaXYPw#6B| zfw8pE2X_<>RCoumS=}t3kQg8ah=CPmz;1bBbA{JPOCtt|f!{HJ`-22UbS$O@_0|Cm zULP@@Lqq`^-x7$@qGK^N2oVr&N&!tNw@(ai%E2#fo?|gJXv!JaGs8G`W^P|7T+a@E zsnZ#E3{p!B5Ce-0WKFk-=l{vi@BhUj>JbCPz)CT|b1k>kgeBRtb!l;U)=JP0C<^AK m2InPU=%W~N@hC2VY5~7Q1JJRU8UzmrT?8}@)DQ!I%D^YBHdBiL literal 0 HcmV?d00001 diff --git a/pkg/metrics/sinks/circonus/circonus.go b/pkg/metrics/sinks/circonus/circonus.go new file mode 100644 index 0000000..87b3ebc --- /dev/null +++ b/pkg/metrics/sinks/circonus/circonus.go @@ -0,0 +1,119 @@ +// Circonus Metrics Sink + +package circonus + +import ( + "strings" + + "github.com/armon/go-metrics" + cgm "github.com/circonus-labs/circonus-gometrics/v3" +) + +// CirconusSink provides an interface to forward metrics to Circonus with +// automatic check creation and metric management +type CirconusSink struct { + metrics *cgm.CirconusMetrics +} + +// Config options for CirconusSink +// See https://github.com/circonus-labs/circonus-gometrics for configuration options +type Config cgm.Config + +// NewCirconusSink - create new metric sink for circonus +// +// one of the following must be supplied: +// - API Token - search for an existing check or create a new check +// - API Token + Check Id - the check identified by check id will be used +// - API Token + Check Submission URL - the check identified by the submission url will be used +// - Check Submission URL - the check identified by the submission url will be used +// metric management will be *disabled* +// +// Note: If submission url is supplied w/o an api token, the public circonus ca cert will be used +// to verify the broker for metrics submission. +func NewCirconusSink(cc *Config) (*CirconusSink, error) { + cfg := cgm.Config{} + if cc != nil { + cfg = cgm.Config(*cc) + } + + metrics, err := cgm.NewCirconusMetrics(&cfg) + if err != nil { + return nil, err + } + + return &CirconusSink{ + metrics: metrics, + }, nil +} + +// Start submitting metrics to Circonus (flush every SubmitInterval) +func (s *CirconusSink) Start() { + s.metrics.Start() +} + +// Flush manually triggers metric submission to Circonus +func (s *CirconusSink) Flush() { + s.metrics.Flush() +} + +// SetGauge sets value for a gauge metric +func (s *CirconusSink) SetGauge(key []string, val float32) { + flatKey := s.flattenKey(key) + s.metrics.SetGauge(flatKey, int64(val)) +} + +// SetGaugeWithLabels sets value for a gauge metric with the given labels +func (s *CirconusSink) SetGaugeWithLabels(key []string, val float32, labels []metrics.Label) { + flatKey := s.flattenKeyLabels(key, labels) + s.metrics.SetGauge(flatKey, int64(val)) +} + +// EmitKey is not implemented in circonus +func (s *CirconusSink) EmitKey(key []string, val float32) { + // NOP +} + +// IncrCounter increments a counter metric +func (s *CirconusSink) IncrCounter(key []string, val float32) { + flatKey := s.flattenKey(key) + s.metrics.IncrementByValue(flatKey, uint64(val)) +} + +// IncrCounterWithLabels increments a counter metric with the given labels +func (s *CirconusSink) IncrCounterWithLabels(key []string, val float32, labels []metrics.Label) { + flatKey := s.flattenKeyLabels(key, labels) + s.metrics.IncrementByValue(flatKey, uint64(val)) +} + +// AddSample adds a sample to a histogram metric +func (s *CirconusSink) AddSample(key []string, val float32) { + flatKey := s.flattenKey(key) + s.metrics.RecordValue(flatKey, float64(val)) +} + +// AddSampleWithLabels adds a sample to a histogram metric with the given labels +func (s *CirconusSink) AddSampleWithLabels(key []string, val float32, labels []metrics.Label) { + flatKey := s.flattenKeyLabels(key, labels) + s.metrics.RecordValue(flatKey, float64(val)) +} + +// Flattens key to Circonus metric name +func (s *CirconusSink) flattenKey(parts []string) string { + joined := strings.Join(parts, "`") + return strings.Map(func(r rune) rune { + switch r { + case ' ': + return '_' + default: + return r + } + }, joined) +} + +// Flattens the key along with labels for formatting, removes spaces +func (s *CirconusSink) flattenKeyLabels(parts []string, labels []metrics.Label) string { + for _, label := range labels { + parts = append(parts, label.Value) + } + return s.flattenKey(parts) +} diff --git a/pkg/metrics/sinks/datadog/dogstatsd.go b/pkg/metrics/sinks/datadog/dogstatsd.go new file mode 100644 index 0000000..fe021d0 --- /dev/null +++ b/pkg/metrics/sinks/datadog/dogstatsd.go @@ -0,0 +1,140 @@ +package datadog + +import ( + "fmt" + "strings" + + "github.com/DataDog/datadog-go/statsd" + "github.com/armon/go-metrics" +) + +// DogStatsdSink provides a MetricSink that can be used +// with a dogstatsd server. It utilizes the Dogstatsd client at github.com/DataDog/datadog-go/statsd +type DogStatsdSink struct { + client *statsd.Client + hostName string + propagateHostname bool +} + +// NewDogStatsdSink is used to create a new DogStatsdSink with sane defaults +func NewDogStatsdSink(addr string, hostName string) (*DogStatsdSink, error) { + client, err := statsd.New(addr) + if err != nil { + return nil, err + } + sink := &DogStatsdSink{ + client: client, + hostName: hostName, + propagateHostname: false, + } + return sink, nil +} + +// SetTags sets common tags on the Dogstatsd Client that will be sent +// along with all dogstatsd packets. +// Ref: http://docs.datadoghq.com/guides/dogstatsd/#tags +func (s *DogStatsdSink) SetTags(tags []string) { + s.client.Tags = tags +} + +// EnableHostnamePropagation forces a Dogstatsd `host` tag with the value specified by `s.HostName` +// Since the go-metrics package has its own mechanism for attaching a hostname to metrics, +// setting the `propagateHostname` flag ensures that `s.HostName` overrides the host tag naively set by the DogStatsd server +func (s *DogStatsdSink) EnableHostNamePropagation() { + s.propagateHostname = true +} + +func (s *DogStatsdSink) flattenKey(parts []string) string { + joined := strings.Join(parts, ".") + return strings.Map(sanitize, joined) +} + +func sanitize(r rune) rune { + switch r { + case ':': + fallthrough + case ' ': + return '_' + default: + return r + } +} + +func (s *DogStatsdSink) parseKey(key []string) ([]string, []metrics.Label) { + // Since DogStatsd supports dimensionality via tags on metric keys, this sink's approach is to splice the hostname out of the key in favor of a `host` tag + // The `host` tag is either forced here, or set downstream by the DogStatsd server + + var labels []metrics.Label + hostName := s.hostName + + // Splice the hostname out of the key + for i, el := range key { + if el == hostName { + key = append(key[:i], key[i+1:]...) + break + } + } + + if s.propagateHostname { + labels = append(labels, metrics.Label{"host", hostName}) + } + return key, labels +} + +// Implementation of methods in the MetricSink interface + +func (s *DogStatsdSink) SetGauge(key []string, val float32) { + s.SetGaugeWithLabels(key, val, nil) +} + +func (s *DogStatsdSink) IncrCounter(key []string, val float32) { + s.IncrCounterWithLabels(key, val, nil) +} + +// EmitKey is not implemented since DogStatsd does not provide a metric type that holds an +// arbitrary number of values +func (s *DogStatsdSink) EmitKey(key []string, val float32) { +} + +func (s *DogStatsdSink) AddSample(key []string, val float32) { + s.AddSampleWithLabels(key, val, nil) +} + +// The following ...WithLabels methods correspond to Datadog's Tag extension to Statsd. +// http://docs.datadoghq.com/guides/dogstatsd/#tags +func (s *DogStatsdSink) SetGaugeWithLabels(key []string, val float32, labels []metrics.Label) { + flatKey, tags := s.getFlatkeyAndCombinedLabels(key, labels) + rate := 1.0 + s.client.Gauge(flatKey, float64(val), tags, rate) +} + +func (s *DogStatsdSink) IncrCounterWithLabels(key []string, val float32, labels []metrics.Label) { + flatKey, tags := s.getFlatkeyAndCombinedLabels(key, labels) + rate := 1.0 + s.client.Count(flatKey, int64(val), tags, rate) +} + +func (s *DogStatsdSink) AddSampleWithLabels(key []string, val float32, labels []metrics.Label) { + flatKey, tags := s.getFlatkeyAndCombinedLabels(key, labels) + rate := 1.0 + s.client.TimeInMilliseconds(flatKey, float64(val), tags, rate) +} + +func (s *DogStatsdSink) getFlatkeyAndCombinedLabels(key []string, labels []metrics.Label) (string, []string) { + key, parsedLabels := s.parseKey(key) + flatKey := s.flattenKey(key) + labels = append(labels, parsedLabels...) + + var tags []string + for _, label := range labels { + label.Name = strings.Map(sanitize, label.Name) + label.Value = strings.Map(sanitize, label.Value) + if label.Value != "" { + tags = append(tags, fmt.Sprintf("%s:%s", label.Name, label.Value)) + } else { + tags = append(tags, label.Name) + } + } + + return flatKey, tags +} diff --git a/pkg/metrics/sinks/prometheus/prometheus.go b/pkg/metrics/sinks/prometheus/prometheus.go new file mode 100644 index 0000000..34d1f00 --- /dev/null +++ b/pkg/metrics/sinks/prometheus/prometheus.go @@ -0,0 +1,437 @@ + +package prometheus + +import ( + "fmt" + "log" + "regexp" + "strings" + "sync" + "time" + + "github.com/armon/go-metrics" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/push" +) + +var ( + // DefaultPrometheusOpts is the default set of options used when creating a + // PrometheusSink. + DefaultPrometheusOpts = PrometheusOpts{ + Expiration: 60 * time.Second, + } +) + +// PrometheusOpts is used to configure the Prometheus Sink +type PrometheusOpts struct { + // Expiration is the duration a metric is valid for, after which it will be + // untracked. If the value is zero, a metric is never expired. + Expiration time.Duration + Registerer prometheus.Registerer + + // Gauges, Summaries, and Counters allow us to pre-declare metrics by giving + // their Name, Help, and ConstLabels to the PrometheusSink when it is created. + // Metrics declared in this way will be initialized at zero and will not be + // deleted or altered when their expiry is reached. + // + // Ex: PrometheusOpts{ + // Expiration: 10 * time.Second, + // Gauges: []GaugeDefinition{ + // { + // Name: []string{ "application", "component", "measurement"}, + // Help: "application_component_measurement provides an example of how to declare static metrics", + // ConstLabels: []metrics.Label{ { Name: "my_label", Value: "does_not_change" }, }, + // }, + // }, + // } + GaugeDefinitions []GaugeDefinition + SummaryDefinitions []SummaryDefinition + CounterDefinitions []CounterDefinition +} + +type PrometheusSink struct { + // If these will ever be copied, they should be converted to *sync.Map values and initialized appropriately + gauges sync.Map + summaries sync.Map + counters sync.Map + expiration time.Duration + help map[string]string +} + +// GaugeDefinition can be provided to PrometheusOpts to declare a constant gauge that is not deleted on expiry. +type GaugeDefinition struct { + Name []string + ConstLabels []metrics.Label + Help string +} + +type gauge struct { + prometheus.Gauge + updatedAt time.Time + // canDelete is set if the metric is created during runtime so we know it's ephemeral and can delete it on expiry. + canDelete bool +} + +// SummaryDefinition can be provided to PrometheusOpts to declare a constant summary that is not deleted on expiry. +type SummaryDefinition struct { + Name []string + ConstLabels []metrics.Label + Help string +} + +type summary struct { + prometheus.Summary + updatedAt time.Time + canDelete bool +} + +// CounterDefinition can be provided to PrometheusOpts to declare a constant counter that is not deleted on expiry. +type CounterDefinition struct { + Name []string + ConstLabels []metrics.Label + Help string +} + +type counter struct { + prometheus.Counter + updatedAt time.Time + canDelete bool +} + +// NewPrometheusSink creates a new PrometheusSink using the default options. +func NewPrometheusSink() (*PrometheusSink, error) { + return NewPrometheusSinkFrom(DefaultPrometheusOpts) +} + +// NewPrometheusSinkFrom creates a new PrometheusSink using the passed options. +func NewPrometheusSinkFrom(opts PrometheusOpts) (*PrometheusSink, error) { + sink := &PrometheusSink{ + gauges: sync.Map{}, + summaries: sync.Map{}, + counters: sync.Map{}, + expiration: opts.Expiration, + help: make(map[string]string), + } + + initGauges(&sink.gauges, opts.GaugeDefinitions, sink.help) + initSummaries(&sink.summaries, opts.SummaryDefinitions, sink.help) + initCounters(&sink.counters, opts.CounterDefinitions, sink.help) + + reg := opts.Registerer + if reg == nil { + reg = prometheus.DefaultRegisterer + } + + return sink, reg.Register(sink) +} + +// Describe is needed to meet the Collector interface. +func (p *PrometheusSink) Describe(c chan<- *prometheus.Desc) { + // We must emit some description otherwise an error is returned. This + // description isn't shown to the user! + prometheus.NewGauge(prometheus.GaugeOpts{Name: "Dummy", Help: "Dummy"}).Describe(c) +} + +// Collect meets the collection interface and allows us to enforce our expiration +// logic to clean up ephemeral metrics if their value haven't been set for a +// duration exceeding our allowed expiration time. +func (p *PrometheusSink) Collect(c chan<- prometheus.Metric) { + p.collectAtTime(c, time.Now()) +} + +// collectAtTime allows internal testing of the expiry based logic here without +// mocking clocks or making tests timing sensitive. +func (p *PrometheusSink) collectAtTime(c chan<- prometheus.Metric, t time.Time) { + expire := p.expiration != 0 + p.gauges.Range(func(k, v interface{}) bool { + if v == nil { + return true + } + g := v.(*gauge) + lastUpdate := g.updatedAt + if expire && lastUpdate.Add(p.expiration).Before(t) { + if g.canDelete { + p.gauges.Delete(k) + return true + } + } + g.Collect(c) + return true + }) + p.summaries.Range(func(k, v interface{}) bool { + if v == nil { + return true + } + s := v.(*summary) + lastUpdate := s.updatedAt + if expire && lastUpdate.Add(p.expiration).Before(t) { + if s.canDelete { + p.summaries.Delete(k) + return true + } + } + s.Collect(c) + return true + }) + p.counters.Range(func(k, v interface{}) bool { + if v == nil { + return true + } + count := v.(*counter) + lastUpdate := count.updatedAt + if expire && lastUpdate.Add(p.expiration).Before(t) { + if count.canDelete { + p.counters.Delete(k) + return true + } + } + count.Collect(c) + return true + }) +} + +func initGauges(m *sync.Map, gauges []GaugeDefinition, help map[string]string) { + for _, g := range gauges { + key, hash := flattenKey(g.Name, g.ConstLabels) + help[fmt.Sprintf("gauge.%s", key)] = g.Help + pG := prometheus.NewGauge(prometheus.GaugeOpts{ + Name: key, + Help: g.Help, + ConstLabels: prometheusLabels(g.ConstLabels), + }) + m.Store(hash, &gauge{Gauge: pG}) + } + return +} + +func initSummaries(m *sync.Map, summaries []SummaryDefinition, help map[string]string) { + for _, s := range summaries { + key, hash := flattenKey(s.Name, s.ConstLabels) + help[fmt.Sprintf("summary.%s", key)] = s.Help + pS := prometheus.NewSummary(prometheus.SummaryOpts{ + Name: key, + Help: s.Help, + MaxAge: 10 * time.Second, + ConstLabels: prometheusLabels(s.ConstLabels), + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, + }) + m.Store(hash, &summary{Summary: pS}) + } + return +} + +func initCounters(m *sync.Map, counters []CounterDefinition, help map[string]string) { + for _, c := range counters { + key, hash := flattenKey(c.Name, c.ConstLabels) + help[fmt.Sprintf("counter.%s", key)] = c.Help + pC := prometheus.NewCounter(prometheus.CounterOpts{ + Name: key, + Help: c.Help, + ConstLabels: prometheusLabels(c.ConstLabels), + }) + m.Store(hash, &counter{Counter: pC}) + } + return +} + +var forbiddenChars = regexp.MustCompile("[ .=\\-/]") + +func flattenKey(parts []string, labels []metrics.Label) (string, string) { + key := strings.Join(parts, "_") + key = forbiddenChars.ReplaceAllString(key, "_") + + hash := key + for _, label := range labels { + hash += fmt.Sprintf(";%s=%s", label.Name, label.Value) + } + + return key, hash +} + +func prometheusLabels(labels []metrics.Label) prometheus.Labels { + l := make(prometheus.Labels) + for _, label := range labels { + l[label.Name] = label.Value + } + return l +} + +func (p *PrometheusSink) SetGauge(parts []string, val float32) { + p.SetGaugeWithLabels(parts, val, nil) +} + +func (p *PrometheusSink) SetGaugeWithLabels(parts []string, val float32, labels []metrics.Label) { + key, hash := flattenKey(parts, labels) + pg, ok := p.gauges.Load(hash) + + // The sync.Map underlying gauges stores pointers to our structs. If we need to make updates, + // rather than modifying the underlying value directly, which would be racy, we make a local + // copy by dereferencing the pointer we get back, making the appropriate changes, and then + // storing a pointer to our local copy. The underlying Prometheus types are threadsafe, + // so there's no issues there. It's possible for racy updates to occur to the updatedAt + // value, but since we're always setting it to time.Now(), it doesn't really matter. + if ok { + localGauge := *pg.(*gauge) + localGauge.Set(float64(val)) + localGauge.updatedAt = time.Now() + p.gauges.Store(hash, &localGauge) + + // The gauge does not exist, create the gauge and allow it to be deleted + } else { + help := key + existingHelp, ok := p.help[fmt.Sprintf("gauge.%s", key)] + if ok { + help = existingHelp + } + g := prometheus.NewGauge(prometheus.GaugeOpts{ + Name: key, + Help: help, + ConstLabels: prometheusLabels(labels), + }) + g.Set(float64(val)) + pg = &gauge{ + Gauge: g, + updatedAt: time.Now(), + canDelete: true, + } + p.gauges.Store(hash, pg) + } +} + +func (p *PrometheusSink) AddSample(parts []string, val float32) { + p.AddSampleWithLabels(parts, val, nil) +} + +func (p *PrometheusSink) AddSampleWithLabels(parts []string, val float32, labels []metrics.Label) { + key, hash := flattenKey(parts, labels) + ps, ok := p.summaries.Load(hash) + + // Does the summary already exist for this sample type? + if ok { + localSummary := *ps.(*summary) + localSummary.Observe(float64(val)) + localSummary.updatedAt = time.Now() + p.summaries.Store(hash, &localSummary) + + // The summary does not exist, create the Summary and allow it to be deleted + } else { + help := key + existingHelp, ok := p.help[fmt.Sprintf("summary.%s", key)] + if ok { + help = existingHelp + } + s := prometheus.NewSummary(prometheus.SummaryOpts{ + Name: key, + Help: help, + MaxAge: 10 * time.Second, + ConstLabels: prometheusLabels(labels), + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, + }) + s.Observe(float64(val)) + ps = &summary{ + Summary: s, + updatedAt: time.Now(), + canDelete: true, + } + p.summaries.Store(hash, ps) + } +} + +// EmitKey is not implemented. Prometheus doesn’t offer a type for which an +// arbitrary number of values is retained, as Prometheus works with a pull +// model, rather than a push model. +func (p *PrometheusSink) EmitKey(key []string, val float32) { +} + +func (p *PrometheusSink) IncrCounter(parts []string, val float32) { + p.IncrCounterWithLabels(parts, val, nil) +} + +func (p *PrometheusSink) IncrCounterWithLabels(parts []string, val float32, labels []metrics.Label) { + key, hash := flattenKey(parts, labels) + pc, ok := p.counters.Load(hash) + + // Does the counter exist? + if ok { + localCounter := *pc.(*counter) + localCounter.Add(float64(val)) + localCounter.updatedAt = time.Now() + p.counters.Store(hash, &localCounter) + + // The counter does not exist yet, create it and allow it to be deleted + } else { + help := key + existingHelp, ok := p.help[fmt.Sprintf("counter.%s", key)] + if ok { + help = existingHelp + } + c := prometheus.NewCounter(prometheus.CounterOpts{ + Name: key, + Help: help, + ConstLabels: prometheusLabels(labels), + }) + c.Add(float64(val)) + pc = &counter{ + Counter: c, + updatedAt: time.Now(), + canDelete: true, + } + p.counters.Store(hash, pc) + } +} + +// PrometheusPushSink wraps a normal prometheus sink and provides an address and facilities to export it to an address +// on an interval. +type PrometheusPushSink struct { + *PrometheusSink + pusher *push.Pusher + address string + pushInterval time.Duration + stopChan chan struct{} +} + +// NewPrometheusPushSink creates a PrometheusPushSink by taking an address, interval, and destination name. +func NewPrometheusPushSink(address string, pushInterval time.Duration, name string) (*PrometheusPushSink, error) { + promSink := &PrometheusSink{ + gauges: sync.Map{}, + summaries: sync.Map{}, + counters: sync.Map{}, + expiration: 60 * time.Second, + } + + pusher := push.New(address, name).Collector(promSink) + + sink := &PrometheusPushSink{ + promSink, + pusher, + address, + pushInterval, + make(chan struct{}), + } + + sink.flushMetrics() + return sink, nil +} + +func (s *PrometheusPushSink) flushMetrics() { + ticker := time.NewTicker(s.pushInterval) + + go func() { + for { + select { + case <-ticker.C: + err := s.pusher.Push() + if err != nil { + log.Printf("[ERR] Error pushing to Prometheus! Err: %s", err) + } + case <-s.stopChan: + ticker.Stop() + return + } + } + }() +} + +func (s *PrometheusPushSink) Shutdown() { + close(s.stopChan) +} diff --git a/pkg/metrics/start.go b/pkg/metrics/start.go new file mode 100644 index 0000000..635aac7 --- /dev/null +++ b/pkg/metrics/start.go @@ -0,0 +1,146 @@ +package metrics + +import ( + "os" + "sync" + "sync/atomic" + "time" + + iradix "github.com/hashicorp/go-immutable-radix" +) + +// Config is used to configure metrics settings +type Config struct { + ServiceName string // Prefixed with keys to separate services + HostName string // Hostname to use. If not provided and EnableHostname, it will be os.Hostname + EnableHostname bool // Enable prefixing gauge values with hostname + EnableHostnameLabel bool // Enable adding hostname to labels + EnableServiceLabel bool // Enable adding service to labels + EnableRuntimeMetrics bool // Enables profiling of runtime metrics (GC, Goroutines, Memory) + EnableTypePrefix bool // Prefixes key with a type ("counter", "gauge", "timer") + TimerGranularity time.Duration // Granularity of timers. + ProfileInterval time.Duration // Interval to profile runtime metrics + + AllowedPrefixes []string // A list of metric prefixes to allow, with '.' as the separator + BlockedPrefixes []string // A list of metric prefixes to block, with '.' as the separator + AllowedLabels []string // A list of metric labels to allow, with '.' as the separator + BlockedLabels []string // A list of metric labels to block, with '.' as the separator + FilterDefault bool // Whether to allow metrics by default +} + +// Metrics represents an instance of a metrics sink that can +// be used to emit +type Metrics struct { + Config + lastNumGC uint32 + sink MetricSink + filter *iradix.Tree + allowedLabels map[string]bool + blockedLabels map[string]bool + filterLock sync.RWMutex // Lock filters and allowedLabels/blockedLabels access +} + +// Shared global metrics instance +var globalMetrics atomic.Value // *Metrics + +func init() { + // Initialize to a blackhole sink to avoid errors + globalMetrics.Store(&Metrics{sink: &EmptySink{}}) +} + +// Default returns the shared global metrics instance. +func Default() *Metrics { + return globalMetrics.Load().(*Metrics) +} + +// DefaultConfig provides a sane default configuration +func DefaultConfig(serviceName string) *Config { + c := &Config{ + ServiceName: serviceName, // Use client provided service + HostName: "", + EnableHostname: true, // Enable hostname prefix + EnableRuntimeMetrics: true, // Enable runtime profiling + EnableTypePrefix: false, // Disable type prefix + TimerGranularity: time.Millisecond, // Timers are in milliseconds + ProfileInterval: time.Second, // Poll runtime every second + FilterDefault: true, // Don't filter metrics by default + } + + // Try to get the hostname + name, _ := os.Hostname() + c.HostName = name + return c +} + +// New is used to create a new instance of Metrics +func New(conf *Config, sink MetricSink) (*Metrics, error) { + met := &Metrics{} + met.Config = *conf + met.sink = sink + met.UpdateFilterAndLabels(conf.AllowedPrefixes, conf.BlockedPrefixes, conf.AllowedLabels, conf.BlockedLabels) + + // Start the runtime collector + if conf.EnableRuntimeMetrics { + go met.collectStats() + } + return met, nil +} + +// NewGlobal is the same as New, but it assigns the metrics object to be +// used globally as well as returning it. +func NewGlobal(conf *Config, sink MetricSink) (*Metrics, error) { + metrics, err := New(conf, sink) + if err == nil { + globalMetrics.Store(metrics) + } + return metrics, err +} + +// Proxy all the methods to the globalMetrics instance +func SetGauge(key []string, val float32) { + globalMetrics.Load().(*Metrics).SetGauge(key, val) +} + +func SetGaugeWithLabels(key []string, val float32, labels []Label) { + globalMetrics.Load().(*Metrics).SetGaugeWithLabels(key, val, labels) +} + +func EmitKey(key []string, val float32) { + globalMetrics.Load().(*Metrics).EmitKey(key, val) +} + +func IncrCounter(key []string, val float32) { + globalMetrics.Load().(*Metrics).IncrCounter(key, val) +} + +func IncrCounterWithLabels(key []string, val float32, labels []Label) { + globalMetrics.Load().(*Metrics).IncrCounterWithLabels(key, val, labels) +} + +func AddSample(key []string, val float32) { + globalMetrics.Load().(*Metrics).AddSample(key, val) +} + +func AddSampleWithLabels(key []string, val float32, labels []Label) { + globalMetrics.Load().(*Metrics).AddSampleWithLabels(key, val, labels) +} + +func MeasureSince(key []string, start time.Time) { + globalMetrics.Load().(*Metrics).MeasureSince(key, start) +} + +func MeasureSinceWithLabels(key []string, start time.Time, labels []Label) { + globalMetrics.Load().(*Metrics).MeasureSinceWithLabels(key, start, labels) +} + +func UpdateFilter(allow, block []string) { + globalMetrics.Load().(*Metrics).UpdateFilter(allow, block) +} + +// UpdateFilterAndLabels set allow/block prefixes of metrics while allowedLabels +// and blockedLabels - when not nil - allow filtering of labels in order to +// block/allow globally labels (especially useful when having large number of +// values for a given label). See README.md for more information about usage. +func UpdateFilterAndLabels(allow, block, allowedLabels, blockedLabels []string) { + globalMetrics.Load().(*Metrics).UpdateFilterAndLabels(allow, block, allowedLabels, blockedLabels) +} diff --git a/pkg/metrics/statsd.go b/pkg/metrics/statsd.go new file mode 100644 index 0000000..1bfffce --- /dev/null +++ b/pkg/metrics/statsd.go @@ -0,0 +1,184 @@ +package metrics + +import ( + "bytes" + "fmt" + "log" + "net" + "net/url" + "strings" + "time" +) + +const ( + // statsdMaxLen is the maximum size of a packet + // to send to statsd + statsdMaxLen = 1400 +) + +// StatsdSink provides a MetricSink that can be used +// with a statsite or statsd metrics server. It uses +// only UDP packets, while StatsiteSink uses TCP. +type StatsdSink struct { + addr string + metricQueue chan string +} + +// NewStatsdSinkFromURL creates an StatsdSink from a URL. It is used +// (and tested) from NewMetricSinkFromURL. +func NewStatsdSinkFromURL(u *url.URL) (MetricSink, error) { + return NewStatsdSink(u.Host) +} + +// NewStatsdSink is used to create a new StatsdSink +func NewStatsdSink(addr string) (*StatsdSink, error) { + s := &StatsdSink{ + addr: addr, + metricQueue: make(chan string, 4096), + } + go s.flushMetrics() + return s, nil +} + +// Close is used to stop flushing to statsd +func (s *StatsdSink) Shutdown() { + close(s.metricQueue) +} + +func (s *StatsdSink) SetGauge(key []string, val float32) { + flatKey := s.flattenKey(key) + s.pushMetric(fmt.Sprintf("%s:%f|g\n", flatKey, val)) +} + +func (s *StatsdSink) SetGaugeWithLabels(key []string, val float32, labels []Label) { + flatKey := s.flattenKeyLabels(key, labels) + s.pushMetric(fmt.Sprintf("%s:%f|g\n", flatKey, val)) +} + +func (s *StatsdSink) EmitKey(key []string, val float32) { + flatKey := s.flattenKey(key) + s.pushMetric(fmt.Sprintf("%s:%f|kv\n", flatKey, val)) +} + +func (s *StatsdSink) IncrCounter(key []string, val float32) { + flatKey := s.flattenKey(key) + s.pushMetric(fmt.Sprintf("%s:%f|c\n", flatKey, val)) +} + +func (s *StatsdSink) IncrCounterWithLabels(key []string, val float32, labels []Label) { + flatKey := s.flattenKeyLabels(key, labels) + s.pushMetric(fmt.Sprintf("%s:%f|c\n", flatKey, val)) +} + +func (s *StatsdSink) AddSample(key []string, val float32) { + flatKey := s.flattenKey(key) + s.pushMetric(fmt.Sprintf("%s:%f|ms\n", flatKey, val)) +} + +func (s *StatsdSink) AddSampleWithLabels(key []string, val float32, labels []Label) { + flatKey := s.flattenKeyLabels(key, labels) + s.pushMetric(fmt.Sprintf("%s:%f|ms\n", flatKey, val)) +} + +// Flattens the key for formatting, removes spaces +func (s *StatsdSink) flattenKey(parts []string) string { + joined := strings.Join(parts, ".") + return strings.Map(func(r rune) rune { + switch r { + case ':': + fallthrough + case ' ': + return '_' + default: + return r + } + }, joined) +} + +// Flattens the key along with labels for formatting, removes spaces +func (s *StatsdSink) flattenKeyLabels(parts []string, labels []Label) string { + for _, label := range labels { + parts = append(parts, label.Value) + } + return s.flattenKey(parts) +} + +// Does a non-blocking push to the metrics queue +func (s *StatsdSink) pushMetric(m string) { + select { + case s.metricQueue <- m: + default: + } +} + +// Flushes metrics +func (s *StatsdSink) flushMetrics() { + var sock net.Conn + var err error + var wait <-chan time.Time + ticker := time.NewTicker(flushInterval) + defer ticker.Stop() + +CONNECT: + // Create a buffer + buf := bytes.NewBuffer(nil) + + // Attempt to connect + sock, err = net.Dial("udp", s.addr) + if err != nil { + log.Printf("[ERR] Error connecting to statsd! Err: %s", err) + goto WAIT + } + + for { + select { + case metric, ok := <-s.metricQueue: + // Get a metric from the queue + if !ok { + goto QUIT + } + + // Check if this would overflow the packet size + if len(metric)+buf.Len() > statsdMaxLen { + _, err := sock.Write(buf.Bytes()) + buf.Reset() + if err != nil { + log.Printf("[ERR] Error writing to statsd! Err: %s", err) + goto WAIT + } + } + + // Append to the buffer + buf.WriteString(metric) + + case <-ticker.C: + if buf.Len() == 0 { + continue + } + + _, err := sock.Write(buf.Bytes()) + buf.Reset() + if err != nil { + log.Printf("[ERR] Error flushing to statsd! Err: %s", err) + goto WAIT + } + } + } + +WAIT: + // Wait for a while + wait = time.After(time.Duration(5) * time.Second) + for { + select { + // Dequeue the messages to avoid backlog + case _, ok := <-s.metricQueue: + if !ok { + goto QUIT + } + case <-wait: + goto CONNECT + } + } +QUIT: + s.metricQueue = nil +} diff --git a/pkg/metrics/statsite.go b/pkg/metrics/statsite.go new file mode 100644 index 0000000..6c0d284 --- /dev/null +++ b/pkg/metrics/statsite.go @@ -0,0 +1,172 @@ +package metrics + +import ( + "bufio" + "fmt" + "log" + "net" + "net/url" + "strings" + "time" +) + +const ( + // We force flush the statsite metrics after this period of + // inactivity. Prevents stats from getting stuck in a buffer + // forever. + flushInterval = 100 * time.Millisecond +) + +// NewStatsiteSinkFromURL creates an StatsiteSink from a URL. It is used +// (and tested) from NewMetricSinkFromURL. +func NewStatsiteSinkFromURL(u *url.URL) (MetricSink, error) { + return NewStatsiteSink(u.Host) +} + +// StatsiteSink provides a MetricSink that can be used with a +// statsite metrics server +type StatsiteSink struct { + addr string + metricQueue chan string +} + +// NewStatsiteSink is used to create a new StatsiteSink +func NewStatsiteSink(addr string) (*StatsiteSink, error) { + s := &StatsiteSink{ + addr: addr, + metricQueue: make(chan string, 4096), + } + go s.flushMetrics() + return s, nil +} + +// Close is used to stop flushing to statsite +func (s *StatsiteSink) Shutdown() { + close(s.metricQueue) +} + +func (s *StatsiteSink) SetGauge(key []string, val float32) { + flatKey := s.flattenKey(key) + s.pushMetric(fmt.Sprintf("%s:%f|g\n", flatKey, val)) +} + +func (s *StatsiteSink) SetGaugeWithLabels(key []string, val float32, labels []Label) { + flatKey := s.flattenKeyLabels(key, labels) + s.pushMetric(fmt.Sprintf("%s:%f|g\n", flatKey, val)) +} + +func (s *StatsiteSink) EmitKey(key []string, val float32) { + flatKey := s.flattenKey(key) + s.pushMetric(fmt.Sprintf("%s:%f|kv\n", flatKey, val)) +} + +func (s *StatsiteSink) IncrCounter(key []string, val float32) { + flatKey := s.flattenKey(key) + s.pushMetric(fmt.Sprintf("%s:%f|c\n", flatKey, val)) +} + +func (s *StatsiteSink) IncrCounterWithLabels(key []string, val float32, labels []Label) { + flatKey := s.flattenKeyLabels(key, labels) + s.pushMetric(fmt.Sprintf("%s:%f|c\n", flatKey, val)) +} + +func (s *StatsiteSink) AddSample(key []string, val float32) { + flatKey := s.flattenKey(key) + s.pushMetric(fmt.Sprintf("%s:%f|ms\n", flatKey, val)) +} + +func (s *StatsiteSink) AddSampleWithLabels(key []string, val float32, labels []Label) { + flatKey := s.flattenKeyLabels(key, labels) + s.pushMetric(fmt.Sprintf("%s:%f|ms\n", flatKey, val)) +} + +// Flattens the key for formatting, removes spaces +func (s *StatsiteSink) flattenKey(parts []string) string { + joined := strings.Join(parts, ".") + return strings.Map(func(r rune) rune { + switch r { + case ':': + fallthrough + case ' ': + return '_' + default: + return r + } + }, joined) +} + +// Flattens the key along with labels for formatting, removes spaces +func (s *StatsiteSink) flattenKeyLabels(parts []string, labels []Label) string { + for _, label := range labels { + parts = append(parts, label.Value) + } + return s.flattenKey(parts) +} + +// Does a non-blocking push to the metrics queue +func (s *StatsiteSink) pushMetric(m string) { + select { + case s.metricQueue <- m: + default: + } +} + +// Flushes metrics +func (s *StatsiteSink) flushMetrics() { + var sock net.Conn + var err error + var wait <-chan time.Time + var buffered *bufio.Writer + ticker := time.NewTicker(flushInterval) + defer ticker.Stop() + +CONNECT: + // Attempt to connect + sock, err = net.Dial("tcp", s.addr) + if err != nil { + log.Printf("[ERR] Error connecting to statsite! Err: %s", err) + goto WAIT + } + + // Create a buffered writer + buffered = bufio.NewWriter(sock) + + for { + select { + case metric, ok := <-s.metricQueue: + // Get a metric from the queue + if !ok { + goto QUIT + } + + // Try to send to statsite + _, err := buffered.Write([]byte(metric)) + if err != nil { + log.Printf("[ERR] Error writing to statsite! Err: %s", err) + goto WAIT + } + case <-ticker.C: + if err := buffered.Flush(); err != nil { + log.Printf("[ERR] Error flushing to statsite! Err: %s", err) + goto WAIT + } + } + } + +WAIT: + // Wait for a while + wait = time.After(time.Duration(5) * time.Second) + for { + select { + // Dequeue the messages to avoid backlog + case _, ok := <-s.metricQueue: + if !ok { + goto QUIT + } + case <-wait: + goto CONNECT + } + } +QUIT: + s.metricQueue = nil +} diff --git a/pkg/mongodb/interfaces/mongodb_database.go b/pkg/mongodb/interfaces/mongodb_database.go new file mode 100644 index 0000000..51ba065 --- /dev/null +++ b/pkg/mongodb/interfaces/mongodb_database.go @@ -0,0 +1,18 @@ +package interfaces + +import "context" + +type MongoDbDatabase interface { + Insert(ctx context.Context, entity interface{}) error + BulkInsert(ctx context.Context, entities []interface{}) error + FindOneById(ctx context.Context, id string, entity interface{}) error + FindOneByFilter(ctx context.Context, entity interface{}, condition string, params ...interface{}) error + FindByFilter(ctx context.Context, entity interface{}, query string, params ...interface{}) error + UpdateOneById(ctx context.Context, id string, entity interface{}) error + UpdateOneByFilter(ctx context.Context, entity interface{}, condition string, params ...interface{}) error + DeleteOneById(ctx context.Context, id string) error + DeleteOneByFilter(ctx context.Context, condition string, params ...interface{}) error + DeleteAllByFilter(ctx context.Context, condition string, params ...interface{}) error +} + + diff --git a/pkg/mongodb/mongodb_database.go b/pkg/mongodb/mongodb_database.go new file mode 100644 index 0000000..6a90326 --- /dev/null +++ b/pkg/mongodb/mongodb_database.go @@ -0,0 +1,128 @@ +package mongodb + +import ( + "context" + "github.com/ereb-or-od/kenobi/pkg/mongodb/interfaces" + "github.com/ereb-or-od/kenobi/pkg/utilities" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type mongodbDatabase struct { + db *mongo.Collection +} + +func (m mongodbDatabase) Insert(ctx context.Context, entity interface{}) error { + if _, err := m.db.InsertOne(ctx, entity); err != nil { + return err + } else { + return nil + } +} + +func (m mongodbDatabase) BulkInsert(ctx context.Context, entities []interface{}) error { + if _, err := m.db.InsertMany(ctx, entities); err != nil { + return err + } else { + return nil + } +} + +func (m mongodbDatabase) FindOneById(ctx context.Context, id string, entity interface{}) error { + objectId, err := utilities.ToObjectID(id) + if err != nil { + return err + } + result := m.db.FindOne(ctx, bson.M{"_id": objectId}) + if err = result.Decode(&entity); err != nil { + return err + } + + return nil +} + +func (m mongodbDatabase) FindOneByFilter(ctx context.Context, entity interface{}, condition string, params ...interface{}) error { + result := m.db.FindOne(ctx, bson.M{condition: params}) + if err := result.Decode(&entity); err != nil { + return err + } + return nil +} + +func (m mongodbDatabase) FindByFilter(ctx context.Context, entity interface{}, condition string, params ...interface{}) error { + if cursor, err := m.db.Find(ctx, bson.M{condition: params}); err != nil { + return err + } else { + var records []interface{} + defer cursor.Close(ctx) + for cursor.Next(ctx) { + var record interface{} + if err = cursor.Decode(&record); err != nil { + // ignored + } + records = append(records, record) + } + entity = records + } + + return nil +} + +func (m mongodbDatabase) UpdateOneById(ctx context.Context, id string, entity interface{}) error { + objectId, err := utilities.ToObjectID(id) + if err != nil { + return err + } + if _, err = m.db.UpdateOne(ctx, bson.M{"_id": objectId}, entity); err != nil { + return err + } else { + return nil + } +} + +func (m mongodbDatabase) UpdateOneByFilter(ctx context.Context, entity interface{}, condition string, params ...interface{}) error { + if _, err := m.db.UpdateOne(ctx, bson.M{condition: params}, entity); err != nil { + return err + } else { + return nil + } +} + +func (m mongodbDatabase) DeleteOneById(ctx context.Context, id string) error { + objectId, err := utilities.ToObjectID(id) + if err != nil { + return err + } + if _, err = m.db.DeleteOne(ctx, bson.M{"_id": objectId}); err != nil { + return err + } else { + return nil + } +} + +func (m mongodbDatabase) DeleteOneByFilter(ctx context.Context, condition string, params ...interface{}) error { + if _, err := m.db.DeleteOne(ctx, bson.M{condition: params}); err != nil { + return err + } else { + return nil + } +} + +func (m mongodbDatabase) DeleteAllByFilter(ctx context.Context, condition string, params ...interface{}) error { + if _, err := m.db.DeleteMany(ctx, bson.M{condition: params}); err != nil { + return err + } else { + return nil + } +} + +func New(connectionString string, database string, collection string) (interfaces.MongoDbDatabase, error) { + if client, err := mongo.Connect(context.Background(), options.Client().ApplyURI(connectionString)); err != nil { + return nil, err + } else { + return &mongodbDatabase{ + db: client.Database(database).Collection(collection), + }, nil + } +} diff --git a/pkg/postgresql/interfaces/postgresql_database.go b/pkg/postgresql/interfaces/postgresql_database.go new file mode 100644 index 0000000..0e816e5 --- /dev/null +++ b/pkg/postgresql/interfaces/postgresql_database.go @@ -0,0 +1,18 @@ +package interfaces + +import "context" + +type PostgreSqlDatabaseProvider interface { + Insert(ctx context.Context, entity interface{}) error + BulkInsert(ctx context.Context, entities []interface{}) error + FindOneById(ctx context.Context, id string, entity interface{}) error + FindOneByFilter(ctx context.Context, entity interface{}, condition string, params ...interface{}) error + FindByFilter(ctx context.Context, entity interface{}, query string, params ...interface{}) error + UpdateOneById(ctx context.Context, id string, entity interface{}) error + UpdateOneByFilter(ctx context.Context, entity interface{}, condition string, params ...interface{}) error + DeleteOneById(ctx context.Context, id string, entity interface{}) error + DeleteOneByFilter(ctx context.Context, entity interface{}, condition string, params ...interface{}) error + DeleteAllByFilter(ctx context.Context, entity interface{}, condition string, params ...interface{}) error +} + + diff --git a/pkg/postgresql/options/postgresql_server_options.go b/pkg/postgresql/options/postgresql_server_options.go new file mode 100644 index 0000000..ebdb740 --- /dev/null +++ b/pkg/postgresql/options/postgresql_server_options.go @@ -0,0 +1,9 @@ +package options + +type PostgreSqlServerOptions struct { + User string + Password string + Addr string + Database string + ApplicationName string +} diff --git a/pkg/postgresql/standalone_postgresql_database.go b/pkg/postgresql/standalone_postgresql_database.go new file mode 100644 index 0000000..9557578 --- /dev/null +++ b/pkg/postgresql/standalone_postgresql_database.go @@ -0,0 +1,104 @@ +package postgresql + +import ( + "context" + "github.com/ereb-or-od/kenobi/pkg/postgresql/interfaces" + "github.com/ereb-or-od/kenobi/pkg/postgresql/options" + "github.com/go-pg/pg/v10" +) + +type standalonePostgresqlDatabase struct { + db *pg.DB +} + +func (s standalonePostgresqlDatabase) FindByFilter(ctx context.Context, entity interface{}, query string, params ...interface{}) error { + if result, err := s.db.Query(entity, query, params); err != nil { + return err + } else { + result.RowsAffected() + return nil + } +} + +func (s standalonePostgresqlDatabase) DeleteOneById(ctx context.Context, id string, entity interface{}) error { + if _, err := s.db.Model(entity).Where("id = ? ", id).Delete(); err != nil { + return err + } else { + return nil + } +} + +func (s standalonePostgresqlDatabase) DeleteOneByFilter(ctx context.Context, entity interface{}, condition string, params ...interface{}) error { + if _, err := s.db.Model(entity).Where(condition, params...).Delete(); err != nil { + return err + } else { + return nil + } +} + +func (s standalonePostgresqlDatabase) DeleteAllByFilter(ctx context.Context, entity interface{}, condition string, params ...interface{}) error { + if _, err := s.db.Model(entity).Where(condition, params...).Delete(); err != nil { + return err + } else { + return nil + } +} + +func (s standalonePostgresqlDatabase) UpdateOneById(ctx context.Context, id string, entity interface{}) error { + if _, err := s.db.Model(entity).Where("id = ? ", id).Update(); err != nil { + return err + } else { + return nil + } +} + +func (s standalonePostgresqlDatabase) UpdateOneByFilter(ctx context.Context, entity interface{}, condition string, params ...interface{}) error { + if _, err := s.db.Model(entity).Where(condition, params...).Update(); err != nil { + return err + } else { + return nil + } +} + +func (s standalonePostgresqlDatabase) FindOneByFilter(ctx context.Context, entity interface{}, condition string, params ...interface{}) error { + if err := s.db.Model(entity).Where(condition, params...).Select(); err != nil { + return err + } else { + return nil + } +} + +func (s standalonePostgresqlDatabase) FindOneById(ctx context.Context, id string, entity interface{}) error { + if err := s.db.Model(entity).Where("id = ?", id).Select(); err != nil { + return err + } else { + return nil + } +} + +func (s standalonePostgresqlDatabase) BulkInsert(ctx context.Context, entities []interface{}) error { + if _, err := s.db.Model(entities).Insert(); err != nil { + return err + } + return nil +} + +func (s standalonePostgresqlDatabase) Insert(ctx context.Context, entity interface{}) error { + if _, err := s.db.Model(entity).Insert(); err != nil { + return err + } + return nil +} + +func New(options *options.PostgreSqlServerOptions) interfaces.PostgreSqlDatabaseProvider { + db := pg.Connect(&pg.Options{ + User: options.User, + Password: options.Password, + Addr: options.Addr, + Database: options.Database, + ApplicationName: options.ApplicationName, + }) + return &standalonePostgresqlDatabase{ + db: db, + } +} diff --git a/pkg/rabbitmq/.DS_Store b/pkg/rabbitmq/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..3bba2279c678e4f901421567cd8ab473d81fb0dc GIT binary patch literal 6148 zcmeHK%}N6?5T3ME(~8)GV2`!GzGdJvXc58i|oJ*c$1w&+6LNOx<|TG`joH}VO5 z9cPkMl0g~cT)OR}_5y6~^{$WMcG zm^6dI8TF2W^g49*vDW<$I|xsr-l$z(+t+a#L~(Cmilc51Q_fGLxT{A^J&e1F8OOH) zDT}h$E?36m-JM2N)wdgysv7TZ)~af&u|1g-<@!c_@33=o|1f@>JUv@>1^$ReRvk{^ z1&x`7-uZ*Luj4C>P3xxljLZNtzzobW18&Qc%X7R&UK%sN4E&A(+8-1uq31BOXtoY) z==wbTZ(8)g?nNMTaJF^@;rx`MOzNS%#7o>nT30y2s1nS zl}QKTS>%=(U= 0; i-- { + w = middlewares[i](w) + } + + return w +} diff --git a/pkg/rabbitmq/consumer/middleware/ack_nack.go b/pkg/rabbitmq/consumer/middleware/ack_nack.go new file mode 100644 index 0000000..7ce92c5 --- /dev/null +++ b/pkg/rabbitmq/consumer/middleware/ack_nack.go @@ -0,0 +1,45 @@ +package middleware + +import ( + "context" + "github.com/ereb-or-od/kenobi/pkg/rabbitmq/consumer" + + "github.com/streadway/amqp" +) + +const Ack = "ack" +const Nack = "nack_requeue" +const Requeue = "requeue" + +func AckNack() consumer.Middleware { + return func(next consumer.Handler) consumer.Handler { + fn := func(ctx context.Context, msg amqp.Delivery) interface{} { + result := next.Handle(ctx, msg) + if result == nil { + return nil + } + + switch result { + case Ack: + if err := msg.Ack(false); err != nil { + return nil + } + return nil + case Nack: + if err := msg.Nack(false, false); err != nil { + return nil + } + return nil + case Requeue: + if err := msg.Nack(false, true); err != nil { + return nil + } + return nil + } + + return result + } + + return consumer.HandlerFunc(fn) + } +} diff --git a/pkg/rabbitmq/consumer/middleware/expire.go b/pkg/rabbitmq/consumer/middleware/expire.go new file mode 100644 index 0000000..e482c79 --- /dev/null +++ b/pkg/rabbitmq/consumer/middleware/expire.go @@ -0,0 +1,43 @@ +package middleware + +import ( + "context" + "github.com/ereb-or-od/kenobi/pkg/rabbitmq/consumer" + + "strconv" + + "time" + + "github.com/streadway/amqp" +) + +func ExpireToTimeout(defaultTimeout time.Duration) func(next consumer.Handler) consumer.Handler { + return wrap(func(ctx context.Context, msg amqp.Delivery, next consumer.Handler) (result interface{}) { + if msg.Expiration == "" { + if defaultTimeout.Nanoseconds() == 0 { + return next.Handle(ctx, msg) + } + + nextCtx, cancelFunc := context.WithTimeout(ctx, defaultTimeout) + defer cancelFunc() + + return next.Handle(nextCtx, msg) + } + + expiration, err := strconv.ParseInt(msg.Expiration, 10, 0) + if err != nil { + + if defaultTimeout.Nanoseconds() != 0 { + nextCtx, cancelFunc := context.WithTimeout(ctx, defaultTimeout) + defer cancelFunc() + + return next.Handle(nextCtx, msg) + } + } + + nextCtx, cancelFunc := context.WithTimeout(ctx, time.Duration(expiration)*time.Millisecond) + defer cancelFunc() + + return next.Handle(nextCtx, msg) + }) +} diff --git a/pkg/rabbitmq/consumer/middleware/has_correlation_id.go b/pkg/rabbitmq/consumer/middleware/has_correlation_id.go new file mode 100644 index 0000000..f865939 --- /dev/null +++ b/pkg/rabbitmq/consumer/middleware/has_correlation_id.go @@ -0,0 +1,18 @@ +package middleware + +import ( + "context" + "github.com/ereb-or-od/kenobi/pkg/rabbitmq/consumer" + + "github.com/streadway/amqp" +) + +func HasCorrelationID() consumer.Middleware { + return wrap(func(ctx context.Context, msg amqp.Delivery, next consumer.Handler) interface{} { + if msg.CorrelationId == "" { + return nack(ctx, msg) + } + + return next.Handle(ctx, msg) + }) +} diff --git a/pkg/rabbitmq/consumer/middleware/has_reply_to.go b/pkg/rabbitmq/consumer/middleware/has_reply_to.go new file mode 100644 index 0000000..cc678c6 --- /dev/null +++ b/pkg/rabbitmq/consumer/middleware/has_reply_to.go @@ -0,0 +1,17 @@ +package middleware + +import ( + "context" + "github.com/ereb-or-od/kenobi/pkg/rabbitmq/consumer" + "github.com/streadway/amqp" +) + +func HasReplyTo() consumer.Middleware { + return wrap(func(ctx context.Context, msg amqp.Delivery, next consumer.Handler) interface{} { + if msg.ReplyTo == "" { + return nack(ctx, msg) + } + + return next.Handle(ctx, msg) + }) +} diff --git a/pkg/rabbitmq/consumer/middleware/middleware.go b/pkg/rabbitmq/consumer/middleware/middleware.go new file mode 100644 index 0000000..4279614 --- /dev/null +++ b/pkg/rabbitmq/consumer/middleware/middleware.go @@ -0,0 +1,32 @@ +package middleware + +import ( + "context" + "github.com/ereb-or-od/kenobi/pkg/rabbitmq/consumer" + "github.com/streadway/amqp" +) + +// contextKey is a value for use with context.WithValue. It's used as +// a pointer so it fits in an interface{} without allocation. This technique +// for defining context keys was copied from Go 1.7's new use of context in net/http. +type contextKey struct { + name string +} + +func (k *contextKey) String() string { + return "amqpextra/middleware context value " + k.name +} + +func nack(ctx context.Context, msg amqp.Delivery) interface{} { + if err := msg.Nack(false, false); err != nil { + } + return nil +} + +func wrap(fn func(ctx context.Context, msg amqp.Delivery, next consumer.Handler) interface{}) func(next consumer.Handler) consumer.Handler { + return func(next consumer.Handler) consumer.Handler { + return consumer.HandlerFunc(func(ctx context.Context, msg amqp.Delivery) interface{} { + return fn(ctx, msg, next) + }) + } +} diff --git a/pkg/rabbitmq/consumer/middleware/recover.go b/pkg/rabbitmq/consumer/middleware/recover.go new file mode 100644 index 0000000..6b6c023 --- /dev/null +++ b/pkg/rabbitmq/consumer/middleware/recover.go @@ -0,0 +1,20 @@ +package middleware + +import ( + "context" + "github.com/ereb-or-od/kenobi/pkg/rabbitmq/consumer" + "github.com/streadway/amqp" +) + +func Recover() consumer.Middleware { + return wrap(func(ctx context.Context, msg amqp.Delivery, next consumer.Handler) (result interface{}) { + defer func() { + if e := recover(); e != nil { + if nackErr := msg.Nack(false, false); nackErr != nil { + } + } + }() + + return next.Handle(ctx, msg) + }) +} diff --git a/pkg/rabbitmq/consumer/worker.go b/pkg/rabbitmq/consumer/worker.go new file mode 100644 index 0000000..1889595 --- /dev/null +++ b/pkg/rabbitmq/consumer/worker.go @@ -0,0 +1,74 @@ +package consumer + +import ( + "context" + logger "github.com/ereb-or-od/kenobi/pkg/logging/interfaces" + "sync" + + "github.com/streadway/amqp" +) + +type Worker interface { + Serve(ctx context.Context, h Handler, msgCh <-chan amqp.Delivery) +} + +type DefaultWorker struct { +} + +func (dw *DefaultWorker) Serve(ctx context.Context, h Handler, msgCh <-chan amqp.Delivery) { + for { + select { + case msg, ok := <-msgCh: + if !ok { + return + } + + if res := h.Handle(ctx, msg); res != nil { + + } + case <-ctx.Done(): + return + } + } +} + +type ParallelWorker struct { + Num int + Logger logger.Logger +} + +func NewParallelWorker(num int) *ParallelWorker { + if num < 1 { + panic("num workers must be greater than zero") + } + + return &ParallelWorker{ + Num: num, + } +} + +func (pw *ParallelWorker) Serve(ctx context.Context, h Handler, msgCh <-chan amqp.Delivery) { + wg := &sync.WaitGroup{} + for i := 0; i < pw.Num; i++ { + wg.Add(1) + go func() { + defer wg.Done() + + for { + select { + case msg, ok := <-msgCh: + if !ok { + return + } + + if res := h.Handle(ctx, msg); res != nil { + } + case <-ctx.Done(): + return + } + } + }() + } + + wg.Wait() +} diff --git a/pkg/rabbitmq/declare.go b/pkg/rabbitmq/declare.go new file mode 100644 index 0000000..c444baf --- /dev/null +++ b/pkg/rabbitmq/declare.go @@ -0,0 +1,56 @@ +package rabbitmq + +import ( + "context" + "github.com/streadway/amqp" + "log" +) + +func TempQueue( + ctx context.Context, + c *Dialer, +) (amqp.Queue, error) { + return Queue( + ctx, + c, + "", + false, + true, + true, + false, + amqp.Table{}, + ) +} + +func Queue( + ctx context.Context, + c *Dialer, + name string, + durable, + autDelete, + exclusive, + noWait bool, + args amqp.Table, +) (amqp.Queue, error) { + conn, err := c.Connection(ctx) + if err != nil { + return amqp.Queue{}, err + } + + ch, err := conn.Channel() + if err != nil { + return amqp.Queue{}, err + } + defer func() { + if closeErr := ch.Close(); closeErr != nil { + log.Print("amqpextra: declare queue: ch close: %w", closeErr) + } + }() + + q, err := ch.QueueDeclare(name, durable, autDelete, exclusive, noWait, args) + if err != nil { + return amqp.Queue{}, err + } + + return q, nil +} diff --git a/pkg/rabbitmq/dialer.go b/pkg/rabbitmq/dialer.go new file mode 100644 index 0000000..5ef1154 --- /dev/null +++ b/pkg/rabbitmq/dialer.go @@ -0,0 +1,450 @@ +package rabbitmq + +import ( + "context" + "errors" + "github.com/ereb-or-od/kenobi/pkg/logging" + logger "github.com/ereb-or-od/kenobi/pkg/logging/interfaces" + "github.com/ereb-or-od/kenobi/pkg/rabbitmq/consumer" + "github.com/ereb-or-od/kenobi/pkg/rabbitmq/publisher" + "sync" + "time" + + "fmt" + "github.com/streadway/amqp" +) + +type State struct { + Ready *Ready + Unready *Unready +} + +type Ready struct{} + +type Unready struct { + Err error +} + +// Option could be used to configure Dialer +type Option func(c *Dialer) + +// AMQPConnection is an interface for streadway's *amqp.Connection +type AMQPConnection interface { + NotifyClose(chan *amqp.Error) chan *amqp.Error + Close() error +} + +// Connection provides access to streadway's *amqp.Connection as well as notification channels +// A notification indicates that something wrong has happened to the connection. +// The client should get a fresh connection from Dialer. +type Connection struct { + amqpConn AMQPConnection + lostCh chan struct{} +} + +// AMQPConnection returns streadway's *amqp.Connection +func (c *Connection) AMQPConnection() *amqp.Connection { + return c.amqpConn.(*amqp.Connection) +} + +// NotifyLost notifies when current connection is lost and new once should be requested +func (c *Connection) NotifyLost() chan struct{} { + return c.lostCh +} + +type config struct { + amqpUrls []string + amqpDial func(url string, c amqp.Config) (AMQPConnection, error) + amqpConfig amqp.Config + + logger logger.Logger + retryPeriod time.Duration + ctx context.Context +} + +// Dialer is responsible for keeping the connection up. +// If connection is lost or closed. It tries dial a server again and again with some wait periods. +// Dialer keep connection up until it Dialer.Close() method called or the context is canceled. +type Dialer struct { + config + + ctx context.Context + cancelFunc context.CancelFunc + + connCh chan *Connection + + mu sync.Mutex + stateChs []chan State + + internalStateChan chan State + + closedCh chan struct{} +} + +// Dial returns established connection or an error. +// It keeps retrying until timeout 30sec is reached. +func Dial(opts ...Option) (*amqp.Connection, error) { + d, err := NewDialer(opts...) + if err != nil { + return nil, err + } + + ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*30) + defer cancelFunc() + + return d.Connection(ctx) +} + +// NewDialer returns Dialer or a configuration error. +func NewDialer(opts ...Option) (*Dialer, error) { + if defaultLogger, err := logging.New(); err != nil { + return nil, err + } else { + c := &Dialer{ + config: config{ + amqpUrls: make([]string, 0, 1), + amqpDial: func(url string, c amqp.Config) (AMQPConnection, error) { + return amqp.DialConfig(url, c) + }, + amqpConfig: amqp.Config{ + Heartbeat: time.Second * 30, + Locale: "en_US", + }, + + retryPeriod: time.Second * 5, + logger: defaultLogger, + }, + internalStateChan: make(chan State), + connCh: make(chan *Connection), + closedCh: make(chan struct{}), + } + + for _, opt := range opts { + opt(c) + } + + for _, readyCh := range c.stateChs { + if readyCh == nil { + return nil, errors.New("state chan must be not nil") + } + + if cap(readyCh) == 0 { + return nil, errors.New("state chan is unbuffered") + } + } + + if len(c.amqpUrls) == 0 { + return nil, fmt.Errorf("url(s) must be set") + } + + for _, url := range c.amqpUrls { + if url == "" { + return nil, fmt.Errorf("url(s) must be not empty") + } + } + + if c.config.ctx != nil { + c.ctx, c.cancelFunc = context.WithCancel(c.config.ctx) + } else { + c.ctx, c.cancelFunc = context.WithCancel(context.Background()) + } + + if c.retryPeriod <= 0 { + return nil, fmt.Errorf("retryPeriod must be greater then zero") + } + + go c.connectState() + + return c, nil + } + +} + +// WithURL configure RabbitMQ servers to dial. +// Dialer dials url by round-robbin +func WithURL(urls ...string) Option { + return func(c *Dialer) { + c.amqpUrls = append(c.amqpUrls, urls...) + } +} + +// WithAMQPDial configure dial function. +// The function takes the url and amqp.Config and returns AMQPConnection. +func WithAMQPDial(dial func(url string, c amqp.Config) (AMQPConnection, error)) Option { + return func(c *Dialer) { + c.amqpDial = dial + } +} + +// WithLogger configure the logger used by Dialer +func WithLogger(l logger.Logger) Option { + return func(c *Dialer) { + c.logger = l + } +} + +// WithLogger configure Dialer context +// The context could used later to stop Dialer +func WithContext(ctx context.Context) Option { + return func(c *Dialer) { + c.config.ctx = ctx + } +} + +// WithRetryPeriod configure how much time to wait before next dial attempt. Default: 5sec. +func WithRetryPeriod(dur time.Duration) Option { + return func(c *Dialer) { + c.retryPeriod = dur + } +} + +// WithConnectionProperties configure connection properties set on dial. +func WithConnectionProperties(props amqp.Table) Option { + return func(c *Dialer) { + c.amqpConfig.Properties = props + } +} + +// WithNotify helps subscribe on Dialer ready/unready events. +func WithNotify(stateCh chan State) Option { + return func(c *Dialer) { + c.stateChs = append(c.stateChs, stateCh) + } +} + +// ConnectionCh returns Connection channel. +// The channel should be used to get established connections. +// The client must subscribe on Connection.NotifyLost(). +// Once lost, client must stop using current connection and get new one from Connection channel. +// Connection channel is closed when Dialer is closed. Don't forget to check for closed connection. +func (c *Dialer) ConnectionCh() <-chan *Connection { + return c.connCh +} + +// Notify could be used to subscribe on Dialer ready/unready events +func (c *Dialer) Notify(stateCh chan State) <-chan State { + if cap(stateCh) == 0 { + panic("state chan is unbuffered") + } + + select { + case state := <-c.internalStateChan: + stateCh <- state + case <-c.NotifyClosed(): + return stateCh + } + + c.mu.Lock() + c.stateChs = append(c.stateChs, stateCh) + c.mu.Unlock() + + return stateCh +} + +// NotifyClosed could be used to subscribe on Dialer closed event. +// Dialer.ConnectionCh() could no longer be used after this point +func (c *Dialer) NotifyClosed() <-chan struct{} { + return c.closedCh +} + +// Close initiate Dialer close. +// Subscribe Dialer.NotifyClosed() to know when it was finally closed. +func (c *Dialer) Close() { + c.cancelFunc() +} + +// Connection returns streadway's *amqp.Connection. +// The client should subscribe on Dialer.NotifyReady(), Dialer.NotifyUnready() events in order to know when the connection is lost. +func (c *Dialer) Connection(ctx context.Context) (*amqp.Connection, error) { + select { + case <-c.ctx.Done(): + return nil, fmt.Errorf("connection closed") + case <-ctx.Done(): + return nil, ctx.Err() + case conn, ok := <-c.connCh: + if !ok { + return nil, fmt.Errorf("connection closed") + } + + return conn.AMQPConnection(), nil + } +} + +// Consumer returns a consumer that support reconnection feature. +func (c *Dialer) Consumer(opts ...consumer.Option) (*consumer.Consumer, error) { + opts = append([]consumer.Option{ + consumer.WithLogger(c.logger), + consumer.WithContext(c.ctx), + }, opts...) + + return NewConsumer(c.ConnectionCh(), opts...) +} + +// Publisher returns a consumer that support reconnection feature. +func (c *Dialer) Publisher(opts ...publisher.Option) (*publisher.Publisher, error) { + opts = append([]publisher.Option{ + publisher.WithLogger(c.logger), + publisher.WithContext(c.ctx), + }, opts...) + + return NewPublisher(c.ConnectionCh(), opts...) +} + +// connectState is a starting point. +// It chooses URL and dials the server. +// Once connection is established it pass control to Dialer.connectedState() +// If connection is closed or lost Dialer.connectedState() returns control back to Dialer.connectState() +// Exits on Dialer.Close() +func (c *Dialer) connectState() { + defer close(c.connCh) + defer close(c.closedCh) + defer c.cancelFunc() + defer c.logger.Debug("[DEBUG] connection closed") + + i := 0 + l := len(c.amqpUrls) + + c.logger.Debug("[DEBUG] connection unready") + state := State{Unready: &Unready{Err: amqp.ErrClosed}} + for { + select { + case <-c.ctx.Done(): + return + default: + } + + url := c.amqpUrls[i] + i = (i + 1) % l + + connCh := make(chan AMQPConnection) + errorCh := make(chan error) + + go func() { + c.logger.Debug("[DEBUG] dialing") + if conn, err := c.amqpDial(url, c.amqpConfig); err != nil { + errorCh <- err + } else { + connCh <- conn + } + }() + + loop2: + for { + select { + case c.internalStateChan <- state: + continue + case conn := <-connCh: + select { + case <-c.ctx.Done(): + c.closeConn(conn) + return + default: + } + + if err := c.connectedState(conn); err != nil { + c.logger.Error("[ERROR] connection unready: %s", err) + state = c.notifyUnready(err) + break loop2 + } + + return + case err := <-errorCh: + c.logger.Error("[ERROR] connection unready: %v", err) + if retryErr := c.waitRetry(err); retryErr != nil { + state = State{Unready: &Unready{Err: retryErr}} + break loop2 + } + + return + } + } + } +} + +// connectedState serves Dialer.ConnectionCh() and Dialer.Connection() methods. +// It shares an established connection with all the clients who requests it. +// Once connection is lost or closed it gives control back to Dialer.connectState(). +func (c *Dialer) connectedState(amqpConn AMQPConnection) error { + defer c.closeConn(amqpConn) + + lostCh := make(chan struct{}) + defer close(lostCh) + internalCloseCh := amqpConn.NotifyClose(make(chan *amqp.Error, 1)) + + conn := &Connection{amqpConn: amqpConn, lostCh: lostCh} + c.logger.Debug("[DEBUG] connection ready") + state := c.notifyReady() + for { + select { + case c.internalStateChan <- state: + continue + case c.connCh <- conn: + continue + case err, ok := <-internalCloseCh: + if !ok { + err = amqp.ErrClosed + } + return err + case <-c.ctx.Done(): + return nil + } + } +} + +func (c *Dialer) notifyUnready(err error) State { + state := State{Unready: &Unready{Err: err}} + c.mu.Lock() + defer c.mu.Unlock() + for _, stateCh := range c.stateChs { + select { + case stateCh <- state: + case <-stateCh: + stateCh <- state + } + } + return state +} + +func (c *Dialer) notifyReady() State { + state := State{Ready: &Ready{}} + c.mu.Lock() + defer c.mu.Unlock() + for _, stateCh := range c.stateChs { + select { + case stateCh <- state: + case <-stateCh: + stateCh <- state + } + } + return state +} + +func (c *Dialer) waitRetry(err error) error { + timer := time.NewTimer(c.retryPeriod) + defer func() { + timer.Stop() + select { + case <-timer.C: + default: + } + }() + state := c.notifyUnready(err) + for { + select { + case c.internalStateChan <- state: + continue + case <-timer.C: + return err + case <-c.ctx.Done(): + return nil + } + } +} + +func (c *Dialer) closeConn(conn AMQPConnection) { + if err := conn.Close(); err == amqp.ErrClosed { + return + } else if err != nil { + c.logger.Error("[ERROR] %s", err) + } +} diff --git a/pkg/rabbitmq/publisher.go b/pkg/rabbitmq/publisher.go new file mode 100644 index 0000000..ccc7cdf --- /dev/null +++ b/pkg/rabbitmq/publisher.go @@ -0,0 +1,54 @@ +package rabbitmq + +import "github.com/ereb-or-od/kenobi/pkg/rabbitmq/publisher" + +func NewPublisher( + connCh <-chan *Connection, + opts ...publisher.Option, +) (*publisher.Publisher, error) { + pubConnCh := make(chan *publisher.Connection) + + p, err := publisher.New(pubConnCh, opts...) + if err != nil { + return nil, err + } + + go proxyPublisherConn(connCh, pubConnCh, p.NotifyClosed()) + + return p, nil +} + +//nolint:dupl // ignore linter err +func proxyPublisherConn( + connCh <-chan *Connection, + publisherConnCh chan *publisher.Connection, + publisherCloseCh <-chan struct{}, +) { + go func() { + defer close(publisherConnCh) + + for { + select { + case conn, ok := <-connCh: + if !ok { + return + } + + publisherConn := publisher.NewConnection( + conn.AMQPConnection(), + conn.NotifyLost(), + ) + + select { + case publisherConnCh <- publisherConn: + case <-conn.NotifyLost(): + continue + case <-publisherCloseCh: + return + } + case <-publisherCloseCh: + return + } + } + }() +} diff --git a/pkg/rabbitmq/publisher/connection.go b/pkg/rabbitmq/publisher/connection.go new file mode 100644 index 0000000..f6febe6 --- /dev/null +++ b/pkg/rabbitmq/publisher/connection.go @@ -0,0 +1,22 @@ +package publisher + + +func NewConnection(amqpConn AMQPConnection, closeCh chan struct{}) *Connection { + return &Connection{ + amqpConn: amqpConn, + notifyClose: closeCh, + } +} + +type Connection struct { + amqpConn AMQPConnection + notifyClose chan struct{} +} + +func (c *Connection) AMQPConnection() AMQPConnection { + return c.amqpConn +} + +func (c *Connection) NotifyClose() chan struct{} { + return c.notifyClose +} diff --git a/pkg/rabbitmq/publisher/publisher.go b/pkg/rabbitmq/publisher/publisher.go new file mode 100644 index 0000000..a97640d --- /dev/null +++ b/pkg/rabbitmq/publisher/publisher.go @@ -0,0 +1,520 @@ +package publisher + +import ( + "context" + "fmt" + logger "github.com/ereb-or-od/kenobi/pkg/logging/interfaces" + "github.com/streadway/amqp" + "strings" + "sync" + "time" +) + +var errChannelClosed = fmt.Errorf("channel closed") + +type AMQPConnection interface { +} + +type Unready struct { + Err error +} + +type Ready struct{} + +type State struct { + Unready *Unready + Ready *Ready +} + +type AMQPChannel interface { + Publish(exchange, key string, mandatory, immediate bool, msg amqp.Publishing) error + NotifyClose(receiver chan *amqp.Error) chan *amqp.Error + NotifyFlow(c chan bool) chan bool + Close() error + NotifyPublish(confirm chan amqp.Confirmation) chan amqp.Confirmation + Confirm(noWait bool) error +} + +type Option func(p *Publisher) + +type Message struct { + Context context.Context + Exchange string + Key string + Mandatory bool + Immediate bool + ErrOnUnready bool + Publishing amqp.Publishing + ResultCh chan error +} + +type Publisher struct { + connCh <-chan *Connection + + ctx context.Context + cancelFunc context.CancelFunc + retryPeriod time.Duration + initFunc func(conn AMQPConnection) (AMQPChannel, error) + logger logger.Logger + + mu sync.Mutex + stateChs []chan State + + confirmation bool + confirmationBuffer uint + + closeCh chan struct{} + + publishingCh chan Message + + internalStateCh chan State +} + +func New( + connCh <-chan *Connection, + opts ...Option, +) (*Publisher, error) { + p := &Publisher{ + connCh: connCh, + + publishingCh: make(chan Message), + closeCh: make(chan struct{}), + internalStateCh: make(chan State), + } + + for _, opt := range opts { + opt(p) + } + + if p.ctx != nil { + p.ctx, p.cancelFunc = context.WithCancel(p.ctx) + } else { + p.ctx, p.cancelFunc = context.WithCancel(context.Background()) + } + + if p.retryPeriod == 0 { + p.retryPeriod = time.Second * 5 + } + + for _, stateCh := range p.stateChs { + if stateCh == nil { + return nil, fmt.Errorf("state chan must be not nil") + } + + if cap(stateCh) == 0 { + return nil, fmt.Errorf("state chan is unbuffered") + } + } + + if p.confirmation && p.confirmationBuffer < 1 { + return nil, fmt.Errorf("confirmation buffer size must be greater than 0") + } + + if p.initFunc == nil { + p.initFunc = func(conn AMQPConnection) (AMQPChannel, error) { + return conn.(*amqp.Connection).Channel() + } + } + + go p.connectionState() + + return p, nil +} + +func WithLogger(l logger.Logger) Option { + return func(p *Publisher) { + p.logger = l + } +} + +func WithContext(ctx context.Context) Option { + return func(p *Publisher) { + p.ctx = ctx + } +} + +func WithRestartSleep(dur time.Duration) Option { + return func(p *Publisher) { + p.retryPeriod = dur + } +} + +func WithInitFunc(f func(conn AMQPConnection) (AMQPChannel, error)) Option { + return func(p *Publisher) { + p.initFunc = f + } +} + +func WithNotify(stateCh chan State) Option { + return func(p *Publisher) { + p.stateChs = append(p.stateChs, stateCh) + } +} + +// WithConfirmation tells publisher to turn on publisher confirm mode. +// The buffer option tells how many messages might be in-flight. +// Once limit is reached no new messages could be published. +// The confirmation result is returned via msg.ResultCh. +func WithConfirmation(buffer uint) Option { + return func(p *Publisher) { + p.confirmationBuffer = buffer + p.confirmation = true + } +} + +func (p *Publisher) Notify(stateCh chan State) <-chan State { + if cap(stateCh) == 0 { + panic("state chan is unbuffered") + } + + select { + case state := <-p.internalStateCh: + stateCh <- state + case <-p.NotifyClosed(): + return stateCh + } + + p.mu.Lock() + p.stateChs = append(p.stateChs, stateCh) + p.mu.Unlock() + + return stateCh +} + +func (p *Publisher) Publish(msg Message) error { + return <-p.Go(msg) +} + +func (p *Publisher) Go(msg Message) <-chan error { + if msg.ResultCh == nil { + msg.ResultCh = make(chan error, 1) + } + if cap(msg.ResultCh) == 0 { + panic("amqpextra: resultCh channel is unbuffered") + } + if msg.Context == nil { + msg.Context = context.Background() + } + var stateCh <-chan State + if msg.ErrOnUnready { + stateCh = p.internalStateCh + } + select { + case <-p.closeCh: + msg.ResultCh <- fmt.Errorf("publisher stopped") + return msg.ResultCh + default: + } + +loop: + for { + select { + case p.publishingCh <- msg: + return msg.ResultCh + + case <-msg.Context.Done(): + msg.ResultCh <- fmt.Errorf("message: %v", msg.Context.Err()) + return msg.ResultCh + + // noinspection GoNilness + case state := <-stateCh: + if state.Unready != nil { + msg.ResultCh <- fmt.Errorf("publisher not ready") + return msg.ResultCh + } + continue loop + case <-p.ctx.Done(): + msg.ResultCh <- fmt.Errorf("publisher stopped") + return msg.ResultCh + } + } +} + +func (p *Publisher) Close() { + p.cancelFunc() +} + +func (p *Publisher) NotifyClosed() <-chan struct{} { + return p.closeCh +} + +func (p *Publisher) connectionState() { + defer p.cancelFunc() + defer close(p.closeCh) + defer p.logger.Debug("[DEBUG] publisher stopped") + + p.logger.Debug("[DEBUG] publisher starting") + state := State{Unready: &Unready{Err: amqp.ErrClosed}} + + for { + select { + case conn, ok := <-p.connCh: + if !ok { + return + } + select { + case <-conn.NotifyClose(): + continue + case <-p.ctx.Done(): + return + default: + } + err := p.channelState(conn.AMQPConnection(), conn.NotifyClose()) + if err != nil { + p.logger.Debug("[DEBUG] publisher unready") + state = State{Unready: &Unready{err}} + continue + } + + return + case p.internalStateCh <- state: + case <-p.ctx.Done(): + return + } + } +} + +func (p *Publisher) channelState(conn AMQPConnection, connCloseCh <-chan struct{}) error { + for { + ch, err := p.initFunc(conn) + if err != nil { + p.logger.Error("[ERROR] init func: %s", err) + return p.waitRetry(err) + } + + var resultChCh chan chan error + + confirmationCloseCh := make(chan struct{}) + confirmationDoneCh := make(chan struct{}) + if p.confirmation { + err = ch.Confirm(false) + if err != nil { + return p.waitRetry(err) + } + + confirmationCh := ch.NotifyPublish(make(chan amqp.Confirmation, p.confirmationBuffer)) + + resultChCh = make(chan chan error, p.confirmationBuffer) + + go p.handleConfirmations(resultChCh, confirmationCh, confirmationCloseCh, confirmationDoneCh) + } else { + close(confirmationDoneCh) + } + + err = p.publishState(ch, connCloseCh, resultChCh) + close(confirmationCloseCh) + if err == errChannelClosed { + <-confirmationDoneCh + continue + } + if err != nil { + p.notifyUnready(err) + } + + p.close(ch) + <-confirmationDoneCh + + return err + } +} + +func (p *Publisher) handleConfirmations( + resultChCh chan chan error, + confirmationCh chan amqp.Confirmation, + confirmationCloseCh, + confirmationDoneCh chan struct{}, +) { + defer close(confirmationDoneCh) + + select { + case state := <-p.internalStateCh: + if state.Unready != nil { + p.logger.Warn("[WARN] handle confirmation unexpected unready") + return + } + + p.logger.Debug("[DEBUG] handle confirmation ready") + case <-confirmationCloseCh: + return + } + + p.logger.Debug("[DEBUG] handle confirmation started") + defer p.logger.Debug("[DEBUG] handle confirmation stopped") + +loop: + for { + select { + case c, ok := <-confirmationCh: + if !ok { + break loop + } + + resultCh := <-resultChCh + if c.Ack { + resultCh <- nil + } else { + resultCh <- fmt.Errorf("confirmation: nack") + } + + continue + case <-confirmationCloseCh: + break loop + } + } + <-confirmationCloseCh + for { + select { + case resultCh := <-resultChCh: + resultCh <- amqp.ErrClosed + continue + default: + return + } + } +} + +func (p *Publisher) publishState(ch AMQPChannel, connCloseCh <-chan struct{}, resultChCh chan chan error) error { + chCloseCh := ch.NotifyClose(make(chan *amqp.Error, 1)) + chFlowCh := ch.NotifyFlow(make(chan bool, 1)) + + p.logger.Debug("[DEBUG] publisher ready") + state := p.notifyReady() + for { + select { + case p.internalStateCh <- state: + continue + case msg := <-p.publishingCh: + p.publish(ch, msg, resultChCh) + case <-chCloseCh: + p.logger.Debug("[DEBUG] channel closed") + return errChannelClosed + case <-connCloseCh: + return amqp.ErrClosed + case resume := <-chFlowCh: + if resume { + continue + } + if err := p.pausedState(chFlowCh, connCloseCh, chCloseCh); err != nil { + return err + } + state = p.notifyReady() + case <-p.ctx.Done(): + return nil + } + } +} + +func (p *Publisher) pausedState(chFlowCh <-chan bool, connCloseCh <-chan struct{}, chCloseCh chan *amqp.Error) error { + p.logger.Warn("[WARN] publisher flow paused") + errFlowPaused := fmt.Errorf("publisher flow paused") + state := p.notifyUnready(errFlowPaused) + for { + select { + case p.internalStateCh <- state: + continue + case resume := <-chFlowCh: + if resume { + p.logger.Info("[INFO] publisher flow resumed") + return nil + } + case <-chCloseCh: + p.logger.Debug("[DEBUG] channel closed") + return errChannelClosed + case <-connCloseCh: + return amqp.ErrClosed + case <-p.ctx.Done(): + return p.ctx.Err() + } + } +} + +func (p *Publisher) publish(ch AMQPChannel, msg Message, resultChCh chan chan error) { + select { + case <-msg.Context.Done(): + msg.ResultCh <- fmt.Errorf("message: %v", msg.Context.Err()) + default: + } + + result := ch.Publish( + msg.Exchange, + msg.Key, + msg.Mandatory, + msg.Immediate, + msg.Publishing, + ) + + if !p.confirmation { + msg.ResultCh <- result + return + } + + if result != nil { + msg.ResultCh <- result + return + } + + select { + case resultChCh <- msg.ResultCh: + case <-p.ctx.Done(): + msg.ResultCh <- p.ctx.Err() + return + } +} + +func (p *Publisher) waitRetry(err error) error { + timer := time.NewTimer(p.retryPeriod) + defer func() { + timer.Stop() + select { + case <-timer.C: + default: + } + }() + state := p.notifyUnready(err) + for { + select { + case p.internalStateCh <- state: + continue + case <-timer.C: + return err + case <-p.ctx.Done(): + return nil + } + } +} + +func (p *Publisher) notifyUnready(err error) State { + state := State{Unready: &Unready{Err: err}} + p.mu.Lock() + defer p.mu.Unlock() + for _, stateCh := range p.stateChs { + select { + case stateCh <- state: + case <-stateCh: + stateCh <- state + } + } + return state +} + +func (p *Publisher) notifyReady() State { + state := State{Ready: &Ready{}} + p.mu.Lock() + defer p.mu.Unlock() + for _, stateCh := range p.stateChs { + select { + case stateCh <- state: + case <-stateCh: + stateCh <- state + } + } + return state +} + +func (p *Publisher) close(ch AMQPChannel) { + if ch != nil { + if err := ch.Close(); err != nil && !strings.Contains(err.Error(), "channel/connection is not open") { + p.logger.Error("[WARN] publisher: channel close: %s", err) + } + } +} diff --git a/pkg/redis/clustered/clustered_redis_server.go b/pkg/redis/clustered/clustered_redis_server.go new file mode 100644 index 0000000..a6b5122 --- /dev/null +++ b/pkg/redis/clustered/clustered_redis_server.go @@ -0,0 +1,68 @@ +package clustered + +import ( + "context" + logger "github.com/ereb-or-od/kenobi/pkg/logging/interfaces" + marshallers "github.com/ereb-or-od/kenobi/pkg/marshalling/interfaces" + "github.com/ereb-or-od/kenobi/pkg/redis/interfaces" + "github.com/go-redis/redis/v8" + "time" +) + +type clusteredRedisServer struct { + logger logger.Logger + client *redis.ClusterClient + marshaller marshallers.Marshaller +} + +func (r clusteredRedisServer) DeleteValueByKey(ctx context.Context, key string) error { + commandResult := r.client.Del(ctx, key) + return commandResult.Err() +} + +func (r clusteredRedisServer) GetValueByKey(ctx context.Context, key string, result interface{}) error { + commandResult := r.client.Get(ctx, key) + if commandResult.Err() != nil { + if commandResult.Err().Error() == "redis: nil" { + return nil + } + return commandResult.Err() + } + + resultAsByteArray, err := commandResult.Bytes() + if err != nil { + return err + } + err = r.marshaller.Unmarshall(resultAsByteArray, &result) + if err != nil { + return err + } + + return nil +} + +func (r clusteredRedisServer) SetValue(ctx context.Context, key string, value interface{}, expiration time.Duration) error { + byteArray, err := r.marshaller.Marshall(&value) + if err != nil { + return err + } + commandResult := r.client.Set(ctx, key, byteArray, expiration) + if commandResult.Err() != nil { + return commandResult.Err() + } + return nil +} + +func New(logger logger.Logger, marshaller marshallers.Marshaller, options *RedisServerOptions) interfaces.RedisServer { + rdb := redis.NewClusterClient(&redis.ClusterOptions{ + Addrs: options.Addresses, + Password: options.Password, + Username: options.Username, + }) + + return &clusteredRedisServer{ + logger: logger, + client: rdb, + marshaller: marshaller, + } +} diff --git a/pkg/redis/clustered/clustered_redis_server_options.go b/pkg/redis/clustered/clustered_redis_server_options.go new file mode 100644 index 0000000..6bf044a --- /dev/null +++ b/pkg/redis/clustered/clustered_redis_server_options.go @@ -0,0 +1,8 @@ +package clustered + +import "github.com/ereb-or-od/kenobi/pkg/redis/options" + +type RedisServerOptions struct { + Addresses []string + *options.RedisServerOptions +} diff --git a/pkg/redis/failover/failover_redis_server.go b/pkg/redis/failover/failover_redis_server.go new file mode 100644 index 0000000..0d096df --- /dev/null +++ b/pkg/redis/failover/failover_redis_server.go @@ -0,0 +1,69 @@ +package failover + +import ( + "context" + logger "github.com/ereb-or-od/kenobi/pkg/logging/interfaces" + marshallers "github.com/ereb-or-od/kenobi/pkg/marshalling/interfaces" + "github.com/ereb-or-od/kenobi/pkg/redis/interfaces" + "github.com/go-redis/redis/v8" + "time" +) + +type failoverRedisServer struct { + logger logger.Logger + client *redis.Client + marshaller marshallers.Marshaller +} + +func (r failoverRedisServer) DeleteValueByKey(ctx context.Context, key string) error { + commandResult := r.client.Del(ctx, key) + return commandResult.Err() +} + +func (r failoverRedisServer) GetValueByKey(ctx context.Context, key string, result interface{}) error { + commandResult := r.client.Get(ctx, key) + if commandResult.Err() != nil { + if commandResult.Err().Error() == "redis: nil" { + return nil + } + return commandResult.Err() + } + + resultAsByteArray, err := commandResult.Bytes() + if err != nil { + return err + } + err = r.marshaller.Unmarshall(resultAsByteArray, &result) + if err != nil { + return err + } + + return nil +} + +func (r failoverRedisServer) SetValue(ctx context.Context, key string, value interface{}, expiration time.Duration) error { + byteArray, err := r.marshaller.Marshall(&value) + if err != nil { + return err + } + commandResult := r.client.Set(ctx, key, byteArray, expiration) + if commandResult.Err() != nil { + return commandResult.Err() + } + return nil +} + +func New(logger logger.Logger, marshaller marshallers.Marshaller, options *RedisServerOptions) interfaces.RedisServer { + rdb := redis.NewFailoverClient(&redis.FailoverOptions{ + MasterName: options.MasterName, + SentinelAddrs: options.SentinelAddrs, + SentinelPassword: options.SentinelPassword, + Username: options.Username, + Password: options.Password, + }) + return &failoverRedisServer{ + logger: logger, + client: rdb, + marshaller: marshaller, + } +} diff --git a/pkg/redis/failover/failover_redis_server_options.go b/pkg/redis/failover/failover_redis_server_options.go new file mode 100644 index 0000000..5c6c435 --- /dev/null +++ b/pkg/redis/failover/failover_redis_server_options.go @@ -0,0 +1,10 @@ +package failover + +import "github.com/ereb-or-od/kenobi/pkg/redis/options" + +type RedisServerOptions struct { + MasterName string + SentinelAddrs []string + SentinelPassword string + *options.RedisServerOptions +} diff --git a/pkg/redis/interfaces/redis_server.go b/pkg/redis/interfaces/redis_server.go new file mode 100644 index 0000000..e8abfe5 --- /dev/null +++ b/pkg/redis/interfaces/redis_server.go @@ -0,0 +1,12 @@ +package interfaces + +import ( + "context" + "time" +) + +type RedisServer interface{ + GetValueByKey(ctx context.Context, key string, result interface{}) error + SetValue(ctx context.Context, key string, value interface{}, expiration time.Duration) error + DeleteValueByKey(ctx context.Context, key string) error +} diff --git a/pkg/redis/options/redis_server_options.go b/pkg/redis/options/redis_server_options.go new file mode 100644 index 0000000..1af9a3b --- /dev/null +++ b/pkg/redis/options/redis_server_options.go @@ -0,0 +1,8 @@ +package options + +type RedisServerOptions struct{ + Username string + Password string + Database int +} + diff --git a/pkg/redis/standalone/standalone_redis_server.go b/pkg/redis/standalone/standalone_redis_server.go new file mode 100644 index 0000000..1b948fc --- /dev/null +++ b/pkg/redis/standalone/standalone_redis_server.go @@ -0,0 +1,67 @@ +package standalone + +import ( + "context" + logger "github.com/ereb-or-od/kenobi/pkg/logging/interfaces" + marshallers "github.com/ereb-or-od/kenobi/pkg/marshalling/interfaces" + "github.com/ereb-or-od/kenobi/pkg/redis/interfaces" + "github.com/go-redis/redis/v8" + "time" +) + +type standaloneRedisServer struct { + logger logger.Logger + client *redis.Client + marshaller marshallers.Marshaller +} + +func (r standaloneRedisServer) DeleteValueByKey(ctx context.Context, key string) error { + commandResult := r.client.Del(ctx, key) + return commandResult.Err() +} + +func (r standaloneRedisServer) GetValueByKey(ctx context.Context, key string, result interface{}) error { + commandResult := r.client.Get(ctx, key) + if commandResult.Err() != nil { + if commandResult.Err().Error() == "redis: nil" { + return nil + } + return commandResult.Err() + } + + resultAsByteArray, err := commandResult.Bytes() + if err != nil { + return err + } + err = r.marshaller.Unmarshall(resultAsByteArray, &result) + if err != nil { + return err + } + + return nil +} + +func (r standaloneRedisServer) SetValue(ctx context.Context, key string, value interface{}, expiration time.Duration) error { + byteArray, err := r.marshaller.Marshall(&value) + if err != nil { + return err + } + commandResult := r.client.Set(ctx, key, byteArray, expiration) + if commandResult.Err() != nil { + return commandResult.Err() + } + return nil +} + +func New(logger logger.Logger, marshaller marshallers.Marshaller, options *StandaloneRedisServerOptions) interfaces.RedisServer { + rdb := redis.NewClient(&redis.Options{ + Addr: options.Address, + Password: options.Password, + }) + + return &standaloneRedisServer{ + logger: logger, + client: rdb, + marshaller: marshaller, + } +} diff --git a/pkg/redis/standalone/standalone_redis_server_options.go b/pkg/redis/standalone/standalone_redis_server_options.go new file mode 100644 index 0000000..ee841d5 --- /dev/null +++ b/pkg/redis/standalone/standalone_redis_server_options.go @@ -0,0 +1,8 @@ +package standalone + +import "github.com/ereb-or-od/kenobi/pkg/redis/options" + +type StandaloneRedisServerOptions struct { + Address string + *options.RedisServerOptions +} diff --git a/pkg/server/kenobi_server.go b/pkg/server/kenobi_server.go new file mode 100644 index 0000000..6efc7f3 --- /dev/null +++ b/pkg/server/kenobi_server.go @@ -0,0 +1,221 @@ +package server + +import ( + "fmt" + controllers "github.com/ereb-or-od/kenobi/pkg/controller" + controllerBase "github.com/ereb-or-od/kenobi/pkg/controller/interfaces" + "github.com/ereb-or-od/kenobi/pkg/http/middlewares" + "github.com/ereb-or-od/kenobi/pkg/logging" + logger "github.com/ereb-or-od/kenobi/pkg/logging/interfaces" + "github.com/ereb-or-od/kenobi/pkg/logging/options" + serverOption "github.com/ereb-or-od/kenobi/pkg/server/options" + "github.com/ereb-or-od/kenobi/pkg/utilities" + "github.com/google/uuid" + "github.com/labstack/echo-contrib/prometheus" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "github.com/labstack/gommon/log" + "github.com/newrelic/go-agent/v3/integrations/nrecho-v4" + "github.com/newrelic/go-agent/v3/newrelic" + apmecho "github.com/opentracing-contrib/echo" + "github.com/opentracing/opentracing-go" + "github.com/tylerb/graceful" + "time" +) + +var ( + defaultServerPort = 80 + defaultExcludedEndpointsFromMetricsAndTracing = []string{"/metrics", "/stats", "/_stats", "/ping", "/health-check", "/healthy"} +) + +type KenobiServer struct { + serverOptions *serverOption.KenobiServerOptions + http *echo.Echo + controller controllerBase.ControllerBase + logger logger.Logger +} + +func New(name string) *KenobiServer { + return &KenobiServer{ + serverOptions: &serverOption.KenobiServerOptions{Name: name}, + } +} + +func (k *KenobiServer) WithLogger(logger logger.Logger) *KenobiServer { + if logger == nil { + panic("logger must be specified") + } + if k.logger != nil { + panic("you can not register logger more than once") + } + k.logger = logger + return k +} + +func (k *KenobiServer) WithDefaultLogger(options ...*options.LoggerOptions) *KenobiServer { + if k.logger != nil { + panic("you can not register logger more than once") + } + if defaultLogger, err := logging.New(options...); err != nil { + panic(err) + } else { + k.logger = defaultLogger + } + + return k +} + +func (k *KenobiServer) UseHttp() *KenobiServer { + k.http = echo.New() + k.http.HideBanner = true + return k +} + +func (k *KenobiServer) UsePrometheus(excludedEndpoints ...string) *KenobiServer { + k.serverOptions.Metric = &serverOption.KenobiServerMetricOptions{ExcludedEndpoints: excludedEndpoints} + p := prometheus.NewPrometheus(k.serverOptions.Name, k.defaultEndpointSkipper) + p.Use(k.http) + + return k +} + +func (k *KenobiServer) UseOpenTracing() *KenobiServer { + opentracing.SetGlobalTracer(opentracing.GlobalTracer()) + k.http.Use(apmecho.Middleware(k.serverOptions.Name)) + return k +} + +func (k *KenobiServer) defaultEndpointSkipper(c echo.Context) bool { + if utilities.ContainsInStringSlice(defaultExcludedEndpointsFromMetricsAndTracing, c.Path()) { + return true + } else { + return false + } +} + +func (k *KenobiServer) WithLoggingMiddleware() *KenobiServer { + k.http.Use(middlewares.LoggingMiddleware(k.logger)) + + return k +} +func (k *KenobiServer) WithGzipMiddleware() *KenobiServer { + k.http.Use(middleware.Gzip()) + return k +} + +func (k *KenobiServer) WithRequestIDMiddleware() *KenobiServer { + k.http.Use(middleware.RequestIDWithConfig(middleware.RequestIDConfig{Generator: func() string { + return uuid.NewString() + }})) + return k +} + +func (k *KenobiServer) WithAllowAnyCORSMiddleware() *KenobiServer { + k.http.Use(middleware.CORSWithConfig(middleware.CORSConfig{ + AllowOrigins: []string{"*"}, + AllowHeaders: []string{"*"}, + AllowMethods: []string{"*"}, + })) + return k +} + +func (k *KenobiServer) WithCORSMiddleware(allowsOrigins []string, allowsHeaders []string, allowsMethods []string) *KenobiServer { + k.http.Use(middleware.CORSWithConfig(middleware.CORSConfig{ + AllowOrigins: allowsOrigins, + AllowHeaders: allowsHeaders, + AllowMethods: allowsMethods, + })) + return k +} + +func (k *KenobiServer) WithTimeoutMiddleware(duration time.Duration) *KenobiServer { + k.http.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{ + Timeout: duration, + })) + return k +} + +func (k *KenobiServer) WithHealthCheckMiddleware(path string, response string) *KenobiServer { + k.http.Use(middlewares.HealthCheckMiddleware(path, response)) + return k +} + +func (k *KenobiServer) WithRecoverMiddleware() *KenobiServer { + k.http.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{ + StackSize: 4 << 10, + DisableStackAll: false, + DisablePrintStack: false, + LogLevel: log.ERROR, + })) + return k +} + +func (k *KenobiServer) WithCustomMiddlewares(middlewares ...echo.MiddlewareFunc) *KenobiServer { + if middlewares == nil || len(middlewares) == 0 { + panic("middlewares must be specified") + } + for _, customMiddleware := range middlewares { + k.http.Use(customMiddleware) + } + return k +} + +func (k *KenobiServer) WithNewRelicMiddleware(licenceKey string) *KenobiServer { + app, err := newrelic.NewApplication( + newrelic.ConfigAppName(k.serverOptions.Name), + newrelic.ConfigLicense(licenceKey)) + if err != nil { + panic(err) + } + k.http.Use(nrecho.Middleware(app)) + return k +} +func (k *KenobiServer) WithController(controller controllerBase.ControllerBase) *KenobiServer { + if controller == nil { + panic("controller must be specified") + } + k.controller = controller + + var ( + httpController controllers.HttpController + ) + switch svc := controller.(type) { + case controllers.HttpController: + httpController = svc + default: + panic("controller must implement to Service interface") + } + + if httpController != nil { + for path, endpintMethod := range *httpController.Endpoints() { + for method, endpointHandler := range endpintMethod { + var endpoint string + if len(httpController.Prefix()) > 0 { + endpoint += fmt.Sprintf("/%s", httpController.Prefix()) + } + if len(httpController.Version()) > 0 { + endpoint += fmt.Sprintf("/%s", httpController.Version()) + } + endpoint += fmt.Sprintf("%s", path) + httpController.Version() + k.http.Add(method, endpoint, endpointHandler) + } + } + } + return k +} + +func (k *KenobiServer) Start() { + k.http.Server.Addr = fmt.Sprintf(":%d", defaultServerPort) + k.http.Logger.Fatal(graceful.ListenAndServe(k.http.Server, 5*time.Second)) +} + +func (k *KenobiServer) StartWithOptions(options *serverOption.KenobiServerStartOptions) { + port := fmt.Sprintf(":%d", options.Port) + if options.GracefullyShutdown { + k.http.Server.Addr = port + k.http.Logger.Fatal(graceful.ListenAndServe(k.http.Server, options.GracefullyShutdownTimeoutPeriod)) + } else { + k.http.Logger.Fatal(k.http.Start(port)) + } +} diff --git a/pkg/server/options/kenobi_server_options.go b/pkg/server/options/kenobi_server_options.go new file mode 100644 index 0000000..d7a9f3c --- /dev/null +++ b/pkg/server/options/kenobi_server_options.go @@ -0,0 +1,26 @@ +package options + +import "time" + +type KenobiServerOptions struct { + Name string + Metric *KenobiServerMetricOptions +} + +type KenobiServerMetricOptions struct{ + ExcludedEndpoints []string +} + +type KenobiServerStartOptions struct { + Port int + GracefullyShutdown bool + GracefullyShutdownTimeoutPeriod time.Duration +} + +type KenobiServerJeagerOptions struct { + AgentHost string + AgentPort string + Endpoint string + User string + Password string +} \ No newline at end of file diff --git a/pkg/utilities/http_utility.go b/pkg/utilities/http_utility.go new file mode 100644 index 0000000..14fbc97 --- /dev/null +++ b/pkg/utilities/http_utility.go @@ -0,0 +1,36 @@ +package utilities + +import ( + "fmt" + "github.com/xiam/to" + "net" + "net/http" +) + +func GetForwardedIP(r *http.Request) string { + return r.Header.Get("X-Forwarded-For") +} +func GetIP(r *http.Request) (string, error) { + ip := to.String(r.Context().Value("ip")) + + ip = r.Header.Get("X-Real-IP") + if len(ip) > 0 { + return ip, nil + } + + // no nginx reverse proxy? + // get IP old fashioned way + ip, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + return "", fmt.Errorf("%q is not IP:port", r.RemoteAddr) + } + + userIP := net.ParseIP(ip) + if userIP == nil { + return "", fmt.Errorf("%q is not IP:port", r.RemoteAddr) + } + return userIP.String(), nil +} + + + diff --git a/pkg/utilities/os_utility.go b/pkg/utilities/os_utility.go new file mode 100644 index 0000000..3ee8950 --- /dev/null +++ b/pkg/utilities/os_utility.go @@ -0,0 +1,11 @@ +package utilities + +import "os" + +func GetHostName() string { + if name, err := os.Hostname(); err != nil { + return "" + } else { + return name + } +} diff --git a/pkg/utilities/string_slice_utility.go b/pkg/utilities/string_slice_utility.go new file mode 100644 index 0000000..629a93f --- /dev/null +++ b/pkg/utilities/string_slice_utility.go @@ -0,0 +1,30 @@ +package utilities + +func CompareSlices(a, b []string) bool { + if (a == nil) != (b == nil) { + return false + } + + if len(a) != len(b) { + return false + } + + for i := range a { + if a[i] != b[i] { + return false + } + } + + return true +} + +func ContainsInStringSlice(slice []string, value string) bool{ + if slice != nil && len(slice) > 0{ + for _, element := range slice{ + if element == value{ + return true + } + } + } + return false +} \ No newline at end of file diff --git a/pkg/utilities/string_utility.go b/pkg/utilities/string_utility.go new file mode 100644 index 0000000..e1e9781 --- /dev/null +++ b/pkg/utilities/string_utility.go @@ -0,0 +1,18 @@ +package utilities + +import ( + "go.mongodb.org/mongo-driver/bson/primitive" + "strings" +) + +func ToObjectID(id string) (primitive.ObjectID, error){ + if objectId, err := primitive.ObjectIDFromHex(id); err != nil{ + return primitive.ObjectID{}, err + }else{ + return objectId, nil + } +} + +func IsStringEmpty(str string) bool { + return len(strings.TrimSpace(str)) == 0 +}