summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorValery Piashchynski <[email protected]>2020-12-26 00:40:31 +0300
committerValery Piashchynski <[email protected]>2020-12-26 00:40:31 +0300
commit7a0dee1a416705c621edbf50e1f43fb39845348f (patch)
tree0007a6b8c8ac9e7d31b8a5f3f7f27669c860d261
parent8526c03822e724bc2ebb64b6197085fea335b782 (diff)
Huge tests refactoring. Reduce running time 2-3x times
-rwxr-xr-xMakefile11
-rw-r--r--cmd/cli/root.go4
-rw-r--r--cmd/cli/workers.go2
-rw-r--r--cmd/main.go18
-rwxr-xr-xgo.mod17
-rwxr-xr-xgo.sum263
-rw-r--r--plugins/checker/config.go5
-rw-r--r--plugins/checker/interface.go11
-rw-r--r--plugins/checker/plugin.go151
-rw-r--r--plugins/checker/rpc.go27
-rw-r--r--plugins/config/interface.go22
-rwxr-xr-xplugins/config/plugin.go75
-rw-r--r--plugins/doc/graphviz.svg169
-rw-r--r--plugins/gzip/plugin.go25
-rw-r--r--plugins/headers/config.go36
-rw-r--r--plugins/headers/plugin.go117
-rw-r--r--plugins/http/attributes/attributes.go (renamed from pkg/plugins/http/attributes/attributes.go)0
-rw-r--r--plugins/http/config.go (renamed from pkg/plugins/http/config.go)0
-rw-r--r--plugins/http/constants.go (renamed from pkg/plugins/http/constants.go)0
-rw-r--r--plugins/http/errors.go (renamed from pkg/plugins/http/errors.go)0
-rw-r--r--plugins/http/errors_windows.go (renamed from pkg/plugins/http/errors_windows.go)0
-rw-r--r--plugins/http/handler.go (renamed from pkg/plugins/http/handler.go)2
-rw-r--r--plugins/http/parse.go (renamed from pkg/plugins/http/parse.go)0
-rw-r--r--plugins/http/plugin.go (renamed from pkg/plugins/http/plugin.go)10
-rw-r--r--plugins/http/request.go (renamed from pkg/plugins/http/request.go)4
-rw-r--r--plugins/http/response.go (renamed from pkg/plugins/http/response.go)0
-rw-r--r--plugins/http/uploads.go (renamed from pkg/plugins/http/uploads.go)2
-rw-r--r--plugins/http/uploads_config.go (renamed from pkg/plugins/http/uploads_config.go)0
-rw-r--r--plugins/informer/interface.go (renamed from pkg/plugins/informer/interface.go)0
-rw-r--r--plugins/informer/plugin.go (renamed from pkg/plugins/informer/plugin.go)2
-rw-r--r--plugins/informer/rpc.go (renamed from pkg/plugins/informer/rpc.go)2
-rw-r--r--plugins/logger/config.go94
-rw-r--r--plugins/logger/encoder.go66
-rw-r--r--plugins/logger/interface.go16
-rw-r--r--plugins/logger/plugin.go69
-rw-r--r--plugins/logger/zap_adapter.go56
-rw-r--r--plugins/metrics/config.go138
-rw-r--r--plugins/metrics/config_test.go89
-rw-r--r--plugins/metrics/doc.go1
-rw-r--r--plugins/metrics/interface.go7
-rw-r--r--plugins/metrics/plugin.go229
-rw-r--r--plugins/metrics/rpc.go294
-rw-r--r--plugins/redis/config.go32
-rw-r--r--plugins/redis/interface.go9
-rw-r--r--plugins/redis/plugin.go75
-rw-r--r--plugins/reload/config.go58
-rw-r--r--plugins/reload/plugin.go159
-rw-r--r--plugins/reload/watcher.go374
-rw-r--r--plugins/resetter/interface.go17
-rw-r--r--plugins/resetter/plugin.go80
-rw-r--r--plugins/resetter/rpc.go30
-rw-r--r--plugins/rpc/config.go47
-rw-r--r--plugins/rpc/doc/plugin_arch.drawio1
-rw-r--r--plugins/rpc/interface.go7
-rw-r--r--plugins/rpc/plugin.go164
-rw-r--r--plugins/rpc/util.go57
-rw-r--r--plugins/server/config.go (renamed from pkg/plugins/server/config.go)0
-rw-r--r--plugins/server/interface.go (renamed from pkg/plugins/server/interface.go)0
-rw-r--r--plugins/server/plugin.go (renamed from pkg/plugins/server/plugin.go)4
-rw-r--r--plugins/static/config.go76
-rw-r--r--plugins/static/plugin.go110
-rw-r--r--tests/mocks/mock_log.go2
-rwxr-xr-xtests/plugins/checker/configs/.rr-checker-init.yaml31
-rw-r--r--tests/plugins/checker/plugin_test.go190
-rwxr-xr-xtests/plugins/config/.rr.yaml18
-rwxr-xr-xtests/plugins/config/config_test.go66
-rwxr-xr-xtests/plugins/config/plugin1.go53
-rw-r--r--tests/plugins/gzip/configs/.rr-http-middlewareNotExist.yaml25
-rw-r--r--tests/plugins/gzip/configs/.rr-http-withGzip.yaml25
-rw-r--r--tests/plugins/gzip/plugin_test.go176
-rw-r--r--tests/plugins/headers/configs/.rr-cors-headers.yaml39
-rw-r--r--tests/plugins/headers/configs/.rr-headers-init.yaml39
-rw-r--r--tests/plugins/headers/configs/.rr-req-headers.yaml32
-rw-r--r--tests/plugins/headers/configs/.rr-res-headers.yaml32
-rw-r--r--tests/plugins/headers/headers_plugin_test.go367
-rw-r--r--tests/plugins/http/attributes_test.go2
-rw-r--r--tests/plugins/http/handler_test.go2
-rw-r--r--tests/plugins/http/http_plugin_test.go (renamed from tests/plugins/http/http_test.go)85
-rw-r--r--tests/plugins/http/parse_test.go2
-rw-r--r--tests/plugins/http/plugin1.go2
-rw-r--r--tests/plugins/http/plugin_middleware.go2
-rw-r--r--tests/plugins/http/response_test.go2
-rw-r--r--tests/plugins/http/uploads_config_test.go2
-rw-r--r--tests/plugins/http/uploads_test.go2
-rw-r--r--tests/plugins/informer/.rr-informer.yaml2
-rw-r--r--tests/plugins/informer/informer_test.go67
-rw-r--r--tests/plugins/informer/test_plugin.go4
-rw-r--r--tests/plugins/logger/.rr.yaml3
-rw-r--r--tests/plugins/logger/logger_test.go79
-rw-r--r--tests/plugins/logger/plugin.go40
-rw-r--r--tests/plugins/metrics/.rr-test.yaml16
-rw-r--r--tests/plugins/metrics/docker-compose.yml7
-rw-r--r--tests/plugins/metrics/metrics_test.go739
-rw-r--r--tests/plugins/metrics/plugin1.go46
-rw-r--r--tests/plugins/mocks/mock_log.go150
-rw-r--r--tests/plugins/redis/plugin1.go43
-rw-r--r--tests/plugins/redis/redis_plugin_test.go120
-rw-r--r--tests/plugins/reload/config_test.go63
-rw-r--r--tests/plugins/reload/configs/.rr-reload-2.yaml44
-rw-r--r--tests/plugins/reload/configs/.rr-reload-3.yaml46
-rw-r--r--tests/plugins/reload/configs/.rr-reload-4.yaml46
-rw-r--r--tests/plugins/reload/configs/.rr-reload.yaml44
-rw-r--r--tests/plugins/reload/reload_plugin_test.go827
-rw-r--r--tests/plugins/resetter/.rr-resetter.yaml16
-rw-r--r--tests/plugins/resetter/resetter_test.go113
-rw-r--r--tests/plugins/resetter/test_plugin.go66
-rwxr-xr-xtests/plugins/rpc/config_test.go135
-rw-r--r--tests/plugins/rpc/configs/.rr-rpc-disabled.yaml6
-rw-r--r--tests/plugins/rpc/configs/.rr.yaml6
-rw-r--r--tests/plugins/rpc/plugin1.go42
-rw-r--r--tests/plugins/rpc/plugin2.go53
-rw-r--r--tests/plugins/rpc/rpc_test.go188
-rw-r--r--tests/plugins/server/plugin_pipes.go4
-rw-r--r--tests/plugins/server/plugin_sockets.go4
-rw-r--r--tests/plugins/server/plugin_tcp.go4
-rw-r--r--tests/plugins/server/server_plugin_test.go (renamed from tests/plugins/server/server_test.go)49
-rw-r--r--tests/plugins/static/config_test.go49
-rw-r--r--tests/plugins/static/configs/.rr-http-static-disabled.yaml33
-rw-r--r--tests/plugins/static/configs/.rr-http-static-files-disable.yaml33
-rw-r--r--tests/plugins/static/configs/.rr-http-static-files.yaml34
-rw-r--r--tests/plugins/static/configs/.rr-http-static.yaml31
-rw-r--r--tests/plugins/static/static_plugin_test.go437
122 files changed, 7833 insertions, 386 deletions
diff --git a/Makefile b/Makefile
index 9f8323a5..01f7d205 100755
--- a/Makefile
+++ b/Makefile
@@ -32,7 +32,18 @@ test: ## Run application tests
go test -v -race -cover -tags=debug -covermode=atomic ./pkg/worker
go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/http
go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/informer
+ go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/reload
go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/server
+ go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/checker
+ go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/config
+ go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/gzip
+ go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/headers
+ go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/logger
+ go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/metrics
+ go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/redis
+ go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/resetter
+ go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/rpc
+ go test -v -race -cover -tags=debug -covermode=atomic ./tests/plugins/static
lint: ## Run application linters
go fmt ./...
diff --git a/cmd/cli/root.go b/cmd/cli/root.go
index febe410b..eb15f565 100644
--- a/cmd/cli/root.go
+++ b/cmd/cli/root.go
@@ -8,9 +8,9 @@ import (
"github.com/spiral/errors"
goridgeRpc "github.com/spiral/goridge/v3/pkg/rpc"
- rpcPlugin "github.com/spiral/roadrunner-plugins/rpc"
+ rpcPlugin "github.com/spiral/roadrunner/v2/plugins/rpc"
- "github.com/spiral/roadrunner-plugins/config"
+ "github.com/spiral/roadrunner/v2/plugins/config"
"github.com/spf13/cobra"
"github.com/spiral/endure"
diff --git a/cmd/cli/workers.go b/cmd/cli/workers.go
index 94c339e2..03639aa4 100644
--- a/cmd/cli/workers.go
+++ b/cmd/cli/workers.go
@@ -13,7 +13,7 @@ import (
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/spiral/errors"
- "github.com/spiral/roadrunner/v2/pkg/plugins/informer"
+ "github.com/spiral/roadrunner/v2/plugins/informer"
"github.com/spiral/roadrunner/v2/tools"
)
diff --git a/cmd/main.go b/cmd/main.go
index ec3d84ff..4e7fc099 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -4,17 +4,17 @@ import (
"log"
"github.com/spiral/endure"
- "github.com/spiral/roadrunner/v2/pkg/plugins/http"
- "github.com/spiral/roadrunner/v2/pkg/plugins/informer"
- "github.com/spiral/roadrunner/v2/pkg/plugins/server"
+ "github.com/spiral/roadrunner/v2/plugins/http"
+ "github.com/spiral/roadrunner/v2/plugins/informer"
+ "github.com/spiral/roadrunner/v2/plugins/server"
- "github.com/spiral/roadrunner-plugins/logger"
- "github.com/spiral/roadrunner-plugins/metrics"
- "github.com/spiral/roadrunner-plugins/redis"
- "github.com/spiral/roadrunner-plugins/reload"
- "github.com/spiral/roadrunner-plugins/resetter"
- "github.com/spiral/roadrunner-plugins/rpc"
"github.com/spiral/roadrunner/v2/cmd/cli"
+ "github.com/spiral/roadrunner/v2/plugins/logger"
+ "github.com/spiral/roadrunner/v2/plugins/metrics"
+ "github.com/spiral/roadrunner/v2/plugins/redis"
+ "github.com/spiral/roadrunner/v2/plugins/reload"
+ "github.com/spiral/roadrunner/v2/plugins/resetter"
+ "github.com/spiral/roadrunner/v2/plugins/rpc"
)
func main() {
diff --git a/go.mod b/go.mod
index e2c12c7f..9eeafd4b 100755
--- a/go.mod
+++ b/go.mod
@@ -3,32 +3,33 @@ module github.com/spiral/roadrunner/v2
go 1.15
require (
+ github.com/NYTimes/gziphandler v1.1.1
+ github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect
+ github.com/alicebob/miniredis/v2 v2.14.1
github.com/buger/goterm v0.0.0-20200322175922-2f3e71b85129
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4
github.com/fatih/color v1.10.0
+ github.com/go-ole/go-ole v1.2.4 // indirect
+ github.com/go-redis/redis/v8 v8.4.4
+ github.com/gofiber/fiber/v2 v2.3.0
github.com/golang/mock v1.4.4
github.com/hashicorp/go-multierror v1.1.0
github.com/json-iterator/go v1.1.10
github.com/mattn/go-runewidth v0.0.9
github.com/olekukonko/tablewriter v0.0.4
+ github.com/prometheus/client_golang v0.9.3
github.com/shirou/gopsutil v3.20.11+incompatible
github.com/spf13/cobra v1.1.1
+ github.com/spf13/viper v1.7.0
github.com/spiral/endure v1.0.0-beta20
github.com/spiral/errors v1.0.6
github.com/spiral/goridge/v3 v3.0.0-beta8
- github.com/spiral/roadrunner-plugins/checker v1.0.1
- github.com/spiral/roadrunner-plugins/config v1.0.1
- github.com/spiral/roadrunner-plugins/logger v1.0.2
- github.com/spiral/roadrunner-plugins/metrics v1.0.1
- github.com/spiral/roadrunner-plugins/redis v1.0.1
- github.com/spiral/roadrunner-plugins/reload v1.0.1
- github.com/spiral/roadrunner-plugins/resetter v1.0.1
- github.com/spiral/roadrunner-plugins/rpc v1.0.2
github.com/stretchr/testify v1.6.1
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a
github.com/vbauerster/mpb/v5 v5.4.0
github.com/yookoala/gofast v0.4.0
go.uber.org/multierr v1.6.0
+ go.uber.org/zap v1.16.0
golang.org/x/net v0.0.0-20201216054612-986b41b23924
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a
golang.org/x/sys v0.0.0-20201221093633-bc327ba9c2f0
diff --git a/go.sum b/go.sum
index 783b9195..2798d923 100755
--- a/go.sum
+++ b/go.sum
@@ -14,52 +14,35 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
-github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
+github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
-github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
-github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM=
github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA=
-github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
-github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
+github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
+github.com/alicebob/miniredis/v2 v2.14.1 h1:GjlbSeoJ24bzdLRs13HoMEeaRZx9kg5nHoRW7QV/nCs=
github.com/alicebob/miniredis/v2 v2.14.1/go.mod h1:uS970Sw5Gs9/iK3yBg0l9Uj9s25wXxSpQUE9EaJ/Blg=
github.com/andybalholm/brotli v1.0.0 h1:7UCwP93aiSfvWpapti8g88vVVGp2qqtGyePsSuDafo4=
github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
-github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
-github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
-github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
-github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
-github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
-github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
-github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
-github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/buger/goterm v0.0.0-20200322175922-2f3e71b85129 h1:gfAMKE626QEuKG3si0pdTRcr/YEbBoxY+3GOH3gWvl4=
github.com/buger/goterm v0.0.0-20200322175922-2f3e71b85129/go.mod h1:u9UyCz2eTrSGy6fbupqJ54eY5c4IC8gREQ1053dK12U=
-github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
-github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
-github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/cenkalti/backoff/v4 v4.1.0 h1:c8LkOFQTzuO0WBM/ae5HdGQuZPfPxp7lqBRwQRm4fSc=
github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
-github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
@@ -67,21 +50,13 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
-github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
-github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
-github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
-github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
-github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
-github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
-github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
-github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -91,19 +66,10 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cu
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4 h1:qk/FSDDxo05wdJH28W+p5yivv7LuLYLRXPPD8KQCtZs=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
-github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
-github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
-github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
-github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
-github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
-github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
-github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
-github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
@@ -112,28 +78,20 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-ini/ini v1.38.1/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
-github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
github.com/go-redis/redis/v8 v8.4.4 h1:fGqgxCTR1sydaKI00oQf3OmkU/DIe/I/fYXvGklCIuc=
github.com/go-redis/redis/v8 v8.4.4/go.mod h1:nA0bQuF0i5JFx4Ta9RZxGKXFrQ8cRWntra97f0196iY=
github.com/go-restit/lzjson v0.0.0-20161206095556-efe3c53acc68/go.mod h1:7vXSKQt83WmbPeyVjCfNT9YDJ5BUFmcwFsEjI9SCvYM=
-github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofiber/fiber/v2 v2.3.0 h1:82ufvLne0cxzdkDOeLkUmteA+z1uve9JQ/ZFsMOnkzc=
github.com/gofiber/fiber/v2 v2.3.0/go.mod h1:f8BRRIMjMdRyt2qmJ/0Sea3j3rwwfufPrh9WNBRiVZ0=
-github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
-github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s=
@@ -149,10 +107,8 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
-github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -166,26 +122,17 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
-github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
-github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
-github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
-github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
-github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
-github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
-github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
-github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
-github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
@@ -200,7 +147,6 @@ github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerX
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
-github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@@ -211,16 +157,10 @@ github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0m
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
-github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
-github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
-github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
-github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
@@ -228,23 +168,18 @@ github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVY
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
-github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.10.7 h1:7rix8v8GpI3ZBb0nSozFRgbtXKv+hOe+qfEpZqybrAg=
github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
-github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
-github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
@@ -253,10 +188,8 @@ github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
-github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
-github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
@@ -279,107 +212,51 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
-github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
-github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
-github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
-github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
-github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
-github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
-github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
-github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
-github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M=
github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
-github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.10.4 h1:NiTx7EEvBzu9sFOD1zORteLSt3o8gnlvZZwSE9TnY9U=
github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuKHUCQ=
-github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
-github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
-github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
-github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
-github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
-github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
-github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
-github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
-github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
-github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
-github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
-github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
-github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
-github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
-github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
-github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
-github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
-github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
-github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
-github.com/prometheus/client_golang v1.9.0 h1:Rrch9mh17XcxvEu9D9DEpb4isxjGBtcevQjKvxPRQIU=
-github.com/prometheus/client_golang v1.9.0/go.mod h1:FqZLKOZnGdFAhOK4nqGHa7D66IdsO+O441Eve7ptJDU=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
-github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
-github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
-github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
-github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
-github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
-github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
-github.com/prometheus/common v0.15.0 h1:4fgOnadei3EZvgRwxJ7RMpG1k1pOZth5Pc13tyspaKM=
-github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
-github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
-github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
-github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
-github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
-github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4=
-github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
-github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
-github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shirou/gopsutil v3.20.11+incompatible h1:LJr4ZQK4mPpIV5gOa4jCOKOGb4ty4DZO54I4FGqIpto=
github.com/shirou/gopsutil v3.20.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
-github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
-github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/smartystreets/assertions v0.0.0-20180820201707-7c9eb446e3cf/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
@@ -387,25 +264,21 @@ github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:X
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
-github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4=
github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
-github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
-github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk=
-github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/spiral/endure v1.0.0-beta20 h1:QD3EJ6CRLgeo/6trfnlUcQhH3vrK8Hvf9ceDpde+yss=
github.com/spiral/endure v1.0.0-beta20/go.mod h1:qCU2/4gAItVESzUK0yPExmUTlTcpRLqJUgcV+nqxn+o=
github.com/spiral/errors v1.0.4/go.mod h1:SwMSZVdZkkJVgXNNafccqOaxWg0XPzVU/dEdUEInE0o=
@@ -415,72 +288,6 @@ github.com/spiral/errors v1.0.6 h1:berk5ShEILSw6DplUVv9Ea1wGdk2WlVKQpuvDngll0U=
github.com/spiral/errors v1.0.6/go.mod h1:SwMSZVdZkkJVgXNNafccqOaxWg0XPzVU/dEdUEInE0o=
github.com/spiral/goridge/v3 v3.0.0-beta8 h1:x8uXCdhY49U1LEvmehnTaD2El6J9ZHAefRdh/QIZ6A4=
github.com/spiral/goridge/v3 v3.0.0-beta8/go.mod h1:XFQGc42KNzo/hPIXPki7mEkFTf9v/T7qFk/TYJjMtzE=
-github.com/spiral/roadrunner-plugins v0.0.0-20201224162001-79cb05174578 h1:zFQuyy/RMMVsQtD6y2RAPO4eBsWwGvsB04BuEmXOVD4=
-github.com/spiral/roadrunner-plugins/a_plugin_tests v0.0.0-20201224162001-79cb05174578 h1:E4cenNwBMi0F9327C7WMKx484MwFs5etbRcYKs1+EgY=
-github.com/spiral/roadrunner-plugins/a_plugin_tests v0.0.0-20201224162001-79cb05174578/go.mod h1:0aZijFwgc35Px78nXqOZNjJjd1jaHGxejGJrd8dJnuc=
-github.com/spiral/roadrunner-plugins/checker v1.0.1 h1:UVMX9zUF0R3ScRFUQg89e5+CLn3I2e5uAKsD4A52iDU=
-github.com/spiral/roadrunner-plugins/checker v1.0.1/go.mod h1:/gnVybeKye5uq/8hgjI7YDoK/ntPd0ej6tWkxlYdiz8=
-github.com/spiral/roadrunner-plugins/checker v1.0.3 h1:fnWX2PBXlNhYp8UERgFx/eMM1Qf4pDJCeUY4NoiWwbo=
-github.com/spiral/roadrunner-plugins/checker v1.0.3/go.mod h1:ZLMg9NJsqUuCG7akU2V6JAFd/9ZLjawzvoJRYZxr5b0=
-github.com/spiral/roadrunner-plugins/config v1.0.0 h1:RPYB8Ha/UeuBGRwtcqNb0uU8R5qe4XpUXVmv/lGSUdQ=
-github.com/spiral/roadrunner-plugins/config v1.0.0/go.mod h1:XcVJLFDUlYPvZ3kVzssmP4fJbEzUvVJf534+eZaotAo=
-github.com/spiral/roadrunner-plugins/config v1.0.1 h1:6n9mFqvy+Fk3LaOWfNSA7lb5o2vJOnGJivNFP7Fg8QA=
-github.com/spiral/roadrunner-plugins/config v1.0.1/go.mod h1:MQGXiAP8Sr6/LjFVcr+AeUElNMKJXnwVEKXn1I5ztXk=
-github.com/spiral/roadrunner-plugins/gzip v1.0.0/go.mod h1:gQSyvp7u8udOwZac5o2vcfrGANIaozyrIU1Q+xXq328=
-github.com/spiral/roadrunner-plugins/headers v1.0.0/go.mod h1:bZAGI0wo465MlYNx0Urpss0GyQDJQY5tcY9YrgiUElE=
-github.com/spiral/roadrunner-plugins/http v1.0.0 h1:8WGAuZOrkYZQWo6n13ip+ZtzhKugZZ+b5W+ivVr7FjI=
-github.com/spiral/roadrunner-plugins/http v1.0.0/go.mod h1:37ReUuAKJDtXH3GjMjRH5q3plBXq5r5lUfltRTVZzDE=
-github.com/spiral/roadrunner-plugins/http v1.0.2 h1:BRmdbx0DYlgYTkvmd1IDZJewC1y30GqBCKRjbTn3OLc=
-github.com/spiral/roadrunner-plugins/http v1.0.2/go.mod h1:JRgVSgJRh6wDmVs1rFJHQ9PM0xtCpzX9vBtVDItXZ/E=
-github.com/spiral/roadrunner-plugins/http v1.0.5 h1:6uHbc2OwhExBE9Psfwc2WDn6KeIXzLjiCCwO9yBH9JU=
-github.com/spiral/roadrunner-plugins/http v1.0.5/go.mod h1:9ZlPZJ1A7M3aBQ/tJYjmuXYPox3smFP8ccsf+EBHd9c=
-github.com/spiral/roadrunner-plugins/informer v1.0.3 h1:6DXs8IRDjBvAr2mGKXVlhx1KLLfVqpXgvP2va79kR1s=
-github.com/spiral/roadrunner-plugins/informer v1.0.3/go.mod h1:OPEJNADBbNQyx0/KuXQbY3Mqemo30vZh6duf6YpRf7M=
-github.com/spiral/roadrunner-plugins/informer v1.0.4 h1:J2DXrQkRRyYDGw/6TIo7qF74Zn3AOHQe+9g0Hj5BwR4=
-github.com/spiral/roadrunner-plugins/informer v1.0.4/go.mod h1:4qvFbVw0ESwE5RJQRiP939QN3SJ8l6aDnrQhL09f5lQ=
-github.com/spiral/roadrunner-plugins/informer v1.0.5 h1:L27oLcHONKL3L4X3uR/6ZSZF9iz27urT+uIsWGOM4e4=
-github.com/spiral/roadrunner-plugins/informer v1.0.5/go.mod h1:BcJewp6L0cEvX8uaYwemsWa7roVCLjQGEsAeBsRvmN0=
-github.com/spiral/roadrunner-plugins/logger v1.0.1 h1:cc/pRE+8bUQgLiF05G7f948UpWtrZmJk/axTh1ywrks=
-github.com/spiral/roadrunner-plugins/logger v1.0.1/go.mod h1:/oX1P56+/joLaAPwZaiRqPVVzM36701kdYqOHu+U3EM=
-github.com/spiral/roadrunner-plugins/logger v1.0.2 h1:4dBDKMUYdwE3jQ6ld1cR7E+ApRE30cDFudHSwDpgmx8=
-github.com/spiral/roadrunner-plugins/logger v1.0.2/go.mod h1:TGafnqSIQak7e+7mkrdatl379VVlK0kZE874ZC5exPk=
-github.com/spiral/roadrunner-plugins/metrics v1.0.0 h1:PEK+AlcMv6yrH/vag7yQm/5sV7gMoWsNn9KWOsDPw/0=
-github.com/spiral/roadrunner-plugins/metrics v1.0.0/go.mod h1:zLd9TdARJSwo6tQjTb0auD7pxN2ODXnsy4evInCMp9o=
-github.com/spiral/roadrunner-plugins/metrics v1.0.1 h1:Ifbioo0BgsMk83sQfv36hpDwqC5iL4SFaEsLKeTRdXs=
-github.com/spiral/roadrunner-plugins/metrics v1.0.1/go.mod h1:1Ixk+pmFcZQEigB/km6ABkCwhTl4ph3YWDZMN/g4YHI=
-github.com/spiral/roadrunner-plugins/redis v1.0.0 h1:/J4JpbNLd0G23BD9VaHsXKR8OHyAnrRQf7rRZMFx57U=
-github.com/spiral/roadrunner-plugins/redis v1.0.0/go.mod h1:VzXRQcNaNONaUxyCW9t9eC+p8UxkXx2kwGeVw3s7zR0=
-github.com/spiral/roadrunner-plugins/redis v1.0.1 h1:RGVbCsADfoEXnIeIJvBRsxoMxVh7Gx5p/YrVnYPTyd8=
-github.com/spiral/roadrunner-plugins/redis v1.0.1/go.mod h1:r9FiOvBzwiWEWNMuQf45j7c8X01nrR3bpS1BxZl0bfY=
-github.com/spiral/roadrunner-plugins/reload v1.0.0 h1:cBevNueyJjG4ykTjd9C4WSV8xNJq3UuXh3ympp3GU0I=
-github.com/spiral/roadrunner-plugins/reload v1.0.0/go.mod h1:IEDqSrV0zbnvCNXigLtp0EoGVsHc34merlU2gokiuDs=
-github.com/spiral/roadrunner-plugins/reload v1.0.1 h1:K7uvITE2B9bZyYWXk2b82erCeWjFBdjcCdfcXDVBjlU=
-github.com/spiral/roadrunner-plugins/reload v1.0.1/go.mod h1:IEDqSrV0zbnvCNXigLtp0EoGVsHc34merlU2gokiuDs=
-github.com/spiral/roadrunner-plugins/resetter v1.0.0 h1:Nx4mIzeoH/IcUfY4LM9xhY0yAXXYkAIAqWCRdMcsr68=
-github.com/spiral/roadrunner-plugins/resetter v1.0.0/go.mod h1:DLFifJk1n3PWViXkT5+qAmzeRcPTowDRSbRqotf+WlE=
-github.com/spiral/roadrunner-plugins/resetter v1.0.1 h1:U9nrxhz8graviJ8LjClFCA3+H346Obhr/xsoCwTlAsQ=
-github.com/spiral/roadrunner-plugins/resetter v1.0.1/go.mod h1:vz7RFxWu0P79OGoGN0w2x63RIqszZCCGjEvMwtHHjDE=
-github.com/spiral/roadrunner-plugins/rpc v1.0.0 h1:cC17yCNqQUNtedKefdeT2P6z7q52L0QakxS4qwB7n+g=
-github.com/spiral/roadrunner-plugins/rpc v1.0.0/go.mod h1:p+ClRf1ibW+xvekf+nGQtvipyrtPJP6WZ/J/DSp+Qck=
-github.com/spiral/roadrunner-plugins/rpc v1.0.1 h1:rbMJNM+4opzoB7ZXi9dbntfix9+cTuQdUMcDIWsaC7k=
-github.com/spiral/roadrunner-plugins/rpc v1.0.1/go.mod h1:iz1a3poEKCppX1fYfneMWOxWBfToYSi2TE1rYD0yBls=
-github.com/spiral/roadrunner-plugins/rpc v1.0.2 h1:1ICsIasYP8W6cEAMEzU6H+VqGh3AFaCWyy4w8VfRYg4=
-github.com/spiral/roadrunner-plugins/rpc v1.0.2/go.mod h1:DAioNb54q5WIOOzdo+2IJB9IysM2WI7f9nzQcNBp2Xk=
-github.com/spiral/roadrunner-plugins/server v1.0.1 h1:yzCw5rgz+KHpJb56QIID3gWH/IBbNtHZD2zvkck1bYc=
-github.com/spiral/roadrunner-plugins/server v1.0.1/go.mod h1:qedfnQFlK1+Jwv5M8mRXJCOWTvF/qfFyULK/0UMBeOk=
-github.com/spiral/roadrunner-plugins/server v1.0.3 h1:Bhr5tK0eW+52/qeew6QMowowOpeUKJv/qqECCmW1X+Y=
-github.com/spiral/roadrunner-plugins/server v1.0.3/go.mod h1:XHfOZwvITuKyUwU79KaeMm8gU5AqzKQg0MCHJ7eBU5c=
-github.com/spiral/roadrunner-plugins/server v1.0.4 h1:i6QWbVM/gLnM3Tb/DDFSFpRnxccJd3mCpENW2rBuTeE=
-github.com/spiral/roadrunner-plugins/server v1.0.4/go.mod h1:XHfOZwvITuKyUwU79KaeMm8gU5AqzKQg0MCHJ7eBU5c=
-github.com/spiral/roadrunner-plugins/server v1.0.6 h1:JOrA/O/DJjuqKE1YTxLSyYh+/FLsJHY9biZkLwZk+LE=
-github.com/spiral/roadrunner-plugins/server v1.0.6/go.mod h1:FyUujaAfNNdXCJHDkWD5pcRolNQ4cOMb6N+KapIMVpk=
-github.com/spiral/roadrunner-plugins/static v1.0.0/go.mod h1:lipuYhoSoZR95DR6hO7MOdaJKQq4JibCPC2V2BDzNXg=
-github.com/spiral/roadrunner/v2 v2.0.0-alpha26/go.mod h1:r7ojuHm9qCVbg4fKcqr4Aqk7VXqZ9YPefr1LOv7HNys=
-github.com/spiral/roadrunner/v2 v2.0.0-alpha28/go.mod h1:Uqtk/47S+qweVjSl4R9XVImEVZc3nMWku4yg3A+f18Q=
-github.com/spiral/roadrunner/v2 v2.0.0-alpha30/go.mod h1:fKmPelVQxWql3cpjt/hYsf/Y2zDKb+DbZXgxdwj2nTc=
-github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
-github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
-github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
@@ -490,10 +297,7 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
-github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
-github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
-github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.18.0 h1:IV0DdMlatq9QO1Cr6wGJPVW1sV1Q8HvZXAIcjorylyM=
@@ -507,31 +311,23 @@ github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6Ac
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yookoala/gofast v0.4.0 h1:dLBjghcsbbZNOEHN8N1X/gh9S6srmJed4WQfG7DlKwo=
github.com/yookoala/gofast v0.4.0/go.mod h1:rfbkoKaQG1bnuTUZcmV3vAlnfpF4FTq8WbQJf2vcpg8=
+github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb h1:ZkM6LRnq40pR1Ox0hTHlnpkcOTuFIDQpZ1IN8rKKhX0=
github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
-go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
-go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
-go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
-go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
-go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/otel v0.15.0 h1:CZFy2lPhxd4HlhZnYK8gRyDotksO3Ip9rBweY1vVYJw=
go.opentelemetry.io/otel v0.15.0/go.mod h1:e4GKElweB8W2gWUqbghw0B8t5MCTccc9212eNHnOHwA=
-go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
-go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
-go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
-go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM=
go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -540,8 +336,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -562,7 +356,6 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
-golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -571,19 +364,15 @@ golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201216054612-986b41b23924 h1:QsnDpLLOKwHBBDa8nDws4DYNc/ryVW2vCpxCs09d4PY=
@@ -597,7 +386,6 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -607,35 +395,26 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201210223839-7e3030f88018/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742 h1:+CBz4km/0KPU3RGTwARGh/noP3bEwtHcq+0YcBQM2JQ=
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201221093633-bc327ba9c2f0 h1:n+DPcgTwkgWzIFpLmoimYR2K2b0Ga5+Os4kayIN0vGo=
@@ -647,13 +426,10 @@ golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180726210403-bfb5194568d3/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -663,7 +439,6 @@ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
@@ -674,20 +449,15 @@ golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc h1:NCy3Ohtk6Iny5V/reW2Ktypo4zIpWBdRJ1uFMjBxdg8=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20200103221440-774c71fcf114 h1:DnSr2mCsxyCE6ZgIkmcWUQY2R5cH/6wL7eIxEmQOMSE=
-golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
-google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I=
@@ -697,21 +467,13 @@ google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRn
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
-google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
-google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
-google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -723,37 +485,28 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
-gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/ini.v1 v1.38.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
-gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
-sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
-sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
diff --git a/plugins/checker/config.go b/plugins/checker/config.go
new file mode 100644
index 00000000..5f952592
--- /dev/null
+++ b/plugins/checker/config.go
@@ -0,0 +1,5 @@
+package checker
+
+type Config struct {
+ Address string
+}
diff --git a/plugins/checker/interface.go b/plugins/checker/interface.go
new file mode 100644
index 00000000..dd9dcada
--- /dev/null
+++ b/plugins/checker/interface.go
@@ -0,0 +1,11 @@
+package checker
+
+// Status consists of status code from the service
+type Status struct {
+ Code int
+}
+
+// Checker interface used to get latest status from plugin
+type Checker interface {
+ Status() Status
+}
diff --git a/plugins/checker/plugin.go b/plugins/checker/plugin.go
new file mode 100644
index 00000000..95f4f68c
--- /dev/null
+++ b/plugins/checker/plugin.go
@@ -0,0 +1,151 @@
+package checker
+
+import (
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/gofiber/fiber/v2"
+ fiberLogger "github.com/gofiber/fiber/v2/middleware/logger"
+ "github.com/spiral/endure"
+ "github.com/spiral/errors"
+ "github.com/spiral/roadrunner/v2/plugins/config"
+ "github.com/spiral/roadrunner/v2/plugins/logger"
+)
+
+const (
+ // PluginName declares public plugin name.
+ PluginName = "status"
+)
+
+type Plugin struct {
+ registry map[string]Checker
+ server *fiber.App
+ log logger.Logger
+ cfg *Config
+}
+
+func (c *Plugin) Init(log logger.Logger, cfg config.Configurer) error {
+ const op = errors.Op("status plugin init")
+ err := cfg.UnmarshalKey(PluginName, &c.cfg)
+ if err != nil {
+ return errors.E(op, errors.Disabled, err)
+ }
+
+ if c.cfg == nil {
+ return errors.E(errors.Disabled)
+ }
+
+ c.registry = make(map[string]Checker)
+ c.log = log
+ return nil
+}
+
+func (c *Plugin) Serve() chan error {
+ errCh := make(chan error, 1)
+ c.server = fiber.New(fiber.Config{
+ ReadTimeout: time.Second * 5,
+ WriteTimeout: time.Second * 5,
+ IdleTimeout: time.Second * 5,
+ })
+ c.server.Group("/v1", c.healthHandler)
+ c.server.Use(fiberLogger.New())
+ c.server.Use("/health", c.healthHandler)
+
+ go func() {
+ err := c.server.Listen(c.cfg.Address)
+ if err != nil {
+ errCh <- err
+ }
+ }()
+
+ return errCh
+}
+
+func (c *Plugin) Stop() error {
+ const op = errors.Op("checker stop")
+ err := c.server.Shutdown()
+ if err != nil {
+ return errors.E(op, err)
+ }
+ return nil
+}
+
+// Reset named service.
+func (c *Plugin) Status(name string) (Status, error) {
+ const op = errors.Op("get status")
+ svc, ok := c.registry[name]
+ if !ok {
+ return Status{}, errors.E(op, errors.Errorf("no such service: %s", name))
+ }
+
+ return svc.Status(), nil
+}
+
+// CollectTarget collecting services which can provide Status.
+func (c *Plugin) CollectTarget(name endure.Named, r Checker) error {
+ c.registry[name.Name()] = r
+ return nil
+}
+
+// Collects declares services to be collected.
+func (c *Plugin) Collects() []interface{} {
+ return []interface{}{
+ c.CollectTarget,
+ }
+}
+
+// Name of the service.
+func (c *Plugin) Name() string {
+ return PluginName
+}
+
+// RPCService returns associated rpc service.
+func (c *Plugin) RPC() interface{} {
+ return &rpc{srv: c, log: c.log}
+}
+
+type Plugins struct {
+ Plugins []string `query:"plugin"`
+}
+
+const template string = "Service: %s: Status: %d\n"
+
+func (c *Plugin) healthHandler(ctx *fiber.Ctx) error {
+ const op = errors.Op("health_handler")
+ plugins := &Plugins{}
+ err := ctx.QueryParser(plugins)
+ if err != nil {
+ return errors.E(op, err)
+ }
+
+ if len(plugins.Plugins) == 0 {
+ ctx.Status(http.StatusOK)
+ _, _ = ctx.WriteString("No plugins provided in query. Query should be in form of: /v1/health?plugin=plugin1&plugin=plugin2 \n")
+ return nil
+ }
+
+ failed := false
+ // iterate over all provided plugins
+ for i := 0; i < len(plugins.Plugins); i++ {
+ // check if the plugin exists
+ if plugin, ok := c.registry[plugins.Plugins[i]]; ok {
+ st := plugin.Status()
+ if st.Code >= 500 {
+ failed = true
+ continue
+ } else if st.Code >= 100 && st.Code <= 400 {
+ _, _ = ctx.WriteString(fmt.Sprintf(template, plugins.Plugins[i], st.Code))
+ }
+ } else {
+ _, _ = ctx.WriteString(fmt.Sprintf("Service: %s not found", plugins.Plugins[i]))
+ }
+ }
+ if failed {
+ ctx.Status(http.StatusInternalServerError)
+ return nil
+ }
+
+ ctx.Status(http.StatusOK)
+ return nil
+}
diff --git a/plugins/checker/rpc.go b/plugins/checker/rpc.go
new file mode 100644
index 00000000..0daa62fe
--- /dev/null
+++ b/plugins/checker/rpc.go
@@ -0,0 +1,27 @@
+package checker
+
+import (
+ "github.com/spiral/errors"
+ "github.com/spiral/roadrunner/v2/plugins/logger"
+)
+
+type rpc struct {
+ srv *Plugin
+ log logger.Logger
+}
+
+// Status return current status of the provided plugin
+func (rpc *rpc) Status(service string, status *Status) error {
+ const op = errors.Op("status")
+ rpc.log.Debug("started Status method", "service", service)
+ st, err := rpc.srv.Status(service)
+ if err != nil {
+ return errors.E(op, err)
+ }
+
+ *status = st
+
+ rpc.log.Debug("status code", "code", st.Code)
+ rpc.log.Debug("successfully finished Status method")
+ return nil
+}
diff --git a/plugins/config/interface.go b/plugins/config/interface.go
new file mode 100644
index 00000000..2a7c67ce
--- /dev/null
+++ b/plugins/config/interface.go
@@ -0,0 +1,22 @@
+package config
+
+type Configurer interface {
+ // UnmarshalKey reads configuration section into configuration object.
+ //
+ // func (h *HttpService) Init(cp config.Configurer) error {
+ // h.config := &HttpConfig{}
+ // if err := configProvider.UnmarshalKey("http", h.config); err != nil {
+ // return err
+ // }
+ // }
+ UnmarshalKey(name string, out interface{}) error
+
+ // Get used to get config section
+ Get(name string) interface{}
+
+ // Overwrite used to overwrite particular values in the unmarshalled config
+ Overwrite(values map[string]interface{}) error
+
+ // Has checks if config section exists.
+ Has(name string) bool
+}
diff --git a/plugins/config/plugin.go b/plugins/config/plugin.go
new file mode 100755
index 00000000..1a170448
--- /dev/null
+++ b/plugins/config/plugin.go
@@ -0,0 +1,75 @@
+package config
+
+import (
+ "bytes"
+ "strings"
+
+ "github.com/spf13/viper"
+ "github.com/spiral/errors"
+)
+
+type Viper struct {
+ viper *viper.Viper
+ Path string
+ Prefix string
+ Type string
+ ReadInCfg []byte
+}
+
+// Inits config provider.
+func (v *Viper) Init() error {
+ const op = errors.Op("viper plugin init")
+ v.viper = viper.New()
+ // If user provided []byte data with config, read it and ignore Path and Prefix
+ if v.ReadInCfg != nil && v.Type != "" {
+ v.viper.SetConfigType("yaml")
+ return v.viper.ReadConfig(bytes.NewBuffer(v.ReadInCfg))
+ }
+
+ // read in environment variables that match
+ v.viper.AutomaticEnv()
+ if v.Prefix == "" {
+ return errors.E(op, errors.Str("prefix should be set"))
+ }
+
+ v.viper.SetEnvPrefix(v.Prefix)
+ if v.Path == "" {
+ return errors.E(op, errors.Str("path should be set"))
+ }
+
+ v.viper.SetConfigFile(v.Path)
+ v.viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
+
+ return v.viper.ReadInConfig()
+}
+
+// Overwrite overwrites existing config with provided values
+func (v *Viper) Overwrite(values map[string]interface{}) error {
+ if len(values) != 0 {
+ for key, value := range values {
+ v.viper.Set(key, value)
+ }
+ }
+
+ return nil
+}
+
+// UnmarshalKey reads configuration section into configuration object.
+func (v *Viper) UnmarshalKey(name string, out interface{}) error {
+ const op = errors.Op("unmarshal key")
+ err := v.viper.UnmarshalKey(name, &out)
+ if err != nil {
+ return errors.E(op, err)
+ }
+ return nil
+}
+
+// Get raw config in a form of config section.
+func (v *Viper) Get(name string) interface{} {
+ return v.viper.Get(name)
+}
+
+// Has checks if config section exists.
+func (v *Viper) Has(name string) bool {
+ return v.viper.IsSet(name)
+}
diff --git a/plugins/doc/graphviz.svg b/plugins/doc/graphviz.svg
new file mode 100644
index 00000000..86f6ab5c
--- /dev/null
+++ b/plugins/doc/graphviz.svg
@@ -0,0 +1,169 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><!-- Generated by graphviz version 2.40.1 (20161225.0304)
+ --><!-- Title: endure Pages: 1 --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="625pt" height="479pt" viewBox="0.00 0.00 624.94 478.79">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 474.786)">
+<title>endure</title>
+<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-474.786 620.9357,-474.786 620.9357,4 -4,4"/>
+<!-- checker -->
+<g id="node1" class="node">
+<title>checker</title>
+<ellipse fill="none" stroke="#000000" cx="412.2429" cy="-377.2862" rx="41.1103" ry="18"/>
+<text text-anchor="middle" x="412.2429" y="-373.0862" font-family="Times,serif" font-size="14.00" fill="#000000">checker</text>
+</g>
+<!-- config -->
+<g id="node2" class="node">
+<title>config</title>
+<ellipse fill="none" stroke="#000000" cx="463.8878" cy="-235.393" rx="35.9154" ry="18"/>
+<text text-anchor="middle" x="463.8878" y="-231.193" font-family="Times,serif" font-size="14.00" fill="#000000">config</text>
+</g>
+<!-- checker&#45;&gt;config -->
+<g id="edge1" class="edge">
+<title>checker-&gt;config</title>
+<path fill="none" stroke="#000000" d="M418.7837,-359.3154C427.6313,-335.0068 443.4953,-291.4209 453.8554,-262.9568"/>
+<polygon fill="#000000" stroke="#000000" points="457.2687,-263.812 457.4,-253.218 450.6908,-261.4178 457.2687,-263.812"/>
+</g>
+<!-- logger -->
+<g id="node3" class="node">
+<title>logger</title>
+<ellipse fill="none" stroke="#000000" cx="35.7071" cy="-310.8928" rx="35.9154" ry="18"/>
+<text text-anchor="middle" x="35.7071" y="-306.6928" font-family="Times,serif" font-size="14.00" fill="#000000">logger</text>
+</g>
+<!-- checker&#45;&gt;logger -->
+<g id="edge2" class="edge">
+<title>checker-&gt;logger</title>
+<path fill="none" stroke="#000000" d="M374.0665,-370.5547C303.7112,-358.1492 154.0014,-331.7513 79.586,-318.6299"/>
+<polygon fill="#000000" stroke="#000000" points="80.0574,-315.1591 69.6015,-316.8693 78.8418,-322.0527 80.0574,-315.1591"/>
+</g>
+<!-- logger&#45;&gt;config -->
+<g id="edge4" class="edge">
+<title>logger-&gt;config</title>
+<path fill="none" stroke="#000000" d="M69.6636,-304.9054C146.6435,-291.3317 334.3698,-258.2305 420.0048,-243.1308"/>
+<polygon fill="#000000" stroke="#000000" points="420.6875,-246.5645 429.9277,-241.3811 419.4719,-239.6708 420.6875,-246.5645"/>
+</g>
+<!-- gzip -->
+<g id="node4" class="node">
+<title>gzip</title>
+<ellipse fill="none" stroke="#000000" cx="531.6651" cy="-102.393" rx="27.8286" ry="18"/>
+<text text-anchor="middle" x="531.6651" y="-98.193" font-family="Times,serif" font-size="14.00" fill="#000000">gzip</text>
+</g>
+<!-- headers -->
+<g id="node5" class="node">
+<title>headers</title>
+<ellipse fill="none" stroke="#000000" cx="576.4118" cy="-235.393" rx="40.548" ry="18"/>
+<text text-anchor="middle" x="576.4118" y="-231.193" font-family="Times,serif" font-size="14.00" fill="#000000">headers</text>
+</g>
+<!-- headers&#45;&gt;config -->
+<g id="edge3" class="edge">
+<title>headers-&gt;config</title>
+<path fill="none" stroke="#000000" d="M535.788,-235.393C527.3742,-235.393 518.4534,-235.393 509.8639,-235.393"/>
+<polygon fill="#000000" stroke="#000000" points="509.607,-231.8931 499.607,-235.393 509.607,-238.8931 509.607,-231.8931"/>
+</g>
+<!-- metrics -->
+<g id="node6" class="node">
+<title>metrics</title>
+<ellipse fill="none" stroke="#000000" cx="412.2429" cy="-93.4998" rx="39.4196" ry="18"/>
+<text text-anchor="middle" x="412.2429" y="-89.2998" font-family="Times,serif" font-size="14.00" fill="#000000">metrics</text>
+</g>
+<!-- metrics&#45;&gt;config -->
+<g id="edge6" class="edge">
+<title>metrics-&gt;config</title>
+<path fill="none" stroke="#000000" d="M418.7837,-111.4707C427.6313,-135.7792 443.4953,-179.3651 453.8554,-207.8292"/>
+<polygon fill="#000000" stroke="#000000" points="450.6908,-209.3682 457.4,-217.5681 457.2687,-206.974 450.6908,-209.3682"/>
+</g>
+<!-- metrics&#45;&gt;logger -->
+<g id="edge5" class="edge">
+<title>metrics-&gt;logger</title>
+<path fill="none" stroke="#000000" d="M387.5373,-107.7636C321.7958,-145.7194 142.5487,-249.2078 68.4432,-291.9926"/>
+<polygon fill="#000000" stroke="#000000" points="66.4391,-289.1082 59.5289,-297.1393 69.9391,-295.1704 66.4391,-289.1082"/>
+</g>
+<!-- redis -->
+<g id="node7" class="node">
+<title>redis</title>
+<ellipse fill="none" stroke="#000000" cx="281.4734" cy="-18" rx="29.6127" ry="18"/>
+<text text-anchor="middle" x="281.4734" y="-13.8" font-family="Times,serif" font-size="14.00" fill="#000000">redis</text>
+</g>
+<!-- redis&#45;&gt;config -->
+<g id="edge8" class="edge">
+<title>redis-&gt;config</title>
+<path fill="none" stroke="#000000" d="M295.1841,-34.3398C326.9308,-72.174 405.6399,-165.9759 443.2445,-210.7914"/>
+<polygon fill="#000000" stroke="#000000" points="440.6581,-213.1541 449.7672,-218.5648 446.0204,-208.6545 440.6581,-213.1541"/>
+</g>
+<!-- redis&#45;&gt;logger -->
+<g id="edge7" class="edge">
+<title>redis-&gt;logger</title>
+<path fill="none" stroke="#000000" d="M267.9098,-34.1644C227.1471,-82.7435 105.5381,-227.6715 56.5241,-286.0841"/>
+<polygon fill="#000000" stroke="#000000" points="53.5843,-284.1426 49.8376,-294.0528 58.9466,-288.6421 53.5843,-284.1426"/>
+</g>
+<!-- reload -->
+<g id="node8" class="node">
+<title>reload</title>
+<ellipse fill="none" stroke="#000000" cx="281.4734" cy="-452.786" rx="35.3315" ry="18"/>
+<text text-anchor="middle" x="281.4734" y="-448.586" font-family="Times,serif" font-size="14.00" fill="#000000">reload</text>
+</g>
+<!-- reload&#45;&gt;config -->
+<g id="edge10" class="edge">
+<title>reload-&gt;config</title>
+<path fill="none" stroke="#000000" d="M295.4842,-436.0885C327.4495,-397.9939 405.8819,-304.5217 443.3335,-259.8887"/>
+<polygon fill="#000000" stroke="#000000" points="446.0824,-262.0576 449.8292,-252.1474 440.7201,-257.5581 446.0824,-262.0576"/>
+</g>
+<!-- reload&#45;&gt;logger -->
+<g id="edge9" class="edge">
+<title>reload-&gt;logger</title>
+<path fill="none" stroke="#000000" d="M257.9083,-439.1807C213.6848,-413.6483 118.2025,-358.5216 68.0211,-329.5493"/>
+<polygon fill="#000000" stroke="#000000" points="69.6111,-326.4259 59.2009,-324.457 66.1111,-332.4881 69.6111,-326.4259"/>
+</g>
+<!-- resetter -->
+<g id="node9" class="node">
+<title>resetter</title>
+<ellipse fill="none" stroke="#000000" cx="132.7678" cy="-426.5652" rx="39.3984" ry="18"/>
+<text text-anchor="middle" x="132.7678" y="-422.3652" font-family="Times,serif" font-size="14.00" fill="#000000">resetter</text>
+</g>
+<!-- reload&#45;&gt;resetter -->
+<g id="edge11" class="edge">
+<title>reload-&gt;resetter</title>
+<path fill="none" stroke="#000000" d="M248.1009,-446.9016C227.9026,-443.3401 201.8366,-438.7439 179.5962,-434.8224"/>
+<polygon fill="#000000" stroke="#000000" points="180.1376,-431.3639 169.6817,-433.0742 178.922,-438.2575 180.1376,-431.3639"/>
+</g>
+<!-- resetter&#45;&gt;logger -->
+<g id="edge12" class="edge">
+<title>resetter-&gt;logger</title>
+<path fill="none" stroke="#000000" d="M118.4461,-409.4974C102.0084,-389.9077 74.9173,-357.6218 56.2379,-335.3605"/>
+<polygon fill="#000000" stroke="#000000" points="58.881,-333.0653 49.7719,-327.6546 53.5187,-337.5649 58.881,-333.0653"/>
+</g>
+<!-- rpc -->
+<g id="node10" class="node">
+<title>rpc</title>
+<ellipse fill="none" stroke="#000000" cx="132.7678" cy="-44.2208" rx="27" ry="18"/>
+<text text-anchor="middle" x="132.7678" y="-40.0208" font-family="Times,serif" font-size="14.00" fill="#000000">rpc</text>
+</g>
+<!-- rpc&#45;&gt;config -->
+<g id="edge13" class="edge">
+<title>rpc-&gt;config</title>
+<path fill="none" stroke="#000000" d="M153.4808,-56.1795C209.3277,-88.4227 363.359,-177.3527 431.1448,-216.4889"/>
+<polygon fill="#000000" stroke="#000000" points="429.7078,-219.7006 440.1181,-221.6696 433.2078,-213.6384 429.7078,-219.7006"/>
+</g>
+<!-- rpc&#45;&gt;logger -->
+<g id="edge14" class="edge">
+<title>rpc-&gt;logger</title>
+<path fill="none" stroke="#000000" d="M126.3994,-61.7179C109.8827,-107.097 65.5725,-228.8383 45.6502,-283.5745"/>
+<polygon fill="#000000" stroke="#000000" points="42.3576,-282.3876 42.2262,-292.9816 48.9354,-284.7818 42.3576,-282.3876"/>
+</g>
+<!-- static -->
+<g id="node11" class="node">
+<title>static</title>
+<ellipse fill="none" stroke="#000000" cx="35.7071" cy="-159.8932" rx="31.3333" ry="18"/>
+<text text-anchor="middle" x="35.7071" y="-155.6932" font-family="Times,serif" font-size="14.00" fill="#000000">static</text>
+</g>
+<!-- static&#45;&gt;config -->
+<g id="edge15" class="edge">
+<title>static-&gt;config</title>
+<path fill="none" stroke="#000000" d="M65.8159,-165.2022C140.1736,-178.3135 332.7753,-212.2743 419.9157,-227.6396"/>
+<polygon fill="#000000" stroke="#000000" points="419.5489,-231.1288 430.0048,-229.4185 420.7645,-224.2351 419.5489,-231.1288"/>
+</g>
+<!-- static&#45;&gt;logger -->
+<g id="edge16" class="edge">
+<title>static-&gt;logger</title>
+<path fill="none" stroke="#000000" d="M35.7071,-178.1073C35.7071,-204.0691 35.7071,-251.9543 35.7071,-282.5696"/>
+<polygon fill="#000000" stroke="#000000" points="32.2072,-282.6141 35.7071,-292.6141 39.2072,-282.6142 32.2072,-282.6141"/>
+</g>
+</g>
+</svg> \ No newline at end of file
diff --git a/plugins/gzip/plugin.go b/plugins/gzip/plugin.go
new file mode 100644
index 00000000..e5b9e4f5
--- /dev/null
+++ b/plugins/gzip/plugin.go
@@ -0,0 +1,25 @@
+package gzip
+
+import (
+ "net/http"
+
+ "github.com/NYTimes/gziphandler"
+)
+
+const PluginName = "gzip"
+
+type Gzip struct{}
+
+func (g *Gzip) Init() error {
+ return nil
+}
+
+func (g *Gzip) Middleware(next http.Handler) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ gziphandler.GzipHandler(next).ServeHTTP(w, r)
+ }
+}
+
+func (g *Gzip) Name() string {
+ return PluginName
+}
diff --git a/plugins/headers/config.go b/plugins/headers/config.go
new file mode 100644
index 00000000..8d4e29c2
--- /dev/null
+++ b/plugins/headers/config.go
@@ -0,0 +1,36 @@
+package headers
+
+// Config declares headers service configuration.
+type Config struct {
+ Headers struct {
+ // CORS settings.
+ CORS *CORSConfig
+
+ // Request headers to add to every payload send to PHP.
+ Request map[string]string
+
+ // Response headers to add to every payload generated by PHP.
+ Response map[string]string
+ }
+}
+
+// CORSConfig headers configuration.
+type CORSConfig struct {
+ // AllowedOrigin: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
+ AllowedOrigin string
+
+ // AllowedHeaders: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers
+ AllowedHeaders string
+
+ // AllowedMethods: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods
+ AllowedMethods string
+
+ // AllowCredentials https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials
+ AllowCredentials *bool
+
+ // ExposeHeaders: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers
+ ExposedHeaders string
+
+ // MaxAge of CORS headers in seconds/
+ MaxAge int
+}
diff --git a/plugins/headers/plugin.go b/plugins/headers/plugin.go
new file mode 100644
index 00000000..f1c6e6f3
--- /dev/null
+++ b/plugins/headers/plugin.go
@@ -0,0 +1,117 @@
+package headers
+
+import (
+ "net/http"
+ "strconv"
+
+ "github.com/spiral/errors"
+ "github.com/spiral/roadrunner/v2/plugins/config"
+)
+
+// ID contains default service name.
+const PluginName = "headers"
+const RootPluginName = "http"
+
+// Service serves headers files. Potentially convert into middleware?
+type Plugin struct {
+ // server configuration (location, forbidden files and etc)
+ cfg *Config
+}
+
+// Init must return configure service and return true if service hasStatus enabled. Must return error in case of
+// misconfiguration. Services must not be used without proper configuration pushed first.
+func (s *Plugin) Init(cfg config.Configurer) error {
+ const op = errors.Op("headers plugin init")
+ err := cfg.UnmarshalKey(RootPluginName, &s.cfg)
+ if err != nil {
+ return errors.E(op, errors.Disabled, err)
+ }
+
+ return nil
+}
+
+// middleware must return true if request/response pair is handled within the middleware.
+func (s *Plugin) Middleware(next http.Handler) http.HandlerFunc {
+ // Define the http.HandlerFunc
+ return func(w http.ResponseWriter, r *http.Request) {
+ if s.cfg.Headers.Request != nil {
+ for k, v := range s.cfg.Headers.Request {
+ r.Header.Add(k, v)
+ }
+ }
+
+ if s.cfg.Headers.Response != nil {
+ for k, v := range s.cfg.Headers.Response {
+ w.Header().Set(k, v)
+ }
+ }
+
+ if s.cfg.Headers.CORS != nil {
+ if r.Method == http.MethodOptions {
+ s.preflightRequest(w)
+ return
+ }
+ s.corsHeaders(w)
+ }
+
+ next.ServeHTTP(w, r)
+ }
+}
+
+func (s *Plugin) Name() string {
+ return PluginName
+}
+
+// configure OPTIONS response
+func (s *Plugin) preflightRequest(w http.ResponseWriter) {
+ headers := w.Header()
+
+ headers.Add("Vary", "Origin")
+ headers.Add("Vary", "Access-Control-Request-Method")
+ headers.Add("Vary", "Access-Control-Request-Headers")
+
+ if s.cfg.Headers.CORS.AllowedOrigin != "" {
+ headers.Set("Access-Control-Allow-Origin", s.cfg.Headers.CORS.AllowedOrigin)
+ }
+
+ if s.cfg.Headers.CORS.AllowedHeaders != "" {
+ headers.Set("Access-Control-Allow-Headers", s.cfg.Headers.CORS.AllowedHeaders)
+ }
+
+ if s.cfg.Headers.CORS.AllowedMethods != "" {
+ headers.Set("Access-Control-Allow-Methods", s.cfg.Headers.CORS.AllowedMethods)
+ }
+
+ if s.cfg.Headers.CORS.AllowCredentials != nil {
+ headers.Set("Access-Control-Allow-Credentials", strconv.FormatBool(*s.cfg.Headers.CORS.AllowCredentials))
+ }
+
+ if s.cfg.Headers.CORS.MaxAge > 0 {
+ headers.Set("Access-Control-Max-Age", strconv.Itoa(s.cfg.Headers.CORS.MaxAge))
+ }
+
+ w.WriteHeader(http.StatusOK)
+}
+
+// configure CORS headers
+func (s *Plugin) corsHeaders(w http.ResponseWriter) {
+ headers := w.Header()
+
+ headers.Add("Vary", "Origin")
+
+ if s.cfg.Headers.CORS.AllowedOrigin != "" {
+ headers.Set("Access-Control-Allow-Origin", s.cfg.Headers.CORS.AllowedOrigin)
+ }
+
+ if s.cfg.Headers.CORS.AllowedHeaders != "" {
+ headers.Set("Access-Control-Allow-Headers", s.cfg.Headers.CORS.AllowedHeaders)
+ }
+
+ if s.cfg.Headers.CORS.ExposedHeaders != "" {
+ headers.Set("Access-Control-Expose-Headers", s.cfg.Headers.CORS.ExposedHeaders)
+ }
+
+ if s.cfg.Headers.CORS.AllowCredentials != nil {
+ headers.Set("Access-Control-Allow-Credentials", strconv.FormatBool(*s.cfg.Headers.CORS.AllowCredentials))
+ }
+}
diff --git a/pkg/plugins/http/attributes/attributes.go b/plugins/http/attributes/attributes.go
index 4c453766..4c453766 100644
--- a/pkg/plugins/http/attributes/attributes.go
+++ b/plugins/http/attributes/attributes.go
diff --git a/pkg/plugins/http/config.go b/plugins/http/config.go
index 00d2940b..00d2940b 100644
--- a/pkg/plugins/http/config.go
+++ b/plugins/http/config.go
diff --git a/pkg/plugins/http/constants.go b/plugins/http/constants.go
index 773d1f46..773d1f46 100644
--- a/pkg/plugins/http/constants.go
+++ b/plugins/http/constants.go
diff --git a/pkg/plugins/http/errors.go b/plugins/http/errors.go
index fb8762ef..fb8762ef 100644
--- a/pkg/plugins/http/errors.go
+++ b/plugins/http/errors.go
diff --git a/pkg/plugins/http/errors_windows.go b/plugins/http/errors_windows.go
index 3d0ba04c..3d0ba04c 100644
--- a/pkg/plugins/http/errors_windows.go
+++ b/plugins/http/errors_windows.go
diff --git a/pkg/plugins/http/handler.go b/plugins/http/handler.go
index 08cee661..1889ed6d 100644
--- a/pkg/plugins/http/handler.go
+++ b/plugins/http/handler.go
@@ -10,9 +10,9 @@ import (
"github.com/hashicorp/go-multierror"
"github.com/spiral/errors"
- "github.com/spiral/roadrunner-plugins/logger"
"github.com/spiral/roadrunner/v2/interfaces/events"
"github.com/spiral/roadrunner/v2/interfaces/pool"
+ "github.com/spiral/roadrunner/v2/plugins/logger"
)
const (
diff --git a/pkg/plugins/http/parse.go b/plugins/http/parse.go
index d4a1604b..d4a1604b 100644
--- a/pkg/plugins/http/parse.go
+++ b/plugins/http/parse.go
diff --git a/pkg/plugins/http/plugin.go b/plugins/http/plugin.go
index d8ca6466..f3708ced 100644
--- a/pkg/plugins/http/plugin.go
+++ b/plugins/http/plugin.go
@@ -15,14 +15,14 @@ import (
"github.com/hashicorp/go-multierror"
"github.com/spiral/endure"
"github.com/spiral/errors"
- "github.com/spiral/roadrunner-plugins/checker"
- "github.com/spiral/roadrunner-plugins/config"
- "github.com/spiral/roadrunner-plugins/logger"
"github.com/spiral/roadrunner/v2/interfaces/pool"
"github.com/spiral/roadrunner/v2/interfaces/worker"
- "github.com/spiral/roadrunner/v2/pkg/plugins/http/attributes"
- "github.com/spiral/roadrunner/v2/pkg/plugins/server"
poolImpl "github.com/spiral/roadrunner/v2/pkg/pool"
+ "github.com/spiral/roadrunner/v2/plugins/checker"
+ "github.com/spiral/roadrunner/v2/plugins/config"
+ "github.com/spiral/roadrunner/v2/plugins/http/attributes"
+ "github.com/spiral/roadrunner/v2/plugins/logger"
+ "github.com/spiral/roadrunner/v2/plugins/server"
"github.com/spiral/roadrunner/v2/util"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
diff --git a/pkg/plugins/http/request.go b/plugins/http/request.go
index 9da05fd8..3983fdde 100644
--- a/pkg/plugins/http/request.go
+++ b/plugins/http/request.go
@@ -9,9 +9,9 @@ import (
"strings"
j "github.com/json-iterator/go"
- "github.com/spiral/roadrunner-plugins/logger"
"github.com/spiral/roadrunner/v2/pkg/payload"
- "github.com/spiral/roadrunner/v2/pkg/plugins/http/attributes"
+ "github.com/spiral/roadrunner/v2/plugins/http/attributes"
+ "github.com/spiral/roadrunner/v2/plugins/logger"
)
var json = j.ConfigCompatibleWithStandardLibrary
diff --git a/pkg/plugins/http/response.go b/plugins/http/response.go
index 17049ce1..17049ce1 100644
--- a/pkg/plugins/http/response.go
+++ b/plugins/http/response.go
diff --git a/pkg/plugins/http/uploads.go b/plugins/http/uploads.go
index 1f14cc0d..d5196844 100644
--- a/pkg/plugins/http/uploads.go
+++ b/plugins/http/uploads.go
@@ -1,7 +1,7 @@
package http
import (
- "github.com/spiral/roadrunner-plugins/logger"
+ "github.com/spiral/roadrunner/v2/plugins/logger"
"io"
"io/ioutil"
diff --git a/pkg/plugins/http/uploads_config.go b/plugins/http/uploads_config.go
index 4c20c8e8..4c20c8e8 100644
--- a/pkg/plugins/http/uploads_config.go
+++ b/plugins/http/uploads_config.go
diff --git a/pkg/plugins/informer/interface.go b/plugins/informer/interface.go
index 27139ae1..27139ae1 100644
--- a/pkg/plugins/informer/interface.go
+++ b/plugins/informer/interface.go
diff --git a/pkg/plugins/informer/plugin.go b/plugins/informer/plugin.go
index e2da7d86..3359cd7e 100644
--- a/pkg/plugins/informer/plugin.go
+++ b/plugins/informer/plugin.go
@@ -3,8 +3,8 @@ package informer
import (
"github.com/spiral/endure"
"github.com/spiral/errors"
- "github.com/spiral/roadrunner-plugins/logger"
"github.com/spiral/roadrunner/v2/interfaces/worker"
+ "github.com/spiral/roadrunner/v2/plugins/logger"
)
const PluginName = "informer"
diff --git a/pkg/plugins/informer/rpc.go b/plugins/informer/rpc.go
index d32d4e3a..98b5681c 100644
--- a/pkg/plugins/informer/rpc.go
+++ b/plugins/informer/rpc.go
@@ -1,8 +1,8 @@
package informer
import (
- "github.com/spiral/roadrunner-plugins/logger"
"github.com/spiral/roadrunner/v2/interfaces/worker"
+ "github.com/spiral/roadrunner/v2/plugins/logger"
"github.com/spiral/roadrunner/v2/tools"
)
diff --git a/plugins/logger/config.go b/plugins/logger/config.go
new file mode 100644
index 00000000..f7a5742c
--- /dev/null
+++ b/plugins/logger/config.go
@@ -0,0 +1,94 @@
+package logger
+
+import (
+ "strings"
+
+ "go.uber.org/zap"
+ "go.uber.org/zap/zapcore"
+)
+
+// ChannelConfig configures loggers per channel.
+type ChannelConfig struct {
+ // Dedicated channels per logger. By default logger allocated via named logger.
+ Channels map[string]Config `json:"channels" yaml:"channels"`
+}
+
+type Config struct {
+ // Mode configures logger based on some default template (development, production, off).
+ Mode string `json:"mode" yaml:"mode"`
+
+ // Level is the minimum enabled logging level. Note that this is a dynamic
+ // level, so calling ChannelConfig.Level.SetLevel will atomically change the log
+ // level of all loggers descended from this config.
+ Level string `json:"level" yaml:"level"`
+
+ // Encoding sets the logger's encoding. Valid values are "json" and
+ // "console", as well as any third-party encodings registered via
+ // RegisterEncoder.
+ Encoding string `json:"encoding" yaml:"encoding"`
+
+ // Output is a list of URLs or file paths to write logging output to.
+ // See Open for details.
+ Output []string `json:"output" yaml:"output"`
+
+ // ErrorOutput is a list of URLs to write internal logger errors to.
+ // The default is standard error.
+ //
+ // Note that this setting only affects internal errors; for sample code that
+ // sends error-level logs to a different location from info- and debug-level
+ // logs, see the package-level AdvancedConfiguration example.
+ ErrorOutput []string `json:"errorOutput" yaml:"errorOutput"`
+}
+
+// ZapConfig converts config into Zap configuration.
+func (cfg *Config) BuildLogger() (*zap.Logger, error) {
+ var zCfg zap.Config
+ switch strings.ToLower(cfg.Mode) {
+ case "off", "none":
+ return zap.NewNop(), nil
+ case "production":
+ zCfg = zap.NewProductionConfig()
+ case "development":
+ zCfg = zap.NewDevelopmentConfig()
+ default:
+ zCfg = zap.Config{
+ Level: zap.NewAtomicLevelAt(zap.DebugLevel),
+ Encoding: "console",
+ EncoderConfig: zapcore.EncoderConfig{
+ MessageKey: "message",
+ LevelKey: "level",
+ TimeKey: "time",
+ NameKey: "name",
+ EncodeName: ColoredHashedNameEncoder,
+ EncodeLevel: ColoredLevelEncoder,
+ EncodeTime: UTCTimeEncoder,
+ EncodeCaller: zapcore.ShortCallerEncoder,
+ },
+ OutputPaths: []string{"stderr"},
+ ErrorOutputPaths: []string{"stderr"},
+ }
+ }
+
+ if cfg.Level != "" {
+ level := zap.NewAtomicLevel()
+ if err := level.UnmarshalText([]byte(cfg.Level)); err == nil {
+ zCfg.Level = level
+ }
+ }
+
+ if cfg.Encoding != "" {
+ zCfg.Encoding = cfg.Encoding
+ }
+
+ if len(cfg.Output) != 0 {
+ zCfg.OutputPaths = cfg.Output
+ }
+
+ if len(cfg.ErrorOutput) != 0 {
+ zCfg.ErrorOutputPaths = cfg.ErrorOutput
+ }
+
+ // todo: https://github.com/uber-go/zap/blob/master/FAQ.md#does-zap-support-log-rotation
+
+ return zCfg.Build()
+}
diff --git a/plugins/logger/encoder.go b/plugins/logger/encoder.go
new file mode 100644
index 00000000..4ff583c4
--- /dev/null
+++ b/plugins/logger/encoder.go
@@ -0,0 +1,66 @@
+package logger
+
+import (
+ "hash/fnv"
+ "strings"
+ "time"
+
+ "github.com/fatih/color"
+ "go.uber.org/zap/zapcore"
+)
+
+var colorMap = []func(string, ...interface{}) string{
+ color.HiYellowString,
+ color.HiGreenString,
+ color.HiBlueString,
+ color.HiRedString,
+ color.HiCyanString,
+ color.HiMagentaString,
+}
+
+// ColoredLevelEncoder colorizes log levels.
+func ColoredLevelEncoder(level zapcore.Level, enc zapcore.PrimitiveArrayEncoder) {
+ switch level {
+ case zapcore.DebugLevel:
+ enc.AppendString(color.HiWhiteString(level.CapitalString()))
+ case zapcore.InfoLevel:
+ enc.AppendString(color.HiCyanString(level.CapitalString()))
+ case zapcore.WarnLevel:
+ enc.AppendString(color.HiYellowString(level.CapitalString()))
+ case zapcore.ErrorLevel, zapcore.DPanicLevel:
+ enc.AppendString(color.HiRedString(level.CapitalString()))
+ case zapcore.PanicLevel, zapcore.FatalLevel:
+ enc.AppendString(color.HiMagentaString(level.CapitalString()))
+ }
+}
+
+// ColoredNameEncoder colorizes service names.
+func ColoredNameEncoder(s string, enc zapcore.PrimitiveArrayEncoder) {
+ if len(s) < 12 {
+ s += strings.Repeat(" ", 12-len(s))
+ }
+
+ enc.AppendString(color.HiGreenString(s))
+}
+
+// ColoredHashedNameEncoder colorizes service names and assigns different colors to different names.
+func ColoredHashedNameEncoder(s string, enc zapcore.PrimitiveArrayEncoder) {
+ if len(s) < 12 {
+ s += strings.Repeat(" ", 12-len(s))
+ }
+
+ colorID := stringHash(s, len(colorMap))
+ enc.AppendString(colorMap[colorID](s))
+}
+
+// UTCTimeEncoder encodes time into short UTC specific timestamp.
+func UTCTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
+ enc.AppendString(t.UTC().Format("2006/01/02 15:04:05"))
+}
+
+// returns string hash
+func stringHash(name string, base int) int {
+ h := fnv.New32a()
+ _, _ = h.Write([]byte(name))
+ return int(h.Sum32()) % base
+}
diff --git a/plugins/logger/interface.go b/plugins/logger/interface.go
new file mode 100644
index 00000000..876629a9
--- /dev/null
+++ b/plugins/logger/interface.go
@@ -0,0 +1,16 @@
+package logger
+
+type (
+ // Logger is an general RR log interface
+ Logger interface {
+ Debug(msg string, keyvals ...interface{})
+ Info(msg string, keyvals ...interface{})
+ Warn(msg string, keyvals ...interface{})
+ Error(msg string, keyvals ...interface{})
+ }
+)
+
+// With creates a child logger and adds structured context to it
+type WithLogger interface {
+ With(keyvals ...interface{}) Logger
+}
diff --git a/plugins/logger/plugin.go b/plugins/logger/plugin.go
new file mode 100644
index 00000000..01bf5cc0
--- /dev/null
+++ b/plugins/logger/plugin.go
@@ -0,0 +1,69 @@
+package logger
+
+import (
+ "github.com/spiral/endure"
+ "github.com/spiral/errors"
+ "github.com/spiral/roadrunner/v2/plugins/config"
+ "go.uber.org/zap"
+)
+
+// PluginName declares plugin name.
+const PluginName = "logs"
+
+// ZapLogger manages zap logger.
+type ZapLogger struct {
+ base *zap.Logger
+ cfg Config
+ channels ChannelConfig
+}
+
+// Init logger service.
+func (z *ZapLogger) Init(cfg config.Configurer) error {
+ const op = errors.Op("zap logger init")
+ err := cfg.UnmarshalKey(PluginName, &z.cfg)
+ if err != nil {
+ return errors.E(op, errors.Disabled, err)
+ }
+
+ err = cfg.UnmarshalKey(PluginName, &z.channels)
+ if err != nil {
+ return errors.E(op, errors.Disabled, err)
+ }
+
+ z.base, err = z.cfg.BuildLogger()
+ if err != nil {
+ return errors.E(op, errors.Disabled, err)
+ }
+ return nil
+}
+
+// DefaultLogger returns default logger.
+func (z *ZapLogger) DefaultLogger() (Logger, error) {
+ return NewZapAdapter(z.base), nil
+}
+
+// NamedLogger returns logger dedicated to the specific channel. Similar to Named() but also reads the core params.
+func (z *ZapLogger) NamedLogger(name string) (Logger, error) {
+ if cfg, ok := z.channels.Channels[name]; ok {
+ l, err := cfg.BuildLogger()
+ if err != nil {
+ return nil, err
+ }
+ return NewZapAdapter(l), nil
+ }
+
+ return NewZapAdapter(z.base.Named(name)), nil
+}
+
+// NamedLogger returns logger dedicated to the specific channel. Similar to Named() but also reads the core params.
+func (z *ZapLogger) ServiceLogger(n endure.Named) (Logger, error) {
+ return z.NamedLogger(n.Name())
+}
+
+// Provides declares factory methods.
+func (z *ZapLogger) Provides() []interface{} {
+ return []interface{}{
+ z.ServiceLogger,
+ z.DefaultLogger,
+ }
+}
diff --git a/plugins/logger/zap_adapter.go b/plugins/logger/zap_adapter.go
new file mode 100644
index 00000000..0a0855b8
--- /dev/null
+++ b/plugins/logger/zap_adapter.go
@@ -0,0 +1,56 @@
+package logger
+
+import (
+ "fmt"
+
+ "go.uber.org/zap"
+)
+
+type ZapAdapter struct {
+ zl *zap.Logger
+}
+
+// Create NewZapAdapter which uses general log interface
+func NewZapAdapter(zapLogger *zap.Logger) *ZapAdapter {
+ return &ZapAdapter{
+ zl: zapLogger.WithOptions(zap.AddCallerSkip(1)),
+ }
+}
+
+func (log *ZapAdapter) fields(keyvals []interface{}) []zap.Field {
+ // we should have even number of keys and values
+ if len(keyvals)%2 != 0 {
+ return []zap.Field{zap.Error(fmt.Errorf("odd number of keyvals pairs: %v", keyvals))}
+ }
+
+ var fields []zap.Field
+ for i := 0; i < len(keyvals); i += 2 {
+ key, ok := keyvals[i].(string)
+ if !ok {
+ key = fmt.Sprintf("%v", keyvals[i])
+ }
+ fields = append(fields, zap.Any(key, keyvals[i+1]))
+ }
+
+ return fields
+}
+
+func (log *ZapAdapter) Debug(msg string, keyvals ...interface{}) {
+ log.zl.Debug(msg, log.fields(keyvals)...)
+}
+
+func (log *ZapAdapter) Info(msg string, keyvals ...interface{}) {
+ log.zl.Info(msg, log.fields(keyvals)...)
+}
+
+func (log *ZapAdapter) Warn(msg string, keyvals ...interface{}) {
+ log.zl.Warn(msg, log.fields(keyvals)...)
+}
+
+func (log *ZapAdapter) Error(msg string, keyvals ...interface{}) {
+ log.zl.Error(msg, log.fields(keyvals)...)
+}
+
+func (log *ZapAdapter) With(keyvals ...interface{}) Logger {
+ return NewZapAdapter(log.zl.With(log.fields(keyvals)...))
+}
diff --git a/plugins/metrics/config.go b/plugins/metrics/config.go
new file mode 100644
index 00000000..9459bc9b
--- /dev/null
+++ b/plugins/metrics/config.go
@@ -0,0 +1,138 @@
+package metrics
+
+import (
+ "fmt"
+
+ "github.com/prometheus/client_golang/prometheus"
+)
+
+// Config configures metrics service.
+type Config struct {
+ // Address to listen
+ Address string
+
+ // Collect define application specific metrics.
+ Collect map[string]Collector
+}
+
+type NamedCollector struct {
+ // Name of the collector
+ Name string `json:"name"`
+
+ // Collector structure
+ Collector `json:"collector"`
+}
+
+// CollectorType represents prometheus collector types
+type CollectorType string
+
+const (
+ // Histogram type
+ Histogram CollectorType = "histogram"
+
+ // Gauge type
+ Gauge CollectorType = "gauge"
+
+ // Counter type
+ Counter CollectorType = "counter"
+
+ // Summary type
+ Summary CollectorType = "summary"
+)
+
+// Collector describes single application specific metric.
+type Collector struct {
+ // Namespace of the metric.
+ Namespace string `json:"namespace"`
+ // Subsystem of the metric.
+ Subsystem string `json:"subsystem"`
+ // Collector type (histogram, gauge, counter, summary).
+ Type CollectorType `json:"type"`
+ // Help of collector.
+ Help string `json:"help"`
+ // Labels for vectorized metrics.
+ Labels []string `json:"labels"`
+ // Buckets for histogram metric.
+ Buckets []float64 `json:"buckets"`
+ // Objectives for the summary opts
+ Objectives map[float64]float64 `json:"objectives"`
+}
+
+// register application specific metrics.
+func (c *Config) getCollectors() (map[string]prometheus.Collector, error) {
+ if c.Collect == nil {
+ return nil, nil
+ }
+
+ collectors := make(map[string]prometheus.Collector)
+
+ for name, m := range c.Collect {
+ var collector prometheus.Collector
+ switch m.Type {
+ case Histogram:
+ opts := prometheus.HistogramOpts{
+ Name: name,
+ Namespace: m.Namespace,
+ Subsystem: m.Subsystem,
+ Help: m.Help,
+ Buckets: m.Buckets,
+ }
+
+ if len(m.Labels) != 0 {
+ collector = prometheus.NewHistogramVec(opts, m.Labels)
+ } else {
+ collector = prometheus.NewHistogram(opts)
+ }
+ case Gauge:
+ opts := prometheus.GaugeOpts{
+ Name: name,
+ Namespace: m.Namespace,
+ Subsystem: m.Subsystem,
+ Help: m.Help,
+ }
+
+ if len(m.Labels) != 0 {
+ collector = prometheus.NewGaugeVec(opts, m.Labels)
+ } else {
+ collector = prometheus.NewGauge(opts)
+ }
+ case Counter:
+ opts := prometheus.CounterOpts{
+ Name: name,
+ Namespace: m.Namespace,
+ Subsystem: m.Subsystem,
+ Help: m.Help,
+ }
+
+ if len(m.Labels) != 0 {
+ collector = prometheus.NewCounterVec(opts, m.Labels)
+ } else {
+ collector = prometheus.NewCounter(opts)
+ }
+ case Summary:
+ opts := prometheus.SummaryOpts{
+ Name: name,
+ Namespace: m.Namespace,
+ Subsystem: m.Subsystem,
+ Help: m.Help,
+ Objectives: m.Objectives,
+ }
+
+ if len(m.Labels) != 0 {
+ collector = prometheus.NewSummaryVec(opts, m.Labels)
+ } else {
+ collector = prometheus.NewSummary(opts)
+ }
+ default:
+ return nil, fmt.Errorf("invalid metric type `%s` for `%s`", m.Type, name)
+ }
+
+ collectors[name] = collector
+ }
+
+ return collectors, nil
+}
+
+func (c *Config) InitDefaults() {
+
+}
diff --git a/plugins/metrics/config_test.go b/plugins/metrics/config_test.go
new file mode 100644
index 00000000..665ec9cd
--- /dev/null
+++ b/plugins/metrics/config_test.go
@@ -0,0 +1,89 @@
+package metrics
+
+import (
+ "bytes"
+ "testing"
+
+ j "github.com/json-iterator/go"
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/stretchr/testify/assert"
+)
+
+var json = j.ConfigCompatibleWithStandardLibrary
+
+func Test_Config_Hydrate_Error1(t *testing.T) {
+ cfg := `{"request": {"From": "Something"}}`
+ c := &Config{}
+ f := new(bytes.Buffer)
+ f.WriteString(cfg)
+
+ err := json.Unmarshal(f.Bytes(), &c)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func Test_Config_Hydrate_Error2(t *testing.T) {
+ cfg := `{"dir": "/dir/"`
+ c := &Config{}
+
+ f := new(bytes.Buffer)
+ f.WriteString(cfg)
+
+ err := json.Unmarshal(f.Bytes(), &c)
+ assert.Error(t, err)
+}
+
+func Test_Config_Metrics(t *testing.T) {
+ cfg := `{
+"collect":{
+ "metric1":{"type": "gauge"},
+ "metric2":{ "type": "counter"},
+ "metric3":{"type": "summary"},
+ "metric4":{"type": "histogram"}
+}
+}`
+ c := &Config{}
+ f := new(bytes.Buffer)
+ f.WriteString(cfg)
+
+ err := json.Unmarshal(f.Bytes(), &c)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ m, err := c.getCollectors()
+ assert.NoError(t, err)
+
+ assert.IsType(t, prometheus.NewGauge(prometheus.GaugeOpts{}), m["metric1"])
+ assert.IsType(t, prometheus.NewCounter(prometheus.CounterOpts{}), m["metric2"])
+ assert.IsType(t, prometheus.NewSummary(prometheus.SummaryOpts{}), m["metric3"])
+ assert.IsType(t, prometheus.NewHistogram(prometheus.HistogramOpts{}), m["metric4"])
+}
+
+func Test_Config_MetricsVector(t *testing.T) {
+ cfg := `{
+"collect":{
+ "metric1":{"type": "gauge","labels":["label"]},
+ "metric2":{ "type": "counter","labels":["label"]},
+ "metric3":{"type": "summary","labels":["label"]},
+ "metric4":{"type": "histogram","labels":["label"]}
+}
+}`
+ c := &Config{}
+ f := new(bytes.Buffer)
+ f.WriteString(cfg)
+
+ err := json.Unmarshal(f.Bytes(), &c)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ m, err := c.getCollectors()
+ assert.NoError(t, err)
+
+ assert.IsType(t, prometheus.NewGaugeVec(prometheus.GaugeOpts{}, []string{}), m["metric1"])
+ assert.IsType(t, prometheus.NewCounterVec(prometheus.CounterOpts{}, []string{}), m["metric2"])
+ assert.IsType(t, prometheus.NewSummaryVec(prometheus.SummaryOpts{}, []string{}), m["metric3"])
+ assert.IsType(t, prometheus.NewHistogramVec(prometheus.HistogramOpts{}, []string{}), m["metric4"])
+}
diff --git a/plugins/metrics/doc.go b/plugins/metrics/doc.go
new file mode 100644
index 00000000..1abe097a
--- /dev/null
+++ b/plugins/metrics/doc.go
@@ -0,0 +1 @@
+package metrics
diff --git a/plugins/metrics/interface.go b/plugins/metrics/interface.go
new file mode 100644
index 00000000..87ba4017
--- /dev/null
+++ b/plugins/metrics/interface.go
@@ -0,0 +1,7 @@
+package metrics
+
+import "github.com/prometheus/client_golang/prometheus"
+
+type StatProvider interface {
+ MetricsCollector() []prometheus.Collector
+}
diff --git a/plugins/metrics/plugin.go b/plugins/metrics/plugin.go
new file mode 100644
index 00000000..fb9096a1
--- /dev/null
+++ b/plugins/metrics/plugin.go
@@ -0,0 +1,229 @@
+package metrics
+
+import (
+ "context"
+ "crypto/tls"
+ "net/http"
+ "sync"
+ "time"
+
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/client_golang/prometheus/promhttp"
+ "github.com/spiral/endure"
+ "github.com/spiral/errors"
+ "github.com/spiral/roadrunner/v2/plugins/config"
+ "github.com/spiral/roadrunner/v2/plugins/logger"
+ "golang.org/x/sys/cpu"
+)
+
+const (
+ // PluginName declares plugin name.
+ PluginName = "metrics"
+ // maxHeaderSize declares max header size for prometheus server
+ maxHeaderSize = 1024 * 1024 * 100 // 104MB
+)
+
+type statsProvider struct {
+ collectors []prometheus.Collector
+ name string
+}
+
+// Plugin to manage application metrics using Prometheus.
+type Plugin struct {
+ cfg Config
+ log logger.Logger
+ mu sync.Mutex // all receivers are pointers
+ http *http.Server
+ collectors sync.Map // all receivers are pointers
+ registry *prometheus.Registry
+}
+
+// Init service.
+func (m *Plugin) Init(cfg config.Configurer, log logger.Logger) error {
+ const op = errors.Op("metrics init")
+ err := cfg.UnmarshalKey(PluginName, &m.cfg)
+ if err != nil {
+ return errors.E(op, errors.Disabled, err)
+ }
+
+ // TODO figure out what is Init
+ m.cfg.InitDefaults()
+
+ m.log = log
+ m.registry = prometheus.NewRegistry()
+
+ // Default
+ err = m.registry.Register(prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{}))
+ if err != nil {
+ return errors.E(op, err)
+ }
+
+ // Default
+ err = m.registry.Register(prometheus.NewGoCollector())
+ if err != nil {
+ return errors.E(op, err)
+ }
+
+ collectors, err := m.cfg.getCollectors()
+ if err != nil {
+ return errors.E(op, err)
+ }
+
+ // Register invocation will be later in the Serve method
+ for k, v := range collectors {
+ m.collectors.Store(k, statsProvider{
+ collectors: []prometheus.Collector{v},
+ name: k,
+ })
+ }
+ return nil
+}
+
+// Register new prometheus collector.
+func (m *Plugin) Register(c prometheus.Collector) error {
+ return m.registry.Register(c)
+}
+
+// Serve prometheus metrics service.
+func (m *Plugin) Serve() chan error {
+ errCh := make(chan error, 1)
+ m.collectors.Range(func(key, value interface{}) bool {
+ // key - name
+ // value - statsProvider struct
+ c := value.(statsProvider)
+ for _, v := range c.collectors {
+ if err := m.registry.Register(v); err != nil {
+ errCh <- err
+ return false
+ }
+ }
+
+ return true
+ })
+
+ var topCipherSuites []uint16
+ var defaultCipherSuitesTLS13 []uint16
+
+ hasGCMAsmAMD64 := cpu.X86.HasAES && cpu.X86.HasPCLMULQDQ
+ hasGCMAsmARM64 := cpu.ARM64.HasAES && cpu.ARM64.HasPMULL
+ // Keep in sync with crypto/aes/cipher_s390x.go.
+ hasGCMAsmS390X := cpu.S390X.HasAES && cpu.S390X.HasAESCBC && cpu.S390X.HasAESCTR && (cpu.S390X.HasGHASH || cpu.S390X.HasAESGCM)
+
+ hasGCMAsm := hasGCMAsmAMD64 || hasGCMAsmARM64 || hasGCMAsmS390X
+
+ if hasGCMAsm {
+ // If AES-GCM hardware is provided then prioritise AES-GCM
+ // cipher suites.
+ topCipherSuites = []uint16{
+ tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
+ tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
+ tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
+ tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
+ tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
+ tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
+ }
+ defaultCipherSuitesTLS13 = []uint16{
+ tls.TLS_AES_128_GCM_SHA256,
+ tls.TLS_CHACHA20_POLY1305_SHA256,
+ tls.TLS_AES_256_GCM_SHA384,
+ }
+ } else {
+ // Without AES-GCM hardware, we put the ChaCha20-Poly1305
+ // cipher suites first.
+ topCipherSuites = []uint16{
+ tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
+ tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
+ tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
+ tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
+ tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
+ tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
+ }
+ defaultCipherSuitesTLS13 = []uint16{
+ tls.TLS_CHACHA20_POLY1305_SHA256,
+ tls.TLS_AES_128_GCM_SHA256,
+ tls.TLS_AES_256_GCM_SHA384,
+ }
+ }
+
+ DefaultCipherSuites := make([]uint16, 0, 22)
+ DefaultCipherSuites = append(DefaultCipherSuites, topCipherSuites...)
+ DefaultCipherSuites = append(DefaultCipherSuites, defaultCipherSuitesTLS13...)
+
+ m.http = &http.Server{
+ Addr: m.cfg.Address,
+ Handler: promhttp.HandlerFor(m.registry, promhttp.HandlerOpts{}),
+ IdleTimeout: time.Hour * 24,
+ ReadTimeout: time.Minute * 60,
+ MaxHeaderBytes: maxHeaderSize,
+ ReadHeaderTimeout: time.Minute * 60,
+ WriteTimeout: time.Minute * 60,
+ TLSConfig: &tls.Config{
+ CurvePreferences: []tls.CurveID{
+ tls.CurveP256,
+ tls.CurveP384,
+ tls.CurveP521,
+ tls.X25519,
+ },
+ CipherSuites: DefaultCipherSuites,
+ MinVersion: tls.VersionTLS12,
+ PreferServerCipherSuites: true,
+ },
+ }
+
+ go func() {
+ err := m.http.ListenAndServe()
+ if err != nil && err != http.ErrServerClosed {
+ errCh <- err
+ return
+ }
+ }()
+
+ return errCh
+}
+
+// Stop prometheus metrics service.
+func (m *Plugin) Stop() error {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+
+ if m.http != nil {
+ // timeout is 10 seconds
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
+ defer cancel()
+ err := m.http.Shutdown(ctx)
+ if err != nil {
+ // Function should be Stop() error
+ m.log.Error("stop error", "error", errors.Errorf("error shutting down the metrics server: error %v", err))
+ }
+ }
+ return nil
+}
+
+// Collects used to collect all plugins which implement metrics.StatProvider interface (and Named)
+func (m *Plugin) Collects() []interface{} {
+ return []interface{}{
+ m.AddStatProvider,
+ }
+}
+
+// Collector returns application specific collector by name or nil if collector not found.
+func (m *Plugin) AddStatProvider(name endure.Named, stat StatProvider) error {
+ m.collectors.Store(name.Name(), statsProvider{
+ collectors: stat.MetricsCollector(),
+ name: name.Name(),
+ })
+ return nil
+}
+
+// RPC interface satisfaction
+func (m *Plugin) Name() string {
+ return PluginName
+}
+
+// RPC interface satisfaction
+func (m *Plugin) RPC() interface{} {
+ return &rpcServer{
+ svc: m,
+ log: m.log,
+ }
+}
diff --git a/plugins/metrics/rpc.go b/plugins/metrics/rpc.go
new file mode 100644
index 00000000..f9c6accb
--- /dev/null
+++ b/plugins/metrics/rpc.go
@@ -0,0 +1,294 @@
+package metrics
+
+import (
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/spiral/errors"
+ "github.com/spiral/roadrunner/v2/plugins/logger"
+)
+
+type rpcServer struct {
+ svc *Plugin
+ log logger.Logger
+}
+
+// Metric represent single metric produced by the application.
+type Metric struct {
+ // Collector name.
+ Name string
+
+ // Collector value.
+ Value float64
+
+ // Labels associated with metric. Only for vector metrics. Must be provided in a form of label values.
+ Labels []string
+}
+
+// Add new metric to the designated collector.
+func (rpc *rpcServer) Add(m *Metric, ok *bool) error {
+ const op = errors.Op("Add metric")
+ rpc.log.Info("Adding metric", "name", m.Name, "value", m.Value, "labels", m.Labels)
+ c, exist := rpc.svc.collectors.Load(m.Name)
+ if !exist {
+ rpc.log.Error("undefined collector", "collector", m.Name)
+ return errors.E(op, errors.Errorf("undefined collector %s, try first Declare the desired collector", m.Name))
+ }
+
+ switch c := c.(type) {
+ case prometheus.Gauge:
+ c.Add(m.Value)
+
+ case *prometheus.GaugeVec:
+ if len(m.Labels) == 0 {
+ rpc.log.Error("required labels for collector", "collector", m.Name)
+ return errors.E(op, errors.Errorf("required labels for collector %s", m.Name))
+ }
+
+ gauge, err := c.GetMetricWithLabelValues(m.Labels...)
+ if err != nil {
+ rpc.log.Error("failed to get metrics with label values", "collector", m.Name, "labels", m.Labels)
+ return errors.E(op, err)
+ }
+ gauge.Add(m.Value)
+ case prometheus.Counter:
+ c.Add(m.Value)
+
+ case *prometheus.CounterVec:
+ if len(m.Labels) == 0 {
+ return errors.E(op, errors.Errorf("required labels for collector `%s`", m.Name))
+ }
+
+ gauge, err := c.GetMetricWithLabelValues(m.Labels...)
+ if err != nil {
+ rpc.log.Error("failed to get metrics with label values", "collector", m.Name, "labels", m.Labels)
+ return errors.E(op, err)
+ }
+ gauge.Add(m.Value)
+
+ default:
+ return errors.E(op, errors.Errorf("collector %s does not support method `Add`", m.Name))
+ }
+
+ // RPC, set ok to true as return value. Need by rpc.Call reply argument
+ *ok = true
+ rpc.log.Info("new metric successfully added", "name", m.Name, "labels", m.Labels, "value", m.Value)
+ return nil
+}
+
+// Sub subtract the value from the specific metric (gauge only).
+func (rpc *rpcServer) Sub(m *Metric, ok *bool) error {
+ const op = errors.Op("Subtracting metric")
+ rpc.log.Info("Subtracting value from metric", "name", m.Name, "value", m.Value, "labels", m.Labels)
+ c, exist := rpc.svc.collectors.Load(m.Name)
+ if !exist {
+ rpc.log.Error("undefined collector", "name", m.Name, "value", m.Value, "labels", m.Labels)
+ return errors.E(op, errors.Errorf("undefined collector %s", m.Name))
+ }
+ if c == nil {
+ // can it be nil ??? I guess can't
+ return errors.E(op, errors.Errorf("undefined collector %s", m.Name))
+ }
+
+ switch c := c.(type) {
+ case prometheus.Gauge:
+ c.Sub(m.Value)
+
+ case *prometheus.GaugeVec:
+ if len(m.Labels) == 0 {
+ rpc.log.Error("required labels for collector, but none was provided", "name", m.Name, "value", m.Value)
+ return errors.E(op, errors.Errorf("required labels for collector %s", m.Name))
+ }
+
+ gauge, err := c.GetMetricWithLabelValues(m.Labels...)
+ if err != nil {
+ rpc.log.Error("failed to get metrics with label values", "collector", m.Name, "labels", m.Labels)
+ return errors.E(op, err)
+ }
+ gauge.Sub(m.Value)
+ default:
+ return errors.E(op, errors.Errorf("collector `%s` does not support method `Sub`", m.Name))
+ }
+ rpc.log.Info("Subtracting operation applied successfully", "name", m.Name, "labels", m.Labels, "value", m.Value)
+
+ *ok = true
+ return nil
+}
+
+// Observe the value (histogram and summary only).
+func (rpc *rpcServer) Observe(m *Metric, ok *bool) error {
+ const op = errors.Op("Observe metrics")
+ rpc.log.Info("Observing metric", "name", m.Name, "value", m.Value, "labels", m.Labels)
+
+ c, exist := rpc.svc.collectors.Load(m.Name)
+ if !exist {
+ rpc.log.Error("undefined collector", "name", m.Name, "value", m.Value, "labels", m.Labels)
+ return errors.E(op, errors.Errorf("undefined collector %s", m.Name))
+ }
+ if c == nil {
+ return errors.E(op, errors.Errorf("undefined collector %s", m.Name))
+ }
+
+ switch c := c.(type) {
+ case *prometheus.SummaryVec:
+ if len(m.Labels) == 0 {
+ return errors.E(op, errors.Errorf("required labels for collector `%s`", m.Name))
+ }
+
+ observer, err := c.GetMetricWithLabelValues(m.Labels...)
+ if err != nil {
+ return errors.E(op, err)
+ }
+ observer.Observe(m.Value)
+
+ case prometheus.Histogram:
+ c.Observe(m.Value)
+
+ case *prometheus.HistogramVec:
+ if len(m.Labels) == 0 {
+ return errors.E(op, errors.Errorf("required labels for collector `%s`", m.Name))
+ }
+
+ observer, err := c.GetMetricWithLabelValues(m.Labels...)
+ if err != nil {
+ rpc.log.Error("failed to get metrics with label values", "collector", m.Name, "labels", m.Labels)
+ return errors.E(op, err)
+ }
+ observer.Observe(m.Value)
+ default:
+ return errors.E(op, errors.Errorf("collector `%s` does not support method `Observe`", m.Name))
+ }
+
+ rpc.log.Info("observe operation finished successfully", "name", m.Name, "labels", m.Labels, "value", m.Value)
+
+ *ok = true
+ return nil
+}
+
+// Declare is used to register new collector in prometheus
+// THE TYPES ARE:
+// NamedCollector -> Collector with the name
+// bool -> RPC reply value
+// RETURNS:
+// error
+func (rpc *rpcServer) Declare(nc *NamedCollector, ok *bool) error {
+ const op = errors.Op("Declare metric")
+ rpc.log.Info("Declaring new metric", "name", nc.Name, "type", nc.Type, "namespace", nc.Namespace)
+ _, exist := rpc.svc.collectors.Load(nc.Name)
+ if exist {
+ rpc.log.Error("metric with provided name already exist", "name", nc.Name, "type", nc.Type, "namespace", nc.Namespace)
+ return errors.E(op, errors.Errorf("tried to register existing collector with the name `%s`", nc.Name))
+ }
+
+ var collector prometheus.Collector
+ switch nc.Type {
+ case Histogram:
+ opts := prometheus.HistogramOpts{
+ Name: nc.Name,
+ Namespace: nc.Namespace,
+ Subsystem: nc.Subsystem,
+ Help: nc.Help,
+ Buckets: nc.Buckets,
+ }
+
+ if len(nc.Labels) != 0 {
+ collector = prometheus.NewHistogramVec(opts, nc.Labels)
+ } else {
+ collector = prometheus.NewHistogram(opts)
+ }
+ case Gauge:
+ opts := prometheus.GaugeOpts{
+ Name: nc.Name,
+ Namespace: nc.Namespace,
+ Subsystem: nc.Subsystem,
+ Help: nc.Help,
+ }
+
+ if len(nc.Labels) != 0 {
+ collector = prometheus.NewGaugeVec(opts, nc.Labels)
+ } else {
+ collector = prometheus.NewGauge(opts)
+ }
+ case Counter:
+ opts := prometheus.CounterOpts{
+ Name: nc.Name,
+ Namespace: nc.Namespace,
+ Subsystem: nc.Subsystem,
+ Help: nc.Help,
+ }
+
+ if len(nc.Labels) != 0 {
+ collector = prometheus.NewCounterVec(opts, nc.Labels)
+ } else {
+ collector = prometheus.NewCounter(opts)
+ }
+ case Summary:
+ opts := prometheus.SummaryOpts{
+ Name: nc.Name,
+ Namespace: nc.Namespace,
+ Subsystem: nc.Subsystem,
+ Help: nc.Help,
+ }
+
+ if len(nc.Labels) != 0 {
+ collector = prometheus.NewSummaryVec(opts, nc.Labels)
+ } else {
+ collector = prometheus.NewSummary(opts)
+ }
+
+ default:
+ return errors.E(op, errors.Errorf("unknown collector type %s", nc.Type))
+ }
+
+ // add collector to sync.Map
+ rpc.svc.collectors.Store(nc.Name, collector)
+ // that method might panic, we handle it by recover
+ err := rpc.svc.Register(collector)
+ if err != nil {
+ *ok = false
+ return errors.E(op, err)
+ }
+
+ rpc.log.Info("metric successfully added", "name", nc.Name, "type", nc.Type, "namespace", nc.Namespace)
+
+ *ok = true
+ return nil
+}
+
+// Set the metric value (only for gaude).
+func (rpc *rpcServer) Set(m *Metric, ok *bool) (err error) {
+ const op = errors.Op("Set metric")
+ rpc.log.Info("Observing metric", "name", m.Name, "value", m.Value, "labels", m.Labels)
+
+ c, exist := rpc.svc.collectors.Load(m.Name)
+ if !exist {
+ return errors.E(op, errors.Errorf("undefined collector %s", m.Name))
+ }
+ if c == nil {
+ return errors.E(op, errors.Errorf("undefined collector %s", m.Name))
+ }
+
+ switch c := c.(type) {
+ case prometheus.Gauge:
+ c.Set(m.Value)
+
+ case *prometheus.GaugeVec:
+ if len(m.Labels) == 0 {
+ rpc.log.Error("required labels for collector", "collector", m.Name)
+ return errors.E(op, errors.Errorf("required labels for collector %s", m.Name))
+ }
+
+ gauge, err := c.GetMetricWithLabelValues(m.Labels...)
+ if err != nil {
+ rpc.log.Error("failed to get metrics with label values", "collector", m.Name, "labels", m.Labels)
+ return errors.E(op, err)
+ }
+ gauge.Set(m.Value)
+
+ default:
+ return errors.E(op, errors.Errorf("collector `%s` does not support method Set", m.Name))
+ }
+
+ rpc.log.Info("set operation finished successfully", "name", m.Name, "labels", m.Labels, "value", m.Value)
+
+ *ok = true
+ return nil
+}
diff --git a/plugins/redis/config.go b/plugins/redis/config.go
new file mode 100644
index 00000000..ebcefed1
--- /dev/null
+++ b/plugins/redis/config.go
@@ -0,0 +1,32 @@
+package redis
+
+import "time"
+
+type Config struct {
+ Addrs []string `yaml:"addrs"`
+ DB int `yaml:"db"`
+ Username string `yaml:"username"`
+ Password string `yaml:"password"`
+ MasterName string `yaml:"master_name"`
+ SentinelPassword string `yaml:"sentinel_password"`
+ RouteByLatency bool `yaml:"route_by_latency"`
+ RouteRandomly bool `yaml:"route_randomly"`
+ MaxRetries int `yaml:"max_retries"`
+ DialTimeout time.Duration `yaml:"dial_timeout"`
+ MinRetryBackoff time.Duration `yaml:"min_retry_backoff"`
+ MaxRetryBackoff time.Duration `yaml:"max_retry_backoff"`
+ PoolSize int `yaml:"pool_size"`
+ MinIdleConns int `yaml:"min_idle_conns"`
+ MaxConnAge time.Duration `yaml:"max_conn_age"`
+ ReadTimeout time.Duration `yaml:"read_timeout"`
+ WriteTimeout time.Duration `yaml:"write_timeout"`
+ PoolTimeout time.Duration `yaml:"pool_timeout"`
+ IdleTimeout time.Duration `yaml:"idle_timeout"`
+ IdleCheckFreq time.Duration `yaml:"idle_check_freq"`
+ ReadOnly bool `yaml:"read_only"`
+}
+
+// InitDefaults initializing fill config with default values
+func (s *Config) InitDefaults() {
+ s.Addrs = []string{"localhost:6379"} // default addr is pointing to local storage
+}
diff --git a/plugins/redis/interface.go b/plugins/redis/interface.go
new file mode 100644
index 00000000..909c8ca4
--- /dev/null
+++ b/plugins/redis/interface.go
@@ -0,0 +1,9 @@
+package redis
+
+import "github.com/go-redis/redis/v8"
+
+// Redis in the redis KV plugin interface
+type Redis interface {
+ // GetClient
+ GetClient() redis.UniversalClient
+}
diff --git a/plugins/redis/plugin.go b/plugins/redis/plugin.go
new file mode 100644
index 00000000..fe465340
--- /dev/null
+++ b/plugins/redis/plugin.go
@@ -0,0 +1,75 @@
+package redis
+
+import (
+ "github.com/go-redis/redis/v8"
+ "github.com/spiral/errors"
+ "github.com/spiral/roadrunner/v2/plugins/config"
+ "github.com/spiral/roadrunner/v2/plugins/logger"
+)
+
+const PluginName = "redis"
+
+type Plugin struct {
+ // config for RR integration
+ cfg *Config
+ // logger
+ log logger.Logger
+ // redis universal client
+ universalClient redis.UniversalClient
+}
+
+func (s *Plugin) GetClient() redis.UniversalClient {
+ return s.universalClient
+}
+
+func (s *Plugin) Init(cfg config.Configurer, log logger.Logger) error {
+ const op = errors.Op("redis plugin init")
+ s.cfg = &Config{}
+ s.cfg.InitDefaults()
+
+ err := cfg.UnmarshalKey(PluginName, &s.cfg)
+ if err != nil {
+ return errors.E(op, errors.Disabled, err)
+ }
+
+ s.log = log
+
+ s.universalClient = redis.NewUniversalClient(&redis.UniversalOptions{
+ Addrs: s.cfg.Addrs,
+ DB: s.cfg.DB,
+ Username: s.cfg.Username,
+ Password: s.cfg.Password,
+ SentinelPassword: s.cfg.SentinelPassword,
+ MaxRetries: s.cfg.MaxRetries,
+ MinRetryBackoff: s.cfg.MaxRetryBackoff,
+ MaxRetryBackoff: s.cfg.MaxRetryBackoff,
+ DialTimeout: s.cfg.DialTimeout,
+ ReadTimeout: s.cfg.ReadTimeout,
+ WriteTimeout: s.cfg.WriteTimeout,
+ PoolSize: s.cfg.PoolSize,
+ MinIdleConns: s.cfg.MinIdleConns,
+ MaxConnAge: s.cfg.MaxConnAge,
+ PoolTimeout: s.cfg.PoolTimeout,
+ IdleTimeout: s.cfg.IdleTimeout,
+ IdleCheckFrequency: s.cfg.IdleCheckFreq,
+ ReadOnly: s.cfg.ReadOnly,
+ RouteByLatency: s.cfg.RouteByLatency,
+ RouteRandomly: s.cfg.RouteRandomly,
+ MasterName: s.cfg.MasterName,
+ })
+
+ return nil
+}
+
+func (s *Plugin) Serve() chan error {
+ errCh := make(chan error, 1)
+ return errCh
+}
+
+func (s Plugin) Stop() error {
+ return s.universalClient.Close()
+}
+
+func (s *Plugin) Name() string {
+ return PluginName
+}
diff --git a/plugins/reload/config.go b/plugins/reload/config.go
new file mode 100644
index 00000000..9ca2c0dc
--- /dev/null
+++ b/plugins/reload/config.go
@@ -0,0 +1,58 @@
+package reload
+
+import (
+ "time"
+
+ "github.com/spiral/errors"
+)
+
+// Config is a Reload configuration point.
+type Config struct {
+ // Interval is a global refresh interval
+ Interval time.Duration
+
+ // Patterns is a global file patterns to watch. It will be applied to every directory in project
+ Patterns []string
+
+ // Services is set of services which would be reloaded in case of FS changes
+ Services map[string]ServiceConfig
+}
+
+type ServiceConfig struct {
+ // Enabled indicates that service must be watched, doest not required when any other option specified
+ Enabled bool
+
+ // Recursive is options to use nested files from root folder
+ Recursive bool
+
+ // Patterns is per-service specific files to watch
+ Patterns []string
+
+ // Dirs is per-service specific dirs which will be combined with Patterns
+ Dirs []string
+
+ // Ignore is set of files which would not be watched
+ Ignore []string
+}
+
+// InitDefaults sets missing values to their default values.
+func InitDefaults(c *Config) {
+ c.Interval = time.Second
+ c.Patterns = []string{".php"}
+}
+
+// Valid validates the configuration.
+func (c *Config) Valid() error {
+ const op = errors.Op("config validation [reload plugin]")
+ if c.Interval < time.Second {
+ return errors.E(op, errors.Str("too short interval"))
+ }
+
+ if c.Services == nil {
+ return errors.E(op, errors.Str("should add at least 1 service"))
+ } else if len(c.Services) == 0 {
+ return errors.E(op, errors.Str("service initialized, however, no config added"))
+ }
+
+ return nil
+}
diff --git a/plugins/reload/plugin.go b/plugins/reload/plugin.go
new file mode 100644
index 00000000..452e03a3
--- /dev/null
+++ b/plugins/reload/plugin.go
@@ -0,0 +1,159 @@
+package reload
+
+import (
+ "os"
+ "strings"
+ "time"
+
+ "github.com/spiral/errors"
+ "github.com/spiral/roadrunner/v2/plugins/config"
+ "github.com/spiral/roadrunner/v2/plugins/logger"
+ "github.com/spiral/roadrunner/v2/plugins/resetter"
+)
+
+// PluginName contains default plugin name.
+const PluginName string = "reload"
+const thresholdChanBuffer uint = 1000
+
+type Plugin struct {
+ cfg *Config
+ log logger.Logger
+ watcher *Watcher
+ services map[string]interface{}
+ res resetter.Resetter
+ stopc chan struct{}
+}
+
+// Init controller service
+func (s *Plugin) Init(cfg config.Configurer, log logger.Logger, res resetter.Resetter) error {
+ const op = errors.Op("reload plugin init")
+ s.cfg = &Config{}
+ InitDefaults(s.cfg)
+ err := cfg.UnmarshalKey(PluginName, &s.cfg)
+ if err != nil {
+ // disable plugin in case of error
+ return errors.E(op, errors.Disabled, err)
+ }
+
+ s.log = log
+ s.res = res
+ s.stopc = make(chan struct{}, 1)
+ s.services = make(map[string]interface{})
+
+ var configs []WatcherConfig
+
+ for serviceName, serviceConfig := range s.cfg.Services {
+ ignored, err := ConvertIgnored(serviceConfig.Ignore)
+ if err != nil {
+ return errors.E(op, err)
+ }
+ configs = append(configs, WatcherConfig{
+ ServiceName: serviceName,
+ Recursive: serviceConfig.Recursive,
+ Directories: serviceConfig.Dirs,
+ FilterHooks: func(filename string, patterns []string) error {
+ for i := 0; i < len(patterns); i++ {
+ if strings.Contains(filename, patterns[i]) {
+ return nil
+ }
+ }
+ return errors.E(op, errors.Skip)
+ },
+ Files: make(map[string]os.FileInfo),
+ Ignored: ignored,
+ FilePatterns: append(serviceConfig.Patterns, s.cfg.Patterns...),
+ })
+ }
+
+ s.watcher, err = NewWatcher(configs, s.log)
+ if err != nil {
+ return errors.E(op, err)
+ }
+
+ return nil
+}
+
+func (s *Plugin) Serve() chan error {
+ const op = errors.Op("reload plugin serve")
+ errCh := make(chan error, 1)
+ if s.cfg.Interval < time.Second {
+ errCh <- errors.E(op, errors.Str("reload interval is too fast"))
+ return errCh
+ }
+
+ // make a map with unique services
+ // so, if we would have a 100 events from http service
+ // in map we would see only 1 key and it's config
+ treshholdc := make(chan struct {
+ serviceConfig ServiceConfig
+ service string
+ }, thresholdChanBuffer)
+
+ // use the same interval
+ timer := time.NewTimer(s.cfg.Interval)
+
+ go func() {
+ for e := range s.watcher.Event {
+ treshholdc <- struct {
+ serviceConfig ServiceConfig
+ service string
+ }{serviceConfig: s.cfg.Services[e.service], service: e.service}
+ }
+ }()
+
+ // map with configs by services
+ updated := make(map[string]ServiceConfig, len(s.cfg.Services))
+
+ go func() {
+ for {
+ select {
+ case cfg := <-treshholdc:
+ // logic is following:
+ // restart
+ timer.Stop()
+ // replace previous value in map by more recent without adding new one
+ updated[cfg.service] = cfg.serviceConfig
+ // if we getting a lot of events, we shouldn't restart particular service on each of it (user doing batch move or very fast typing)
+ // instead, we are resetting the timer and wait for s.cfg.Interval time
+ // If there is no more events, we restart service only once
+ timer.Reset(s.cfg.Interval)
+ case <-timer.C:
+ if len(updated) > 0 {
+ for name := range updated {
+ err := s.res.ResetByName(name)
+ if err != nil {
+ timer.Stop()
+ errCh <- errors.E(op, err)
+ return
+ }
+ }
+ // zero map
+ updated = make(map[string]ServiceConfig, len(s.cfg.Services))
+ }
+ case <-s.stopc:
+ timer.Stop()
+ return
+ }
+ }
+ }()
+
+ go func() {
+ err := s.watcher.StartPolling(s.cfg.Interval)
+ if err != nil {
+ errCh <- errors.E(op, err)
+ return
+ }
+ }()
+
+ return errCh
+}
+
+func (s *Plugin) Stop() error {
+ s.watcher.Stop()
+ s.stopc <- struct{}{}
+ return nil
+}
+
+func (s *Plugin) Name() string {
+ return PluginName
+}
diff --git a/plugins/reload/watcher.go b/plugins/reload/watcher.go
new file mode 100644
index 00000000..c232f16f
--- /dev/null
+++ b/plugins/reload/watcher.go
@@ -0,0 +1,374 @@
+package reload
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "sync"
+ "time"
+
+ "github.com/spiral/errors"
+ "github.com/spiral/roadrunner/v2/plugins/logger"
+)
+
+// SimpleHook is used to filter by simple criteria, CONTAINS
+type SimpleHook func(filename string, pattern []string) error
+
+// An Event describes an event that is received when files or directory
+// changes occur. It includes the os.FileInfo of the changed file or
+// directory and the type of event that's occurred and the full path of the file.
+type Event struct {
+ Path string
+ Info os.FileInfo
+
+ service string // type of service, http, grpc, etc...
+}
+
+type WatcherConfig struct {
+ // service name
+ ServiceName string
+
+ // Recursive or just add by singe directory
+ Recursive bool
+
+ // Directories used per-service
+ Directories []string
+
+ // simple hook, just CONTAINS
+ FilterHooks func(filename string, pattern []string) error
+
+ // path to file with Files
+ Files map[string]os.FileInfo
+
+ // Ignored Directories, used map for O(1) amortized get
+ Ignored map[string]struct{}
+
+ // FilePatterns to ignore
+ FilePatterns []string
+}
+
+type Watcher struct {
+ // main event channel
+ Event chan Event
+ close chan struct{}
+
+ // =============================
+ mu *sync.Mutex
+
+ // indicates is walker started or not
+ started bool
+
+ // config for each service
+ // need pointer here to assign files
+ watcherConfigs map[string]WatcherConfig
+
+ // logger
+ log logger.Logger
+}
+
+// Options is used to set Watcher Options
+type Options func(*Watcher)
+
+// NewWatcher returns new instance of File Watcher
+func NewWatcher(configs []WatcherConfig, log logger.Logger, options ...Options) (*Watcher, error) {
+ w := &Watcher{
+ Event: make(chan Event),
+ mu: &sync.Mutex{},
+
+ log: log,
+
+ close: make(chan struct{}),
+
+ //workingDir: workDir,
+ watcherConfigs: make(map[string]WatcherConfig),
+ }
+
+ // add watcherConfigs by service names
+ for _, v := range configs {
+ w.watcherConfigs[v.ServiceName] = v
+ }
+
+ // apply options
+ for _, option := range options {
+ option(w)
+ }
+ err := w.initFs()
+ if err != nil {
+ return nil, err
+ }
+
+ return w, nil
+}
+
+// initFs makes initial map with files
+func (w *Watcher) initFs() error {
+ const op = errors.Op("init fs")
+ for srvName, config := range w.watcherConfigs {
+ fileList, err := w.retrieveFileList(srvName, config)
+ if err != nil {
+ return errors.E(op, err)
+ }
+ // workaround. in golang you can't assign to map in struct field
+ tmp := w.watcherConfigs[srvName]
+ tmp.Files = fileList
+ w.watcherConfigs[srvName] = tmp
+ }
+ return nil
+}
+
+// ConvertIgnored is used to convert slice to map with ignored files
+func ConvertIgnored(ignored []string) (map[string]struct{}, error) {
+ if len(ignored) == 0 {
+ return nil, nil
+ }
+
+ ign := make(map[string]struct{}, len(ignored))
+ for i := 0; i < len(ignored); i++ {
+ abs, err := filepath.Abs(ignored[i])
+ if err != nil {
+ return nil, err
+ }
+ ign[abs] = struct{}{}
+ }
+
+ return ign, nil
+}
+
+// https://en.wikipedia.org/wiki/Inotify
+// SetMaxFileEvents sets max file notify events for Watcher
+// In case of file watch errors, this value can be increased system-wide
+// For linux: set --> fs.inotify.max_user_watches = 600000 (under /etc/<choose_name_here>.conf)
+// Add apply: sudo sysctl -p --system
+// func SetMaxFileEvents(events int) Options {
+// return func(watcher *Watcher) {
+// watcher.maxFileWatchEvents = events
+// }
+//
+// }
+
+// pass map from outside
+func (w *Watcher) retrieveFilesSingle(serviceName, path string) (map[string]os.FileInfo, error) {
+ const op = errors.Op("retrieve")
+ stat, err := os.Stat(path)
+ if err != nil {
+ return nil, err
+ }
+
+ filesList := make(map[string]os.FileInfo, 10)
+ filesList[path] = stat
+
+ // if it's not a dir, return
+ if !stat.IsDir() {
+ return filesList, nil
+ }
+
+ fileInfoList, err := ioutil.ReadDir(path)
+ if err != nil {
+ return nil, err
+ }
+
+ // recursive calls are slow in compare to goto
+ // so, we will add files with goto pattern
+outer:
+ for i := 0; i < len(fileInfoList); i++ {
+ // if file in ignored --> continue
+ if _, ignored := w.watcherConfigs[serviceName].Ignored[path]; ignored {
+ continue
+ }
+
+ // if filename does not contain pattern --> ignore that file
+ if w.watcherConfigs[serviceName].FilePatterns != nil && w.watcherConfigs[serviceName].FilterHooks != nil {
+ err = w.watcherConfigs[serviceName].FilterHooks(fileInfoList[i].Name(), w.watcherConfigs[serviceName].FilePatterns)
+ if errors.Is(errors.Skip, err) {
+ continue outer
+ }
+ }
+
+ filesList[fileInfoList[i].Name()] = fileInfoList[i]
+ }
+
+ return filesList, nil
+}
+
+func (w *Watcher) StartPolling(duration time.Duration) error {
+ w.mu.Lock()
+ const op = errors.Op("start polling")
+ if w.started {
+ w.mu.Unlock()
+ return errors.E(op, errors.Str("already started"))
+ }
+
+ w.started = true
+ w.mu.Unlock()
+
+ return w.waitEvent(duration)
+}
+
+// this is blocking operation
+func (w *Watcher) waitEvent(d time.Duration) error {
+ ticker := time.NewTicker(d)
+ for {
+ select {
+ case <-w.close:
+ ticker.Stop()
+ // just exit
+ // no matter for the pollEvents
+ return nil
+ case <-ticker.C:
+ // this is not very effective way
+ // because we have to wait on Lock
+ // better is to listen files in parallel, but, since that would be used in debug... TODO
+ for serviceName := range w.watcherConfigs {
+ // TODO sync approach
+ fileList, _ := w.retrieveFileList(serviceName, w.watcherConfigs[serviceName])
+ w.pollEvents(w.watcherConfigs[serviceName].ServiceName, fileList)
+ }
+ }
+ }
+}
+
+// retrieveFileList get file list for service
+func (w *Watcher) retrieveFileList(serviceName string, config WatcherConfig) (map[string]os.FileInfo, error) {
+ fileList := make(map[string]os.FileInfo)
+ if config.Recursive {
+ // walk through directories recursively
+ for i := 0; i < len(config.Directories); i++ {
+ // full path is workdir/relative_path
+ fullPath, err := filepath.Abs(config.Directories[i])
+ if err != nil {
+ return nil, err
+ }
+ list, err := w.retrieveFilesRecursive(serviceName, fullPath)
+ if err != nil {
+ return nil, err
+ }
+
+ for k := range list {
+ fileList[k] = list[k]
+ }
+ }
+ return fileList, nil
+ }
+
+ for i := 0; i < len(config.Directories); i++ {
+ // full path is workdir/relative_path
+ fullPath, err := filepath.Abs(config.Directories[i])
+ if err != nil {
+ return nil, err
+ }
+
+ // list is pathToFiles with files
+ list, err := w.retrieveFilesSingle(serviceName, fullPath)
+ if err != nil {
+ return nil, err
+ }
+
+ for pathToFile, file := range list {
+ fileList[pathToFile] = file
+ }
+ }
+
+ return fileList, nil
+}
+
+func (w *Watcher) retrieveFilesRecursive(serviceName, root string) (map[string]os.FileInfo, error) {
+ fileList := make(map[string]os.FileInfo)
+
+ return fileList, filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
+ const op = errors.Op("retrieve files recursive")
+ if err != nil {
+ return errors.E(op, err)
+ }
+
+ // If path is ignored and it's a directory, skip the directory. If it's
+ // ignored and it's a single file, skip the file.
+ _, ignored := w.watcherConfigs[serviceName].Ignored[path]
+ if ignored {
+ if info.IsDir() {
+ // if it's dir, ignore whole
+ return filepath.SkipDir
+ }
+ return nil
+ }
+
+ // if filename does not contain pattern --> ignore that file
+ err = w.watcherConfigs[serviceName].FilterHooks(info.Name(), w.watcherConfigs[serviceName].FilePatterns)
+ if errors.Is(errors.Skip, err) {
+ return nil
+ }
+
+ // Add the path and it's info to the file list.
+ fileList[path] = info
+ return nil
+ })
+}
+
+func (w *Watcher) pollEvents(serviceName string, files map[string]os.FileInfo) {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+
+ // Store create and remove events for use to check for rename events.
+ creates := make(map[string]os.FileInfo)
+ removes := make(map[string]os.FileInfo)
+
+ // Check for removed files.
+ for pth := range w.watcherConfigs[serviceName].Files {
+ if _, found := files[pth]; !found {
+ removes[pth] = w.watcherConfigs[serviceName].Files[pth]
+ w.log.Debug("file added to the list of removed files", "path", pth, "name", w.watcherConfigs[serviceName].Files[pth].Name(), "size", w.watcherConfigs[serviceName].Files[pth].Size())
+ }
+ }
+
+ // Check for created files, writes and chmods.
+ for pth := range files {
+ if files[pth].IsDir() {
+ continue
+ }
+ oldInfo, found := w.watcherConfigs[serviceName].Files[pth]
+ if !found {
+ // A file was created.
+ creates[pth] = files[pth]
+ w.log.Debug("file was created", "path", pth, "name", files[pth].Name(), "size", files[pth].Size())
+ continue
+ }
+
+ if oldInfo.ModTime() != files[pth].ModTime() || oldInfo.Mode() != files[pth].Mode() {
+ w.watcherConfigs[serviceName].Files[pth] = files[pth]
+ w.log.Debug("file was updated", "path", pth, "name", files[pth].Name(), "size", files[pth].Size())
+ w.Event <- Event{
+ Path: pth,
+ Info: files[pth],
+ service: serviceName,
+ }
+ }
+ }
+
+ // Send all the remaining create and remove events.
+ for pth := range creates {
+ // add file to the plugin watch files
+ w.watcherConfigs[serviceName].Files[pth] = creates[pth]
+ w.log.Debug("file was added to watcher", "path", pth, "name", creates[pth].Name(), "size", creates[pth].Size())
+
+ w.Event <- Event{
+ Path: pth,
+ Info: creates[pth],
+ service: serviceName,
+ }
+ }
+
+ for pth := range removes {
+ // delete path from the config
+ delete(w.watcherConfigs[serviceName].Files, pth)
+ w.log.Debug("file was removed from watcher", "path", pth, "name", removes[pth].Name(), "size", removes[pth].Size())
+
+ w.Event <- Event{
+ Path: pth,
+ Info: removes[pth],
+ service: serviceName,
+ }
+ }
+}
+
+func (w *Watcher) Stop() {
+ w.close <- struct{}{}
+}
diff --git a/plugins/resetter/interface.go b/plugins/resetter/interface.go
new file mode 100644
index 00000000..47d8d791
--- /dev/null
+++ b/plugins/resetter/interface.go
@@ -0,0 +1,17 @@
+package resetter
+
+// If plugin implements Resettable interface, than it state can be resetted without reload in runtime via RPC/HTTP
+type Resettable interface {
+ // Reset reload all plugins
+ Reset() error
+}
+
+// Resetter interface is the Resetter plugin main interface
+type Resetter interface {
+ // Reset all registered plugins
+ ResetAll() error
+ // Reset by plugin name
+ ResetByName(string) error
+ // GetAll registered plugins
+ GetAll() []string
+}
diff --git a/plugins/resetter/plugin.go b/plugins/resetter/plugin.go
new file mode 100644
index 00000000..5d294086
--- /dev/null
+++ b/plugins/resetter/plugin.go
@@ -0,0 +1,80 @@
+package resetter
+
+import (
+ "github.com/spiral/endure"
+ "github.com/spiral/errors"
+ "github.com/spiral/roadrunner/v2/plugins/logger"
+)
+
+const PluginName = "resetter"
+
+type Plugin struct {
+ registry map[string]Resettable
+ log logger.Logger
+}
+
+func (p *Plugin) ResetAll() error {
+ const op = errors.Op("reset all")
+ for name := range p.registry {
+ err := p.registry[name].Reset()
+ if err != nil {
+ return errors.E(op, err)
+ }
+ }
+ return nil
+}
+
+func (p *Plugin) ResetByName(plugin string) error {
+ const op = errors.Op("reset by name")
+ if plugin, ok := p.registry[plugin]; ok {
+ return plugin.Reset()
+ }
+ return errors.E(op, errors.Errorf("can't find plugin: %s", plugin))
+}
+
+func (p *Plugin) GetAll() []string {
+ all := make([]string, 0, len(p.registry))
+ for name := range p.registry {
+ all = append(all, name)
+ }
+ return all
+}
+
+func (p *Plugin) Init(log logger.Logger) error {
+ p.registry = make(map[string]Resettable)
+ p.log = log
+ return nil
+}
+
+// Reset named service.
+func (p *Plugin) Reset(name string) error {
+ svc, ok := p.registry[name]
+ if !ok {
+ return errors.E("no such service", errors.Str(name))
+ }
+
+ return svc.Reset()
+}
+
+// RegisterTarget resettable service.
+func (p *Plugin) RegisterTarget(name endure.Named, r Resettable) error {
+ p.registry[name.Name()] = r
+ return nil
+}
+
+// Collects declares services to be collected.
+func (p *Plugin) Collects() []interface{} {
+ return []interface{}{
+ p.RegisterTarget,
+ }
+}
+
+// Name of the service.
+func (p *Plugin) Name() string {
+ return PluginName
+}
+
+// RPCService returns associated rpc service.
+func (p *Plugin) RPC() interface{} {
+ return &rpc{srv: p, log: p.log}
+}
diff --git a/plugins/resetter/rpc.go b/plugins/resetter/rpc.go
new file mode 100644
index 00000000..69c955b0
--- /dev/null
+++ b/plugins/resetter/rpc.go
@@ -0,0 +1,30 @@
+package resetter
+
+import "github.com/spiral/roadrunner/v2/plugins/logger"
+
+type rpc struct {
+ srv *Plugin
+ log logger.Logger
+}
+
+// List all resettable plugins.
+func (rpc *rpc) List(_ bool, list *[]string) error {
+ rpc.log.Debug("started List method")
+ *list = make([]string, 0)
+
+ for name := range rpc.srv.registry {
+ *list = append(*list, name)
+ }
+ rpc.log.Debug("services list", "services", *list)
+
+ rpc.log.Debug("finished List method")
+ return nil
+}
+
+// Reset named plugin.
+func (rpc *rpc) Reset(service string, done *bool) error {
+ rpc.log.Debug("started Reset method for the service", "service", service)
+ defer rpc.log.Debug("finished Reset method for the service", "service", service)
+ *done = true
+ return rpc.srv.Reset(service)
+}
diff --git a/plugins/rpc/config.go b/plugins/rpc/config.go
new file mode 100644
index 00000000..7f3474d7
--- /dev/null
+++ b/plugins/rpc/config.go
@@ -0,0 +1,47 @@
+package rpc
+
+import (
+ "errors"
+ "net"
+ "strings"
+)
+
+// Config defines RPC service config.
+type Config struct {
+ // Listen string
+ Listen string
+
+ // Disabled disables RPC service.
+ Disabled bool
+}
+
+// InitDefaults allows to init blank config with pre-defined set of default values.
+func (c *Config) InitDefaults() {
+ if c.Listen == "" {
+ c.Listen = "tcp://127.0.0.1:6001"
+ }
+}
+
+// Valid returns nil if config is valid.
+func (c *Config) Valid() error {
+ if dsn := strings.Split(c.Listen, "://"); len(dsn) != 2 {
+ return errors.New("invalid socket DSN (tcp://:6001, unix://file.sock)")
+ }
+
+ return nil
+}
+
+// Listener creates new rpc socket Listener.
+func (c *Config) Listener() (net.Listener, error) {
+ return CreateListener(c.Listen)
+}
+
+// Dialer creates rpc socket Dialer.
+func (c *Config) Dialer() (net.Conn, error) {
+ dsn := strings.Split(c.Listen, "://")
+ if len(dsn) != 2 {
+ return nil, errors.New("invalid socket DSN (tcp://:6001, unix://file.sock)")
+ }
+
+ return net.Dial(dsn[0], dsn[1])
+}
diff --git a/plugins/rpc/doc/plugin_arch.drawio b/plugins/rpc/doc/plugin_arch.drawio
new file mode 100644
index 00000000..dec5f0b2
--- /dev/null
+++ b/plugins/rpc/doc/plugin_arch.drawio
@@ -0,0 +1 @@
+<mxfile host="Electron" modified="2020-10-19T17:14:19.125Z" agent="5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/13.7.9 Chrome/85.0.4183.121 Electron/10.1.3 Safari/537.36" etag="2J39x4EyFr1zaE9BXKM4" version="13.7.9" type="device"><diagram id="q2oMKs6VHyn7y0AfAXBL" name="Page-1">7Vttc9o4EP41zLQfksE2GPIxQHPXu7RlQntt7ptiC1sX2XJlOUB//a1sGdtIJDQFnE6YyUys1YutfR7trlai44yj5R8cJeEH5mPasbv+suNMOrZtORcO/JOSVSEZWv1CEHDiq0aVYEZ+YCXsKmlGfJw2GgrGqCBJU+ixOMaeaMgQ52zRbDZntPnWBAVYE8w8RHXpV+KLUEkt96Kq+BOTIFSvHtqDoiJCZWM1kzREPlvURM67jjPmjIniKVqOMZXKK/VS9LvaUrv+MI5jsUuHL/zu0yx7//HT3Pln8vfN59vvS/usVHMqVuWMsQ8KUEXGRcgCFiP6rpKOOMtiH8thu1Cq2lwzloDQAuF/WIiVQhNlgoEoFBFVtXhJxLfa860c6ryvSpOlGjkvrMpCLPjqW71Q6yWLVbe8VPabs1hcoYhQKRizjBPMYcIf8UJVqq+8gGKhC6mArTpWohQG8lSrfz88xF8/ds/+uiLe7MsXtLiyZ2clVxEPsHik3WDNBFhCmEUYvh36cUyRIA/N70CKy8G6XQU3PCjEfwZ9q030K8RvazVPoV8BftvA+7dE33KOBP9jX/mAaKbedDOFkbpTmgUk1qjRBH4REoFnCcr1sADj3wT55xVv0PMD5gIvayJdU6rWGSi3otyMYw3OlWRRme21VwlrFtsdHEi9jqbe9zERha+ak0DTL0xVNJWIKAliePZAMaA+ZyQVQsA5XaqKiPh+sShxSn6gu3woiU7CSCzyCfVHnf5EjgXrMC103go+3Q18hho6QwM4pfPcOzg9DZwJTnDspyBk8Rqk8ylnDxCB8N8DLcveD1z2BlxWWa4vpu4x8epreOmuK/YvZcQnIaAoTYm34XeO5kMMun/aFRjdj45QDYG+AYBStrMHUW+YSgpWBOgNtxCgHKJwgapXPercGKhvbwxkbQxUKEYbKCfJetrP542r8aa0vt0U9gsE1rpzKfWVeK97ia+Xc41glolhB1viA32Jj+3O5YhIXc9loAHFEczdpRKWO95Ay/2eyZ1UrqqzQq8S14tkmeurrIanQP0vRvmVQYA052WwVAwHE7+rXrHBp/bCI3f4tPu1jMGReyCwLT06KoLPVPDMExnHmvrSBYkoinGpIVWz07oUcm8y8kJC/Wu0YpmcXiqQd1+WRiHj5AcMi0qIoJqXMNhuo8VM9lQLO1/oeFqiY22IPqBlo+E1SoUSeIxSlKTkbj2NCGwhiUdMCBbt0/k8P47uuQarULapE8Vye4diytDg+ke7R2hAKHaPx4wyIMYkZgWBCKUbopJDFM/FVgalsOEhcXCdt5n0KsmNUoUUMeg7p3kgEoI/wHG+axZIbPUHI9DyWIYl4BnsMZStqpw7iwT22WMWw1wQycHFwKMFTsUvU+Tx1fk0cUr34e7GE/tQBqV0SxpNpJGeYf6QK+VNjMX5TeK9PbGlTbb07ZbZYl1sYUsKTCEeltvAIlKr+aNuSqHqxJw2mTMwBC7HZY6eOSiYMydYni3IeHH8aILnxIk9c8Lq9tomxQ7pCUpyqAszUZ4lWc/iw3qXqQjwOc+8n1kaSRydJI6BEBTdYTqF3WixH57woq1h0/ryueDsGLAOD0UFPeNQ2AcYPmT+G7FK8NvCTMjHkzdply1HdCfmIzhDHvMIR3Av9jDVrKTOjjnUCzPaRzpN1Ra+Ciafk9Xo/nK6wmAsfpMMhrZ+DazZmsHoNTNdPcvgD1xDpmuwB4dgpIX9dLxY8aTKdZ78wp7osn2t/lQyw8SZg3kFPTmqcSZGkTIsgNeJLS2yxZTMOCpb9IizMigcByQFmyITGlYxV4A2o0iqyc+PvOGvYYPmTNbl2Xgzq17Wgdie/Ia1cYFkqO8pHftAx2FGVPUMVVJkul8VLK61cXJl67gc6pTSbAvcVgJ245259TW5Vm5M1k6i9xPlO7uG+b1Ww3zdOVdXCk5h/pHsgtM0C64p7WNywqWz3j8tdsgLX0tXHJ+itiNFbVsu176UIN/SL7xMOQOFR2lOl7a9fN3MP4rYHpbzxq7dsGk/1O1QMzT6nYOAqSAZFqaPvY78hYecQIBjzJGQgbNgsk2UeaH8Ji93RdLvefdY3ohDeZyNlx7G8iGjJMqvA5/pV61fE9YGy93fU6ANxer3NcWNwupXSs67/wE=</diagram></mxfile> \ No newline at end of file
diff --git a/plugins/rpc/interface.go b/plugins/rpc/interface.go
new file mode 100644
index 00000000..683fd2ec
--- /dev/null
+++ b/plugins/rpc/interface.go
@@ -0,0 +1,7 @@
+package rpc
+
+// RPCer declares the ability to create set of public RPC methods.
+type RPCer interface {
+ // Provides RPC methods for the given service.
+ RPC() interface{}
+}
diff --git a/plugins/rpc/plugin.go b/plugins/rpc/plugin.go
new file mode 100644
index 00000000..6a83326b
--- /dev/null
+++ b/plugins/rpc/plugin.go
@@ -0,0 +1,164 @@
+package rpc
+
+import (
+ "net"
+ "net/rpc"
+ "sync/atomic"
+
+ "github.com/spiral/endure"
+ "github.com/spiral/errors"
+ goridgeRpc "github.com/spiral/goridge/v3/pkg/rpc"
+ "github.com/spiral/roadrunner/v2/plugins/config"
+ "github.com/spiral/roadrunner/v2/plugins/logger"
+)
+
+// PluginName contains default plugin name.
+const PluginName = "RPC"
+
+type pluggable struct {
+ service RPCer
+ name string
+}
+
+// Plugin is RPC service.
+type Plugin struct {
+ cfg Config
+ log logger.Logger
+ rpc *rpc.Server
+ services []pluggable
+ listener net.Listener
+ closed *uint32
+}
+
+// Init rpc service. Must return true if service is enabled.
+func (s *Plugin) Init(cfg config.Configurer, log logger.Logger) error {
+ const op = errors.Op("RPC plugin init")
+ if !cfg.Has(PluginName) {
+ return errors.E(op, errors.Disabled)
+ }
+
+ err := cfg.UnmarshalKey(PluginName, &s.cfg)
+ if err != nil {
+ return errors.E(op, errors.Disabled, err)
+ }
+ s.cfg.InitDefaults()
+
+ if s.cfg.Disabled {
+ return errors.E(op, errors.Disabled)
+ }
+
+ s.log = log
+ state := uint32(0)
+ s.closed = &state
+ atomic.StoreUint32(s.closed, 0)
+
+ return s.cfg.Valid()
+}
+
+// Serve serves the service.
+func (s *Plugin) Serve() chan error {
+ const op = errors.Op("register service")
+ errCh := make(chan error, 1)
+
+ s.rpc = rpc.NewServer()
+
+ services := make([]string, 0, len(s.services))
+
+ // Attach all services
+ for i := 0; i < len(s.services); i++ {
+ err := s.Register(s.services[i].name, s.services[i].service.RPC())
+ if err != nil {
+ errCh <- errors.E(op, err)
+ return errCh
+ }
+
+ services = append(services, s.services[i].name)
+ }
+
+ var err error
+ s.listener, err = s.cfg.Listener()
+ if err != nil {
+ errCh <- err
+ return errCh
+ }
+
+ s.log.Debug("Started RPC service", "address", s.cfg.Listen, "services", services)
+
+ go func() {
+ for {
+ conn, err := s.listener.Accept()
+ if err != nil {
+ if atomic.LoadUint32(s.closed) == 1 {
+ // just log and continue, this is not a critical issue, we just called Stop
+ s.log.Warn("listener accept error, connection closed", "error", err)
+ return
+ }
+
+ s.log.Error("listener accept error", "error", err)
+ errCh <- errors.E(errors.Op("listener accept"), errors.Serve, err)
+ return
+ }
+
+ go s.rpc.ServeCodec(goridgeRpc.NewCodec(conn))
+ }
+ }()
+
+ return errCh
+}
+
+// Stop stops the service.
+func (s *Plugin) Stop() error {
+ // store closed state
+ atomic.StoreUint32(s.closed, 1)
+ err := s.listener.Close()
+ if err != nil {
+ return errors.E(errors.Op("stop RPC socket"), err)
+ }
+ return nil
+}
+
+// Name contains service name.
+func (s *Plugin) Name() string {
+ return PluginName
+}
+
+// Depends declares services to collect for RPC.
+func (s *Plugin) Collects() []interface{} {
+ return []interface{}{
+ s.RegisterPlugin,
+ }
+}
+
+// RegisterPlugin registers RPC service plugin.
+func (s *Plugin) RegisterPlugin(name endure.Named, p RPCer) {
+ s.services = append(s.services, pluggable{
+ service: p,
+ name: name.Name(),
+ })
+}
+
+// Register publishes in the server the set of methods of the
+// receiver value that satisfy the following conditions:
+// - exported method of exported type
+// - two arguments, both of exported type
+// - the second argument is a pointer
+// - one return value, of type error
+// It returns an error if the receiver is not an exported type or has
+// no suitable methods. It also logs the error using package log.
+func (s *Plugin) Register(name string, svc interface{}) error {
+ if s.rpc == nil {
+ return errors.E("RPC service is not configured")
+ }
+
+ return s.rpc.RegisterName(name, svc)
+}
+
+// Client creates new RPC client.
+func (s *Plugin) Client() (*rpc.Client, error) {
+ conn, err := s.cfg.Dialer()
+ if err != nil {
+ return nil, err
+ }
+
+ return rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)), nil
+}
diff --git a/plugins/rpc/util.go b/plugins/rpc/util.go
new file mode 100644
index 00000000..29a475a4
--- /dev/null
+++ b/plugins/rpc/util.go
@@ -0,0 +1,57 @@
+package rpc
+
+import (
+ "errors"
+ "fmt"
+ "net"
+ "os"
+ "strings"
+ "syscall"
+
+ "github.com/valyala/tcplisten"
+)
+
+// CreateListener crates socket listener based on DSN definition.
+func CreateListener(address string) (net.Listener, error) {
+ dsn := strings.Split(address, "://")
+ if len(dsn) != 2 {
+ return nil, errors.New("invalid DSN (tcp://:6001, unix://file.sock)")
+ }
+
+ if dsn[0] != "unix" && dsn[0] != "tcp" {
+ return nil, errors.New("invalid Protocol (tcp://:6001, unix://file.sock)")
+ }
+
+ // create unix listener
+ if dsn[0] == "unix" {
+ // check if the file exist
+ if fileExists(dsn[1]) {
+ err := syscall.Unlink(dsn[1])
+ if err != nil {
+ return nil, fmt.Errorf("error during the unlink syscall: error %v", err)
+ }
+ }
+ return net.Listen(dsn[0], dsn[1])
+ }
+
+ // configure and create tcp4 listener
+ cfg := tcplisten.Config{
+ ReusePort: true,
+ DeferAccept: true,
+ FastOpen: true,
+ Backlog: 0,
+ }
+
+ // only tcp4 is currently supported
+ return cfg.NewListener("tcp4", dsn[1])
+}
+
+// fileExists checks if a file exists and is not a directory before we
+// try using it to prevent further errors.
+func fileExists(filename string) bool {
+ info, err := os.Stat(filename)
+ if os.IsNotExist(err) {
+ return false
+ }
+ return !info.IsDir()
+}
diff --git a/pkg/plugins/server/config.go b/plugins/server/config.go
index 4bef3c5f..4bef3c5f 100644
--- a/pkg/plugins/server/config.go
+++ b/plugins/server/config.go
diff --git a/pkg/plugins/server/interface.go b/plugins/server/interface.go
index 9c1079ea..9c1079ea 100644
--- a/pkg/plugins/server/interface.go
+++ b/plugins/server/interface.go
diff --git a/pkg/plugins/server/plugin.go b/plugins/server/plugin.go
index 3d90c95b..b280d253 100644
--- a/pkg/plugins/server/plugin.go
+++ b/plugins/server/plugin.go
@@ -8,8 +8,8 @@ import (
"strings"
"github.com/spiral/errors"
- "github.com/spiral/roadrunner-plugins/config"
- "github.com/spiral/roadrunner-plugins/logger"
+ "github.com/spiral/roadrunner/v2/plugins/config"
+ "github.com/spiral/roadrunner/v2/plugins/logger"
// core imports
"github.com/spiral/roadrunner/v2/interfaces/events"
diff --git a/plugins/static/config.go b/plugins/static/config.go
new file mode 100644
index 00000000..f5d26b2d
--- /dev/null
+++ b/plugins/static/config.go
@@ -0,0 +1,76 @@
+package static
+
+import (
+ "os"
+ "path"
+ "strings"
+
+ "github.com/spiral/errors"
+)
+
+// Config describes file location and controls access to them.
+type Config struct {
+ Static struct {
+ // Dir contains name of directory to control access to.
+ Dir string
+
+ // Forbid specifies list of file extensions which are forbidden for access.
+ // Example: .php, .exe, .bat, .htaccess and etc.
+ Forbid []string
+
+ // Always specifies list of extensions which must always be served by static
+ // service, even if file not found.
+ Always []string
+
+ // Request headers to add to every static.
+ Request map[string]string
+
+ // Response headers to add to every static.
+ Response map[string]string
+ }
+}
+
+// Valid returns nil if config is valid.
+func (c *Config) Valid() error {
+ const op = errors.Op("static plugin validation")
+ st, err := os.Stat(c.Static.Dir)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return errors.E(op, errors.Errorf("root directory '%s' does not exists", c.Static.Dir))
+ }
+
+ return err
+ }
+
+ if !st.IsDir() {
+ return errors.E(op, errors.Errorf("invalid root directory '%s'", c.Static.Dir))
+ }
+
+ return nil
+}
+
+// AlwaysForbid must return true if file extension is not allowed for the upload.
+func (c *Config) AlwaysForbid(filename string) bool {
+ ext := strings.ToLower(path.Ext(filename))
+
+ for _, v := range c.Static.Forbid {
+ if ext == v {
+ return true
+ }
+ }
+
+ return false
+}
+
+// AlwaysServe must indicate that file is expected to be served by static service.
+func (c *Config) AlwaysServe(filename string) bool {
+ ext := strings.ToLower(path.Ext(filename))
+
+ for _, v := range c.Static.Always {
+ if ext == v {
+ return true
+ }
+ }
+
+ return false
+}
diff --git a/plugins/static/plugin.go b/plugins/static/plugin.go
new file mode 100644
index 00000000..06b384df
--- /dev/null
+++ b/plugins/static/plugin.go
@@ -0,0 +1,110 @@
+package static
+
+import (
+ "net/http"
+ "path"
+
+ "github.com/spiral/errors"
+ "github.com/spiral/roadrunner/v2/plugins/config"
+ "github.com/spiral/roadrunner/v2/plugins/logger"
+)
+
+// ID contains default service name.
+const PluginName = "static"
+
+const RootPluginName = "http"
+
+// Plugin serves static files. Potentially convert into middleware?
+type Plugin struct {
+ // server configuration (location, forbidden files and etc)
+ cfg *Config
+
+ log logger.Logger
+
+ // root is initiated http directory
+ root http.Dir
+}
+
+// Init must return configure service and return true if service hasStatus enabled. Must return error in case of
+// misconfiguration. Services must not be used without proper configuration pushed first.
+func (s *Plugin) Init(cfg config.Configurer, log logger.Logger) error {
+ const op = errors.Op("static plugin init")
+ err := cfg.UnmarshalKey(RootPluginName, &s.cfg)
+ if err != nil {
+ return errors.E(op, errors.Disabled, err)
+ }
+
+ s.log = log
+ s.root = http.Dir(s.cfg.Static.Dir)
+
+ err = s.cfg.Valid()
+ if err != nil {
+ return errors.E(op, errors.Disabled, err)
+ }
+
+ return nil
+}
+
+func (s *Plugin) Name() string {
+ return PluginName
+}
+
+// middleware must return true if request/response pair is handled within the middleware.
+func (s *Plugin) Middleware(next http.Handler) http.HandlerFunc {
+ // Define the http.HandlerFunc
+ return func(w http.ResponseWriter, r *http.Request) {
+ if s.cfg.Static.Request != nil {
+ for k, v := range s.cfg.Static.Request {
+ r.Header.Add(k, v)
+ }
+ }
+
+ if s.cfg.Static.Response != nil {
+ for k, v := range s.cfg.Static.Response {
+ w.Header().Set(k, v)
+ }
+ }
+
+ if !s.handleStatic(w, r) {
+ next.ServeHTTP(w, r)
+ }
+ }
+}
+
+func (s *Plugin) handleStatic(w http.ResponseWriter, r *http.Request) bool {
+ fPath := path.Clean(r.URL.Path)
+
+ if s.cfg.AlwaysForbid(fPath) {
+ return false
+ }
+
+ f, err := s.root.Open(fPath)
+ if err != nil {
+ s.log.Error("file open error", "error", err)
+ if s.cfg.AlwaysServe(fPath) {
+ w.WriteHeader(404)
+ return true
+ }
+
+ return false
+ }
+ defer func() {
+ err = f.Close()
+ if err != nil {
+ s.log.Error("file closing error", "error", err)
+ }
+ }()
+
+ d, err := f.Stat()
+ if err != nil {
+ return false
+ }
+
+ // do not serve directories
+ if d.IsDir() {
+ return false
+ }
+
+ http.ServeContent(w, r, d.Name(), d.ModTime(), f)
+ return true
+}
diff --git a/tests/mocks/mock_log.go b/tests/mocks/mock_log.go
index 8e3e2836..e9631805 100644
--- a/tests/mocks/mock_log.go
+++ b/tests/mocks/mock_log.go
@@ -4,7 +4,7 @@ import (
"reflect"
"github.com/golang/mock/gomock"
- "github.com/spiral/roadrunner-plugins/logger"
+ "github.com/spiral/roadrunner/v2/plugins/logger"
)
// MockLogger is a mock of Logger interface.
diff --git a/tests/plugins/checker/configs/.rr-checker-init.yaml b/tests/plugins/checker/configs/.rr-checker-init.yaml
new file mode 100755
index 00000000..1273529a
--- /dev/null
+++ b/tests/plugins/checker/configs/.rr-checker-init.yaml
@@ -0,0 +1,31 @@
+rpc:
+ listen: tcp://127.0.0.1:6005
+ disabled: false
+
+server:
+ command: "php ../../http/client.php echo pipes"
+ user: ""
+ group: ""
+ env:
+ "RR_HTTP": "true"
+ relay: "pipes"
+ relayTimeout: "20s"
+
+status:
+ address: "127.0.0.1:34333"
+logs:
+ mode: development
+ level: debug
+http:
+ debug: true
+ address: 127.0.0.1:11933
+ maxRequestSize: 1024
+ middleware: [ "" ]
+ uploads:
+ forbid: [ ".php", ".exe", ".bat" ]
+ trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ]
+ pool:
+ numWorkers: 2
+ maxJobs: 0
+ allocateTimeout: 60s
+ destroyTimeout: 60s \ No newline at end of file
diff --git a/tests/plugins/checker/plugin_test.go b/tests/plugins/checker/plugin_test.go
new file mode 100644
index 00000000..c346d91a
--- /dev/null
+++ b/tests/plugins/checker/plugin_test.go
@@ -0,0 +1,190 @@
+package checker
+
+import (
+ "io/ioutil"
+ "net"
+ "net/http"
+ "net/rpc"
+ "os"
+ "os/signal"
+ "sync"
+ "syscall"
+ "testing"
+ "time"
+
+ "github.com/spiral/endure"
+ goridgeRpc "github.com/spiral/goridge/v3/pkg/rpc"
+ "github.com/spiral/roadrunner/v2/plugins/checker"
+ "github.com/spiral/roadrunner/v2/plugins/config"
+ httpPlugin "github.com/spiral/roadrunner/v2/plugins/http"
+ "github.com/spiral/roadrunner/v2/plugins/logger"
+ rpcPlugin "github.com/spiral/roadrunner/v2/plugins/rpc"
+ "github.com/spiral/roadrunner/v2/plugins/server"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestStatusHttp(t *testing.T) {
+ cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel))
+ assert.NoError(t, err)
+
+ cfg := &config.Viper{
+ Path: "configs/.rr-checker-init.yaml",
+ Prefix: "rr",
+ }
+
+ err = cont.RegisterAll(
+ cfg,
+ &logger.ZapLogger{},
+ &server.Plugin{},
+ &httpPlugin.Plugin{},
+ &checker.Plugin{},
+ )
+ assert.NoError(t, err)
+
+ err = cont.Init()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ch, err := cont.Serve()
+ assert.NoError(t, err)
+
+ sig := make(chan os.Signal, 1)
+ signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
+
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
+
+ stopCh := make(chan struct{}, 1)
+
+ go func() {
+ defer wg.Done()
+ for {
+ select {
+ case e := <-ch:
+ assert.Fail(t, "error", e.Error.Error())
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ case <-sig:
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ case <-stopCh:
+ // timeout
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ }
+ }
+ }()
+
+ time.Sleep(time.Second)
+ t.Run("CheckerGetStatus", checkHTTPStatus)
+
+ stopCh <- struct{}{}
+ wg.Wait()
+}
+
+const resp = `Service: http: Status: 200
+Service: rpc not found`
+
+func checkHTTPStatus(t *testing.T) {
+ req, err := http.NewRequest("GET", "http://127.0.0.1:34333/v1/health?plugin=http&plugin=rpc", nil)
+ assert.NoError(t, err)
+
+ r, err := http.DefaultClient.Do(req)
+ assert.NoError(t, err)
+ b, err := ioutil.ReadAll(r.Body)
+ assert.NoError(t, err)
+ assert.Equal(t, 200, r.StatusCode)
+ assert.Equal(t, resp, string(b))
+
+ err = r.Body.Close()
+ assert.NoError(t, err)
+}
+
+func TestStatusRPC(t *testing.T) {
+ cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel))
+ assert.NoError(t, err)
+
+ cfg := &config.Viper{
+ Path: "configs/.rr-checker-init.yaml",
+ Prefix: "rr",
+ }
+
+ err = cont.RegisterAll(
+ cfg,
+ &rpcPlugin.Plugin{},
+ &logger.ZapLogger{},
+ &server.Plugin{},
+ &httpPlugin.Plugin{},
+ &checker.Plugin{},
+ )
+ assert.NoError(t, err)
+
+ err = cont.Init()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ch, err := cont.Serve()
+ assert.NoError(t, err)
+
+ sig := make(chan os.Signal, 1)
+ signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
+
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
+
+ stopCh := make(chan struct{}, 1)
+
+ go func() {
+ defer wg.Done()
+ for {
+ select {
+ case e := <-ch:
+ assert.Fail(t, "error", e.Error.Error())
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ case <-sig:
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ case <-stopCh:
+ // timeout
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ }
+ }
+ }()
+
+ time.Sleep(time.Second)
+ t.Run("CheckerGetStatusRpc", checkRPCStatus)
+ stopCh <- struct{}{}
+ wg.Wait()
+}
+
+func checkRPCStatus(t *testing.T) {
+ conn, err := net.Dial("tcp", "127.0.0.1:6005")
+ assert.NoError(t, err)
+ client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn))
+
+ st := &checker.Status{}
+
+ err = client.Call("status.Status", "http", &st)
+ assert.NoError(t, err)
+ assert.Equal(t, st.Code, 200)
+}
diff --git a/tests/plugins/config/.rr.yaml b/tests/plugins/config/.rr.yaml
new file mode 100755
index 00000000..732a1366
--- /dev/null
+++ b/tests/plugins/config/.rr.yaml
@@ -0,0 +1,18 @@
+reload:
+ enabled: true
+ interval: 1s
+ patterns: [".php"]
+ services:
+ http:
+ recursive: true
+ ignore: ["vendor"]
+ patterns: [".php", ".go",".md",]
+ dirs: ["."]
+ jobs:
+ recursive: false
+ ignore: ["service/metrics"]
+ dirs: ["./jobs"]
+ rpc:
+ recursive: true
+ patterns: [".json"]
+ dirs: [""]
diff --git a/tests/plugins/config/config_test.go b/tests/plugins/config/config_test.go
new file mode 100755
index 00000000..858fcb80
--- /dev/null
+++ b/tests/plugins/config/config_test.go
@@ -0,0 +1,66 @@
+package config
+
+import (
+ "os"
+ "os/signal"
+ "testing"
+ "time"
+
+ "github.com/spiral/endure"
+ "github.com/spiral/roadrunner/v2/plugins/config"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestViperProvider_Init(t *testing.T) {
+ container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.ErrorLevel))
+ if err != nil {
+ t.Fatal(err)
+ }
+ vp := &config.Viper{}
+ vp.Path = ".rr.yaml"
+ vp.Prefix = "rr"
+ err = container.Register(vp)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = container.Register(&Foo{})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = container.Init()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ errCh, err := container.Serve()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // stop by CTRL+C
+ c := make(chan os.Signal, 1)
+ signal.Notify(c, os.Interrupt)
+
+ tt := time.NewTicker(time.Second * 2)
+
+ for {
+ select {
+ case e := <-errCh:
+ assert.NoError(t, e.Error)
+ assert.NoError(t, container.Stop())
+ return
+ case <-c:
+ er := container.Stop()
+ if er != nil {
+ panic(er)
+ }
+ return
+ case <-tt.C:
+ tt.Stop()
+ assert.NoError(t, container.Stop())
+ return
+ }
+ }
+}
diff --git a/tests/plugins/config/plugin1.go b/tests/plugins/config/plugin1.go
new file mode 100755
index 00000000..2afe79a4
--- /dev/null
+++ b/tests/plugins/config/plugin1.go
@@ -0,0 +1,53 @@
+package config
+
+import (
+ "errors"
+ "time"
+
+ "github.com/spiral/roadrunner/v2/plugins/config"
+)
+
+// ReloadConfig is a Reload configuration point.
+type ReloadConfig struct {
+ Interval time.Duration
+ Patterns []string
+ Services map[string]ServiceConfig
+}
+
+type ServiceConfig struct {
+ Enabled bool
+ Recursive bool
+ Patterns []string
+ Dirs []string
+ Ignore []string
+}
+
+type Foo struct {
+ configProvider config.Configurer
+}
+
+// Depends on S2 and DB (S3 in the current case)
+func (f *Foo) Init(p config.Configurer) error {
+ f.configProvider = p
+ return nil
+}
+
+func (f *Foo) Serve() chan error {
+ errCh := make(chan error, 1)
+
+ r := &ReloadConfig{}
+ err := f.configProvider.UnmarshalKey("reload", r)
+ if err != nil {
+ errCh <- err
+ }
+
+ if len(r.Patterns) == 0 {
+ errCh <- errors.New("should be at least one pattern, but got 0")
+ }
+
+ return errCh
+}
+
+func (f *Foo) Stop() error {
+ return nil
+}
diff --git a/tests/plugins/gzip/configs/.rr-http-middlewareNotExist.yaml b/tests/plugins/gzip/configs/.rr-http-middlewareNotExist.yaml
new file mode 100644
index 00000000..a2d12706
--- /dev/null
+++ b/tests/plugins/gzip/configs/.rr-http-middlewareNotExist.yaml
@@ -0,0 +1,25 @@
+server:
+ command: "php ../../psr-worker.php"
+ user: ""
+ group: ""
+ env:
+ "RR_HTTP": "true"
+ relay: "pipes"
+ relayTimeout: "20s"
+
+http:
+ debug: true
+ address: 127.0.0.1:18103
+ maxRequestSize: 1024
+ middleware: [ "gzip", "foo" ]
+ uploads:
+ forbid: [ ".php", ".exe", ".bat" ]
+ trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ]
+ pool:
+ numWorkers: 2
+ maxJobs: 0
+ allocateTimeout: 60s
+ destroyTimeout: 60s
+logs:
+ mode: development
+ level: error \ No newline at end of file
diff --git a/tests/plugins/gzip/configs/.rr-http-withGzip.yaml b/tests/plugins/gzip/configs/.rr-http-withGzip.yaml
new file mode 100644
index 00000000..aff3efdb
--- /dev/null
+++ b/tests/plugins/gzip/configs/.rr-http-withGzip.yaml
@@ -0,0 +1,25 @@
+server:
+ command: "php ../../psr-worker.php"
+ user: ""
+ group: ""
+ env:
+ "RR_HTTP": "true"
+ relay: "pipes"
+ relayTimeout: "20s"
+
+http:
+ debug: true
+ address: 127.0.0.1:18953
+ maxRequestSize: 1024
+ middleware: [ "gzip" ]
+ uploads:
+ forbid: [ ".php", ".exe", ".bat" ]
+ trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ]
+ pool:
+ numWorkers: 2
+ maxJobs: 0
+ allocateTimeout: 60s
+ destroyTimeout: 60s
+logs:
+ mode: development
+ level: error \ No newline at end of file
diff --git a/tests/plugins/gzip/plugin_test.go b/tests/plugins/gzip/plugin_test.go
new file mode 100644
index 00000000..b09d430e
--- /dev/null
+++ b/tests/plugins/gzip/plugin_test.go
@@ -0,0 +1,176 @@
+package gzip
+
+import (
+ "net/http"
+ "os"
+ "os/signal"
+ "sync"
+ "syscall"
+ "testing"
+
+ "github.com/golang/mock/gomock"
+ "github.com/spiral/endure"
+ "github.com/spiral/roadrunner/v2/plugins/config"
+ "github.com/spiral/roadrunner/v2/plugins/gzip"
+ httpPlugin "github.com/spiral/roadrunner/v2/plugins/http"
+ "github.com/spiral/roadrunner/v2/plugins/logger"
+ "github.com/spiral/roadrunner/v2/plugins/server"
+ "github.com/spiral/roadrunner/v2/tests/mocks"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestGzipPlugin(t *testing.T) {
+ cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel))
+ assert.NoError(t, err)
+
+ cfg := &config.Viper{
+ Path: "configs/.rr-http-withGzip.yaml",
+ Prefix: "rr",
+ }
+
+ err = cont.RegisterAll(
+ cfg,
+ &logger.ZapLogger{},
+ &server.Plugin{},
+ &httpPlugin.Plugin{},
+ &gzip.Gzip{},
+ )
+ assert.NoError(t, err)
+
+ err = cont.Init()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ch, err := cont.Serve()
+ assert.NoError(t, err)
+
+ sig := make(chan os.Signal, 1)
+ signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
+
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
+
+ stopCh := make(chan struct{}, 1)
+
+ go func() {
+ defer wg.Done()
+ for {
+ select {
+ case e := <-ch:
+ assert.Fail(t, "error", e.Error.Error())
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ case <-sig:
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ case <-stopCh:
+ // timeout
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ }
+ }
+ }()
+
+ t.Run("GzipCheckHeader", headerCheck)
+
+ stopCh <- struct{}{}
+ wg.Wait()
+}
+
+func headerCheck(t *testing.T) {
+ req, err := http.NewRequest("GET", "http://localhost:18953", nil)
+ assert.NoError(t, err)
+ client := &http.Client{
+ Transport: &http.Transport{
+ DisableCompression: false,
+ },
+ }
+
+ r, err := client.Do(req)
+ assert.NoError(t, err)
+ assert.True(t, r.Uncompressed)
+
+ err = r.Body.Close()
+ assert.NoError(t, err)
+}
+
+func TestMiddlewareNotExist(t *testing.T) {
+ cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel))
+ assert.NoError(t, err)
+
+ cfg := &config.Viper{
+ Path: "configs/.rr-http-middlewareNotExist.yaml",
+ Prefix: "rr",
+ }
+
+ controller := gomock.NewController(t)
+ mockLogger := mocks.NewMockLogger(controller)
+
+ mockLogger.EXPECT().Warn("requested middleware does not exist", "requested", "foo").AnyTimes()
+ mockLogger.EXPECT().Info("worker constructed", "pid", gomock.Any()).AnyTimes()
+ mockLogger.EXPECT().Info(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() // placeholder for the workerlogerror
+
+ err = cont.RegisterAll(
+ cfg,
+ mockLogger,
+ &server.Plugin{},
+ &httpPlugin.Plugin{},
+ &gzip.Gzip{},
+ )
+ assert.NoError(t, err)
+
+ err = cont.Init()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ch, err := cont.Serve()
+ assert.NoError(t, err)
+
+ sig := make(chan os.Signal, 1)
+ signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
+
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
+
+ stopCh := make(chan struct{}, 1)
+
+ go func() {
+ defer wg.Done()
+ for {
+ select {
+ case e := <-ch:
+ assert.Fail(t, "error", e.Error.Error())
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ case <-sig:
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ case <-stopCh:
+ // timeout
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ }
+ }
+ }()
+
+ stopCh <- struct{}{}
+ wg.Wait()
+}
diff --git a/tests/plugins/headers/configs/.rr-cors-headers.yaml b/tests/plugins/headers/configs/.rr-cors-headers.yaml
new file mode 100644
index 00000000..9d4e8b36
--- /dev/null
+++ b/tests/plugins/headers/configs/.rr-cors-headers.yaml
@@ -0,0 +1,39 @@
+server:
+ command: "php ../../http/client.php headers pipes"
+ user: ""
+ group: ""
+ env:
+ "RR_HTTP": "true"
+ relay: "pipes"
+ relayTimeout: "20s"
+
+http:
+ debug: true
+ address: 127.0.0.1:22855
+ maxRequestSize: 1024
+ middleware: [ "headers" ]
+ uploads:
+ forbid: [ ".php", ".exe", ".bat" ]
+ trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ]
+ # Additional HTTP headers and CORS control.
+ headers:
+ cors:
+ allowedOrigin: "*"
+ allowedHeaders: "*"
+ allowedMethods: "GET,POST,PUT,DELETE"
+ allowCredentials: true
+ exposedHeaders: "Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma"
+ maxAge: 600
+ request:
+ "input": "custom-header"
+ response:
+ "output": "output-header"
+ pool:
+ numWorkers: 2
+ maxJobs: 0
+ allocateTimeout: 60s
+ destroyTimeout: 60s
+logs:
+ mode: development
+ level: error
+
diff --git a/tests/plugins/headers/configs/.rr-headers-init.yaml b/tests/plugins/headers/configs/.rr-headers-init.yaml
new file mode 100644
index 00000000..8d63a187
--- /dev/null
+++ b/tests/plugins/headers/configs/.rr-headers-init.yaml
@@ -0,0 +1,39 @@
+server:
+ command: "php ../../http/client.php echo pipes"
+ user: ""
+ group: ""
+ env:
+ "RR_HTTP": "true"
+ relay: "pipes"
+ relayTimeout: "20s"
+
+http:
+ debug: true
+ address: 127.0.0.1:33453
+ maxRequestSize: 1024
+ middleware: [ "headers" ]
+ uploads:
+ forbid: [ ".php", ".exe", ".bat" ]
+ trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ]
+ # Additional HTTP headers and CORS control.
+ headers:
+ cors:
+ allowedOrigin: "*"
+ allowedHeaders: "*"
+ allowedMethods: "GET,POST,PUT,DELETE"
+ allowCredentials: true
+ exposedHeaders: "Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma"
+ maxAge: 600
+ request:
+ "Example-Request-Header": "Value"
+ response:
+ "X-Powered-By": "RoadRunner"
+ pool:
+ numWorkers: 2
+ maxJobs: 0
+ allocateTimeout: 60s
+ destroyTimeout: 60s
+logs:
+ mode: development
+ level: error
+
diff --git a/tests/plugins/headers/configs/.rr-req-headers.yaml b/tests/plugins/headers/configs/.rr-req-headers.yaml
new file mode 100644
index 00000000..f8ab9bec
--- /dev/null
+++ b/tests/plugins/headers/configs/.rr-req-headers.yaml
@@ -0,0 +1,32 @@
+server:
+ command: "php ../../http/client.php header pipes"
+ user: ""
+ group: ""
+ env:
+ "RR_HTTP": "true"
+ relay: "pipes"
+ relayTimeout: "20s"
+
+http:
+ debug: true
+ address: 127.0.0.1:22655
+ maxRequestSize: 1024
+ middleware: [ "headers" ]
+ uploads:
+ forbid: [ ".php", ".exe", ".bat" ]
+ trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ]
+ # Additional HTTP headers and CORS control.
+ headers:
+ request:
+ "input": "custom-header"
+ response:
+ "output": "output-header"
+ pool:
+ numWorkers: 2
+ maxJobs: 0
+ allocateTimeout: 60s
+ destroyTimeout: 60s
+logs:
+ mode: development
+ level: error
+
diff --git a/tests/plugins/headers/configs/.rr-res-headers.yaml b/tests/plugins/headers/configs/.rr-res-headers.yaml
new file mode 100644
index 00000000..36ab4eb3
--- /dev/null
+++ b/tests/plugins/headers/configs/.rr-res-headers.yaml
@@ -0,0 +1,32 @@
+server:
+ command: "php ../../http/client.php header pipes"
+ user: ""
+ group: ""
+ env:
+ "RR_HTTP": "true"
+ relay: "pipes"
+ relayTimeout: "20s"
+
+http:
+ debug: true
+ address: 127.0.0.1:22455
+ maxRequestSize: 1024
+ middleware: [ "headers" ]
+ uploads:
+ forbid: [ ".php", ".exe", ".bat" ]
+ trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ]
+ # Additional HTTP headers and CORS control.
+ headers:
+ request:
+ "input": "custom-header"
+ response:
+ "output": "output-header"
+ pool:
+ numWorkers: 2
+ maxJobs: 0
+ allocateTimeout: 60s
+ destroyTimeout: 60s
+logs:
+ mode: development
+ level: error
+
diff --git a/tests/plugins/headers/headers_plugin_test.go b/tests/plugins/headers/headers_plugin_test.go
new file mode 100644
index 00000000..a2ad3357
--- /dev/null
+++ b/tests/plugins/headers/headers_plugin_test.go
@@ -0,0 +1,367 @@
+package headers
+
+import (
+ "io/ioutil"
+ "net/http"
+ "os"
+ "os/signal"
+ "sync"
+ "syscall"
+ "testing"
+ "time"
+
+ "github.com/spiral/endure"
+ "github.com/spiral/roadrunner/v2/plugins/config"
+ "github.com/spiral/roadrunner/v2/plugins/headers"
+ httpPlugin "github.com/spiral/roadrunner/v2/plugins/http"
+ "github.com/spiral/roadrunner/v2/plugins/logger"
+ "github.com/spiral/roadrunner/v2/plugins/server"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestHeadersInit(t *testing.T) {
+ cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel))
+ assert.NoError(t, err)
+
+ cfg := &config.Viper{
+ Path: "configs/.rr-headers-init.yaml",
+ Prefix: "rr",
+ }
+
+ err = cont.RegisterAll(
+ cfg,
+ &logger.ZapLogger{},
+ &server.Plugin{},
+ &httpPlugin.Plugin{},
+ &headers.Plugin{},
+ )
+ assert.NoError(t, err)
+
+ err = cont.Init()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ch, err := cont.Serve()
+ assert.NoError(t, err)
+
+ sig := make(chan os.Signal, 1)
+ signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
+
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
+
+ stopCh := make(chan struct{}, 1)
+
+ go func() {
+ defer wg.Done()
+ for {
+ select {
+ case e := <-ch:
+ assert.Fail(t, "error", e.Error.Error())
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ case <-sig:
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ case <-stopCh:
+ // timeout
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ }
+ }
+ }()
+
+ stopCh <- struct{}{}
+ wg.Wait()
+}
+
+func TestRequestHeaders(t *testing.T) {
+ cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel))
+ assert.NoError(t, err)
+
+ cfg := &config.Viper{
+ Path: "configs/.rr-req-headers.yaml",
+ Prefix: "rr",
+ }
+
+ err = cont.RegisterAll(
+ cfg,
+ &logger.ZapLogger{},
+ &server.Plugin{},
+ &httpPlugin.Plugin{},
+ &headers.Plugin{},
+ )
+ assert.NoError(t, err)
+
+ err = cont.Init()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ch, err := cont.Serve()
+ assert.NoError(t, err)
+
+ sig := make(chan os.Signal, 1)
+ signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
+
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
+
+ stopCh := make(chan struct{}, 1)
+
+ go func() {
+ defer wg.Done()
+ for {
+ select {
+ case e := <-ch:
+ assert.Fail(t, "error", e.Error.Error())
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ case <-sig:
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ case <-stopCh:
+ // timeout
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ }
+ }
+ }()
+
+ time.Sleep(time.Second)
+ t.Run("RequestHeaders", reqHeaders)
+
+ stopCh <- struct{}{}
+ wg.Wait()
+}
+
+func reqHeaders(t *testing.T) {
+ req, err := http.NewRequest("GET", "http://localhost:22655?hello=value", nil)
+ assert.NoError(t, err)
+
+ r, err := http.DefaultClient.Do(req)
+ assert.NoError(t, err)
+
+ b, err := ioutil.ReadAll(r.Body)
+ assert.NoError(t, err)
+
+ assert.Equal(t, 200, r.StatusCode)
+ assert.Equal(t, "CUSTOM-HEADER", string(b))
+
+ err = r.Body.Close()
+ assert.NoError(t, err)
+}
+
+func TestResponseHeaders(t *testing.T) {
+ cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel))
+ assert.NoError(t, err)
+
+ cfg := &config.Viper{
+ Path: "configs/.rr-res-headers.yaml",
+ Prefix: "rr",
+ }
+
+ err = cont.RegisterAll(
+ cfg,
+ &logger.ZapLogger{},
+ &server.Plugin{},
+ &httpPlugin.Plugin{},
+ &headers.Plugin{},
+ )
+ assert.NoError(t, err)
+
+ err = cont.Init()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ch, err := cont.Serve()
+ assert.NoError(t, err)
+
+ sig := make(chan os.Signal, 1)
+ signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
+
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
+
+ stopCh := make(chan struct{}, 1)
+
+ go func() {
+ defer wg.Done()
+ for {
+ select {
+ case e := <-ch:
+ assert.Fail(t, "error", e.Error.Error())
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ case <-sig:
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ case <-stopCh:
+ // timeout
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ }
+ }
+ }()
+
+ time.Sleep(time.Second)
+ t.Run("ResponseHeaders", resHeaders)
+
+ stopCh <- struct{}{}
+ wg.Wait()
+}
+
+func resHeaders(t *testing.T) {
+ req, err := http.NewRequest("GET", "http://localhost:22455?hello=value", nil)
+ assert.NoError(t, err)
+
+ r, err := http.DefaultClient.Do(req)
+ assert.NoError(t, err)
+
+ assert.Equal(t, "output-header", r.Header.Get("output"))
+
+ b, err := ioutil.ReadAll(r.Body)
+ assert.NoError(t, err)
+ assert.Equal(t, 200, r.StatusCode)
+ assert.Equal(t, "CUSTOM-HEADER", string(b))
+
+ err = r.Body.Close()
+ assert.NoError(t, err)
+}
+
+func TestCORSHeaders(t *testing.T) {
+ cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel))
+ assert.NoError(t, err)
+
+ cfg := &config.Viper{
+ Path: "configs/.rr-cors-headers.yaml",
+ Prefix: "rr",
+ }
+
+ err = cont.RegisterAll(
+ cfg,
+ &logger.ZapLogger{},
+ &server.Plugin{},
+ &httpPlugin.Plugin{},
+ &headers.Plugin{},
+ )
+ assert.NoError(t, err)
+
+ err = cont.Init()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ch, err := cont.Serve()
+ assert.NoError(t, err)
+
+ sig := make(chan os.Signal, 1)
+ signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
+
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
+
+ stopCh := make(chan struct{}, 1)
+
+ go func() {
+ defer wg.Done()
+ for {
+ select {
+ case e := <-ch:
+ assert.Fail(t, "error", e.Error.Error())
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ case <-sig:
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ case <-stopCh:
+ // timeout
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ }
+ }
+ }()
+
+ time.Sleep(time.Second)
+ t.Run("CORSHeaders", corsHeaders)
+ t.Run("CORSHeadersPass", corsHeadersPass)
+
+ stopCh <- struct{}{}
+ wg.Wait()
+}
+
+func corsHeadersPass(t *testing.T) {
+ req, err := http.NewRequest("GET", "http://localhost:22855", nil)
+ assert.NoError(t, err)
+
+ r, err := http.DefaultClient.Do(req)
+ assert.NoError(t, err)
+
+ assert.Equal(t, "true", r.Header.Get("Access-Control-Allow-Credentials"))
+ assert.Equal(t, "*", r.Header.Get("Access-Control-Allow-Headers"))
+ assert.Equal(t, "*", r.Header.Get("Access-Control-Allow-Origin"))
+ assert.Equal(t, "true", r.Header.Get("Access-Control-Allow-Credentials"))
+
+ _, err = ioutil.ReadAll(r.Body)
+ assert.NoError(t, err)
+ assert.Equal(t, 200, r.StatusCode)
+
+ err = r.Body.Close()
+ assert.NoError(t, err)
+}
+
+func corsHeaders(t *testing.T) {
+ req, err := http.NewRequest("OPTIONS", "http://localhost:22855", nil)
+ assert.NoError(t, err)
+
+ r, err := http.DefaultClient.Do(req)
+ assert.NoError(t, err)
+
+ assert.Equal(t, "true", r.Header.Get("Access-Control-Allow-Credentials"))
+ assert.Equal(t, "*", r.Header.Get("Access-Control-Allow-Headers"))
+ assert.Equal(t, "GET,POST,PUT,DELETE", r.Header.Get("Access-Control-Allow-Methods"))
+ assert.Equal(t, "*", r.Header.Get("Access-Control-Allow-Origin"))
+ assert.Equal(t, "600", r.Header.Get("Access-Control-Max-Age"))
+ assert.Equal(t, "true", r.Header.Get("Access-Control-Allow-Credentials"))
+
+ _, err = ioutil.ReadAll(r.Body)
+ assert.NoError(t, err)
+ assert.Equal(t, 200, r.StatusCode)
+
+ err = r.Body.Close()
+ assert.NoError(t, err)
+}
diff --git a/tests/plugins/http/attributes_test.go b/tests/plugins/http/attributes_test.go
index 54998906..69200a30 100644
--- a/tests/plugins/http/attributes_test.go
+++ b/tests/plugins/http/attributes_test.go
@@ -4,7 +4,7 @@ import (
"net/http"
"testing"
- "github.com/spiral/roadrunner/v2/pkg/plugins/http/attributes"
+ "github.com/spiral/roadrunner/v2/plugins/http/attributes"
"github.com/stretchr/testify/assert"
)
diff --git a/tests/plugins/http/handler_test.go b/tests/plugins/http/handler_test.go
index 193bf0cf..18558296 100644
--- a/tests/plugins/http/handler_test.go
+++ b/tests/plugins/http/handler_test.go
@@ -11,8 +11,8 @@ import (
"strings"
"github.com/spiral/roadrunner/v2/pkg/pipe"
- httpPlugin "github.com/spiral/roadrunner/v2/pkg/plugins/http"
poolImpl "github.com/spiral/roadrunner/v2/pkg/pool"
+ httpPlugin "github.com/spiral/roadrunner/v2/plugins/http"
"github.com/stretchr/testify/assert"
"net/http"
diff --git a/tests/plugins/http/http_test.go b/tests/plugins/http/http_plugin_test.go
index f4a357f2..88857df5 100644
--- a/tests/plugins/http/http_test.go
+++ b/tests/plugins/http/http_plugin_test.go
@@ -19,18 +19,18 @@ import (
"github.com/golang/mock/gomock"
"github.com/spiral/endure"
goridgeRpc "github.com/spiral/goridge/v3/pkg/rpc"
- "github.com/spiral/roadrunner-plugins/config"
- "github.com/spiral/roadrunner-plugins/logger"
- "github.com/spiral/roadrunner-plugins/resetter"
"github.com/spiral/roadrunner/v2/interfaces/events"
- "github.com/spiral/roadrunner/v2/pkg/plugins/informer"
- "github.com/spiral/roadrunner/v2/pkg/plugins/server"
+ "github.com/spiral/roadrunner/v2/plugins/config"
+ "github.com/spiral/roadrunner/v2/plugins/informer"
+ "github.com/spiral/roadrunner/v2/plugins/logger"
+ "github.com/spiral/roadrunner/v2/plugins/resetter"
+ "github.com/spiral/roadrunner/v2/plugins/server"
"github.com/spiral/roadrunner/v2/tests/mocks"
"github.com/spiral/roadrunner/v2/tools"
"github.com/yookoala/gofast"
- rpcPlugin "github.com/spiral/roadrunner-plugins/rpc"
- httpPlugin "github.com/spiral/roadrunner/v2/pkg/plugins/http"
+ httpPlugin "github.com/spiral/roadrunner/v2/plugins/http"
+ rpcPlugin "github.com/spiral/roadrunner/v2/plugins/rpc"
"github.com/stretchr/testify/assert"
)
@@ -74,7 +74,7 @@ func TestHTTPInit(t *testing.T) {
wg := &sync.WaitGroup{}
wg.Add(1)
- tt := time.NewTimer(time.Second * 5)
+ stopCh := make(chan struct{}, 1)
go func() {
defer wg.Done()
@@ -92,7 +92,7 @@ func TestHTTPInit(t *testing.T) {
assert.FailNow(t, "error", err.Error())
}
return
- case <-tt.C:
+ case <-stopCh:
// timeout
err = cont.Stop()
if err != nil {
@@ -103,6 +103,7 @@ func TestHTTPInit(t *testing.T) {
}
}()
+ stopCh <- struct{}{}
wg.Wait()
}
@@ -140,8 +141,9 @@ func TestHTTPInformerReset(t *testing.T) {
wg := &sync.WaitGroup{}
wg.Add(1)
+ stopCh := make(chan struct{}, 1)
+
go func() {
- tt := time.NewTimer(time.Second * 10)
defer wg.Done()
for {
select {
@@ -157,7 +159,7 @@ func TestHTTPInformerReset(t *testing.T) {
assert.FailNow(t, "error", err.Error())
}
return
- case <-tt.C:
+ case <-stopCh:
// timeout
err = cont.Stop()
if err != nil {
@@ -174,6 +176,8 @@ func TestHTTPInformerReset(t *testing.T) {
t.Run("HTTPResetTest", resetTest)
t.Run("HTTPEchoTestAfter", echoHTTP)
+ stopCh <- struct{}{}
+
wg.Wait()
}
@@ -258,7 +262,8 @@ func TestSSL(t *testing.T) {
wg := &sync.WaitGroup{}
wg.Add(1)
- tt := time.NewTimer(time.Second * 10)
+
+ stopCh := make(chan struct{}, 1)
go func() {
defer wg.Done()
@@ -276,7 +281,7 @@ func TestSSL(t *testing.T) {
assert.FailNow(t, "error", err.Error())
}
return
- case <-tt.C:
+ case <-stopCh:
// timeout
err = cont.Stop()
if err != nil {
@@ -291,6 +296,8 @@ func TestSSL(t *testing.T) {
t.Run("SSLEcho", sslEcho)
t.Run("SSLNoRedirect", sslNoRedirect)
t.Run("fCGIecho", fcgiEcho)
+
+ stopCh <- struct{}{}
wg.Wait()
}
@@ -392,7 +399,8 @@ func TestSSLRedirect(t *testing.T) {
wg := &sync.WaitGroup{}
wg.Add(1)
- tt := time.NewTimer(time.Second * 10)
+ stopCh := make(chan struct{}, 1)
+
go func() {
defer wg.Done()
for {
@@ -409,7 +417,7 @@ func TestSSLRedirect(t *testing.T) {
assert.FailNow(t, "error", err.Error())
}
return
- case <-tt.C:
+ case <-stopCh:
// timeout
err = cont.Stop()
if err != nil {
@@ -422,6 +430,8 @@ func TestSSLRedirect(t *testing.T) {
time.Sleep(time.Second * 1)
t.Run("SSLRedirect", sslRedirect)
+
+ stopCh <- struct{}{}
wg.Wait()
}
@@ -477,7 +487,8 @@ func TestSSLPushPipes(t *testing.T) {
wg := &sync.WaitGroup{}
wg.Add(1)
- tt := time.NewTimer(time.Second * 10)
+ stopCh := make(chan struct{}, 1)
+
go func() {
defer wg.Done()
for {
@@ -494,7 +505,7 @@ func TestSSLPushPipes(t *testing.T) {
assert.FailNow(t, "error", err.Error())
}
return
- case <-tt.C:
+ case <-stopCh:
// timeout
err = cont.Stop()
if err != nil {
@@ -507,6 +518,8 @@ func TestSSLPushPipes(t *testing.T) {
time.Sleep(time.Second * 1)
t.Run("SSLPush", sslPush)
+
+ stopCh <- struct{}{}
wg.Wait()
}
@@ -565,7 +578,7 @@ func TestFastCGI_RequestUri(t *testing.T) {
wg := &sync.WaitGroup{}
wg.Add(1)
- tt := time.NewTimer(time.Second * 10)
+ stopCh := make(chan struct{}, 1)
go func() {
defer wg.Done()
@@ -583,7 +596,7 @@ func TestFastCGI_RequestUri(t *testing.T) {
assert.FailNow(t, "error", err.Error())
}
return
- case <-tt.C:
+ case <-stopCh:
// timeout
err = cont.Stop()
if err != nil {
@@ -596,6 +609,8 @@ func TestFastCGI_RequestUri(t *testing.T) {
time.Sleep(time.Second * 1)
t.Run("FastCGIServiceRequestUri", fcgiReqURI)
+
+ stopCh <- struct{}{}
wg.Wait()
}
@@ -650,7 +665,7 @@ func TestH2CUpgrade(t *testing.T) {
wg := &sync.WaitGroup{}
wg.Add(1)
- tt := time.NewTimer(time.Second * 10)
+ stopCh := make(chan struct{}, 1)
go func() {
defer wg.Done()
@@ -668,7 +683,7 @@ func TestH2CUpgrade(t *testing.T) {
assert.FailNow(t, "error", err.Error())
}
return
- case <-tt.C:
+ case <-stopCh:
// timeout
err = cont.Stop()
if err != nil {
@@ -681,6 +696,8 @@ func TestH2CUpgrade(t *testing.T) {
time.Sleep(time.Second * 1)
t.Run("H2cUpgrade", h2cUpgrade)
+
+ stopCh <- struct{}{}
wg.Wait()
}
@@ -739,7 +756,7 @@ func TestH2C(t *testing.T) {
wg := &sync.WaitGroup{}
wg.Add(1)
- tt := time.NewTimer(time.Second * 10)
+ stopCh := make(chan struct{}, 1)
go func() {
defer wg.Done()
@@ -757,7 +774,7 @@ func TestH2C(t *testing.T) {
assert.FailNow(t, "error", err.Error())
}
return
- case <-tt.C:
+ case <-stopCh:
// timeout
err = cont.Stop()
if err != nil {
@@ -770,6 +787,8 @@ func TestH2C(t *testing.T) {
time.Sleep(time.Second * 1)
t.Run("H2c", h2c)
+
+ stopCh <- struct{}{}
wg.Wait()
}
@@ -829,7 +848,7 @@ func TestHttpMiddleware(t *testing.T) {
wg := &sync.WaitGroup{}
wg.Add(1)
- tt := time.NewTimer(time.Second * 20)
+ stopCh := make(chan struct{}, 1)
go func() {
defer wg.Done()
@@ -847,7 +866,7 @@ func TestHttpMiddleware(t *testing.T) {
assert.FailNow(t, "error", err.Error())
}
return
- case <-tt.C:
+ case <-stopCh:
// timeout
err = cont.Stop()
if err != nil {
@@ -860,6 +879,8 @@ func TestHttpMiddleware(t *testing.T) {
time.Sleep(time.Second * 1)
t.Run("MiddlewareTest", middleware)
+
+ stopCh <- struct{}{}
wg.Wait()
}
@@ -895,7 +916,7 @@ func middleware(t *testing.T) {
}
func TestHttpEchoErr(t *testing.T) {
- cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel))
+ cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel))
assert.NoError(t, err)
rIn := `
@@ -971,7 +992,7 @@ logs:
wg := &sync.WaitGroup{}
wg.Add(1)
- tt := time.NewTimer(time.Second * 10)
+ stopCh := make(chan struct{}, 1)
go func() {
defer wg.Done()
@@ -989,7 +1010,7 @@ logs:
assert.FailNow(t, "error", err.Error())
}
return
- case <-tt.C:
+ case <-stopCh:
// timeout
err = cont.Stop()
if err != nil {
@@ -1002,6 +1023,8 @@ logs:
time.Sleep(time.Second * 1)
t.Run("HttpEchoError", echoError)
+
+ stopCh <- struct{}{}
wg.Wait()
}
@@ -1054,7 +1077,7 @@ func TestHttpEnvVariables(t *testing.T) {
wg := &sync.WaitGroup{}
wg.Add(1)
- tt := time.NewTimer(time.Second * 10)
+ stopCh := make(chan struct{}, 1)
go func() {
defer wg.Done()
@@ -1072,7 +1095,7 @@ func TestHttpEnvVariables(t *testing.T) {
assert.FailNow(t, "error", err.Error())
}
return
- case <-tt.C:
+ case <-stopCh:
// timeout
err = cont.Stop()
if err != nil {
@@ -1085,6 +1108,8 @@ func TestHttpEnvVariables(t *testing.T) {
time.Sleep(time.Second * 1)
t.Run("EnvVariablesTest", envVarsTest)
+
+ stopCh <- struct{}{}
wg.Wait()
}
diff --git a/tests/plugins/http/parse_test.go b/tests/plugins/http/parse_test.go
index df217202..5cc1ce32 100644
--- a/tests/plugins/http/parse_test.go
+++ b/tests/plugins/http/parse_test.go
@@ -3,7 +3,7 @@ package http
import (
"testing"
- "github.com/spiral/roadrunner/v2/pkg/plugins/http"
+ "github.com/spiral/roadrunner/v2/plugins/http"
)
var samples = []struct {
diff --git a/tests/plugins/http/plugin1.go b/tests/plugins/http/plugin1.go
index da887da8..0ec31211 100644
--- a/tests/plugins/http/plugin1.go
+++ b/tests/plugins/http/plugin1.go
@@ -1,7 +1,7 @@
package http
import (
- "github.com/spiral/roadrunner-plugins/config"
+ "github.com/spiral/roadrunner/v2/plugins/config"
)
type Plugin1 struct {
diff --git a/tests/plugins/http/plugin_middleware.go b/tests/plugins/http/plugin_middleware.go
index 6d67725d..8d02524d 100644
--- a/tests/plugins/http/plugin_middleware.go
+++ b/tests/plugins/http/plugin_middleware.go
@@ -3,7 +3,7 @@ package http
import (
"net/http"
- "github.com/spiral/roadrunner-plugins/config"
+ "github.com/spiral/roadrunner/v2/plugins/config"
)
type PluginMiddleware struct {
diff --git a/tests/plugins/http/response_test.go b/tests/plugins/http/response_test.go
index 229163a2..a9cbf91a 100644
--- a/tests/plugins/http/response_test.go
+++ b/tests/plugins/http/response_test.go
@@ -7,7 +7,7 @@ import (
"testing"
"github.com/spiral/roadrunner/v2/pkg/payload"
- httpPlugin "github.com/spiral/roadrunner/v2/pkg/plugins/http"
+ httpPlugin "github.com/spiral/roadrunner/v2/plugins/http"
"github.com/stretchr/testify/assert"
)
diff --git a/tests/plugins/http/uploads_config_test.go b/tests/plugins/http/uploads_config_test.go
index 4a7927c5..e76078ee 100644
--- a/tests/plugins/http/uploads_config_test.go
+++ b/tests/plugins/http/uploads_config_test.go
@@ -4,7 +4,7 @@ import (
"os"
"testing"
- httpPlugin "github.com/spiral/roadrunner/v2/pkg/plugins/http"
+ httpPlugin "github.com/spiral/roadrunner/v2/plugins/http"
"github.com/stretchr/testify/assert"
)
diff --git a/tests/plugins/http/uploads_test.go b/tests/plugins/http/uploads_test.go
index e71ff8c1..7bb25cbf 100644
--- a/tests/plugins/http/uploads_test.go
+++ b/tests/plugins/http/uploads_test.go
@@ -17,8 +17,8 @@ import (
j "github.com/json-iterator/go"
"github.com/spiral/roadrunner/v2/pkg/pipe"
- httpPlugin "github.com/spiral/roadrunner/v2/pkg/plugins/http"
poolImpl "github.com/spiral/roadrunner/v2/pkg/pool"
+ httpPlugin "github.com/spiral/roadrunner/v2/plugins/http"
"github.com/stretchr/testify/assert"
)
diff --git a/tests/plugins/informer/.rr-informer.yaml b/tests/plugins/informer/.rr-informer.yaml
index d817684f..e50ca9c9 100644
--- a/tests/plugins/informer/.rr-informer.yaml
+++ b/tests/plugins/informer/.rr-informer.yaml
@@ -1,5 +1,5 @@
server:
- command: "php ../../client.php echo pipes"
+ command: "php ../../http/client.php echo pipes"
user: ""
group: ""
env:
diff --git a/tests/plugins/informer/informer_test.go b/tests/plugins/informer/informer_test.go
index 15063d7e..d9fc2143 100644
--- a/tests/plugins/informer/informer_test.go
+++ b/tests/plugins/informer/informer_test.go
@@ -5,17 +5,18 @@ import (
"net/rpc"
"os"
"os/signal"
+ "sync"
"syscall"
"testing"
"time"
"github.com/spiral/endure"
goridgeRpc "github.com/spiral/goridge/v3/pkg/rpc"
- "github.com/spiral/roadrunner-plugins/config"
- "github.com/spiral/roadrunner-plugins/logger"
- rpcPlugin "github.com/spiral/roadrunner-plugins/rpc"
- "github.com/spiral/roadrunner/v2/pkg/plugins/informer"
- "github.com/spiral/roadrunner/v2/pkg/plugins/server"
+ "github.com/spiral/roadrunner/v2/plugins/config"
+ "github.com/spiral/roadrunner/v2/plugins/informer"
+ "github.com/spiral/roadrunner/v2/plugins/logger"
+ rpcPlugin "github.com/spiral/roadrunner/v2/plugins/rpc"
+ "github.com/spiral/roadrunner/v2/plugins/server"
"github.com/spiral/roadrunner/v2/tools"
"github.com/stretchr/testify/assert"
)
@@ -52,33 +53,43 @@ func TestInformerInit(t *testing.T) {
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
- tt := time.NewTimer(time.Second * 15)
+ stopCh := make(chan struct{}, 1)
- t.Run("InformerRpcTest", informerRPCTest)
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
- for {
- select {
- case e := <-ch:
- assert.Fail(t, "error", e.Error.Error())
- err = cont.Stop()
- if err != nil {
- assert.FailNow(t, "error", err.Error())
- }
- case <-sig:
- err = cont.Stop()
- if err != nil {
- assert.FailNow(t, "error", err.Error())
+ go func() {
+ defer wg.Done()
+ for {
+ select {
+ case e := <-ch:
+ assert.Fail(t, "error", e.Error.Error())
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ case <-sig:
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ case <-stopCh:
+ // timeout
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
}
- return
- case <-tt.C:
- // timeout
- err = cont.Stop()
- if err != nil {
- assert.FailNow(t, "error", err.Error())
- }
- return
}
- }
+ }()
+
+ time.Sleep(time.Second)
+ t.Run("InformerRpcTest", informerRPCTest)
+
+ stopCh <- struct{}{}
+ wg.Wait()
}
func informerRPCTest(t *testing.T) {
diff --git a/tests/plugins/informer/test_plugin.go b/tests/plugins/informer/test_plugin.go
index 95adfb07..ba281d02 100644
--- a/tests/plugins/informer/test_plugin.go
+++ b/tests/plugins/informer/test_plugin.go
@@ -4,10 +4,10 @@ import (
"context"
"time"
- "github.com/spiral/roadrunner-plugins/config"
"github.com/spiral/roadrunner/v2/interfaces/worker"
- "github.com/spiral/roadrunner/v2/pkg/plugins/server"
poolImpl "github.com/spiral/roadrunner/v2/pkg/pool"
+ "github.com/spiral/roadrunner/v2/plugins/config"
+ "github.com/spiral/roadrunner/v2/plugins/server"
)
var testPoolConfig = poolImpl.Config{
diff --git a/tests/plugins/logger/.rr.yaml b/tests/plugins/logger/.rr.yaml
new file mode 100644
index 00000000..cb555ec3
--- /dev/null
+++ b/tests/plugins/logger/.rr.yaml
@@ -0,0 +1,3 @@
+logs:
+ mode: development
+ level: debug \ No newline at end of file
diff --git a/tests/plugins/logger/logger_test.go b/tests/plugins/logger/logger_test.go
new file mode 100644
index 00000000..cc788be3
--- /dev/null
+++ b/tests/plugins/logger/logger_test.go
@@ -0,0 +1,79 @@
+package logger
+
+import (
+ "os"
+ "os/signal"
+ "sync"
+ "testing"
+
+ "github.com/spiral/endure"
+ "github.com/spiral/roadrunner/v2/plugins/config"
+ "github.com/spiral/roadrunner/v2/plugins/logger"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestLogger(t *testing.T) {
+ container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.ErrorLevel))
+ if err != nil {
+ t.Fatal(err)
+ }
+ // config plugin
+ vp := &config.Viper{}
+ vp.Path = ".rr.yaml"
+ vp.Prefix = "rr"
+ err = container.Register(vp)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = container.Register(&Plugin{})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = container.Register(&logger.ZapLogger{})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = container.Init()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ errCh, err := container.Serve()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // stop by CTRL+C
+ c := make(chan os.Signal, 1)
+ signal.Notify(c, os.Interrupt)
+
+ stopCh := make(chan struct{}, 1)
+
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
+
+ go func() {
+ defer wg.Done()
+ for {
+ select {
+ case e := <-errCh:
+ assert.NoError(t, e.Error)
+ assert.NoError(t, container.Stop())
+ return
+ case <-c:
+ err = container.Stop()
+ assert.NoError(t, err)
+ return
+ case <-stopCh:
+ assert.NoError(t, container.Stop())
+ return
+ }
+ }
+ }()
+
+ stopCh <- struct{}{}
+ wg.Wait()
+}
diff --git a/tests/plugins/logger/plugin.go b/tests/plugins/logger/plugin.go
new file mode 100644
index 00000000..9ddf9ec9
--- /dev/null
+++ b/tests/plugins/logger/plugin.go
@@ -0,0 +1,40 @@
+package logger
+
+import (
+ "github.com/spiral/errors"
+ "github.com/spiral/roadrunner/v2/plugins/config"
+ "github.com/spiral/roadrunner/v2/plugins/logger"
+)
+
+type Plugin struct {
+ config config.Configurer
+ log logger.Logger
+}
+
+func (p1 *Plugin) Init(cfg config.Configurer, log logger.Logger) error {
+ p1.config = cfg
+ p1.log = log
+ return nil
+}
+
+func (p1 *Plugin) Serve() chan error {
+ errCh := make(chan error, 1)
+ p1.log.Error("error", "test", errors.E(errors.Str("test")))
+ p1.log.Info("error", "test", errors.E(errors.Str("test")))
+ p1.log.Debug("error", "test", errors.E(errors.Str("test")))
+ p1.log.Warn("error", "test", errors.E(errors.Str("test")))
+
+ p1.log.Error("error", "test")
+ p1.log.Info("error", "test")
+ p1.log.Debug("error", "test")
+ p1.log.Warn("error", "test")
+ return errCh
+}
+
+func (p1 *Plugin) Stop() error {
+ return nil
+}
+
+func (p1 *Plugin) Name() string {
+ return "logger_plugin"
+}
diff --git a/tests/plugins/metrics/.rr-test.yaml b/tests/plugins/metrics/.rr-test.yaml
new file mode 100644
index 00000000..37c50395
--- /dev/null
+++ b/tests/plugins/metrics/.rr-test.yaml
@@ -0,0 +1,16 @@
+rpc:
+ listen: tcp://127.0.0.1:6001
+ disabled: false
+
+metrics:
+ # prometheus client address (path /metrics added automatically)
+ address: localhost:2112
+ collect:
+ app_metric:
+ type: histogram
+ help: "Custom application metric"
+ labels: [ "type" ]
+ buckets: [ 0.1, 0.2, 0.3, 1.0 ]
+logs:
+ mode: development
+ level: error \ No newline at end of file
diff --git a/tests/plugins/metrics/docker-compose.yml b/tests/plugins/metrics/docker-compose.yml
new file mode 100644
index 00000000..610633b4
--- /dev/null
+++ b/tests/plugins/metrics/docker-compose.yml
@@ -0,0 +1,7 @@
+version: '3.7'
+
+services:
+ prometheus:
+ image: prom/prometheus
+ ports:
+ - 9090:9090
diff --git a/tests/plugins/metrics/metrics_test.go b/tests/plugins/metrics/metrics_test.go
new file mode 100644
index 00000000..c94d51bc
--- /dev/null
+++ b/tests/plugins/metrics/metrics_test.go
@@ -0,0 +1,739 @@
+package metrics
+
+import (
+ "io/ioutil"
+ "net"
+ "net/http"
+ "net/rpc"
+ "os"
+ "os/signal"
+ "syscall"
+ "testing"
+ "time"
+
+ "github.com/spiral/endure"
+ goridgeRpc "github.com/spiral/goridge/v3/pkg/rpc"
+ "github.com/spiral/roadrunner/v2/plugins/config"
+ "github.com/spiral/roadrunner/v2/plugins/logger"
+ "github.com/spiral/roadrunner/v2/plugins/metrics"
+ rpcPlugin "github.com/spiral/roadrunner/v2/plugins/rpc"
+ "github.com/stretchr/testify/assert"
+)
+
+const dialAddr = "127.0.0.1:6001"
+const dialNetwork = "tcp"
+const getAddr = "http://localhost:2112/metrics"
+
+// get request and return body
+func get() (string, error) {
+ r, err := http.Get(getAddr)
+ if err != nil {
+ return "", err
+ }
+
+ b, err := ioutil.ReadAll(r.Body)
+ if err != nil {
+ return "", err
+ }
+
+ err = r.Body.Close()
+ if err != nil {
+ return "", err
+ }
+ // unsafe
+ return string(b), err
+}
+
+func TestMetricsInit(t *testing.T) {
+ cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ cfg := &config.Viper{}
+ cfg.Prefix = "rr"
+ cfg.Path = ".rr-test.yaml"
+
+ err = cont.RegisterAll(
+ cfg,
+ &metrics.Plugin{},
+ &rpcPlugin.Plugin{},
+ &logger.ZapLogger{},
+ &Plugin1{},
+ )
+ assert.NoError(t, err)
+
+ err = cont.Init()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ch, err := cont.Serve()
+ assert.NoError(t, err)
+
+ sig := make(chan os.Signal, 1)
+ signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
+
+ tt := time.NewTimer(time.Second * 5)
+
+ out, err := get()
+ assert.NoError(t, err)
+
+ assert.Contains(t, out, "go_gc_duration_seconds")
+
+ for {
+ select {
+ case e := <-ch:
+ assert.Fail(t, "error", e.Error.Error())
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ case <-sig:
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ case <-tt.C:
+ // timeout
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ }
+ }
+}
+
+func TestMetricsGaugeCollector(t *testing.T) {
+ cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ cfg := &config.Viper{}
+ cfg.Prefix = "rr"
+ cfg.Path = ".rr-test.yaml"
+
+ err = cont.RegisterAll(
+ cfg,
+ &metrics.Plugin{},
+ &rpcPlugin.Plugin{},
+ &logger.ZapLogger{},
+ &Plugin1{},
+ )
+ assert.NoError(t, err)
+
+ err = cont.Init()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ch, err := cont.Serve()
+ assert.NoError(t, err)
+
+ sig := make(chan os.Signal, 1)
+ signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
+
+ time.Sleep(time.Second)
+ tt := time.NewTimer(time.Second * 5)
+
+ out, err := get()
+ assert.NoError(t, err)
+ assert.Contains(t, out, "my_gauge 100")
+ assert.Contains(t, out, "my_gauge2 100")
+
+ out, err = get()
+ assert.NoError(t, err)
+ assert.Contains(t, out, "go_gc_duration_seconds")
+
+ for {
+ select {
+ case e := <-ch:
+ assert.Fail(t, "error", e.Error.Error())
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ case <-sig:
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ case <-tt.C:
+ // timeout
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ }
+ }
+}
+
+func TestMetricsDifferentRPCCalls(t *testing.T) {
+ cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ cfg := &config.Viper{}
+ cfg.Prefix = "rr"
+ cfg.Path = ".rr-test.yaml"
+
+ err = cont.RegisterAll(
+ cfg,
+ &metrics.Plugin{},
+ &rpcPlugin.Plugin{},
+ &logger.ZapLogger{},
+ )
+ assert.NoError(t, err)
+
+ err = cont.Init()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ch, err := cont.Serve()
+ assert.NoError(t, err)
+
+ sig := make(chan os.Signal, 1)
+ signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
+
+ go func() {
+ tt := time.NewTimer(time.Minute * 3)
+ for {
+ select {
+ case e := <-ch:
+ assert.Fail(t, "error", e.Error.Error())
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ case <-sig:
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ case <-tt.C:
+ // timeout
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ }
+ }
+ }()
+
+ t.Run("DeclareMetric", declareMetricsTest)
+ genericOut, err := get()
+ assert.NoError(t, err)
+ assert.Contains(t, genericOut, "test_metrics_named_collector")
+
+ t.Run("AddMetric", addMetricsTest)
+ genericOut, err = get()
+ assert.NoError(t, err)
+ assert.Contains(t, genericOut, "test_metrics_named_collector 10000")
+
+ t.Run("SetMetric", setMetric)
+ genericOut, err = get()
+ assert.NoError(t, err)
+ assert.Contains(t, genericOut, "user_gauge_collector 100")
+
+ t.Run("VectorMetric", vectorMetric)
+ genericOut, err = get()
+ assert.NoError(t, err)
+ assert.Contains(t, genericOut, "gauge_2_collector{section=\"first\",type=\"core\"} 100")
+
+ t.Run("MissingSection", missingSection)
+ t.Run("SetWithoutLabels", setWithoutLabels)
+ t.Run("SetOnHistogram", setOnHistogram)
+ t.Run("MetricSub", subMetric)
+ genericOut, err = get()
+ assert.NoError(t, err)
+ assert.Contains(t, genericOut, "sub_gauge_subMetric 1")
+
+ t.Run("SubVector", subVector)
+ genericOut, err = get()
+ assert.NoError(t, err)
+ assert.Contains(t, genericOut, "sub_gauge_subVector{section=\"first\",type=\"core\"} 1")
+
+ t.Run("RegisterHistogram", registerHistogram)
+
+ genericOut, err = get()
+ assert.NoError(t, err)
+ assert.Contains(t, genericOut, `TYPE histogram_registerHistogram`)
+
+ // check buckets
+ assert.Contains(t, genericOut, `histogram_registerHistogram_bucket{le="0.1"} 0`)
+ assert.Contains(t, genericOut, `histogram_registerHistogram_bucket{le="0.2"} 0`)
+ assert.Contains(t, genericOut, `histogram_registerHistogram_bucket{le="0.5"} 0`)
+ assert.Contains(t, genericOut, `histogram_registerHistogram_bucket{le="+Inf"} 0`)
+ assert.Contains(t, genericOut, `histogram_registerHistogram_sum 0`)
+ assert.Contains(t, genericOut, `histogram_registerHistogram_count 0`)
+
+ t.Run("CounterMetric", counterMetric)
+ genericOut, err = get()
+ assert.NoError(t, err)
+ assert.Contains(t, genericOut, "HELP default_default_counter_CounterMetric test_counter")
+ assert.Contains(t, genericOut, `default_default_counter_CounterMetric{section="section2",type="type2"}`)
+
+ t.Run("ObserveMetric", observeMetric)
+ genericOut, err = get()
+ assert.NoError(t, err)
+ assert.Contains(t, genericOut, "observe_observeMetric")
+
+ t.Run("ObserveMetricNotEnoughLabels", observeMetricNotEnoughLabels)
+
+ close(sig)
+}
+
+func observeMetricNotEnoughLabels(t *testing.T) {
+ conn, err := net.Dial(dialNetwork, dialAddr)
+ assert.NoError(t, err)
+ defer func() {
+ _ = conn.Close()
+ }()
+ client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn))
+ var ret bool
+
+ nc := metrics.NamedCollector{
+ Name: "observe_observeMetricNotEnoughLabels",
+ Collector: metrics.Collector{
+ Namespace: "default",
+ Subsystem: "default",
+ Help: "test_observe",
+ Type: metrics.Histogram,
+ Labels: []string{"type", "section"},
+ },
+ }
+
+ err = client.Call("metrics.Declare", nc, &ret)
+ assert.NoError(t, err)
+ assert.True(t, ret)
+ ret = false
+
+ assert.Error(t, client.Call("metrics.Observe", metrics.Metric{
+ Name: "observe_observeMetric",
+ Value: 100.0,
+ Labels: []string{"test"},
+ }, &ret))
+ assert.False(t, ret)
+}
+
+func observeMetric(t *testing.T) {
+ conn, err := net.Dial(dialNetwork, dialAddr)
+ assert.NoError(t, err)
+ defer func() {
+ _ = conn.Close()
+ }()
+ client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn))
+ var ret bool
+
+ nc := metrics.NamedCollector{
+ Name: "observe_observeMetric",
+ Collector: metrics.Collector{
+ Namespace: "default",
+ Subsystem: "default",
+ Help: "test_observe",
+ Type: metrics.Histogram,
+ Labels: []string{"type", "section"},
+ },
+ }
+
+ err = client.Call("metrics.Declare", nc, &ret)
+ assert.NoError(t, err)
+ assert.True(t, ret)
+ ret = false
+
+ assert.NoError(t, client.Call("metrics.Observe", metrics.Metric{
+ Name: "observe_observeMetric",
+ Value: 100.0,
+ Labels: []string{"test", "test2"},
+ }, &ret))
+ assert.True(t, ret)
+}
+
+func counterMetric(t *testing.T) {
+ conn, err := net.Dial(dialNetwork, dialAddr)
+ assert.NoError(t, err)
+ defer func() {
+ _ = conn.Close()
+ }()
+ client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn))
+ var ret bool
+
+ nc := metrics.NamedCollector{
+ Name: "counter_CounterMetric",
+ Collector: metrics.Collector{
+ Namespace: "default",
+ Subsystem: "default",
+ Help: "test_counter",
+ Type: metrics.Counter,
+ Labels: []string{"type", "section"},
+ },
+ }
+
+ err = client.Call("metrics.Declare", nc, &ret)
+ assert.NoError(t, err)
+ assert.True(t, ret)
+
+ ret = false
+
+ assert.NoError(t, client.Call("metrics.Add", metrics.Metric{
+ Name: "counter_CounterMetric",
+ Value: 100.0,
+ Labels: []string{"type2", "section2"},
+ }, &ret))
+ assert.True(t, ret)
+}
+
+func registerHistogram(t *testing.T) {
+ conn, err := net.Dial(dialNetwork, dialAddr)
+ assert.NoError(t, err)
+ defer func() {
+ _ = conn.Close()
+ }()
+ client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn))
+ var ret bool
+
+ nc := metrics.NamedCollector{
+ Name: "histogram_registerHistogram",
+ Collector: metrics.Collector{
+ Help: "test_histogram",
+ Type: metrics.Histogram,
+ Buckets: []float64{0.1, 0.2, 0.5},
+ },
+ }
+
+ err = client.Call("metrics.Declare", nc, &ret)
+ assert.NoError(t, err)
+ assert.True(t, ret)
+
+ ret = false
+
+ m := metrics.Metric{
+ Name: "histogram_registerHistogram",
+ Value: 10000,
+ Labels: nil,
+ }
+
+ err = client.Call("metrics.Add", m, &ret)
+ assert.Error(t, err)
+ assert.False(t, ret)
+}
+
+func subVector(t *testing.T) {
+ conn, err := net.Dial(dialNetwork, dialAddr)
+ assert.NoError(t, err)
+ defer func() {
+ _ = conn.Close()
+ }()
+
+ client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn))
+ var ret bool
+
+ nc := metrics.NamedCollector{
+ Name: "sub_gauge_subVector",
+ Collector: metrics.Collector{
+ Namespace: "default",
+ Subsystem: "default",
+ Type: metrics.Gauge,
+ Labels: []string{"type", "section"},
+ },
+ }
+
+ err = client.Call("metrics.Declare", nc, &ret)
+ assert.NoError(t, err)
+ assert.True(t, ret)
+ ret = false
+
+ m := metrics.Metric{
+ Name: "sub_gauge_subVector",
+ Value: 100000,
+ Labels: []string{"core", "first"},
+ }
+
+ err = client.Call("metrics.Add", m, &ret)
+ assert.NoError(t, err)
+ assert.True(t, ret)
+ ret = false
+
+ m = metrics.Metric{
+ Name: "sub_gauge_subVector",
+ Value: 99999,
+ Labels: []string{"core", "first"},
+ }
+
+ err = client.Call("metrics.Sub", m, &ret)
+ assert.NoError(t, err)
+ assert.True(t, ret)
+}
+
+func subMetric(t *testing.T) {
+ conn, err := net.Dial(dialNetwork, dialAddr)
+ assert.NoError(t, err)
+ defer func() {
+ _ = conn.Close()
+ }()
+ client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn))
+ var ret bool
+
+ nc := metrics.NamedCollector{
+ Name: "sub_gauge_subMetric",
+ Collector: metrics.Collector{
+ Namespace: "default",
+ Subsystem: "default",
+ Type: metrics.Gauge,
+ },
+ }
+
+ err = client.Call("metrics.Declare", nc, &ret)
+ assert.NoError(t, err)
+ assert.True(t, ret)
+ ret = false
+
+ m := metrics.Metric{
+ Name: "sub_gauge_subMetric",
+ Value: 100000,
+ }
+
+ err = client.Call("metrics.Add", m, &ret)
+ assert.NoError(t, err)
+ assert.True(t, ret)
+ ret = false
+
+ m = metrics.Metric{
+ Name: "sub_gauge_subMetric",
+ Value: 99999,
+ }
+
+ err = client.Call("metrics.Sub", m, &ret)
+ assert.NoError(t, err)
+ assert.True(t, ret)
+}
+
+func setOnHistogram(t *testing.T) {
+ conn, err := net.Dial(dialNetwork, dialAddr)
+ assert.NoError(t, err)
+ defer func() {
+ _ = conn.Close()
+ }()
+ client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn))
+ var ret bool
+
+ nc := metrics.NamedCollector{
+ Name: "histogram_setOnHistogram",
+ Collector: metrics.Collector{
+ Namespace: "default",
+ Subsystem: "default",
+ Type: metrics.Histogram,
+ Labels: []string{"type", "section"},
+ },
+ }
+
+ err = client.Call("metrics.Declare", nc, &ret)
+ assert.NoError(t, err)
+ assert.True(t, ret)
+
+ ret = false
+
+ m := metrics.Metric{
+ Name: "gauge_setOnHistogram",
+ Value: 100.0,
+ }
+
+ err = client.Call("metrics.Set", m, &ret) // expected 2 label values but got 1 in []string{"missing"}
+ assert.Error(t, err)
+ assert.False(t, ret)
+}
+
+func setWithoutLabels(t *testing.T) {
+ conn, err := net.Dial(dialNetwork, dialAddr)
+ assert.NoError(t, err)
+ defer func() {
+ _ = conn.Close()
+ }()
+ client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn))
+ var ret bool
+
+ nc := metrics.NamedCollector{
+ Name: "gauge_setWithoutLabels",
+ Collector: metrics.Collector{
+ Namespace: "default",
+ Subsystem: "default",
+ Type: metrics.Gauge,
+ Labels: []string{"type", "section"},
+ },
+ }
+
+ err = client.Call("metrics.Declare", nc, &ret)
+ assert.NoError(t, err)
+ assert.True(t, ret)
+
+ ret = false
+
+ m := metrics.Metric{
+ Name: "gauge_setWithoutLabels",
+ Value: 100.0,
+ }
+
+ err = client.Call("metrics.Set", m, &ret) // expected 2 label values but got 1 in []string{"missing"}
+ assert.Error(t, err)
+ assert.False(t, ret)
+}
+
+func missingSection(t *testing.T) {
+ conn, err := net.Dial(dialNetwork, dialAddr)
+ assert.NoError(t, err)
+ defer func() {
+ _ = conn.Close()
+ }()
+ client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn))
+ var ret bool
+
+ nc := metrics.NamedCollector{
+ Name: "gauge_missing_section_collector",
+ Collector: metrics.Collector{
+ Namespace: "default",
+ Subsystem: "default",
+ Type: metrics.Gauge,
+ Labels: []string{"type", "section"},
+ },
+ }
+
+ err = client.Call("metrics.Declare", nc, &ret)
+ assert.NoError(t, err)
+ assert.True(t, ret)
+
+ ret = false
+
+ m := metrics.Metric{
+ Name: "gauge_missing_section_collector",
+ Value: 100.0,
+ Labels: []string{"missing"},
+ }
+
+ err = client.Call("metrics.Set", m, &ret) // expected 2 label values but got 1 in []string{"missing"}
+ assert.Error(t, err)
+ assert.False(t, ret)
+}
+
+func vectorMetric(t *testing.T) {
+ conn, err := net.Dial(dialNetwork, dialAddr)
+ assert.NoError(t, err)
+ defer func() {
+ _ = conn.Close()
+ }()
+ client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn))
+ var ret bool
+
+ nc := metrics.NamedCollector{
+ Name: "gauge_2_collector",
+ Collector: metrics.Collector{
+ Namespace: "default",
+ Subsystem: "default",
+ Type: metrics.Gauge,
+ Labels: []string{"type", "section"},
+ },
+ }
+
+ err = client.Call("metrics.Declare", nc, &ret)
+ assert.NoError(t, err)
+ assert.True(t, ret)
+
+ ret = false
+
+ m := metrics.Metric{
+ Name: "gauge_2_collector",
+ Value: 100.0,
+ Labels: []string{"core", "first"},
+ }
+
+ err = client.Call("metrics.Set", m, &ret)
+ assert.NoError(t, err)
+ assert.True(t, ret)
+}
+
+func setMetric(t *testing.T) {
+ conn, err := net.Dial(dialNetwork, dialAddr)
+ assert.NoError(t, err)
+ defer func() {
+ _ = conn.Close()
+ }()
+ client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn))
+ var ret bool
+
+ nc := metrics.NamedCollector{
+ Name: "user_gauge_collector",
+ Collector: metrics.Collector{
+ Namespace: "default",
+ Subsystem: "default",
+ Type: metrics.Gauge,
+ },
+ }
+
+ err = client.Call("metrics.Declare", nc, &ret)
+ assert.NoError(t, err)
+ assert.True(t, ret)
+ ret = false
+
+ m := metrics.Metric{
+ Name: "user_gauge_collector",
+ Value: 100.0,
+ }
+
+ err = client.Call("metrics.Set", m, &ret)
+ assert.NoError(t, err)
+ assert.True(t, ret)
+}
+
+func addMetricsTest(t *testing.T) {
+ conn, err := net.Dial(dialNetwork, dialAddr)
+ assert.NoError(t, err)
+ defer func() {
+ _ = conn.Close()
+ }()
+ client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn))
+ var ret bool
+
+ m := metrics.Metric{
+ Name: "test_metrics_named_collector",
+ Value: 10000,
+ Labels: nil,
+ }
+
+ err = client.Call("metrics.Add", m, &ret)
+ assert.NoError(t, err)
+ assert.True(t, ret)
+}
+
+func declareMetricsTest(t *testing.T) {
+ conn, err := net.Dial(dialNetwork, dialAddr)
+ assert.NoError(t, err)
+ defer func() {
+ _ = conn.Close()
+ }()
+ client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn))
+ var ret bool
+
+ nc := metrics.NamedCollector{
+ Name: "test_metrics_named_collector",
+ Collector: metrics.Collector{
+ Namespace: "default",
+ Subsystem: "default",
+ Type: metrics.Counter,
+ Help: "NO HELP!",
+ Labels: nil,
+ Buckets: nil,
+ },
+ }
+
+ err = client.Call("metrics.Declare", nc, &ret)
+ assert.NoError(t, err)
+ assert.True(t, ret)
+}
diff --git a/tests/plugins/metrics/plugin1.go b/tests/plugins/metrics/plugin1.go
new file mode 100644
index 00000000..ae024a8a
--- /dev/null
+++ b/tests/plugins/metrics/plugin1.go
@@ -0,0 +1,46 @@
+package metrics
+
+import (
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/spiral/roadrunner/v2/plugins/config"
+)
+
+// Gauge //////////////
+type Plugin1 struct {
+ config config.Configurer
+}
+
+func (p1 *Plugin1) Init(cfg config.Configurer) error {
+ p1.config = cfg
+ return nil
+}
+
+func (p1 *Plugin1) Serve() chan error {
+ errCh := make(chan error, 1)
+ return errCh
+}
+
+func (p1 *Plugin1) Stop() error {
+ return nil
+}
+
+func (p1 *Plugin1) Name() string {
+ return "metrics_test.plugin1"
+}
+
+func (p1 *Plugin1) MetricsCollector() []prometheus.Collector {
+ collector := prometheus.NewGauge(prometheus.GaugeOpts{
+ Name: "my_gauge",
+ Help: "My gauge value",
+ })
+
+ collector.Set(100)
+
+ collector2 := prometheus.NewGauge(prometheus.GaugeOpts{
+ Name: "my_gauge2",
+ Help: "My gauge2 value",
+ })
+
+ collector2.Set(100)
+ return []prometheus.Collector{collector, collector2}
+}
diff --git a/tests/plugins/mocks/mock_log.go b/tests/plugins/mocks/mock_log.go
new file mode 100644
index 00000000..e9631805
--- /dev/null
+++ b/tests/plugins/mocks/mock_log.go
@@ -0,0 +1,150 @@
+package mocks
+
+import (
+ "reflect"
+
+ "github.com/golang/mock/gomock"
+ "github.com/spiral/roadrunner/v2/plugins/logger"
+)
+
+// MockLogger is a mock of Logger interface.
+type MockLogger struct {
+ ctrl *gomock.Controller
+ recorder *MockLoggerMockRecorder
+}
+
+// MockLoggerMockRecorder is the mock recorder for MockLogger.
+type MockLoggerMockRecorder struct {
+ mock *MockLogger
+}
+
+// NewMockLogger creates a new mock instance.
+func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
+ mock := &MockLogger{ctrl: ctrl}
+ mock.recorder = &MockLoggerMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
+ return m.recorder
+}
+
+func (m *MockLogger) Init() error {
+ mock := &MockLogger{ctrl: m.ctrl}
+ mock.recorder = &MockLoggerMockRecorder{mock}
+ return nil
+}
+
+// Debug mocks base method.
+func (m *MockLogger) Debug(msg string, keyvals ...interface{}) {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{msg}
+ for _, a := range keyvals {
+ varargs = append(varargs, a)
+ }
+ m.ctrl.Call(m, "Debug", varargs...)
+}
+
+// Warn mocks base method.
+func (m *MockLogger) Warn(msg string, keyvals ...interface{}) {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{msg}
+ for _, a := range keyvals {
+ varargs = append(varargs, a)
+ }
+ m.ctrl.Call(m, "Warn", varargs...)
+}
+
+// Info mocks base method.
+func (m *MockLogger) Info(msg string, keyvals ...interface{}) {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{msg}
+ for _, a := range keyvals {
+ varargs = append(varargs, a)
+ }
+ m.ctrl.Call(m, "Info", varargs...)
+}
+
+// Error mocks base method.
+func (m *MockLogger) Error(msg string, keyvals ...interface{}) {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{msg}
+ for _, a := range keyvals {
+ varargs = append(varargs, a)
+ }
+ m.ctrl.Call(m, "Error", varargs...)
+}
+
+// Warn indicates an expected call of Warn.
+func (mr *MockLoggerMockRecorder) Warn(msg interface{}, keyvals ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{msg}, keyvals...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockLogger)(nil).Warn), varargs...)
+}
+
+// Debug indicates an expected call of Debug.
+func (mr *MockLoggerMockRecorder) Debug(msg interface{}, keyvals ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{msg}, keyvals...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), varargs...)
+}
+
+// Error indicates an expected call of Error.
+func (mr *MockLoggerMockRecorder) Error(msg interface{}, keyvals ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{msg}, keyvals...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), varargs...)
+}
+
+func (mr *MockLoggerMockRecorder) Init() error {
+ return nil
+}
+
+// Info indicates an expected call of Info.
+func (mr *MockLoggerMockRecorder) Info(msg interface{}, keyvals ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]interface{}{msg}, keyvals...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogger)(nil).Info), varargs...)
+}
+
+// MockWithLogger is a mock of WithLogger interface.
+type MockWithLogger struct {
+ ctrl *gomock.Controller
+ recorder *MockWithLoggerMockRecorder
+}
+
+// MockWithLoggerMockRecorder is the mock recorder for MockWithLogger.
+type MockWithLoggerMockRecorder struct {
+ mock *MockWithLogger
+}
+
+// NewMockWithLogger creates a new mock instance.
+func NewMockWithLogger(ctrl *gomock.Controller) *MockWithLogger {
+ mock := &MockWithLogger{ctrl: ctrl}
+ mock.recorder = &MockWithLoggerMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockWithLogger) EXPECT() *MockWithLoggerMockRecorder {
+ return m.recorder
+}
+
+// With mocks base method.
+func (m *MockWithLogger) With(keyvals ...interface{}) logger.Logger {
+ m.ctrl.T.Helper()
+ varargs := []interface{}{}
+ for _, a := range keyvals {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "With", varargs...)
+ ret0, _ := ret[0].(logger.Logger)
+ return ret0
+}
+
+// With indicates an expected call of With.
+func (mr *MockWithLoggerMockRecorder) With(keyvals ...interface{}) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "With", reflect.TypeOf((*MockWithLogger)(nil).With), keyvals...)
+}
diff --git a/tests/plugins/redis/plugin1.go b/tests/plugins/redis/plugin1.go
new file mode 100644
index 00000000..e50213e5
--- /dev/null
+++ b/tests/plugins/redis/plugin1.go
@@ -0,0 +1,43 @@
+package redis
+
+import (
+ "context"
+ "time"
+
+ "github.com/go-redis/redis/v8"
+ "github.com/spiral/errors"
+ redisPlugin "github.com/spiral/roadrunner/v2/plugins/redis"
+)
+
+type Plugin1 struct {
+ redisClient redis.UniversalClient
+}
+
+func (p *Plugin1) Init(redis redisPlugin.Redis) error {
+ p.redisClient = redis.GetClient()
+ return nil
+}
+
+func (p *Plugin1) Serve() chan error {
+ const op = errors.Op("plugin1 serve")
+ errCh := make(chan error, 1)
+ p.redisClient.Set(context.Background(), "foo", "bar", time.Minute)
+
+ stringCmd := p.redisClient.Get(context.Background(), "foo")
+ data, err := stringCmd.Result()
+ if err != nil {
+ errCh <- errors.E(op, err)
+ return errCh
+ }
+
+ if data != "bar" {
+ errCh <- errors.E(op, errors.Str("no such key"))
+ return errCh
+ }
+
+ return errCh
+}
+
+func (p *Plugin1) Stop() error {
+ return nil
+}
diff --git a/tests/plugins/redis/redis_plugin_test.go b/tests/plugins/redis/redis_plugin_test.go
new file mode 100644
index 00000000..eba05752
--- /dev/null
+++ b/tests/plugins/redis/redis_plugin_test.go
@@ -0,0 +1,120 @@
+package redis
+
+import (
+ "fmt"
+ "os"
+ "os/signal"
+ "sync"
+ "syscall"
+ "testing"
+
+ "github.com/alicebob/miniredis/v2"
+ "github.com/golang/mock/gomock"
+ "github.com/spiral/endure"
+ "github.com/spiral/roadrunner/v2/plugins/config"
+ "github.com/spiral/roadrunner/v2/plugins/redis"
+ "github.com/spiral/roadrunner/v2/tests/mocks"
+ "github.com/stretchr/testify/assert"
+)
+
+func redisConfig(port string) string {
+ cfg := `
+redis:
+ addrs:
+ - 'localhost:%s'
+ master_name: ''
+ username: ''
+ password: ''
+ db: 0
+ sentinel_password: ''
+ route_by_latency: false
+ route_randomly: false
+ dial_timeout: 0
+ max_retries: 1
+ min_retry_backoff: 0
+ max_retry_backoff: 0
+ pool_size: 0
+ min_idle_conns: 0
+ max_conn_age: 0
+ read_timeout: 0
+ write_timeout: 0
+ pool_timeout: 0
+ idle_timeout: 0
+ idle_check_freq: 0
+ read_only: false
+`
+ return fmt.Sprintf(cfg, port)
+}
+
+func TestRedisInit(t *testing.T) {
+ cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ s, err := miniredis.Run()
+ assert.NoError(t, err)
+
+ c := redisConfig(s.Port())
+
+ cfg := &config.Viper{}
+ cfg.Type = "yaml"
+ cfg.ReadInCfg = []byte(c)
+
+ controller := gomock.NewController(t)
+ mockLogger := mocks.NewMockLogger(controller)
+
+ err = cont.RegisterAll(
+ cfg,
+ mockLogger,
+ &redis.Plugin{},
+ &Plugin1{},
+ )
+ assert.NoError(t, err)
+
+ err = cont.Init()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ch, err := cont.Serve()
+ assert.NoError(t, err)
+
+ sig := make(chan os.Signal, 1)
+ signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
+
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
+
+ stopCh := make(chan struct{}, 1)
+
+ go func() {
+ defer wg.Done()
+ for {
+ select {
+ case e := <-ch:
+ assert.Fail(t, "error", e.Error.Error())
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ case <-sig:
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ case <-stopCh:
+ // timeout
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ }
+ }
+ }()
+
+ stopCh <- struct{}{}
+ wg.Wait()
+}
diff --git a/tests/plugins/reload/config_test.go b/tests/plugins/reload/config_test.go
new file mode 100644
index 00000000..72c11070
--- /dev/null
+++ b/tests/plugins/reload/config_test.go
@@ -0,0 +1,63 @@
+package reload
+
+import (
+ "testing"
+ "time"
+
+ "github.com/spiral/roadrunner/v2/plugins/reload"
+ "github.com/stretchr/testify/assert"
+)
+
+func Test_Config_Valid(t *testing.T) {
+ services := make(map[string]reload.ServiceConfig)
+ services["test"] = reload.ServiceConfig{
+ Recursive: false,
+ Patterns: nil,
+ Dirs: nil,
+ Ignore: nil,
+ }
+
+ cfg := &reload.Config{
+ Interval: time.Second,
+ Patterns: nil,
+ Services: services,
+ }
+ assert.NoError(t, cfg.Valid())
+}
+
+func Test_Fake_ServiceConfig(t *testing.T) {
+ services := make(map[string]reload.ServiceConfig)
+ cfg := &reload.Config{
+ Interval: time.Microsecond,
+ Patterns: nil,
+ Services: services,
+ }
+ assert.Error(t, cfg.Valid())
+}
+
+func Test_Interval(t *testing.T) {
+ services := make(map[string]reload.ServiceConfig)
+ services["test"] = reload.ServiceConfig{
+ Enabled: false,
+ Recursive: false,
+ Patterns: nil,
+ Dirs: nil,
+ Ignore: nil,
+ }
+
+ cfg := &reload.Config{
+ Interval: time.Millisecond, // should crash here
+ Patterns: nil,
+ Services: services,
+ }
+ assert.Error(t, cfg.Valid())
+}
+
+func Test_NoServiceConfig(t *testing.T) {
+ cfg := &reload.Config{
+ Interval: time.Second,
+ Patterns: nil,
+ Services: nil,
+ }
+ assert.Error(t, cfg.Valid())
+}
diff --git a/tests/plugins/reload/configs/.rr-reload-2.yaml b/tests/plugins/reload/configs/.rr-reload-2.yaml
new file mode 100644
index 00000000..ab32b2d1
--- /dev/null
+++ b/tests/plugins/reload/configs/.rr-reload-2.yaml
@@ -0,0 +1,44 @@
+server:
+ command: php ../../psr-worker-bench.php
+ user: ''
+ group: ''
+ env:
+ RR_HTTP: 'true'
+ relay: pipes
+ relayTimeout: 20s
+http:
+ debug: true
+ address: '127.0.0.1:27388'
+ maxRequestSize: 1024
+ middleware:
+ - ''
+ uploads:
+ forbid:
+ - .php
+ - .exe
+ - .bat
+ trustedSubnets:
+ - 10.0.0.0/8
+ - 127.0.0.0/8
+ - 172.16.0.0/12
+ - 192.168.0.0/16
+ - '::1/128'
+ - 'fc00::/7'
+ - 'fe80::/10'
+ pool:
+ numWorkers: 2
+ maxJobs: 0
+ allocateTimeout: 60s
+ destroyTimeout: 60s
+logs:
+ mode: development
+ level: error
+reload:
+ interval: 2s
+ patterns:
+ - .txt
+ services:
+ http:
+ dirs:
+ - './unit_tests'
+ recursive: true
diff --git a/tests/plugins/reload/configs/.rr-reload-3.yaml b/tests/plugins/reload/configs/.rr-reload-3.yaml
new file mode 100644
index 00000000..881d9b88
--- /dev/null
+++ b/tests/plugins/reload/configs/.rr-reload-3.yaml
@@ -0,0 +1,46 @@
+server:
+ command: php ../../psr-worker-bench.php
+ user: ''
+ group: ''
+ env:
+ RR_HTTP: 'true'
+ relay: pipes
+ relayTimeout: 20s
+http:
+ debug: true
+ address: '127.0.0.1:37388'
+ maxRequestSize: 1024
+ middleware:
+ - ''
+ uploads:
+ forbid:
+ - .php
+ - .exe
+ - .bat
+ trustedSubnets:
+ - 10.0.0.0/8
+ - 127.0.0.0/8
+ - 172.16.0.0/12
+ - 192.168.0.0/16
+ - '::1/128'
+ - 'fc00::/7'
+ - 'fe80::/10'
+ pool:
+ numWorkers: 2
+ maxJobs: 0
+ allocateTimeout: 60s
+ destroyTimeout: 60s
+logs:
+ mode: development
+ level: error
+reload:
+ interval: 2s
+ patterns:
+ - .txt
+ services:
+ http:
+ dirs:
+ - './unit_tests'
+ - './unit_tests_copied'
+ - './dir1'
+ recursive: true
diff --git a/tests/plugins/reload/configs/.rr-reload-4.yaml b/tests/plugins/reload/configs/.rr-reload-4.yaml
new file mode 100644
index 00000000..d47df558
--- /dev/null
+++ b/tests/plugins/reload/configs/.rr-reload-4.yaml
@@ -0,0 +1,46 @@
+server:
+ command: php ../../psr-worker-bench.php
+ user: ''
+ group: ''
+ env:
+ RR_HTTP: 'true'
+ relay: pipes
+ relayTimeout: 20s
+http:
+ debug: true
+ address: '127.0.0.1:22766'
+ maxRequestSize: 1024
+ middleware:
+ - ''
+ uploads:
+ forbid:
+ - .php
+ - .exe
+ - .bat
+ trustedSubnets:
+ - 10.0.0.0/8
+ - 127.0.0.0/8
+ - 172.16.0.0/12
+ - 192.168.0.0/16
+ - '::1/128'
+ - 'fc00::/7'
+ - 'fe80::/10'
+ pool:
+ numWorkers: 2
+ maxJobs: 0
+ allocateTimeout: 60s
+ destroyTimeout: 60s
+logs:
+ mode: development
+ level: error
+reload:
+ interval: 2s
+ patterns:
+ - .aaa
+ services:
+ http:
+ dirs:
+ - './unit_tests'
+ - './unit_tests_copied'
+ - './dir1'
+ recursive: false
diff --git a/tests/plugins/reload/configs/.rr-reload.yaml b/tests/plugins/reload/configs/.rr-reload.yaml
new file mode 100644
index 00000000..794c41f2
--- /dev/null
+++ b/tests/plugins/reload/configs/.rr-reload.yaml
@@ -0,0 +1,44 @@
+server:
+ command: php ../../psr-worker-bench.php
+ user: ''
+ group: ''
+ env:
+ RR_HTTP: 'true'
+ relay: pipes
+ relayTimeout: 20s
+http:
+ debug: true
+ address: '127.0.0.1:22388'
+ maxRequestSize: 1024
+ middleware:
+ - ''
+ uploads:
+ forbid:
+ - .php
+ - .exe
+ - .bat
+ trustedSubnets:
+ - 10.0.0.0/8
+ - 127.0.0.0/8
+ - 172.16.0.0/12
+ - 192.168.0.0/16
+ - '::1/128'
+ - 'fc00::/7'
+ - 'fe80::/10'
+ pool:
+ numWorkers: 2
+ maxJobs: 0
+ allocateTimeout: 60s
+ destroyTimeout: 60s
+logs:
+ mode: development
+ level: error
+reload:
+ interval: 1s
+ patterns:
+ - .txt
+ services:
+ http:
+ dirs:
+ - './unit_tests'
+ recursive: true
diff --git a/tests/plugins/reload/reload_plugin_test.go b/tests/plugins/reload/reload_plugin_test.go
new file mode 100644
index 00000000..c83d4787
--- /dev/null
+++ b/tests/plugins/reload/reload_plugin_test.go
@@ -0,0 +1,827 @@
+package reload
+
+import (
+ "io"
+ "io/ioutil"
+ "math/rand"
+ "os"
+ "os/signal"
+ "path/filepath"
+ "strconv"
+ "sync"
+ "syscall"
+ "testing"
+ "time"
+
+ "github.com/golang/mock/gomock"
+ "github.com/spiral/endure"
+ "github.com/spiral/errors"
+ "github.com/spiral/roadrunner/v2/plugins/config"
+ httpPlugin "github.com/spiral/roadrunner/v2/plugins/http"
+ "github.com/spiral/roadrunner/v2/plugins/reload"
+ "github.com/spiral/roadrunner/v2/plugins/resetter"
+ "github.com/spiral/roadrunner/v2/plugins/server"
+ "github.com/spiral/roadrunner/v2/tests/mocks"
+ "github.com/stretchr/testify/assert"
+)
+
+const testDir string = "unit_tests"
+const testCopyToDir string = "unit_tests_copied"
+const dir1 string = "dir1"
+const hugeNumberOfFiles uint = 500
+
+func TestReloadInit(t *testing.T) {
+ cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel))
+ assert.NoError(t, err)
+
+ cfg := &config.Viper{
+ Path: "configs/.rr-reload.yaml",
+ Prefix: "rr",
+ }
+
+ // try to remove, skip error
+ assert.NoError(t, freeResources(testDir))
+ err = os.Mkdir(testDir, 0755)
+ assert.NoError(t, err)
+
+ controller := gomock.NewController(t)
+ mockLogger := mocks.NewMockLogger(controller)
+
+ mockLogger.EXPECT().Info("worker constructed", "pid", gomock.Any()).AnyTimes()
+ mockLogger.EXPECT().Debug("http handler response received", "elapsed", gomock.Any(), "remote address", "127.0.0.1").Times(1)
+ mockLogger.EXPECT().Debug("file was created", "path", gomock.Any(), "name", "file.txt", "size", gomock.Any()).Times(2)
+ mockLogger.EXPECT().Debug("file was added to watcher", "path", gomock.Any(), "name", "file.txt", "size", gomock.Any()).Times(2)
+ mockLogger.EXPECT().Info("HTTP plugin got restart request. Restarting...").Times(1)
+ mockLogger.EXPECT().Info("HTTP workers Pool successfully restarted").Times(1)
+ mockLogger.EXPECT().Info("HTTP listeners successfully re-added").Times(1)
+ mockLogger.EXPECT().Info("HTTP plugin successfully restarted").Times(1)
+ mockLogger.EXPECT().Info(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() // placeholder for the workerlogerror
+
+ err = cont.RegisterAll(
+ cfg,
+ mockLogger,
+ &server.Plugin{},
+ &httpPlugin.Plugin{},
+ &reload.Plugin{},
+ &resetter.Plugin{},
+ )
+ assert.NoError(t, err)
+
+ err = cont.Init()
+ assert.NoError(t, err)
+
+ ch, err := cont.Serve()
+ assert.NoError(t, err)
+
+ sig := make(chan os.Signal, 1)
+ signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
+
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
+
+ stopCh := make(chan struct{}, 1)
+
+ go func() {
+ defer wg.Done()
+ for {
+ select {
+ case e := <-ch:
+ assert.Fail(t, "error", e.Error.Error())
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ case <-sig:
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ case <-stopCh:
+ // timeout
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ }
+ }
+ }()
+
+ t.Run("ReloadTestInit", reloadTestInit)
+
+ stopCh <- struct{}{}
+ wg.Wait()
+ assert.NoError(t, freeResources(testDir))
+}
+
+func reloadTestInit(t *testing.T) {
+ err := ioutil.WriteFile(filepath.Join(testDir, "file.txt"), //nolint:gosec
+ []byte{}, 0755)
+ assert.NoError(t, err)
+}
+
+func TestReloadHugeNumberOfFiles(t *testing.T) {
+ cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel))
+ assert.NoError(t, err)
+
+ cfg := &config.Viper{
+ Path: "configs/.rr-reload.yaml",
+ Prefix: "rr",
+ }
+
+ // try to remove, skip error
+ assert.NoError(t, freeResources(testDir))
+ assert.NoError(t, freeResources(testCopyToDir))
+
+ assert.NoError(t, os.Mkdir(testDir, 0755))
+ assert.NoError(t, os.Mkdir(testCopyToDir, 0755))
+
+ controller := gomock.NewController(t)
+ mockLogger := mocks.NewMockLogger(controller)
+
+ mockLogger.EXPECT().Info("worker constructed", "pid", gomock.Any()).AnyTimes()
+ mockLogger.EXPECT().Debug("file added to the list of removed files", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).AnyTimes()
+ mockLogger.EXPECT().Debug("http handler response received", "elapsed", gomock.Any(), "remote address", "127.0.0.1").Times(1)
+ mockLogger.EXPECT().Debug("file was created", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(1)
+ mockLogger.EXPECT().Debug("file was updated", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(1)
+ mockLogger.EXPECT().Debug("file was added to watcher", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(1)
+ mockLogger.EXPECT().Info("HTTP plugin got restart request. Restarting...").MinTimes(1)
+ mockLogger.EXPECT().Info("HTTP workers Pool successfully restarted").MinTimes(1)
+ mockLogger.EXPECT().Info("HTTP listeners successfully re-added").MinTimes(1)
+ mockLogger.EXPECT().Info("HTTP plugin successfully restarted").MinTimes(1)
+ mockLogger.EXPECT().Info(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() // placeholder for the workerlogerror
+
+ err = cont.RegisterAll(
+ cfg,
+ mockLogger,
+ &server.Plugin{},
+ &httpPlugin.Plugin{},
+ &reload.Plugin{},
+ &resetter.Plugin{},
+ )
+ assert.NoError(t, err)
+
+ err = cont.Init()
+ assert.NoError(t, err)
+
+ ch, err := cont.Serve()
+ assert.NoError(t, err)
+
+ sig := make(chan os.Signal, 1)
+ signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
+
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
+
+ stopCh := make(chan struct{}, 1)
+
+ go func() {
+ defer wg.Done()
+ for {
+ select {
+ case e := <-ch:
+ assert.Fail(t, "error", e.Error.Error())
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ case <-sig:
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ case <-stopCh:
+ // timeout
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ }
+ }
+ }()
+
+ t.Run("ReloadTestHugeNumberOfFiles", reloadHugeNumberOfFiles)
+ t.Run("ReloadRandomlyChangeFile", randomlyChangeFile)
+
+ stopCh <- struct{}{}
+ wg.Wait()
+
+ assert.NoError(t, freeResources(testDir))
+ assert.NoError(t, freeResources(testCopyToDir))
+}
+
+func randomlyChangeFile(t *testing.T) {
+ // we know, that directory contains 500 files (0-499)
+ // let's try to randomly change it
+ for i := 0; i < 10; i++ {
+ // rand sleep
+ rSleep := rand.Int63n(500) // nolint:gosec
+ time.Sleep(time.Millisecond * time.Duration(rSleep))
+ rNum := rand.Int63n(int64(hugeNumberOfFiles)) // nolint:gosec
+ err := ioutil.WriteFile(filepath.Join(testDir, "file_"+strconv.Itoa(int(rNum))+".txt"), []byte("Hello, Gophers!"), 0755) // nolint:gosec
+ assert.NoError(t, err)
+ }
+}
+
+func reloadHugeNumberOfFiles(t *testing.T) {
+ for i := uint(0); i < hugeNumberOfFiles; i++ {
+ assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".txt"))
+ }
+}
+
+// Should be events only about creating files with txt ext
+func TestReloadFilterFileExt(t *testing.T) {
+ cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel))
+ assert.NoError(t, err)
+
+ cfg := &config.Viper{
+ Path: "configs/.rr-reload-2.yaml",
+ Prefix: "rr",
+ }
+
+ // try to remove, skip error
+ assert.NoError(t, freeResources(testDir))
+ assert.NoError(t, os.Mkdir(testDir, 0755))
+
+ controller := gomock.NewController(t)
+ mockLogger := mocks.NewMockLogger(controller)
+
+ mockLogger.EXPECT().Info("worker constructed", "pid", gomock.Any()).AnyTimes()
+ mockLogger.EXPECT().Debug("http handler response received", "elapsed", gomock.Any(), "remote address", "127.0.0.1").Times(1)
+ mockLogger.EXPECT().Debug("file was created", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(100)
+ mockLogger.EXPECT().Debug("file was added to watcher", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(1)
+ mockLogger.EXPECT().Debug("file added to the list of removed files", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).AnyTimes()
+ mockLogger.EXPECT().Info("HTTP plugin got restart request. Restarting...").Times(1)
+ mockLogger.EXPECT().Info("HTTP workers Pool successfully restarted").Times(1)
+ mockLogger.EXPECT().Info("HTTP listeners successfully re-added").Times(1)
+ mockLogger.EXPECT().Info("HTTP plugin successfully restarted").Times(1)
+ mockLogger.EXPECT().Info(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() // placeholder for the workerlogerror
+
+ err = cont.RegisterAll(
+ cfg,
+ mockLogger,
+ &server.Plugin{},
+ &httpPlugin.Plugin{},
+ &reload.Plugin{},
+ &resetter.Plugin{},
+ )
+ assert.NoError(t, err)
+
+ err = cont.Init()
+ assert.NoError(t, err)
+
+ ch, err := cont.Serve()
+ assert.NoError(t, err)
+
+ sig := make(chan os.Signal, 1)
+ signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
+
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
+
+ stopCh := make(chan struct{}, 1)
+
+ go func() {
+ defer wg.Done()
+ for {
+ select {
+ case e := <-ch:
+ assert.Fail(t, "error", e.Error.Error())
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ case <-sig:
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ case <-stopCh:
+ // timeout
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ }
+ }
+ }()
+
+ t.Run("ReloadMakeFiles", reloadMakeFiles)
+ t.Run("ReloadFilteredExt", reloadFilteredExt)
+
+ stopCh <- struct{}{}
+ wg.Wait()
+
+ assert.NoError(t, freeResources(testDir))
+}
+
+func reloadMakeFiles(t *testing.T) {
+ for i := uint(0); i < 100; i++ {
+ assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".txt"))
+ }
+ for i := uint(0); i < 100; i++ {
+ assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".abc"))
+ }
+ for i := uint(0); i < 100; i++ {
+ assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".def"))
+ }
+}
+
+func reloadFilteredExt(t *testing.T) {
+ // change files with abc extension
+ for i := 0; i < 10; i++ {
+ // rand sleep
+ rSleep := rand.Int63n(1000) // nolint:gosec
+ time.Sleep(time.Millisecond * time.Duration(rSleep))
+ rNum := rand.Int63n(int64(hugeNumberOfFiles)) // nolint:gosec
+ err := ioutil.WriteFile(filepath.Join(testDir, "file_"+strconv.Itoa(int(rNum))+".abc"), []byte("Hello, Gophers!"), 0755) // nolint:gosec
+ assert.NoError(t, err)
+ }
+
+ // change files with def extension
+ for i := 0; i < 10; i++ {
+ // rand sleep
+ rSleep := rand.Int63n(1000) // nolint:gosec
+ time.Sleep(time.Millisecond * time.Duration(rSleep))
+ rNum := rand.Int63n(int64(hugeNumberOfFiles)) // nolint:gosec
+ err := ioutil.WriteFile(filepath.Join(testDir, "file_"+strconv.Itoa(int(rNum))+".def"), []byte("Hello, Gophers!"), 0755) // nolint:gosec
+ assert.NoError(t, err)
+ }
+}
+
+// Should be events only about creating files with txt ext
+func TestReloadCopy500(t *testing.T) {
+ cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel))
+ assert.NoError(t, err)
+
+ cfg := &config.Viper{
+ Path: "configs/.rr-reload-3.yaml",
+ Prefix: "rr",
+ }
+
+ // try to remove, skip error
+ assert.NoError(t, freeResources(testDir))
+ assert.NoError(t, freeResources(testCopyToDir))
+ assert.NoError(t, freeResources(dir1))
+
+ assert.NoError(t, os.Mkdir(testDir, 0755))
+ assert.NoError(t, os.Mkdir(testCopyToDir, 0755))
+ assert.NoError(t, os.Mkdir(dir1, 0755))
+
+ controller := gomock.NewController(t)
+ mockLogger := mocks.NewMockLogger(controller)
+ //
+ mockLogger.EXPECT().Info("worker constructed", "pid", gomock.Any()).AnyTimes()
+ mockLogger.EXPECT().Debug("http handler response received", "elapsed", gomock.Any(), "remote address", "127.0.0.1").Times(1)
+ mockLogger.EXPECT().Debug("file was created", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(50)
+ mockLogger.EXPECT().Debug("file was added to watcher", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(50)
+ mockLogger.EXPECT().Debug("file added to the list of removed files", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(50)
+ mockLogger.EXPECT().Debug("file was removed from watcher", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(50)
+ mockLogger.EXPECT().Debug("file was updated", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(50)
+ mockLogger.EXPECT().Info("HTTP plugin got restart request. Restarting...").MinTimes(1)
+ mockLogger.EXPECT().Info("HTTP workers Pool successfully restarted").MinTimes(1)
+ mockLogger.EXPECT().Info("HTTP listeners successfully re-added").MinTimes(1)
+ mockLogger.EXPECT().Info("HTTP plugin successfully restarted").MinTimes(1)
+ mockLogger.EXPECT().Info(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() // placeholder for the workerlogerror
+
+ err = cont.RegisterAll(
+ cfg,
+ mockLogger,
+ &server.Plugin{},
+ &httpPlugin.Plugin{},
+ &reload.Plugin{},
+ &resetter.Plugin{},
+ )
+ assert.NoError(t, err)
+
+ err = cont.Init()
+ assert.NoError(t, err)
+
+ ch, err := cont.Serve()
+ assert.NoError(t, err)
+
+ sig := make(chan os.Signal, 1)
+ signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
+
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
+
+ stopCh := make(chan struct{}, 1)
+
+ go func() {
+ defer wg.Done()
+ for {
+ select {
+ case e := <-ch:
+ assert.Fail(t, "error", e.Error.Error())
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ case <-sig:
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ case <-stopCh:
+ // timeout
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ }
+ }
+ }()
+
+ // Scenario
+ // 1
+ // Create 3k files with txt, abc, def extensions
+ // Copy files to the unit_tests_copy dir
+ // 2
+ // Delete both dirs, recreate
+ // Create 3k files with txt, abc, def extensions
+ // Move files to the unit_tests_copy dir
+ // 3
+ // Recursive
+
+ t.Run("ReloadMake300Files", reloadMake300Files)
+ t.Run("ReloadCopyFiles", reloadCopyFiles)
+ t.Run("ReloadRecursiveDirsSupport", copyFilesRecursive)
+ t.Run("RandomChangesInRecursiveDirs", randomChangesInRecursiveDirs)
+ t.Run("RemoveFilesSupport", removeFilesSupport)
+ t.Run("ReloadMoveSupport", reloadMoveSupport)
+
+ assert.NoError(t, freeResources(testDir))
+ assert.NoError(t, freeResources(testCopyToDir))
+ assert.NoError(t, freeResources(dir1))
+
+ stopCh <- struct{}{}
+ wg.Wait()
+}
+
+func reloadMoveSupport(t *testing.T) {
+ t.Run("MoveSupportCopy", copyFilesRecursive)
+ // move some files
+ for i := 0; i < 10; i++ {
+ // rand sleep
+ rSleep := rand.Int63n(500) // nolint:gosec
+ time.Sleep(time.Millisecond * time.Duration(rSleep))
+ rNum := rand.Int63n(int64(100)) // nolint:gosec
+ rDir := rand.Int63n(9) // nolint:gosec
+ rExt := rand.Int63n(3) // nolint:gosec
+
+ ext := []string{
+ ".txt",
+ ".abc",
+ ".def",
+ }
+
+ // change files with def extension
+ dirs := []string{
+ "dir1",
+ "dir1/dir2",
+ "dir1/dir2/dir3",
+ "dir1/dir2/dir3/dir4",
+ "dir1/dir2/dir3/dir4/dir5",
+ "dir1/dir2/dir3/dir4/dir5/dir6",
+ "dir1/dir2/dir3/dir4/dir5/dir6/dir7",
+ "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8",
+ "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9",
+ "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9/dir10",
+ }
+
+ // move file
+ err := os.Rename(filepath.Join(dirs[rDir], "file_"+strconv.Itoa(int(rNum))+ext[rExt]), filepath.Join(dirs[rDir+1], "file_"+strconv.Itoa(int(rNum))+ext[rExt]))
+ assert.NoError(t, err)
+ }
+}
+
+func removeFilesSupport(t *testing.T) {
+ // remove some files
+ for i := 0; i < 10; i++ {
+ // rand sleep
+ rSleep := rand.Int63n(500) // nolint:gosec
+ time.Sleep(time.Millisecond * time.Duration(rSleep))
+ rNum := rand.Int63n(int64(100)) // nolint:gosec
+ rDir := rand.Int63n(10) // nolint:gosec
+ rExt := rand.Int63n(3) // nolint:gosec
+
+ ext := []string{
+ ".txt",
+ ".abc",
+ ".def",
+ }
+
+ // change files with def extension
+ dirs := []string{
+ "dir1",
+ "dir1/dir2",
+ "dir1/dir2/dir3",
+ "dir1/dir2/dir3/dir4",
+ "dir1/dir2/dir3/dir4/dir5",
+ "dir1/dir2/dir3/dir4/dir5/dir6",
+ "dir1/dir2/dir3/dir4/dir5/dir6/dir7",
+ "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8",
+ "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9",
+ "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9/dir10",
+ }
+ // here can be a situation, when file already deleted
+ _ = os.Remove(filepath.Join(dirs[rDir], "file_"+strconv.Itoa(int(rNum))+ext[rExt]))
+ }
+}
+
+func randomChangesInRecursiveDirs(t *testing.T) {
+ // change files with def extension
+ dirs := []string{
+ "dir1",
+ "dir1/dir2",
+ "dir1/dir2/dir3",
+ "dir1/dir2/dir3/dir4",
+ "dir1/dir2/dir3/dir4/dir5",
+ "dir1/dir2/dir3/dir4/dir5/dir6",
+ "dir1/dir2/dir3/dir4/dir5/dir6/dir7",
+ "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8",
+ "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9",
+ "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9/dir10",
+ }
+
+ ext := []string{
+ ".txt",
+ ".abc",
+ ".def",
+ }
+
+ filenames := []string{
+ "file_", // should be update
+ "foo_", // should be created
+ "bar_", // should be created
+ }
+ for i := 0; i < 10; i++ {
+ // rand sleep
+ rSleep := rand.Int63n(500) // nolint:gosec
+ time.Sleep(time.Millisecond * time.Duration(rSleep))
+ rNum := rand.Int63n(int64(100)) // nolint:gosec
+ rDir := rand.Int63n(10) // nolint:gosec
+ rExt := rand.Int63n(3) // nolint:gosec
+ rName := rand.Int63n(3) // nolint:gosec
+
+ err := ioutil.WriteFile(filepath.Join(dirs[rDir], filenames[rName]+strconv.Itoa(int(rNum))+ext[rExt]), []byte("Hello, Gophers!"), 0755) // nolint:gosec
+ assert.NoError(t, err)
+ }
+}
+
+func copyFilesRecursive(t *testing.T) {
+ err := copyDir(testDir, "dir1")
+ assert.NoError(t, err)
+ err = copyDir(testDir, "dir1/dir2")
+ assert.NoError(t, err)
+ err = copyDir(testDir, "dir1/dir2/dir3")
+ assert.NoError(t, err)
+ err = copyDir(testDir, "dir1/dir2/dir3/dir4")
+ assert.NoError(t, err)
+ err = copyDir(testDir, "dir1/dir2/dir3/dir4/dir5")
+ assert.NoError(t, err)
+ err = copyDir(testDir, "dir1/dir2/dir3/dir4/dir5/dir6")
+ assert.NoError(t, err)
+ err = copyDir(testDir, "dir1/dir2/dir3/dir4/dir5/dir6/dir7")
+ assert.NoError(t, err)
+ err = copyDir(testDir, "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8")
+ assert.NoError(t, err)
+ err = copyDir(testDir, "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9")
+ assert.NoError(t, err)
+ err = copyDir(testDir, "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9/dir10")
+ assert.NoError(t, err)
+}
+
+func reloadCopyFiles(t *testing.T) {
+ err := copyDir(testDir, testCopyToDir)
+ assert.NoError(t, err)
+
+ assert.NoError(t, freeResources(testDir))
+ assert.NoError(t, freeResources(testCopyToDir))
+
+ assert.NoError(t, os.Mkdir(testDir, 0755))
+ assert.NoError(t, os.Mkdir(testCopyToDir, 0755))
+
+ // recreate files
+ for i := uint(0); i < 100; i++ {
+ assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".txt"))
+ }
+ for i := uint(0); i < 100; i++ {
+ assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".abc"))
+ }
+ for i := uint(0); i < 100; i++ {
+ assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".def"))
+ }
+
+ err = copyDir(testDir, testCopyToDir)
+ assert.NoError(t, err)
+}
+
+func reloadMake300Files(t *testing.T) {
+ for i := uint(0); i < 100; i++ {
+ assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".txt"))
+ }
+ for i := uint(0); i < 100; i++ {
+ assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".abc"))
+ }
+ for i := uint(0); i < 100; i++ {
+ assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".def"))
+ }
+}
+
+func TestReloadNoRecursion(t *testing.T) {
+ cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel))
+ assert.NoError(t, err)
+
+ cfg := &config.Viper{
+ Path: "configs/.rr-reload-4.yaml",
+ Prefix: "rr",
+ }
+
+ // try to remove, skip error
+ assert.NoError(t, freeResources(testDir))
+ assert.NoError(t, freeResources(testCopyToDir))
+ assert.NoError(t, freeResources(dir1))
+
+ assert.NoError(t, os.Mkdir(testDir, 0755))
+ assert.NoError(t, os.Mkdir(dir1, 0755))
+ assert.NoError(t, os.Mkdir(testCopyToDir, 0755))
+
+ controller := gomock.NewController(t)
+ mockLogger := mocks.NewMockLogger(controller)
+
+ // http server should not be restarted. all event from wrong file extensions should be skipped
+ mockLogger.EXPECT().Info("worker constructed", "pid", gomock.Any()).MinTimes(1)
+ mockLogger.EXPECT().Debug("http handler response received", "elapsed", gomock.Any(), "remote address", "127.0.0.1").Times(1)
+ mockLogger.EXPECT().Debug("file added to the list of removed files", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(1)
+ mockLogger.EXPECT().Info(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() // placeholder for the workerlogerror
+
+ err = cont.RegisterAll(
+ cfg,
+ mockLogger,
+ &server.Plugin{},
+ &httpPlugin.Plugin{},
+ &reload.Plugin{},
+ &resetter.Plugin{},
+ )
+ assert.NoError(t, err)
+
+ err = cont.Init()
+ assert.NoError(t, err)
+
+ ch, err := cont.Serve()
+ assert.NoError(t, err)
+
+ sig := make(chan os.Signal, 1)
+ signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
+
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
+
+ stopCh := make(chan struct{}, 1)
+
+ go func() {
+ defer wg.Done()
+ for {
+ select {
+ case e := <-ch:
+ assert.Fail(t, "error", e.Error.Error())
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ case <-sig:
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ case <-stopCh:
+ // timeout
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ }
+ }
+ }()
+
+ t.Run("ReloadMakeFiles", reloadMakeFiles) // make files in the testDir
+ t.Run("ReloadCopyFilesRecursive", reloadCopyFiles)
+
+ stopCh <- struct{}{}
+ wg.Wait()
+
+ assert.NoError(t, freeResources(testDir))
+ assert.NoError(t, freeResources(testCopyToDir))
+ assert.NoError(t, freeResources(dir1))
+}
+
+// ========================================================================
+
+func freeResources(path string) error {
+ return os.RemoveAll(path)
+}
+
+func makeFile(filename string) error {
+ return ioutil.WriteFile(filepath.Join(testDir, filename), []byte{}, 0755) //nolint:gosec
+}
+
+func copyDir(src string, dst string) error {
+ src = filepath.Clean(src)
+ dst = filepath.Clean(dst)
+
+ si, err := os.Stat(src)
+ if err != nil {
+ return err
+ }
+ if !si.IsDir() {
+ return errors.E(errors.Str("source is not a directory"))
+ }
+
+ _, err = os.Stat(dst)
+ if err != nil && !os.IsNotExist(err) {
+ return err
+ }
+
+ err = os.MkdirAll(dst, si.Mode())
+ if err != nil {
+ return err
+ }
+
+ entries, err := ioutil.ReadDir(src)
+ if err != nil {
+ return err
+ }
+
+ for _, entry := range entries {
+ srcPath := filepath.Join(src, entry.Name())
+ dstPath := filepath.Join(dst, entry.Name())
+
+ if entry.IsDir() {
+ err = copyDir(srcPath, dstPath)
+ if err != nil {
+ return err
+ }
+ } else {
+ // Skip symlinks.
+ if entry.Mode()&os.ModeSymlink != 0 {
+ continue
+ }
+
+ err = copyFile(srcPath, dstPath)
+ if err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+func copyFile(src, dst string) error {
+ in, err := os.Open(src)
+ if err != nil {
+ return errors.E(err)
+ }
+ defer func() {
+ _ = in.Close()
+ }()
+
+ out, err := os.Create(dst)
+ if err != nil {
+ return errors.E(err)
+ }
+ defer func() {
+ _ = out.Close()
+ }()
+
+ _, err = io.Copy(out, in)
+ if err != nil {
+ return errors.E(err)
+ }
+
+ err = out.Sync()
+ if err != nil {
+ return errors.E(err)
+ }
+
+ si, err := os.Stat(src)
+ if err != nil {
+ return errors.E(err)
+ }
+ err = os.Chmod(dst, si.Mode())
+ if err != nil {
+ return errors.E(err)
+ }
+ return nil
+}
diff --git a/tests/plugins/resetter/.rr-resetter.yaml b/tests/plugins/resetter/.rr-resetter.yaml
new file mode 100644
index 00000000..e50ca9c9
--- /dev/null
+++ b/tests/plugins/resetter/.rr-resetter.yaml
@@ -0,0 +1,16 @@
+server:
+ command: "php ../../http/client.php echo pipes"
+ user: ""
+ group: ""
+ env:
+ "RR_CONFIG": "/some/place/on/the/C134"
+ "RR_CONFIG2": "C138"
+ relay: "pipes"
+ relayTimeout: "20s"
+
+rpc:
+ listen: tcp://127.0.0.1:6001
+ disabled: false
+logs:
+ mode: development
+ level: error \ No newline at end of file
diff --git a/tests/plugins/resetter/resetter_test.go b/tests/plugins/resetter/resetter_test.go
new file mode 100644
index 00000000..89dd43c7
--- /dev/null
+++ b/tests/plugins/resetter/resetter_test.go
@@ -0,0 +1,113 @@
+package resetter
+
+import (
+ "net"
+ "net/rpc"
+ "os"
+ "os/signal"
+ "sync"
+ "syscall"
+ "testing"
+ "time"
+
+ "github.com/spiral/endure"
+ goridgeRpc "github.com/spiral/goridge/v3/pkg/rpc"
+ "github.com/spiral/roadrunner/v2/plugins/config"
+ "github.com/spiral/roadrunner/v2/plugins/logger"
+ "github.com/spiral/roadrunner/v2/plugins/resetter"
+ rpcPlugin "github.com/spiral/roadrunner/v2/plugins/rpc"
+ "github.com/spiral/roadrunner/v2/plugins/server"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestResetterInit(t *testing.T) {
+ cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ cfg := &config.Viper{
+ Path: ".rr-resetter.yaml",
+ Prefix: "rr",
+ }
+
+ err = cont.RegisterAll(
+ cfg,
+ &server.Plugin{},
+ &logger.ZapLogger{},
+ &resetter.Plugin{},
+ &rpcPlugin.Plugin{},
+ &Plugin1{},
+ )
+ assert.NoError(t, err)
+
+ err = cont.Init()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ch, err := cont.Serve()
+ assert.NoError(t, err)
+
+ sig := make(chan os.Signal, 1)
+ signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
+
+ stopCh := make(chan struct{}, 1)
+
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
+
+ go func() {
+ defer wg.Done()
+ for {
+ select {
+ case e := <-ch:
+ assert.Fail(t, "error", e.Error.Error())
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ case <-sig:
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ case <-stopCh:
+ // timeout
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ }
+ }
+ }()
+
+ time.Sleep(time.Second)
+
+ t.Run("ResetterRpcTest", resetterRPCTest)
+ stopCh <- struct{}{}
+ wg.Wait()
+}
+
+func resetterRPCTest(t *testing.T) {
+ conn, err := net.Dial("tcp", "127.0.0.1:6001")
+ assert.NoError(t, err)
+ client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn))
+ // WorkerList contains list of workers.
+
+ var ret bool
+ err = client.Call("resetter.Reset", "resetter.plugin1", &ret)
+ assert.NoError(t, err)
+ assert.True(t, ret)
+ ret = false
+
+ var services []string
+ err = client.Call("resetter.List", nil, &services)
+ assert.NotNil(t, services)
+ assert.NoError(t, err)
+ if services[0] != "resetter.plugin1" {
+ t.Fatal("no enough services")
+ }
+}
diff --git a/tests/plugins/resetter/test_plugin.go b/tests/plugins/resetter/test_plugin.go
new file mode 100644
index 00000000..7d53bca0
--- /dev/null
+++ b/tests/plugins/resetter/test_plugin.go
@@ -0,0 +1,66 @@
+package resetter
+
+import (
+ "context"
+ "time"
+
+ poolImpl "github.com/spiral/roadrunner/v2/pkg/pool"
+ "github.com/spiral/roadrunner/v2/plugins/config"
+ "github.com/spiral/roadrunner/v2/plugins/server"
+)
+
+var testPoolConfig = poolImpl.Config{
+ NumWorkers: 10,
+ MaxJobs: 100,
+ AllocateTimeout: time.Second * 10,
+ DestroyTimeout: time.Second * 10,
+ Supervisor: &poolImpl.SupervisorConfig{
+ WatchTick: 60,
+ TTL: 1000,
+ IdleTTL: 10,
+ ExecTTL: 10,
+ MaxWorkerMemory: 1000,
+ },
+}
+
+// Gauge //////////////
+type Plugin1 struct {
+ config config.Configurer
+ server server.Server
+}
+
+func (p1 *Plugin1) Init(cfg config.Configurer, server server.Server) error {
+ p1.config = cfg
+ p1.server = server
+ return nil
+}
+
+func (p1 *Plugin1) Serve() chan error {
+ errCh := make(chan error, 1)
+ return errCh
+}
+
+func (p1 *Plugin1) Stop() error {
+ return nil
+}
+
+func (p1 *Plugin1) Name() string {
+ return "resetter.plugin1"
+}
+
+func (p1 *Plugin1) Reset() error {
+ pool, err := p1.server.NewWorkerPool(context.Background(), testPoolConfig, nil)
+ if err != nil {
+ panic(err)
+ }
+ pool.Destroy(context.Background())
+
+ pool, err = p1.server.NewWorkerPool(context.Background(), testPoolConfig, nil)
+ if err != nil {
+ panic(err)
+ }
+
+ _ = pool
+
+ return nil
+}
diff --git a/tests/plugins/rpc/config_test.go b/tests/plugins/rpc/config_test.go
new file mode 100755
index 00000000..df5fa391
--- /dev/null
+++ b/tests/plugins/rpc/config_test.go
@@ -0,0 +1,135 @@
+package rpc
+
+import (
+ "runtime"
+ "testing"
+
+ "github.com/spiral/roadrunner/v2/plugins/rpc"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestConfig_Listener(t *testing.T) {
+ cfg := &rpc.Config{Listen: "tcp://:18001"}
+
+ ln, err := cfg.Listener()
+ assert.NoError(t, err)
+ assert.NotNil(t, ln)
+ defer func() {
+ err := ln.Close()
+ if err != nil {
+ t.Errorf("error closing the listener: error %v", err)
+ }
+ }()
+
+ assert.Equal(t, "tcp", ln.Addr().Network())
+ if runtime.GOOS == "windows" {
+ assert.Equal(t, "[::]:18001", ln.Addr().String())
+ } else {
+ assert.Equal(t, "0.0.0.0:18001", ln.Addr().String())
+ }
+}
+
+func TestConfig_ListenerUnix(t *testing.T) {
+ cfg := &rpc.Config{Listen: "unix://file.sock"}
+
+ ln, err := cfg.Listener()
+ assert.NoError(t, err)
+ assert.NotNil(t, ln)
+ defer func() {
+ err := ln.Close()
+ if err != nil {
+ t.Errorf("error closing the listener: error %v", err)
+ }
+ }()
+
+ assert.Equal(t, "unix", ln.Addr().Network())
+ assert.Equal(t, "file.sock", ln.Addr().String())
+}
+
+func Test_Config_Error(t *testing.T) {
+ cfg := &rpc.Config{Listen: "uni:unix.sock"}
+ ln, err := cfg.Listener()
+ assert.Nil(t, ln)
+ assert.Error(t, err)
+ assert.Equal(t, "invalid DSN (tcp://:6001, unix://file.sock)", err.Error())
+}
+
+func Test_Config_ErrorMethod(t *testing.T) {
+ cfg := &rpc.Config{Listen: "xinu://unix.sock"}
+
+ ln, err := cfg.Listener()
+ assert.Nil(t, ln)
+ assert.Error(t, err)
+}
+
+func TestConfig_Dialer(t *testing.T) {
+ cfg := &rpc.Config{Listen: "tcp://:18001"}
+
+ ln, _ := cfg.Listener()
+ defer func() {
+ err := ln.Close()
+ if err != nil {
+ t.Errorf("error closing the listener: error %v", err)
+ }
+ }()
+
+ conn, err := cfg.Dialer()
+ assert.NoError(t, err)
+ assert.NotNil(t, conn)
+ defer func() {
+ err := conn.Close()
+ if err != nil {
+ t.Errorf("error closing the connection: error %v", err)
+ }
+ }()
+
+ assert.Equal(t, "tcp", conn.RemoteAddr().Network())
+ assert.Equal(t, "127.0.0.1:18001", conn.RemoteAddr().String())
+}
+
+func TestConfig_DialerUnix(t *testing.T) {
+ cfg := &rpc.Config{Listen: "unix://file.sock"}
+
+ ln, _ := cfg.Listener()
+ defer func() {
+ err := ln.Close()
+ if err != nil {
+ t.Errorf("error closing the listener: error %v", err)
+ }
+ }()
+
+ conn, err := cfg.Dialer()
+ assert.NoError(t, err)
+ assert.NotNil(t, conn)
+ defer func() {
+ err := conn.Close()
+ if err != nil {
+ t.Errorf("error closing the connection: error %v", err)
+ }
+ }()
+
+ assert.Equal(t, "unix", conn.RemoteAddr().Network())
+ assert.Equal(t, "file.sock", conn.RemoteAddr().String())
+}
+
+func Test_Config_DialerError(t *testing.T) {
+ cfg := &rpc.Config{Listen: "uni:unix.sock"}
+ ln, err := cfg.Dialer()
+ assert.Nil(t, ln)
+ assert.Error(t, err)
+ assert.Equal(t, "invalid socket DSN (tcp://:6001, unix://file.sock)", err.Error())
+}
+
+func Test_Config_DialerErrorMethod(t *testing.T) {
+ cfg := &rpc.Config{Listen: "xinu://unix.sock"}
+
+ ln, err := cfg.Dialer()
+ assert.Nil(t, ln)
+ assert.Error(t, err)
+}
+
+func Test_Config_Defaults(t *testing.T) {
+ c := &rpc.Config{}
+ c.InitDefaults()
+ assert.Equal(t, "tcp://127.0.0.1:6001", c.Listen)
+}
diff --git a/tests/plugins/rpc/configs/.rr-rpc-disabled.yaml b/tests/plugins/rpc/configs/.rr-rpc-disabled.yaml
new file mode 100644
index 00000000..d5c185e7
--- /dev/null
+++ b/tests/plugins/rpc/configs/.rr-rpc-disabled.yaml
@@ -0,0 +1,6 @@
+rpc:
+ listen: tcp://127.0.0.1:6001
+ disabled: true
+logs:
+ mode: development
+ level: error \ No newline at end of file
diff --git a/tests/plugins/rpc/configs/.rr.yaml b/tests/plugins/rpc/configs/.rr.yaml
new file mode 100644
index 00000000..d2cb6c70
--- /dev/null
+++ b/tests/plugins/rpc/configs/.rr.yaml
@@ -0,0 +1,6 @@
+rpc:
+ listen: tcp://127.0.0.1:6001
+ disabled: false
+logs:
+ mode: development
+ level: error \ No newline at end of file
diff --git a/tests/plugins/rpc/plugin1.go b/tests/plugins/rpc/plugin1.go
new file mode 100644
index 00000000..6843b396
--- /dev/null
+++ b/tests/plugins/rpc/plugin1.go
@@ -0,0 +1,42 @@
+package rpc
+
+import (
+ "fmt"
+
+ "github.com/spiral/roadrunner/v2/plugins/config"
+)
+
+type Plugin1 struct {
+ config config.Configurer
+}
+
+func (p1 *Plugin1) Init(cfg config.Configurer) error {
+ p1.config = cfg
+ return nil
+}
+
+func (p1 *Plugin1) Serve() chan error {
+ errCh := make(chan error, 1)
+ return errCh
+}
+
+func (p1 *Plugin1) Stop() error {
+ return nil
+}
+
+func (p1 *Plugin1) Name() string {
+ return "rpc_test.plugin1"
+}
+
+func (p1 *Plugin1) RPC() interface{} {
+ return &PluginRPC{srv: p1}
+}
+
+type PluginRPC struct {
+ srv *Plugin1
+}
+
+func (r *PluginRPC) Hello(in string, out *string) error {
+ *out = fmt.Sprintf("Hello, username: %s", in)
+ return nil
+}
diff --git a/tests/plugins/rpc/plugin2.go b/tests/plugins/rpc/plugin2.go
new file mode 100644
index 00000000..2c47158f
--- /dev/null
+++ b/tests/plugins/rpc/plugin2.go
@@ -0,0 +1,53 @@
+package rpc
+
+import (
+ "net"
+ "net/rpc"
+ "time"
+
+ "github.com/spiral/errors"
+ goridgeRpc "github.com/spiral/goridge/v3/pkg/rpc"
+)
+
+// plugin2 makes a call to the plugin1 via RPC
+// this is just a simulation of external call FOR TEST
+// you don't need to do such things :)
+type Plugin2 struct {
+}
+
+func (p2 *Plugin2) Init() error {
+ return nil
+}
+
+func (p2 *Plugin2) Serve() chan error {
+ errCh := make(chan error, 1)
+
+ go func() {
+ time.Sleep(time.Second * 3)
+
+ conn, err := net.Dial("tcp", "127.0.0.1:6001")
+ if err != nil {
+ errCh <- errors.E(errors.Serve, err)
+ return
+ }
+ client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn))
+ var ret string
+ err = client.Call("rpc_test.plugin1.Hello", "Valery", &ret)
+ if err != nil {
+ errCh <- err
+ return
+ }
+ if ret != "Hello, username: Valery" {
+ errCh <- errors.E("wrong response")
+ return
+ }
+ // to stop exec
+ errCh <- errors.E(errors.Disabled)
+ }()
+
+ return errCh
+}
+
+func (p2 *Plugin2) Stop() error {
+ return nil
+}
diff --git a/tests/plugins/rpc/rpc_test.go b/tests/plugins/rpc/rpc_test.go
new file mode 100644
index 00000000..98959b28
--- /dev/null
+++ b/tests/plugins/rpc/rpc_test.go
@@ -0,0 +1,188 @@
+package rpc
+
+import (
+ "os"
+ "os/signal"
+ "sync"
+ "syscall"
+ "testing"
+ "time"
+
+ "github.com/spiral/endure"
+ "github.com/spiral/errors"
+ "github.com/spiral/roadrunner/v2/plugins/config"
+ "github.com/spiral/roadrunner/v2/plugins/logger"
+ "github.com/spiral/roadrunner/v2/plugins/rpc"
+ "github.com/stretchr/testify/assert"
+)
+
+// graph https://bit.ly/3ensdNb
+func TestRpcInit(t *testing.T) {
+ cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = cont.Register(&Plugin1{})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = cont.Register(&Plugin2{})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ v := &config.Viper{}
+ v.Path = "configs/.rr.yaml"
+ v.Prefix = "rr"
+ err = cont.Register(v)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = cont.Register(&rpc.Plugin{})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = cont.Register(&logger.ZapLogger{})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = cont.Init()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ch, err := cont.Serve()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ sig := make(chan os.Signal, 1)
+
+ signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
+
+ tt := time.NewTimer(time.Second * 10)
+
+ go func() {
+ defer wg.Done()
+ defer tt.Stop()
+ for {
+ select {
+ case e := <-ch:
+ // just stop, this is ok
+ if errors.Is(errors.Disabled, e.Error) {
+ return
+ }
+ assert.Fail(t, "error", e.Error.Error())
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ case <-sig:
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ case <-tt.C:
+ // timeout
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ assert.Fail(t, "timeout")
+ }
+ }
+ }()
+
+ wg.Wait()
+}
+
+// graph https://bit.ly/3ensdNb
+func TestRpcDisabled(t *testing.T) {
+ cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = cont.Register(&Plugin1{})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = cont.Register(&Plugin2{})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ v := &config.Viper{}
+ v.Path = "configs/.rr-rpc-disabled.yaml"
+ v.Prefix = "rr"
+ err = cont.Register(v)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = cont.Register(&rpc.Plugin{})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = cont.Register(&logger.ZapLogger{})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = cont.Init()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ch, err := cont.Serve()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ sig := make(chan os.Signal, 1)
+
+ signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
+ tt := time.NewTimer(time.Second * 20)
+
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
+
+ go func() {
+ defer wg.Done()
+ defer tt.Stop()
+ for {
+ select {
+ case e := <-ch:
+ // RPC is turned off, should be and dial error
+ if errors.Is(errors.Disabled, e.Error) {
+ assert.FailNow(t, "should not be disabled error")
+ }
+ assert.Error(t, e.Error)
+ err = cont.Stop()
+ assert.Error(t, err)
+ return
+ case <-sig:
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ case <-tt.C:
+ // timeout
+ return
+ }
+ }
+ }()
+
+ wg.Wait()
+}
diff --git a/tests/plugins/server/plugin_pipes.go b/tests/plugins/server/plugin_pipes.go
index 6f31395a..5eb2fed1 100644
--- a/tests/plugins/server/plugin_pipes.go
+++ b/tests/plugins/server/plugin_pipes.go
@@ -5,12 +5,12 @@ import (
"time"
"github.com/spiral/errors"
- "github.com/spiral/roadrunner-plugins/config"
"github.com/spiral/roadrunner/v2/interfaces/pool"
"github.com/spiral/roadrunner/v2/pkg/payload"
- "github.com/spiral/roadrunner/v2/pkg/plugins/server"
poolImpl "github.com/spiral/roadrunner/v2/pkg/pool"
"github.com/spiral/roadrunner/v2/pkg/worker"
+ "github.com/spiral/roadrunner/v2/plugins/config"
+ "github.com/spiral/roadrunner/v2/plugins/server"
)
const ConfigSection = "server"
diff --git a/tests/plugins/server/plugin_sockets.go b/tests/plugins/server/plugin_sockets.go
index 1820b4c1..ede67ded 100644
--- a/tests/plugins/server/plugin_sockets.go
+++ b/tests/plugins/server/plugin_sockets.go
@@ -4,11 +4,11 @@ import (
"context"
"github.com/spiral/errors"
- "github.com/spiral/roadrunner-plugins/config"
"github.com/spiral/roadrunner/v2/interfaces/pool"
"github.com/spiral/roadrunner/v2/pkg/payload"
- "github.com/spiral/roadrunner/v2/pkg/plugins/server"
"github.com/spiral/roadrunner/v2/pkg/worker"
+ "github.com/spiral/roadrunner/v2/plugins/config"
+ "github.com/spiral/roadrunner/v2/plugins/server"
)
type Foo2 struct {
diff --git a/tests/plugins/server/plugin_tcp.go b/tests/plugins/server/plugin_tcp.go
index 01fa0cf5..98c13b2b 100644
--- a/tests/plugins/server/plugin_tcp.go
+++ b/tests/plugins/server/plugin_tcp.go
@@ -4,11 +4,11 @@ import (
"context"
"github.com/spiral/errors"
- "github.com/spiral/roadrunner-plugins/config"
"github.com/spiral/roadrunner/v2/interfaces/pool"
"github.com/spiral/roadrunner/v2/pkg/payload"
- "github.com/spiral/roadrunner/v2/pkg/plugins/server"
"github.com/spiral/roadrunner/v2/pkg/worker"
+ "github.com/spiral/roadrunner/v2/plugins/config"
+ "github.com/spiral/roadrunner/v2/plugins/server"
)
type Foo3 struct {
diff --git a/tests/plugins/server/server_test.go b/tests/plugins/server/server_plugin_test.go
index 75111ef7..d63b0ccd 100644
--- a/tests/plugins/server/server_test.go
+++ b/tests/plugins/server/server_plugin_test.go
@@ -3,13 +3,14 @@ package server
import (
"os"
"os/signal"
+ "sync"
"testing"
"time"
"github.com/spiral/endure"
- "github.com/spiral/roadrunner-plugins/config"
- "github.com/spiral/roadrunner-plugins/logger"
- "github.com/spiral/roadrunner/v2/pkg/plugins/server"
+ "github.com/spiral/roadrunner/v2/plugins/config"
+ "github.com/spiral/roadrunner/v2/plugins/logger"
+ "github.com/spiral/roadrunner/v2/plugins/server"
"github.com/stretchr/testify/assert"
)
@@ -56,27 +57,31 @@ func TestAppPipes(t *testing.T) {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
- // stop after 10 seconds
- tt := time.NewTicker(time.Second * 10)
-
- for {
- select {
- case e := <-errCh:
- assert.NoError(t, e.Error)
- assert.NoError(t, container.Stop())
- return
- case <-c:
- er := container.Stop()
- if er != nil {
- panic(er)
+ tt := time.NewTimer(time.Second * 10)
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
+
+ go func() {
+ defer wg.Done()
+ defer tt.Stop()
+ for {
+ select {
+ case e := <-errCh:
+ assert.NoError(t, e.Error)
+ assert.NoError(t, container.Stop())
+ return
+ case <-c:
+ er := container.Stop()
+ assert.NoError(t, er)
+ return
+ case <-tt.C:
+ assert.NoError(t, container.Stop())
+ return
}
- return
- case <-tt.C:
- tt.Stop()
- assert.NoError(t, container.Stop())
- return
}
- }
+ }()
+
+ wg.Wait()
}
func TestAppSockets(t *testing.T) {
diff --git a/tests/plugins/static/config_test.go b/tests/plugins/static/config_test.go
new file mode 100644
index 00000000..f458eed3
--- /dev/null
+++ b/tests/plugins/static/config_test.go
@@ -0,0 +1,49 @@
+package static
+
+import (
+ "testing"
+
+ "github.com/spiral/roadrunner/v2/plugins/static"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestConfig_Forbids(t *testing.T) {
+ cfg := static.Config{Static: struct {
+ Dir string
+ Forbid []string
+ Always []string
+ Request map[string]string
+ Response map[string]string
+ }{Dir: "", Forbid: []string{".php"}, Always: nil, Request: nil, Response: nil}}
+
+ assert.True(t, cfg.AlwaysForbid("index.php"))
+ assert.True(t, cfg.AlwaysForbid("index.PHP"))
+ assert.True(t, cfg.AlwaysForbid("phpadmin/index.bak.php"))
+ assert.False(t, cfg.AlwaysForbid("index.html"))
+}
+
+func TestConfig_Valid(t *testing.T) {
+ assert.NoError(t, (&static.Config{Static: struct {
+ Dir string
+ Forbid []string
+ Always []string
+ Request map[string]string
+ Response map[string]string
+ }{Dir: "./"}}).Valid())
+
+ assert.Error(t, (&static.Config{Static: struct {
+ Dir string
+ Forbid []string
+ Always []string
+ Request map[string]string
+ Response map[string]string
+ }{Dir: "./config.go"}}).Valid())
+
+ assert.Error(t, (&static.Config{Static: struct {
+ Dir string
+ Forbid []string
+ Always []string
+ Request map[string]string
+ Response map[string]string
+ }{Dir: "./dir/"}}).Valid())
+}
diff --git a/tests/plugins/static/configs/.rr-http-static-disabled.yaml b/tests/plugins/static/configs/.rr-http-static-disabled.yaml
new file mode 100644
index 00000000..d8ee15e0
--- /dev/null
+++ b/tests/plugins/static/configs/.rr-http-static-disabled.yaml
@@ -0,0 +1,33 @@
+server:
+ command: "php ../../http/client.php pid pipes"
+ user: ""
+ group: ""
+ env:
+ "RR_HTTP": "true"
+ relay: "pipes"
+ relayTimeout: "20s"
+
+http:
+ debug: true
+ address: 127.0.0.1:21234
+ maxRequestSize: 1024
+ middleware: [ "gzip", "static" ]
+ trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ]
+ uploads:
+ forbid: [ ".php", ".exe", ".bat" ]
+ static:
+ dir: "abc" #not exists
+ forbid: [ ".php", ".htaccess" ]
+ request:
+ "Example-Request-Header": "Value"
+ # Automatically add headers to every response.
+ response:
+ "X-Powered-By": "RoadRunner"
+ pool:
+ numWorkers: 2
+ maxJobs: 0
+ allocateTimeout: 60s
+ destroyTimeout: 60s
+logs:
+ mode: development
+ level: error \ No newline at end of file
diff --git a/tests/plugins/static/configs/.rr-http-static-files-disable.yaml b/tests/plugins/static/configs/.rr-http-static-files-disable.yaml
new file mode 100644
index 00000000..563d95cf
--- /dev/null
+++ b/tests/plugins/static/configs/.rr-http-static-files-disable.yaml
@@ -0,0 +1,33 @@
+server:
+ command: "php ../../http/client.php echo pipes"
+ user: ""
+ group: ""
+ env:
+ "RR_HTTP": "true"
+ relay: "pipes"
+ relayTimeout: "20s"
+
+http:
+ debug: true
+ address: 127.0.0.1:45877
+ maxRequestSize: 1024
+ middleware: [ "gzip", "static" ]
+ trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ]
+ uploads:
+ forbid: [ ".php", ".exe", ".bat" ]
+ static:
+ dir: "../../../tests"
+ forbid: [ ".php" ]
+ request:
+ "Example-Request-Header": "Value"
+ # Automatically add headers to every response.
+ response:
+ "X-Powered-By": "RoadRunner"
+ pool:
+ numWorkers: 2
+ maxJobs: 0
+ allocateTimeout: 60s
+ destroyTimeout: 60s
+logs:
+ mode: development
+ level: error \ No newline at end of file
diff --git a/tests/plugins/static/configs/.rr-http-static-files.yaml b/tests/plugins/static/configs/.rr-http-static-files.yaml
new file mode 100644
index 00000000..8961c6f4
--- /dev/null
+++ b/tests/plugins/static/configs/.rr-http-static-files.yaml
@@ -0,0 +1,34 @@
+server:
+ command: "php ../../http/client.php echo pipes"
+ user: ""
+ group: ""
+ env:
+ "RR_HTTP": "true"
+ relay: "pipes"
+ relayTimeout: "20s"
+
+http:
+ debug: true
+ address: 127.0.0.1:34653
+ maxRequestSize: 1024
+ middleware: [ "gzip", "static" ]
+ trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ]
+ uploads:
+ forbid: [ ".php", ".exe", ".bat" ]
+ static:
+ dir: "../../../tests"
+ forbid: [ ".php", ".htaccess" ]
+ always: [ ".ico" ]
+ request:
+ "Example-Request-Header": "Value"
+ # Automatically add headers to every response.
+ response:
+ "X-Powered-By": "RoadRunner"
+ pool:
+ numWorkers: 2
+ maxJobs: 0
+ allocateTimeout: 60s
+ destroyTimeout: 60s
+logs:
+ mode: development
+ level: error \ No newline at end of file
diff --git a/tests/plugins/static/configs/.rr-http-static.yaml b/tests/plugins/static/configs/.rr-http-static.yaml
new file mode 100644
index 00000000..0a1f5df4
--- /dev/null
+++ b/tests/plugins/static/configs/.rr-http-static.yaml
@@ -0,0 +1,31 @@
+server:
+ command: "php ../../http/client.php pid pipes"
+ user: ""
+ group: ""
+ env:
+ "RR_HTTP": "true"
+ relay: "pipes"
+ relayTimeout: "20s"
+
+http:
+ address: 127.0.0.1:21603
+ maxRequestSize: 1024
+ middleware: [ "gzip", "static" ]
+ trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ]
+ uploads:
+ forbid: [ ".php", ".exe", ".bat" ]
+ static:
+ dir: "../../../tests"
+ forbid: [ "" ]
+ request:
+ "input": "custom-header"
+ response:
+ "output": "output-header"
+ pool:
+ numWorkers: 2
+ maxJobs: 0
+ allocateTimeout: 60s
+ destroyTimeout: 60s
+logs:
+ mode: development
+ level: error \ No newline at end of file
diff --git a/tests/plugins/static/static_plugin_test.go b/tests/plugins/static/static_plugin_test.go
new file mode 100644
index 00000000..74daaa16
--- /dev/null
+++ b/tests/plugins/static/static_plugin_test.go
@@ -0,0 +1,437 @@
+package static
+
+import (
+ "bytes"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "os/signal"
+ "sync"
+ "syscall"
+ "testing"
+ "time"
+
+ "github.com/golang/mock/gomock"
+ "github.com/spiral/endure"
+ "github.com/spiral/roadrunner/v2/plugins/config"
+ "github.com/spiral/roadrunner/v2/plugins/gzip"
+ httpPlugin "github.com/spiral/roadrunner/v2/plugins/http"
+ "github.com/spiral/roadrunner/v2/plugins/logger"
+ "github.com/spiral/roadrunner/v2/plugins/server"
+ "github.com/spiral/roadrunner/v2/plugins/static"
+ "github.com/spiral/roadrunner/v2/tests/mocks"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestStaticPlugin(t *testing.T) {
+ cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel))
+ assert.NoError(t, err)
+
+ cfg := &config.Viper{
+ Path: "configs/.rr-http-static.yaml",
+ Prefix: "rr",
+ }
+
+ err = cont.RegisterAll(
+ cfg,
+ &logger.ZapLogger{},
+ &server.Plugin{},
+ &httpPlugin.Plugin{},
+ &gzip.Gzip{},
+ &static.Plugin{},
+ )
+ assert.NoError(t, err)
+
+ err = cont.Init()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ch, err := cont.Serve()
+ assert.NoError(t, err)
+
+ sig := make(chan os.Signal, 1)
+ signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
+
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
+
+ stopCh := make(chan struct{}, 1)
+
+ go func() {
+ defer wg.Done()
+ for {
+ select {
+ case e := <-ch:
+ assert.Fail(t, "error", e.Error.Error())
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ case <-sig:
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ case <-stopCh:
+ // timeout
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ }
+ }
+ }()
+
+ time.Sleep(time.Second)
+ t.Run("ServeSample", serveStaticSample)
+ t.Run("StaticNotForbid", staticNotForbid)
+ t.Run("StaticHeaders", staticHeaders)
+
+ stopCh <- struct{}{}
+ wg.Wait()
+}
+
+func staticHeaders(t *testing.T) {
+ req, err := http.NewRequest("GET", "http://localhost:21603/client.php", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if resp.Header.Get("Output") != "output-header" {
+ t.Fatal("can't find output header in response")
+ }
+
+ b, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ defer func() {
+ _ = resp.Body.Close()
+ }()
+
+ assert.Equal(t, all("../../../tests/client.php"), string(b))
+ assert.Equal(t, all("../../../tests/client.php"), string(b))
+}
+
+func staticNotForbid(t *testing.T) {
+ b, r, err := get("http://localhost:21603/client.php")
+ assert.NoError(t, err)
+ assert.Equal(t, all("../../../tests/client.php"), b)
+ assert.Equal(t, all("../../../tests/client.php"), b)
+ _ = r.Body.Close()
+}
+
+func serveStaticSample(t *testing.T) {
+ b, r, err := get("http://localhost:21603/sample.txt")
+ assert.NoError(t, err)
+ assert.Equal(t, "sample", b)
+ _ = r.Body.Close()
+}
+
+func TestStaticDisabled(t *testing.T) {
+ cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel))
+ assert.NoError(t, err)
+
+ cfg := &config.Viper{
+ Path: "configs/.rr-http-static-disabled.yaml",
+ Prefix: "rr",
+ }
+
+ err = cont.RegisterAll(
+ cfg,
+ &logger.ZapLogger{},
+ &server.Plugin{},
+ &httpPlugin.Plugin{},
+ &gzip.Gzip{},
+ &static.Plugin{},
+ )
+ assert.NoError(t, err)
+
+ err = cont.Init()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ch, err := cont.Serve()
+ assert.NoError(t, err)
+
+ sig := make(chan os.Signal, 1)
+ signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
+
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
+
+ stopCh := make(chan struct{}, 1)
+
+ go func() {
+ defer wg.Done()
+ for {
+ select {
+ case e := <-ch:
+ assert.Fail(t, "error", e.Error.Error())
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ case <-sig:
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ case <-stopCh:
+ // timeout
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ }
+ }
+ }()
+
+ time.Sleep(time.Second)
+ t.Run("StaticDisabled", staticDisabled)
+
+ stopCh <- struct{}{}
+ wg.Wait()
+}
+
+func staticDisabled(t *testing.T) {
+ _, r, err := get("http://localhost:21234/sample.txt") //nolint:bodyclose
+ assert.Error(t, err)
+ assert.Nil(t, r)
+}
+
+func TestStaticFilesDisabled(t *testing.T) {
+ cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel))
+ assert.NoError(t, err)
+
+ cfg := &config.Viper{
+ Path: "configs/.rr-http-static-files-disable.yaml",
+ Prefix: "rr",
+ }
+
+ err = cont.RegisterAll(
+ cfg,
+ &logger.ZapLogger{},
+ &server.Plugin{},
+ &httpPlugin.Plugin{},
+ &gzip.Gzip{},
+ &static.Plugin{},
+ )
+ assert.NoError(t, err)
+
+ err = cont.Init()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ch, err := cont.Serve()
+ assert.NoError(t, err)
+
+ sig := make(chan os.Signal, 1)
+ signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
+
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
+
+ stopCh := make(chan struct{}, 1)
+
+ go func() {
+ defer wg.Done()
+ for {
+ select {
+ case e := <-ch:
+ assert.Fail(t, "error", e.Error.Error())
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ case <-sig:
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ case <-stopCh:
+ // timeout
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ }
+ }
+ }()
+
+ time.Sleep(time.Second)
+ t.Run("StaticFilesDisabled", staticFilesDisabled)
+
+ stopCh <- struct{}{}
+ wg.Wait()
+}
+
+func staticFilesDisabled(t *testing.T) {
+ b, r, err := get("http://localhost:45877/client.php?hello=world")
+ if err != nil {
+ t.Fatal(err)
+ }
+ assert.Equal(t, "WORLD", b)
+ _ = r.Body.Close()
+}
+
+func TestStaticFilesForbid(t *testing.T) {
+ cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel))
+ assert.NoError(t, err)
+
+ cfg := &config.Viper{
+ Path: "configs/.rr-http-static-files.yaml",
+ Prefix: "rr",
+ }
+
+ controller := gomock.NewController(t)
+ mockLogger := mocks.NewMockLogger(controller)
+
+ mockLogger.EXPECT().Info("worker constructed", "pid", gomock.Any()).AnyTimes()
+ mockLogger.EXPECT().Debug("http handler response received", "elapsed", gomock.Any(), "remote address", "127.0.0.1").AnyTimes()
+ mockLogger.EXPECT().Error("file open error", "error", gomock.Any()).AnyTimes()
+ mockLogger.EXPECT().Info(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() // placeholder for the workerlogerror
+
+ err = cont.RegisterAll(
+ cfg,
+ mockLogger,
+ &server.Plugin{},
+ &httpPlugin.Plugin{},
+ &gzip.Gzip{},
+ &static.Plugin{},
+ )
+ assert.NoError(t, err)
+
+ err = cont.Init()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ch, err := cont.Serve()
+ assert.NoError(t, err)
+
+ sig := make(chan os.Signal, 1)
+ signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
+
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
+
+ stopCh := make(chan struct{}, 1)
+
+ go func() {
+ defer wg.Done()
+ for {
+ select {
+ case e := <-ch:
+ assert.Fail(t, "error", e.Error.Error())
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ case <-sig:
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ case <-stopCh:
+ // timeout
+ err = cont.Stop()
+ if err != nil {
+ assert.FailNow(t, "error", err.Error())
+ }
+ return
+ }
+ }
+ }()
+
+ time.Sleep(time.Second)
+ t.Run("StaticTestFilesDir", staticTestFilesDir)
+ t.Run("StaticNotFound", staticNotFound)
+ t.Run("StaticFilesForbid", staticFilesForbid)
+ t.Run("StaticFilesAlways", staticFilesAlways)
+
+ stopCh <- struct{}{}
+ wg.Wait()
+}
+
+func staticTestFilesDir(t *testing.T) {
+ b, r, err := get("http://localhost:34653/http?hello=world")
+ assert.NoError(t, err)
+ assert.Equal(t, "WORLD", b)
+ _ = r.Body.Close()
+}
+
+func staticNotFound(t *testing.T) {
+ b, _, _ := get("http://localhost:34653/client.XXX?hello=world") //nolint:bodyclose
+ assert.Equal(t, "WORLD", b)
+}
+
+func staticFilesAlways(t *testing.T) {
+ _, r, err := get("http://localhost:34653/favicon.ico")
+ assert.NoError(t, err)
+ assert.Equal(t, 404, r.StatusCode)
+ _ = r.Body.Close()
+}
+
+func staticFilesForbid(t *testing.T) {
+ b, r, err := get("http://localhost:34653/client.php?hello=world")
+ if err != nil {
+ t.Fatal(err)
+ }
+ assert.Equal(t, "WORLD", b)
+ _ = r.Body.Close()
+}
+
+// HELPERS
+func get(url string) (string, *http.Response, error) {
+ r, err := http.Get(url) //nolint:gosec
+ if err != nil {
+ return "", nil, err
+ }
+
+ b, err := ioutil.ReadAll(r.Body)
+ if err != nil {
+ return "", nil, err
+ }
+
+ err = r.Body.Close()
+ if err != nil {
+ return "", nil, err
+ }
+
+ return string(b), r, err
+}
+
+func all(fn string) string {
+ f, _ := os.Open(fn)
+
+ b := new(bytes.Buffer)
+ _, err := io.Copy(b, f)
+ if err != nil {
+ return ""
+ }
+
+ err = f.Close()
+ if err != nil {
+ return ""
+ }
+
+ return b.String()
+}