diff --git a/.github/workflows/binaries.yml b/.github/workflows/binaries.yml index 62fd30c8..1fa916c8 100644 --- a/.github/workflows/binaries.yml +++ b/.github/workflows/binaries.yml @@ -51,7 +51,7 @@ jobs: needs: - draft-release env: - X_GO_DISTRIBUTION: "https://go.dev/dl/go1.23.8.linux-amd64.tar.gz" + X_GO_DISTRIBUTION: "https://go.dev/dl/go1.23.10.linux-amd64.tar.gz" APIFIREWALL_NAMESPACE: "github.com/wallarm/api-firewall" strategy: matrix: @@ -162,7 +162,7 @@ jobs: needs: - draft-release env: - X_GO_VERSION: "1.23.8" + X_GO_VERSION: "1.23.10" APIFIREWALL_NAMESPACE: "github.com/wallarm/api-firewall" strategy: matrix: @@ -272,19 +272,19 @@ jobs: include: - arch: armv6 distro: bookworm - go_distribution: https://go.dev/dl/go1.23.8.linux-armv6l.tar.gz + go_distribution: https://go.dev/dl/go1.23.10.linux-armv6l.tar.gz artifact: armv6-libc - arch: aarch64 distro: bookworm - go_distribution: https://go.dev/dl/go1.23.8.linux-arm64.tar.gz + go_distribution: https://go.dev/dl/go1.23.10.linux-arm64.tar.gz artifact: arm64-libc - arch: armv6 distro: alpine_latest - go_distribution: https://go.dev/dl/go1.23.8.linux-armv6l.tar.gz + go_distribution: https://go.dev/dl/go1.23.10.linux-armv6l.tar.gz artifact: armv6-musl - arch: aarch64 distro: alpine_latest - go_distribution: https://go.dev/dl/go1.23.8.linux-arm64.tar.gz + go_distribution: https://go.dev/dl/go1.23.10.linux-arm64.tar.gz artifact: arm64-musl steps: - uses: actions/checkout@v4 diff --git a/Makefile b/Makefile index 3631eb90..3eebd2f5 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -VERSION := 0.9.1 +VERSION := 0.9.2 NAMESPACE := github.com/wallarm/api-firewall .DEFAULT_GOAL := build @@ -43,7 +43,7 @@ stop_k6_tests: run_k6_tests: stop_k6_tests @docker compose -f resources/test/docker-compose-api-mode.yml up --build --detach --force-recreate - docker run --rm -i --network host grafana/k6 run --vus 100 --iterations 1200 -v - 0 { @@ -228,6 +256,9 @@ func Run(logger zerolog.Logger) error { case err := <-updSpecErrors: return errors.Wrap(err, "regular updater error") + case err := <-metricsErrors: + return errors.Wrap(err, "metrics error") + case sig := <-shutdown: logger.Info().Msgf("%s: %v: Start shutdown", logPrefix, sig) diff --git a/cmd/api-firewall/internal/handlers/api/updater.go b/cmd/api-firewall/internal/handlers/api/updater.go index 114379c0..c252351c 100644 --- a/cmd/api-firewall/internal/handlers/api/updater.go +++ b/cmd/api-firewall/internal/handlers/api/updater.go @@ -1,6 +1,7 @@ package api import ( + "github.com/wallarm/api-firewall/internal/platform/metrics" "os" "runtime/debug" "sync" @@ -35,10 +36,11 @@ type Specification struct { health *Health lock *sync.RWMutex allowedIPCache *allowiplist.AllowedIPsType + metrics metrics.Metrics } // NewHandlerUpdater function defines configuration updater controller -func NewHandlerUpdater(lock *sync.RWMutex, logger zerolog.Logger, sqlLiteStorage storage.DBOpenAPILoader, cfg *config.APIMode, api *fasthttp.Server, shutdown chan os.Signal, health *Health, allowedIPCache *allowiplist.AllowedIPsType, waf coraza.WAF) updater.Updater { +func NewHandlerUpdater(lock *sync.RWMutex, logger zerolog.Logger, metrics metrics.Metrics, sqlLiteStorage storage.DBOpenAPILoader, cfg *config.APIMode, api *fasthttp.Server, shutdown chan os.Signal, health *Health, allowedIPCache *allowiplist.AllowedIPsType, waf coraza.WAF) updater.Updater { return &Specification{ logger: logger, waf: waf, @@ -51,6 +53,7 @@ func NewHandlerUpdater(lock *sync.RWMutex, logger zerolog.Logger, sqlLiteStorage health: health, lock: lock, allowedIPCache: allowedIPCache, + metrics: metrics, } } @@ -100,7 +103,7 @@ func (s *Specification) Run() { s.lock.Lock() s.sqlLiteStorage = newSpecDB - s.api.Handler = Handlers(s.lock, s.cfg, s.shutdown, s.logger, s.sqlLiteStorage, s.allowedIPCache, s.waf) + s.api.Handler = Handlers(s.lock, s.cfg, s.shutdown, s.logger, s.metrics, s.sqlLiteStorage, s.allowedIPCache, s.waf) s.health.OpenAPIDB = s.sqlLiteStorage if err := s.sqlLiteStorage.AfterLoad(s.cfg.PathToSpecDB); err != nil { s.logger.Error().Err(err).Msgf("%s: error in after specification loading function", logPrefix) diff --git a/cmd/api-firewall/tests/main_api_mode_bench_test.go b/cmd/api-firewall/tests/main_api_mode_bench_test.go index beb17ae1..cb19fc05 100644 --- a/cmd/api-firewall/tests/main_api_mode_bench_test.go +++ b/cmd/api-firewall/tests/main_api_mode_bench_test.go @@ -13,6 +13,7 @@ import ( "github.com/valyala/fasthttp" handlersAPI "github.com/wallarm/api-firewall/cmd/api-firewall/internal/handlers/api" + "github.com/wallarm/api-firewall/internal/platform/metrics" "github.com/wallarm/api-firewall/internal/platform/storage" "github.com/wallarm/api-firewall/internal/platform/web" ) @@ -35,7 +36,7 @@ func BenchmarkAPIModeBasic(b *testing.B) { shutdown := make(chan os.Signal, 1) signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM) - handler := handlersAPI.Handlers(&lock, &cfg, shutdown, logger, specStorage, nil, nil) + handler := handlersAPI.Handlers(&lock, &cfg, shutdown, logger, metrics.NewPrometheusMetrics(false), specStorage, nil, nil) p, err := json.Marshal(map[string]any{ "firstname": "test", diff --git a/cmd/api-firewall/tests/main_api_mode_test.go b/cmd/api-firewall/tests/main_api_mode_test.go index cd0824c1..436b0800 100644 --- a/cmd/api-firewall/tests/main_api_mode_test.go +++ b/cmd/api-firewall/tests/main_api_mode_test.go @@ -26,6 +26,7 @@ import ( handlersAPI "github.com/wallarm/api-firewall/cmd/api-firewall/internal/handlers/api" "github.com/wallarm/api-firewall/internal/config" + "github.com/wallarm/api-firewall/internal/platform/metrics" "github.com/wallarm/api-firewall/internal/platform/storage" "github.com/wallarm/api-firewall/internal/platform/web" "github.com/wallarm/api-firewall/pkg/APIMode/validator" @@ -747,7 +748,7 @@ func (s *APIModeServiceTests) testAPIRunBasic(t *testing.T) { func (s *APIModeServiceTests) testAPIModeSuccess(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) p, err := json.Marshal(map[string]any{ "firstname": "test", @@ -810,7 +811,7 @@ func (s *APIModeServiceTests) testAPIModeSuccess(t *testing.T) { func (s *APIModeServiceTests) testAPIModeMissedMultipleReqParams(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) p, err := json.Marshal(map[string]any{ "firstname": "test", @@ -904,7 +905,7 @@ func (s *APIModeServiceTests) testAPIModeMissedMultipleReqParams(t *testing.T) { func (s *APIModeServiceTests) testAPIModeSuccessEmptyPathParameter(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) req := fasthttp.AcquireRequest() req.SetRequestURI(fmt.Sprintf("/absolute-redirect/%d", rand.Uint32())) @@ -941,7 +942,7 @@ func (s *APIModeServiceTests) testAPIModeSuccessEmptyPathParameter(t *testing.T) func (s *APIModeServiceTests) testAPIModeSuccessMultipartStringParameter(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) req := fasthttp.AcquireRequest() req.SetRequestURI("/redirect-to") @@ -1009,7 +1010,7 @@ func (s *APIModeServiceTests) testAPIModeSuccessMultipartStringParameter(t *test func (s *APIModeServiceTests) testAPIModeOneSchemeMultipleIDs(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) // one schema p, err := json.Marshal(map[string]any{ @@ -1093,7 +1094,7 @@ func (s *APIModeServiceTests) testAPIModeOneSchemeMultipleIDs(t *testing.T) { func (s *APIModeServiceTests) testAPIModeTwoDifferentSchemesMultipleIDs(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) // one schema p, err := json.Marshal(map[string]any{ @@ -1200,7 +1201,7 @@ func (s *APIModeServiceTests) testAPIModeTwoDifferentSchemesMultipleIDs(t *testi func (s *APIModeServiceTests) testAPIModeTwoSchemesMultipleIDs(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) p, err := json.Marshal(map[string]any{ "firstname": "test", @@ -1308,7 +1309,7 @@ func (s *APIModeServiceTests) testAPIModeTwoSchemesMultipleIDs(t *testing.T) { func (s *APIModeServiceTests) testAPIModeJSONParseError(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) req := fasthttp.AcquireRequest() req.SetRequestURI("/test/signup") @@ -1332,7 +1333,7 @@ func (s *APIModeServiceTests) testAPIModeJSONParseError(t *testing.T) { func (s *APIModeServiceTests) testAPIModeInvalidCTParseError(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) p, err := json.Marshal(map[string]any{ "firstname": "test", @@ -1368,7 +1369,7 @@ func (s *APIModeServiceTests) testAPIModeInvalidCTParseError(t *testing.T) { func (s *APIModeServiceTests) testAPIModeCTNotInSpec(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) p, err := json.Marshal(map[string]any{ "firstname": "test", @@ -1404,7 +1405,7 @@ func (s *APIModeServiceTests) testAPIModeCTNotInSpec(t *testing.T) { func (s *APIModeServiceTests) testAPIModeEmptyBody(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) req := fasthttp.AcquireRequest() req.SetRequestURI("/test/signup") @@ -1428,7 +1429,7 @@ func (s *APIModeServiceTests) testAPIModeEmptyBody(t *testing.T) { func (s *APIModeServiceTests) testAPIModeNoXWallarmSchemaIDHeader(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) p, err := json.Marshal(map[string]any{ "firstname": "test", @@ -1530,7 +1531,7 @@ func (s *APIModeServiceTests) testAPIModeNoXWallarmSchemaIDHeader(t *testing.T) func (s *APIModeServiceTests) testAPIModeMethodAndPathNotFound(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) p, err := json.Marshal(map[string]any{ "firstname": "test", @@ -1621,7 +1622,7 @@ func (s *APIModeServiceTests) testAPIModeMethodAndPathNotFound(t *testing.T) { func (s *APIModeServiceTests) testAPIModeRequiredQueryParameterMissed(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) req := fasthttp.AcquireRequest() req.SetRequestURI("/test/query?id=" + uuid.New().String()) @@ -1657,7 +1658,7 @@ func (s *APIModeServiceTests) testAPIModeRequiredQueryParameterMissed(t *testing func (s *APIModeServiceTests) testAPIModeRequiredHeaderParameterMissed(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) xReqTestValue := uuid.New() @@ -1696,7 +1697,7 @@ func (s *APIModeServiceTests) testAPIModeRequiredHeaderParameterMissed(t *testin func (s *APIModeServiceTests) testAPIModeRequiredCookieParameterMissed(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) req := fasthttp.AcquireRequest() req.SetRequestURI("/test/cookies/request") @@ -1733,7 +1734,7 @@ func (s *APIModeServiceTests) testAPIModeRequiredCookieParameterMissed(t *testin func (s *APIModeServiceTests) testAPIModeRequiredBodyMissed(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) p, err := json.Marshal(map[string]any{ "status": uuid.New().String(), @@ -1784,7 +1785,7 @@ func (s *APIModeServiceTests) testAPIModeRequiredBodyMissed(t *testing.T) { func (s *APIModeServiceTests) testAPIModeRequiredBodyParameterMissed(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) p, err := json.Marshal(map[string]any{ "status": uuid.New().String(), @@ -1847,7 +1848,7 @@ func (s *APIModeServiceTests) testAPIModeRequiredBodyParameterMissed(t *testing. // Invalid parameters errors func (s *APIModeServiceTests) testAPIModeRequiredQueryParameterInvalidValue(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) req := fasthttp.AcquireRequest() req.SetRequestURI("/test/query?id=" + uuid.New().String()) @@ -1897,7 +1898,7 @@ func (s *APIModeServiceTests) testAPIModeRequiredQueryParameterInvalidValue(t *t func (s *APIModeServiceTests) testAPIModeRequiredHeaderParameterInvalidValue(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) xReqTestValue := uuid.New() @@ -1953,7 +1954,7 @@ func (s *APIModeServiceTests) testAPIModeRequiredHeaderParameterInvalidValue(t * func (s *APIModeServiceTests) testAPIModeRequiredCookieParameterInvalidValue(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) req := fasthttp.AcquireRequest() req.SetRequestURI("/test/cookies/request") @@ -2005,7 +2006,7 @@ func (s *APIModeServiceTests) testAPIModeRequiredCookieParameterInvalidValue(t * func (s *APIModeServiceTests) testAPIModeRequiredBodyParameterInvalidValue(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) p, err := json.Marshal(map[string]any{ "status": uuid.New().String(), @@ -2160,7 +2161,7 @@ func (s *APIModeServiceTests) testAPIModeRequiredBodyParameterInvalidValue(t *te // security requirements func (s *APIModeServiceTests) testAPIModeBasicAuthFailed(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) req := fasthttp.AcquireRequest() req.SetRequestURI("/test/security/basic") @@ -2229,7 +2230,7 @@ func (s *APIModeServiceTests) testAPIModeBasicAuthFailed(t *testing.T) { func (s *APIModeServiceTests) testAPIModeBearerTokenFailed(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) req := fasthttp.AcquireRequest() req.SetRequestURI("/test/security/bearer") @@ -2298,7 +2299,7 @@ func (s *APIModeServiceTests) testAPIModeBearerTokenFailed(t *testing.T) { func (s *APIModeServiceTests) testAPIModeAPITokenCookieFailed(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) req := fasthttp.AcquireRequest() req.SetRequestURI("/test/security/cookie") @@ -2367,7 +2368,7 @@ func (s *APIModeServiceTests) testAPIModeAPITokenCookieFailed(t *testing.T) { // unknown parameters func (s *APIModeServiceTests) testAPIModeUnknownParameterBodyJSON(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) p, err := json.Marshal(map[string]any{ "firstname": "test", @@ -2437,7 +2438,7 @@ func (s *APIModeServiceTests) testAPIModeUnknownParameterBodyJSON(t *testing.T) func (s *APIModeServiceTests) testAPIModeUnknownParameterBodyPost(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) req := fasthttp.AcquireRequest() req.SetRequestURI("/test/signup") @@ -2491,7 +2492,7 @@ func (s *APIModeServiceTests) testAPIModeUnknownParameterBodyPost(t *testing.T) func (s *APIModeServiceTests) testAPIModeUnknownParameterQuery(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) req := fasthttp.AcquireRequest() req.SetRequestURI("/test/query?uparam=test&id=" + uuid.New().String()) @@ -2530,7 +2531,7 @@ func (s *APIModeServiceTests) testAPIModeUnknownParameterQuery(t *testing.T) { func (s *APIModeServiceTests) testAPIModeUnknownParameterTextPlainCT(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) req := fasthttp.AcquireRequest() req.SetRequestURI("/test/plain") @@ -2554,7 +2555,7 @@ func (s *APIModeServiceTests) testAPIModeUnknownParameterTextPlainCT(t *testing. func (s *APIModeServiceTests) testAPIModeUnknownParameterInvalidCT(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) req := fasthttp.AcquireRequest() req.SetRequestURI("/test/unknownCT") @@ -2603,7 +2604,7 @@ func (s *APIModeServiceTests) testAPIModePassOptionsRequest(t *testing.T) { PassOptionsRequests: true, } - handler := handlersAPI.Handlers(s.lock, &cfgPassOptions, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfgPassOptions, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) req := fasthttp.AcquireRequest() req.SetRequestURI("/test/signup") @@ -2625,7 +2626,7 @@ func (s *APIModeServiceTests) testAPIModePassOptionsRequest(t *testing.T) { func (s *APIModeServiceTests) testAPIModeMultipartOptionalParams(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) req := fasthttp.AcquireRequest() req.SetRequestURI("/test/multipart") @@ -2692,7 +2693,7 @@ func (s *APIModeServiceTests) testAPIModeMultipartOptionalParams(t *testing.T) { func (s *APIModeServiceTests) testAPIModeInvalidRouteInRequest(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) p, err := json.Marshal(map[string]any{ "firstname": "test", @@ -2748,7 +2749,7 @@ func (s *APIModeServiceTests) testAPIModeInvalidRouteInRequest(t *testing.T) { func (s *APIModeServiceTests) testAPIModeInvalidRouteInRequestInMultipleSchemas(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) p, err := json.Marshal(map[string]any{ "firstname": "test", @@ -2806,7 +2807,7 @@ func (s *APIModeServiceTests) testAPIModeInvalidRouteInRequestInMultipleSchemas( func (s *APIModeServiceTests) testAPIModeAllMethods(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) // check all supported methods: GET POST PUT PATCH DELETE TRACE OPTIONS HEAD for _, m := range []string{"GET", "POST", "PUT", "PATCH", "DELETE", "TRACE", "OPTIONS", "HEAD"} { @@ -2848,7 +2849,7 @@ func (s *APIModeServiceTests) testAPIModeAllMethods(t *testing.T) { func (s *APIModeServiceTests) testConflictsInThePath(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) // check all related paths for _, path := range []string{"/path/testValue1", "/path/value1.php"} { @@ -2890,7 +2891,7 @@ func (s *APIModeServiceTests) testConflictsInThePath(t *testing.T) { func (s *APIModeServiceTests) testObjectInQuery(t *testing.T) { - handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &cfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) for _, path := range []string{"/query/paramsObject?f.0%5Bf%5D%5B0%5D=test"} { @@ -2940,7 +2941,7 @@ func (s *APIModeServiceTests) testAPIModeMissedMultipleReqParamsLimitedResponse( MaxErrorsInResponse: 1, } - handler := handlersAPI.Handlers(s.lock, &updatedCfg, s.shutdown, s.logger, s.dbSpec, nil, nil) + handler := handlersAPI.Handlers(s.lock, &updatedCfg, s.shutdown, s.logger, metrics.NewPrometheusMetrics(false), s.dbSpec, nil, nil) p, err := json.Marshal(map[string]any{ "firstname": "test", diff --git a/cmd/api-firewall/tests/updater_test.go b/cmd/api-firewall/tests/updater_test.go index ad6732eb..14298c73 100644 --- a/cmd/api-firewall/tests/updater_test.go +++ b/cmd/api-firewall/tests/updater_test.go @@ -15,6 +15,7 @@ import ( handlersAPI "github.com/wallarm/api-firewall/cmd/api-firewall/internal/handlers/api" "github.com/wallarm/api-firewall/internal/config" + "github.com/wallarm/api-firewall/internal/platform/metrics" "github.com/wallarm/api-firewall/internal/platform/storage" "github.com/wallarm/api-firewall/internal/platform/web" "github.com/wallarm/api-firewall/pkg/APIMode/validator" @@ -50,7 +51,7 @@ func TestUpdaterBasic(t *testing.T) { signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM) api := fasthttp.Server{} - api.Handler = handlersAPI.Handlers(&lock, &cfgUpdater, shutdown, logger, specStorage, nil, nil) + api.Handler = handlersAPI.Handlers(&lock, &cfgUpdater, shutdown, logger, metrics.NewPrometheusMetrics(false), specStorage, nil, nil) // invalid route in the old spec req := fasthttp.AcquireRequest() @@ -125,7 +126,7 @@ func TestUpdaterBasic(t *testing.T) { // start updater updSpecErrors := make(chan error, 1) health := handlersAPI.Health{} - updater := handlersAPI.NewHandlerUpdater(&lock, logger, specStorage, &cfgUpdater, &api, shutdown, &health, nil, nil) + updater := handlersAPI.NewHandlerUpdater(&lock, logger, metrics.NewPrometheusMetrics(false), specStorage, &cfgUpdater, &api, shutdown, &health, nil, nil) go func() { t.Logf("starting specification regular update process every %.0f seconds", cfgUpdater.SpecificationUpdatePeriod.Seconds()) updSpecErrors <- updater.Start() @@ -226,7 +227,7 @@ func TestUpdaterFromEmptyDB(t *testing.T) { signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM) api := fasthttp.Server{} - api.Handler = handlersAPI.Handlers(&lock, &cfgUpdater, shutdown, logger, specStorage, nil, nil) + api.Handler = handlersAPI.Handlers(&lock, &cfgUpdater, shutdown, logger, metrics.NewPrometheusMetrics(false), specStorage, nil, nil) health := handlersAPI.Health{} // invalid route in the old spec @@ -266,7 +267,7 @@ func TestUpdaterFromEmptyDB(t *testing.T) { // start updater updSpecErrors := make(chan error, 1) - updater := handlersAPI.NewHandlerUpdater(&lock, logger, specStorage, &cfgUpdater, &api, shutdown, &health, nil, nil) + updater := handlersAPI.NewHandlerUpdater(&lock, logger, metrics.NewPrometheusMetrics(false), specStorage, &cfgUpdater, &api, shutdown, &health, nil, nil) go func() { t.Logf("starting specification regular update process every %.0f seconds", cfgUpdater.SpecificationUpdatePeriod.Seconds()) updSpecErrors <- updater.Start() @@ -367,7 +368,7 @@ func TestUpdaterToEmptyDB(t *testing.T) { signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM) api := fasthttp.Server{} - api.Handler = handlersAPI.Handlers(&lock, &cfgUpdater, shutdown, logger, specStorage, nil, nil) + api.Handler = handlersAPI.Handlers(&lock, &cfgUpdater, shutdown, logger, metrics.NewPrometheusMetrics(false), specStorage, nil, nil) health := handlersAPI.Health{} // invalid route in the old spec @@ -450,7 +451,7 @@ func TestUpdaterToEmptyDB(t *testing.T) { // start updater updSpecErrors := make(chan error, 1) - updater := handlersAPI.NewHandlerUpdater(&lock, logger, specStorage, &cfgUpdaterEmpty, &api, shutdown, &health, nil, nil) + updater := handlersAPI.NewHandlerUpdater(&lock, logger, metrics.NewPrometheusMetrics(false), specStorage, &cfgUpdaterEmpty, &api, shutdown, &health, nil, nil) go func() { t.Logf("starting specification regular update process every %.0f seconds", cfgUpdater.SpecificationUpdatePeriod.Seconds()) updSpecErrors <- updater.Start() @@ -516,7 +517,7 @@ func TestUpdaterInvalidDBSchema(t *testing.T) { signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM) api := fasthttp.Server{} - api.Handler = handlersAPI.Handlers(&lock, &cfgUpdater, shutdown, logger, specStorage, nil, nil) + api.Handler = handlersAPI.Handlers(&lock, &cfgUpdater, shutdown, logger, metrics.NewPrometheusMetrics(false), specStorage, nil, nil) req := fasthttp.AcquireRequest() req.SetRequestURI("/test/new") @@ -560,7 +561,7 @@ func TestUpdaterInvalidDBFile(t *testing.T) { signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM) api := fasthttp.Server{} - api.Handler = handlersAPI.Handlers(&lock, &cfgUpdater, shutdown, logger, specStorage, nil, nil) + api.Handler = handlersAPI.Handlers(&lock, &cfgUpdater, shutdown, logger, metrics.NewPrometheusMetrics(false), specStorage, nil, nil) req := fasthttp.AcquireRequest() req.SetRequestURI("/test/new") @@ -604,7 +605,7 @@ func TestUpdaterToInvalidDB(t *testing.T) { signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM) api := fasthttp.Server{} - api.Handler = handlersAPI.Handlers(&lock, &cfgUpdater, shutdown, logger, specStorage, nil, nil) + api.Handler = handlersAPI.Handlers(&lock, &cfgUpdater, shutdown, logger, metrics.NewPrometheusMetrics(false), specStorage, nil, nil) health := handlersAPI.Health{} // invalid route in the old spec @@ -687,7 +688,7 @@ func TestUpdaterToInvalidDB(t *testing.T) { // start updater updSpecErrors := make(chan error, 1) - updater := handlersAPI.NewHandlerUpdater(&lock, logger, specStorage, &cfgUpdaterEmpty, &api, shutdown, &health, nil, nil) + updater := handlersAPI.NewHandlerUpdater(&lock, logger, metrics.NewPrometheusMetrics(false), specStorage, &cfgUpdaterEmpty, &api, shutdown, &health, nil, nil) go func() { t.Logf("starting specification regular update process every %.0f seconds", cfgUpdater.SpecificationUpdatePeriod.Seconds()) updSpecErrors <- updater.Start() @@ -753,7 +754,7 @@ func TestUpdaterFromInvalidDB(t *testing.T) { signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM) api := fasthttp.Server{} - api.Handler = handlersAPI.Handlers(&lock, &cfgUpdater, shutdown, logger, specStorage, nil, nil) + api.Handler = handlersAPI.Handlers(&lock, &cfgUpdater, shutdown, logger, metrics.NewPrometheusMetrics(false), specStorage, nil, nil) health := handlersAPI.Health{} // invalid route in the old spec @@ -783,7 +784,7 @@ func TestUpdaterFromInvalidDB(t *testing.T) { // start updater updSpecErrors := make(chan error, 1) - updater := handlersAPI.NewHandlerUpdater(&lock, logger, specStorage, &cfgUpdater, &api, shutdown, &health, nil, nil) + updater := handlersAPI.NewHandlerUpdater(&lock, logger, metrics.NewPrometheusMetrics(false), specStorage, &cfgUpdater, &api, shutdown, &health, nil, nil) go func() { t.Logf("starting specification regular update process every %.0f seconds", cfgUpdater.SpecificationUpdatePeriod.Seconds()) updSpecErrors <- updater.Start() @@ -884,7 +885,7 @@ func TestUpdaterToNotExistDB(t *testing.T) { signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM) api := fasthttp.Server{} - api.Handler = handlersAPI.Handlers(&lock, &cfgUpdater, shutdown, logger, specStorage, nil, nil) + api.Handler = handlersAPI.Handlers(&lock, &cfgUpdater, shutdown, logger, metrics.NewPrometheusMetrics(false), specStorage, nil, nil) health := handlersAPI.Health{} // invalid route in the old spec @@ -967,7 +968,7 @@ func TestUpdaterToNotExistDB(t *testing.T) { // start updater updSpecErrors := make(chan error, 1) - updater := handlersAPI.NewHandlerUpdater(&lock, logger, specStorage, &cfgUpdaterEmpty, &api, shutdown, &health, nil, nil) + updater := handlersAPI.NewHandlerUpdater(&lock, logger, metrics.NewPrometheusMetrics(false), specStorage, &cfgUpdaterEmpty, &api, shutdown, &health, nil, nil) go func() { t.Logf("starting specification regular update process every %.0f seconds", cfgUpdater.SpecificationUpdatePeriod.Seconds()) updSpecErrors <- updater.Start() @@ -1033,7 +1034,7 @@ func TestUpdaterFromNotExistDB(t *testing.T) { signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM) api := fasthttp.Server{} - api.Handler = handlersAPI.Handlers(&lock, &cfgUpdater, shutdown, logger, specStorage, nil, nil) + api.Handler = handlersAPI.Handlers(&lock, &cfgUpdater, shutdown, logger, metrics.NewPrometheusMetrics(false), specStorage, nil, nil) health := handlersAPI.Health{} // invalid route in the old spec @@ -1063,7 +1064,7 @@ func TestUpdaterFromNotExistDB(t *testing.T) { // start updater updSpecErrors := make(chan error, 1) - updater := handlersAPI.NewHandlerUpdater(&lock, logger, specStorage, &cfgUpdater, &api, shutdown, &health, nil, nil) + updater := handlersAPI.NewHandlerUpdater(&lock, logger, metrics.NewPrometheusMetrics(false), specStorage, &cfgUpdater, &api, shutdown, &health, nil, nil) go func() { t.Logf("starting specification regular update process every %.0f seconds", cfgUpdater.SpecificationUpdatePeriod.Seconds()) updSpecErrors <- updater.Start() diff --git a/cmd/api-firewall/tests/updater_v2_test.go b/cmd/api-firewall/tests/updater_v2_test.go index d5082925..18eeea58 100644 --- a/cmd/api-firewall/tests/updater_v2_test.go +++ b/cmd/api-firewall/tests/updater_v2_test.go @@ -17,6 +17,7 @@ import ( handlersAPI "github.com/wallarm/api-firewall/cmd/api-firewall/internal/handlers/api" "github.com/wallarm/api-firewall/internal/config" + "github.com/wallarm/api-firewall/internal/platform/metrics" "github.com/wallarm/api-firewall/internal/platform/storage" "github.com/wallarm/api-firewall/internal/platform/web" "github.com/wallarm/api-firewall/pkg/APIMode/validator" @@ -214,7 +215,7 @@ func TestLoadBasicV2(t *testing.T) { signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM) api := fasthttp.Server{} - api.Handler = handlersAPI.Handlers(&lock, &cfg, shutdown, logger, specStorage, nil, nil) + api.Handler = handlersAPI.Handlers(&lock, &cfg, shutdown, logger, metrics.NewPrometheusMetrics(false), specStorage, nil, nil) // invalid route in the old spec req := fasthttp.AcquireRequest() @@ -317,7 +318,7 @@ func TestUpdaterBasicV2(t *testing.T) { signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM) api := fasthttp.Server{} - api.Handler = handlersAPI.Handlers(&lock, &cfg, shutdown, logger, specStorage, nil, nil) + api.Handler = handlersAPI.Handlers(&lock, &cfg, shutdown, logger, metrics.NewPrometheusMetrics(false), specStorage, nil, nil) health := handlersAPI.Health{} // invalid route in the old spec @@ -392,7 +393,7 @@ func TestUpdaterBasicV2(t *testing.T) { // start updater updSpecErrors := make(chan error, 1) - updater := handlersAPI.NewHandlerUpdater(&lock, logger, specStorage, &cfgV2, &api, shutdown, &health, nil, nil) + updater := handlersAPI.NewHandlerUpdater(&lock, logger, metrics.NewPrometheusMetrics(false), specStorage, &cfgV2, &api, shutdown, &health, nil, nil) go func() { t.Logf("starting specification regular update process every %.0f seconds", cfg.SpecificationUpdatePeriod.Seconds()) updSpecErrors <- updater.Start() @@ -447,7 +448,7 @@ func TestUpdaterBasicV2(t *testing.T) { // start updater second time. updNewSpecErrors := make(chan error, 1) - updater = handlersAPI.NewHandlerUpdater(&lock, logger, specStorage, &cfgV2, &api, shutdown, &health, nil, nil) + updater = handlersAPI.NewHandlerUpdater(&lock, logger, metrics.NewPrometheusMetrics(false), specStorage, &cfgV2, &api, shutdown, &health, nil, nil) go func() { t.Logf("starting specification regular update process every %.0f seconds", cfg.SpecificationUpdatePeriod.Seconds()) updNewSpecErrors <- updater.Start() @@ -513,7 +514,7 @@ func TestUpdaterFromEmptyDBV2(t *testing.T) { signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM) api := fasthttp.Server{} - api.Handler = handlersAPI.Handlers(&lock, &cfg, shutdown, logger, specStorage, nil, nil) + api.Handler = handlersAPI.Handlers(&lock, &cfg, shutdown, logger, metrics.NewPrometheusMetrics(false), specStorage, nil, nil) health := handlersAPI.Health{} // invalid route in the old spec @@ -553,7 +554,7 @@ func TestUpdaterFromEmptyDBV2(t *testing.T) { // start updater updSpecErrors := make(chan error, 1) - updater := handlersAPI.NewHandlerUpdater(&lock, logger, specStorage, &cfgV2, &api, shutdown, &health, nil, nil) + updater := handlersAPI.NewHandlerUpdater(&lock, logger, metrics.NewPrometheusMetrics(false), specStorage, &cfgV2, &api, shutdown, &health, nil, nil) go func() { t.Logf("starting specification regular update process every %.0f seconds", cfg.SpecificationUpdatePeriod.Seconds()) updSpecErrors <- updater.Start() @@ -654,7 +655,7 @@ func TestUpdaterToEmptyDBV2(t *testing.T) { signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM) api := fasthttp.Server{} - api.Handler = handlersAPI.Handlers(&lock, &cfg, shutdown, logger, specStorage, nil, nil) + api.Handler = handlersAPI.Handlers(&lock, &cfg, shutdown, logger, metrics.NewPrometheusMetrics(false), specStorage, nil, nil) health := handlersAPI.Health{} // invalid route in the old spec @@ -737,7 +738,7 @@ func TestUpdaterToEmptyDBV2(t *testing.T) { // start updater updSpecErrors := make(chan error, 1) - updater := handlersAPI.NewHandlerUpdater(&lock, logger, specStorage, &cfgV2Empty, &api, shutdown, &health, nil, nil) + updater := handlersAPI.NewHandlerUpdater(&lock, logger, metrics.NewPrometheusMetrics(false), specStorage, &cfgV2Empty, &api, shutdown, &health, nil, nil) go func() { t.Logf("starting specification regular update process every %.0f seconds", cfg.SpecificationUpdatePeriod.Seconds()) updSpecErrors <- updater.Start() @@ -803,7 +804,7 @@ func TestUpdaterInvalidDBSchemaV2(t *testing.T) { signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM) api := fasthttp.Server{} - api.Handler = handlersAPI.Handlers(&lock, &cfg, shutdown, logger, specStorage, nil, nil) + api.Handler = handlersAPI.Handlers(&lock, &cfg, shutdown, logger, metrics.NewPrometheusMetrics(false), specStorage, nil, nil) req := fasthttp.AcquireRequest() req.SetRequestURI("/") @@ -847,7 +848,7 @@ func TestUpdaterToInvalidDBV2(t *testing.T) { signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM) api := fasthttp.Server{} - api.Handler = handlersAPI.Handlers(&lock, &cfg, shutdown, logger, specStorage, nil, nil) + api.Handler = handlersAPI.Handlers(&lock, &cfg, shutdown, logger, metrics.NewPrometheusMetrics(false), specStorage, nil, nil) health := handlersAPI.Health{} // invalid route in the old spec @@ -930,7 +931,7 @@ func TestUpdaterToInvalidDBV2(t *testing.T) { // start updater updSpecErrors := make(chan error, 1) - updater := handlersAPI.NewHandlerUpdater(&lock, logger, specStorage, &cfgV2Invalid, &api, shutdown, &health, nil, nil) + updater := handlersAPI.NewHandlerUpdater(&lock, logger, metrics.NewPrometheusMetrics(false), specStorage, &cfgV2Invalid, &api, shutdown, &health, nil, nil) go func() { t.Logf("starting specification regular update process every %.0f seconds", cfg.SpecificationUpdatePeriod.Seconds()) updSpecErrors <- updater.Start() @@ -996,7 +997,7 @@ func TestUpdaterFromInvalidDBV2(t *testing.T) { signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM) api := fasthttp.Server{} - api.Handler = handlersAPI.Handlers(&lock, &cfg, shutdown, logger, specStorage, nil, nil) + api.Handler = handlersAPI.Handlers(&lock, &cfg, shutdown, logger, metrics.NewPrometheusMetrics(false), specStorage, nil, nil) health := handlersAPI.Health{} // invalid route in the old spec @@ -1026,7 +1027,7 @@ func TestUpdaterFromInvalidDBV2(t *testing.T) { // start updater updSpecErrors := make(chan error, 1) - updater := handlersAPI.NewHandlerUpdater(&lock, logger, specStorage, &cfgV2, &api, shutdown, &health, nil, nil) + updater := handlersAPI.NewHandlerUpdater(&lock, logger, metrics.NewPrometheusMetrics(false), specStorage, &cfgV2, &api, shutdown, &health, nil, nil) go func() { t.Logf("starting specification regular update process every %.0f seconds", cfg.SpecificationUpdatePeriod.Seconds()) updSpecErrors <- updater.Start() @@ -1127,7 +1128,7 @@ func TestUpdaterFromV1DBToV2(t *testing.T) { signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM) api := fasthttp.Server{} - api.Handler = handlersAPI.Handlers(&lock, &cfg, shutdown, logger, specStorage, nil, nil) + api.Handler = handlersAPI.Handlers(&lock, &cfg, shutdown, logger, metrics.NewPrometheusMetrics(false), specStorage, nil, nil) health := handlersAPI.Health{} // invalid route in the old spec @@ -1216,7 +1217,7 @@ func TestUpdaterFromV1DBToV2(t *testing.T) { // start updater updSpecErrors := make(chan error, 1) - updater := handlersAPI.NewHandlerUpdater(&lock, logger, specStorage, &cfgV2, &api, shutdown, &health, nil, nil) + updater := handlersAPI.NewHandlerUpdater(&lock, logger, metrics.NewPrometheusMetrics(false), specStorage, &cfgV2, &api, shutdown, &health, nil, nil) go func() { t.Logf("starting specification regular update process every %.0f seconds", cfg.SpecificationUpdatePeriod.Seconds()) updSpecErrors <- updater.Start() @@ -1317,7 +1318,7 @@ func TestUpdaterFromV2DBToV1(t *testing.T) { signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM) api := fasthttp.Server{} - api.Handler = handlersAPI.Handlers(&lock, &cfg, shutdown, logger, specStorage, nil, nil) + api.Handler = handlersAPI.Handlers(&lock, &cfg, shutdown, logger, metrics.NewPrometheusMetrics(false), specStorage, nil, nil) health := handlersAPI.Health{} // invalid route in the old spec @@ -1357,7 +1358,7 @@ func TestUpdaterFromV2DBToV1(t *testing.T) { // start updater updSpecErrors := make(chan error, 1) - updater := handlersAPI.NewHandlerUpdater(&lock, logger, specStorage, &cfg, &api, shutdown, &health, nil, nil) + updater := handlersAPI.NewHandlerUpdater(&lock, logger, metrics.NewPrometheusMetrics(false), specStorage, &cfg, &api, shutdown, &health, nil, nil) go func() { t.Logf("starting specification regular update process every %.0f seconds", cfg.SpecificationUpdatePeriod.Seconds()) updSpecErrors <- updater.Start() diff --git a/cmd/api-firewall/tests/wallarm_api2_update.db b/cmd/api-firewall/tests/wallarm_api2_update.db index 0b1333e7..e26b2db1 100644 Binary files a/cmd/api-firewall/tests/wallarm_api2_update.db and b/cmd/api-firewall/tests/wallarm_api2_update.db differ diff --git a/demo/docker-compose/OWASP_CoreRuleSet/docker-compose.yml b/demo/docker-compose/OWASP_CoreRuleSet/docker-compose.yml index a94c49de..963ceab1 100644 --- a/demo/docker-compose/OWASP_CoreRuleSet/docker-compose.yml +++ b/demo/docker-compose/OWASP_CoreRuleSet/docker-compose.yml @@ -2,7 +2,7 @@ version: "3.8" services: api-firewall: container_name: api-firewall - image: wallarm/api-firewall:v0.9.1 + image: wallarm/api-firewall:v0.9.2 restart: on-failure environment: APIFW_URL: "http://0.0.0.0:8080" diff --git a/demo/docker-compose/docker-compose-api-mode.yml b/demo/docker-compose/docker-compose-api-mode.yml index 8f5c13eb..b224ad77 100644 --- a/demo/docker-compose/docker-compose-api-mode.yml +++ b/demo/docker-compose/docker-compose-api-mode.yml @@ -2,7 +2,7 @@ version: '3.8' services: api-firewall: container_name: api-firewall - image: wallarm/api-firewall:v0.9.1 + image: wallarm/api-firewall:v0.9.2 restart: on-failure environment: APIFW_MODE: "api" diff --git a/demo/docker-compose/docker-compose-graphql-mode.yml b/demo/docker-compose/docker-compose-graphql-mode.yml index af9d2682..2ed88828 100644 --- a/demo/docker-compose/docker-compose-graphql-mode.yml +++ b/demo/docker-compose/docker-compose-graphql-mode.yml @@ -2,7 +2,7 @@ version: '3.8' services: api-firewall: container_name: api-firewall - image: wallarm/api-firewall:v0.9.1 + image: wallarm/api-firewall:v0.9.2 restart: on-failure environment: APIFW_MODE: "graphql" diff --git a/demo/docker-compose/docker-compose.yml b/demo/docker-compose/docker-compose.yml index 01ae3f57..dcd95266 100644 --- a/demo/docker-compose/docker-compose.yml +++ b/demo/docker-compose/docker-compose.yml @@ -2,7 +2,7 @@ version: "3.8" services: api-firewall: container_name: api-firewall - image: wallarm/api-firewall:v0.9.1 + image: wallarm/api-firewall:v0.9.2 restart: on-failure environment: APIFW_URL: "http://0.0.0.0:8080" diff --git a/demo/interface/api-mode/main.go b/demo/interface/api-mode/main.go index 1ae78af2..bf6c89fa 100644 --- a/demo/interface/api-mode/main.go +++ b/demo/interface/api-mode/main.go @@ -12,9 +12,12 @@ import ( "time" "github.com/pkg/errors" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/rs/zerolog" strconv2 "github.com/savsgio/gotils/strconv" "github.com/valyala/fasthttp" + "github.com/valyala/fasthttp/fasthttpadaptor" "github.com/wallarm/api-firewall/demo/interface/api-mode/internal/updater" "github.com/wallarm/api-firewall/internal/config" @@ -25,7 +28,7 @@ const logMainPrefix = "Wallarm API-Firewall" var ( updateTime = 60 * time.Second - apiHost = "0.0.0.0:8080" + apiHost = "0.0.0.0:8282" ) func main() { @@ -39,7 +42,8 @@ func main() { signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM) apiFirewall, err := APIMode.NewAPIFirewall( - APIMode.WithPathToDB("./wallarm_apifw_test.db"), + APIMode.WithPathToDB("pkg/APIMode/wallarm_apifw_test.db"), + APIMode.EnablePrometheusMetrics(), ) if err != nil { logger.Err(err) @@ -88,6 +92,42 @@ func main() { ctx.Response.SetBody(response) } + // Start Metrics logging + metrics, err := apiFirewall.GetPrometheusCollectors() + if err != nil { + logger.Err(err).Msgf("%s: error getting prometheus metrics: %s", logMainPrefix, err) + } + + metricsErrors := make(chan error, 1) + + if metrics != nil { + reg := prometheus.NewRegistry() + reg.MustRegister(metrics...) + + fastPrometheusHandler := fasthttpadaptor.NewFastHTTPHandler(promhttp.HandlerFor(reg, promhttp.HandlerOpts{})) + metricsHandler := func(ctx *fasthttp.RequestCtx) { + switch string(ctx.Path()) { + case "/metrics": + fastPrometheusHandler(ctx) + return + default: + ctx.Error("Unsupported path", fasthttp.StatusNotFound) + } + } + + metricsAPI := fasthttp.Server{ + Handler: metricsHandler, + NoDefaultServerHeader: true, + Logger: &logger, + } + + // Start the service listening for requests. + go func() { + logger.Info().Msgf("%s:Prometheus Metrics: API listening on 0.0.0.0:9010/metrics", logMainPrefix) + metricsErrors <- metricsAPI.ListenAndServe("0.0.0.0:9010") + }() + } + // ========================================================================= // Init ZeroLogger @@ -129,6 +169,9 @@ func main() { case err := <-updSpecErrors: logger.Err(errors.Wrap(err, "regular updater error")) return + case err := <-metricsErrors: + logger.Err(errors.Wrap(err, "metrics error")) + return case sig := <-shutdown: logger.Info().Msgf("%s: %v: Start shutdown", logMainPrefix, sig) diff --git a/docs/configuration-guides/allowlist.md b/docs/configuration-guides/allowlist.md index 25adca12..10e43dc6 100644 --- a/docs/configuration-guides/allowlist.md +++ b/docs/configuration-guides/allowlist.md @@ -33,7 +33,7 @@ docker run --rm -it --network api-firewall-network --network-alias api-firewall -e APIFW_URL= -e APIFW_SERVER_URL= \ -e APIFW_REQUEST_VALIDATION= -e APIFW_RESPONSE_VALIDATION= \ -e APIFW_ALLOW_IP_FILE=/opt/ip-allowlist.txt -e APIFW_ALLOW_IP_HEADER_NAME="X-Real-IP" \ - -p 8088:8088 wallarm/api-firewall:v0.9.1 + -p 8088:8088 wallarm/api-firewall:v0.9.2 ``` | Environment variable | Description | diff --git a/docs/installation-guides/api-mode.md b/docs/installation-guides/api-mode.md index 633b69b9..0e72c031 100644 --- a/docs/installation-guides/api-mode.md +++ b/docs/installation-guides/api-mode.md @@ -38,7 +38,7 @@ Use the following command to run the API Firewall container: ``` docker run --rm -it -v :/var/lib/wallarm-api/1/wallarm_api.db \ - -e APIFW_MODE=API -p 8282:8282 wallarm/api-firewall:v0.9.1 + -e APIFW_MODE=API -p 8282:8282 wallarm/api-firewall:v0.9.2 ``` You can pass to the container the following variables: diff --git a/docs/installation-guides/docker-container.md b/docs/installation-guides/docker-container.md index ee31ebd8..70ae5a0a 100644 --- a/docs/installation-guides/docker-container.md +++ b/docs/installation-guides/docker-container.md @@ -27,7 +27,7 @@ networks: services: api-firewall: container_name: api-firewall - image: wallarm/api-firewall:v0.9.1 + image: wallarm/api-firewall:v0.9.2 restart: on-failure volumes: - : @@ -169,6 +169,6 @@ To start API Firewall on Docker, you can also use regular Docker commands as in -v : -e APIFW_API_SPECS= \ -e APIFW_URL= -e APIFW_SERVER_URL= \ -e APIFW_REQUEST_VALIDATION= -e APIFW_RESPONSE_VALIDATION= \ - -p 8088:8088 wallarm/api-firewall:v0.9.1 + -p 8088:8088 wallarm/api-firewall:v0.9.2 ``` 4. When the environment is started, test it and enable traffic on API Firewall following steps 6 and 7. diff --git a/docs/installation-guides/graphql/docker-container.md b/docs/installation-guides/graphql/docker-container.md index b0dfa3e8..cfd573f5 100644 --- a/docs/installation-guides/graphql/docker-container.md +++ b/docs/installation-guides/graphql/docker-container.md @@ -29,7 +29,7 @@ networks: services: api-firewall: container_name: api-firewall - image: wallarm/api-firewall:v0.9.1 + image: wallarm/api-firewall:v0.9.2 restart: on-failure volumes: - : @@ -200,6 +200,6 @@ To start API Firewall on Docker, you can also use regular Docker commands as in -e APIFW_GRAPHQL_MAX_QUERY_COMPLEXITY= \ -e APIFW_GRAPHQL_MAX_QUERY_DEPTH= -e APIFW_GRAPHQL_NODE_COUNT_LIMIT= \ -e APIFW_GRAPHQL_INTROSPECTION= \ - -p 8088:8088 wallarm/api-firewall:v0.9.1 + -p 8088:8088 wallarm/api-firewall:v0.9.2 ``` 4. When the environment is started, test it and enable traffic on API Firewall following steps 6 and 7. diff --git a/docs/release-notes.md b/docs/release-notes.md index 5240124e..e94f89d6 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,12 @@ This page describes new releases of Wallarm API Firewall. +## v0.9.2 (2025-06-06) + +* Added Prometheus metrics support in the `API` mode +* Added support of the env vars in the API-Firewall pkg +* Dependency upgrade + ## v0.9.1 (2025-04-23) * Added the `APIFW_API_MODE_MAX_ERRORS_IN_RESPONSE` [environment variable to limit the number of returned validation errors](installation-guides/api-mode.md#running-the-api-firewall-container) in the `API` mode diff --git a/go.mod b/go.mod index f747c057..08ee76a4 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/wallarm/api-firewall go 1.23.0 -toolchain go1.23.8 +toolchain go1.23.10 require ( github.com/andybalholm/brotli v1.1.1 @@ -22,14 +22,15 @@ require ( github.com/klauspost/compress v1.18.0 github.com/mattn/go-sqlite3 v1.14.28 github.com/pkg/errors v0.9.1 + github.com/prometheus/client_golang v1.22.0 github.com/rs/zerolog v1.34.0 github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287 github.com/spf13/viper v1.20.1 github.com/stretchr/testify v1.10.0 - github.com/valyala/fasthttp v1.61.0 + github.com/valyala/fasthttp v1.62.0 github.com/valyala/fastjson v1.6.4 github.com/wundergraph/graphql-go-tools v1.67.4 - golang.org/x/sync v0.13.0 + golang.org/x/sync v0.15.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -37,6 +38,7 @@ require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/sprig v2.22.0+incompatible // indirect + github.com/beorn7/perks v1.0.1 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/corazawaf/libinjection-go v0.2.2 // indirect @@ -66,6 +68,7 @@ require ( github.com/mitchellh/copystructure v1.0.0 // indirect github.com/mitchellh/reflectwalk v1.0.0 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nats-io/nats.go v1.34.1 // indirect github.com/nats-io/nkeys v0.4.7 // indirect github.com/nats-io/nuid v1.0.1 // indirect @@ -75,6 +78,9 @@ require ( github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/r3labs/sse/v2 v2.10.0 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect @@ -92,14 +98,14 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.37.0 // indirect + golang.org/x/crypto v0.38.0 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect golang.org/x/mod v0.24.0 // indirect - golang.org/x/net v0.39.0 // indirect - golang.org/x/sys v0.32.0 // indirect - golang.org/x/text v0.24.0 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect golang.org/x/tools v0.31.0 // indirect - google.golang.org/protobuf v1.36.1 // indirect + google.golang.org/protobuf v1.36.5 // indirect gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect gopkg.in/go-playground/assert.v1 v1.2.1 // indirect nhooyr.io/websocket v1.8.17 // indirect diff --git a/go.sum b/go.sum index fcfe8d51..b8f31c80 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7X github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/ardanlabs/conf v1.5.0 h1:5TwP6Wu9Xi07eLFEpiCUF3oQXh9UzHMDVnD3u/I5d5c= github.com/ardanlabs/conf v1.5.0/go.mod h1:ILsMo9dMqYzCxDjDXTiwMI0IgxOJd0MOiucbQY2wlJw= +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/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -73,8 +75,8 @@ github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVI github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -118,6 +120,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/logrusorgru/aurora/v3 v3.0.0 h1:R6zcoZZbvVcGMvDCKo45A9U/lzYyzl5NfYIvznmDfE4= @@ -145,6 +149,8 @@ github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/I github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/nats-io/nats.go v1.34.1 h1:syWey5xaNHZgicYBemv0nohUPPmaLteiBEUT6Q5+F/4= github.com/nats-io/nats.go v1.34.1/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= @@ -167,6 +173,14 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/r3labs/sse/v2 v2.10.0 h1:hFEkLLFY4LDifoHdiCN/LlGBAdVJYsANaLqNYa1l/v0= github.com/r3labs/sse/v2 v2.10.0/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -226,8 +240,8 @@ github.com/valllabh/ocsf-schema-golang v1.0.3 h1:eR8k/3jP/OOqB8LRCtdJ4U+vlgd/gk5 github.com/valllabh/ocsf-schema-golang v1.0.3/go.mod h1:sZ3as9xqm1SSK5feFWIR2CuGeGRhsM7TR1MbpBctzPk= 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.61.0 h1:VV08V0AfoRaFurP1EWKvQQdPTZHiUzaVoulX1aBDgzU= -github.com/valyala/fasthttp v1.61.0/go.mod h1:wRIV/4cMwUPWnRcDno9hGnYZGh78QzODFfo1LTUhBog= +github.com/valyala/fasthttp v1.62.0 h1:8dKRBX/y2rCzyc6903Zu1+3qN0H/d2MsxPPmVNamiH0= +github.com/valyala/fasthttp v1.62.0/go.mod h1:FCINgr4GKdKqV8Q0xv8b+UxPV+H/O5nNFo3D+r54Htg= github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/vektah/gqlparser/v2 v2.5.11 h1:JJxLtXIoN7+3x6MBdtIP59TP1RANnY7pXOaDnADQSf8= @@ -260,8 +274,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -285,8 +299,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -294,8 +308,8 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= -golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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= @@ -313,8 +327,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -330,8 +344,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= @@ -348,8 +362,8 @@ golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ 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-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= -google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y= gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/helm/api-firewall/Chart.yaml b/helm/api-firewall/Chart.yaml index bdf2daa7..36daed69 100644 --- a/helm/api-firewall/Chart.yaml +++ b/helm/api-firewall/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v1 name: api-firewall version: 0.7.2 -appVersion: 0.9.1 +appVersion: 0.9.2 description: Wallarm OpenAPI-based API Firewall home: https://github.com/wallarm/api-firewall icon: https://static.wallarm.com/wallarm-logo.svg diff --git a/internal/config/api.go b/internal/config/api.go index 5fa0fca1..1f3f6c88 100644 --- a/internal/config/api.go +++ b/internal/config/api.go @@ -6,6 +6,7 @@ type APIMode struct { APIFWInit APIFWServer ModSecurity + Metrics Metrics AllowIP AllowIP TLS TLS diff --git a/internal/config/metrics.go b/internal/config/metrics.go new file mode 100644 index 00000000..2495f116 --- /dev/null +++ b/internal/config/metrics.go @@ -0,0 +1,11 @@ +package config + +import "time" + +type Metrics struct { + EndpointName string `conf:"default:metrics,env:METRICS_ENDPOINT_NAME" validate:"required,url"` + Host string `conf:"default:0.0.0.0:9010,env:METRICS_HOST" validate:"required"` + Enabled bool `conf:"default:false,env:METRICS_ENABLED"` + ReadTimeout time.Duration `conf:"default:5s"` + WriteTimeout time.Duration `conf:"default:5s"` +} diff --git a/internal/platform/metrics/metrics.go b/internal/platform/metrics/metrics.go new file mode 100644 index 00000000..a10c5a84 --- /dev/null +++ b/internal/platform/metrics/metrics.go @@ -0,0 +1,11 @@ +package metrics + +import ( + "time" +) + +type Metrics interface { + IncErrorTypeCounter(err string, schemaID int) + IncHTTPRequestStat(start time.Time, schemaID int, statusCode int) + IncHTTPRequestTotalCountOnly(schemaID int, statusCode int) +} diff --git a/internal/platform/metrics/prometheus.go b/internal/platform/metrics/prometheus.go new file mode 100644 index 00000000..fcfb4124 --- /dev/null +++ b/internal/platform/metrics/prometheus.go @@ -0,0 +1,155 @@ +package metrics + +import ( + "errors" + "fmt" + "net/url" + strconv2 "strconv" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/rs/zerolog" + "github.com/valyala/fasthttp" + "github.com/valyala/fasthttp/fasthttpadaptor" + + "github.com/wallarm/api-firewall/internal/config" +) + +const logMetricsPrefix = "Prometheus metrics" + +var ( + // Counter: Total number of errors + TotalErrors = prometheus.NewCounter( + prometheus.CounterOpts{ + Name: "wallarm_apifw_service_errors_total", + Help: "Total number of errors occurred in the APIFW service.", + }) + + // Counter: Errors by types + ErrorTypeCounter = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "wallarm_apifw_service_errors_by_type", + Help: "Total number of errors by type and endpoint.", + }, + []string{"error_type", "schema_id"}, + ) + + // Counter: Total number of HTTP requests + HttpRequestsTotal = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "wallarm_apifw_http_requests_total", + Help: "Total number of HTTP requests", + }, + []string{"schema_id", "status_code"}, + ) + + // Histogram: HTTP request duration + HttpRequestDuration = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "wallarm_apifw_http_request_duration_seconds", + Help: "HTTP request duration in seconds", + Buckets: []float64{.001, .005, .025, .05, .25, .5, 1, 2.5, 5}, + }, + []string{"schema_id"}, + ) +) + +type Options struct { + EndpointName string + Host string + ReadTimeout time.Duration + WriteTimeout time.Duration +} + +type PrometheusMetrics struct { + logger *zerolog.Logger + serviceOpts *Options + enabled bool + registry *prometheus.Registry +} + +var _ Metrics = (*PrometheusMetrics)(nil) + +func NewPrometheusMetrics(enabled bool) *PrometheusMetrics { + return &PrometheusMetrics{enabled: enabled} +} + +func (p *PrometheusMetrics) StartService(logger *zerolog.Logger, options *Options) error { + + p.logger = logger + p.serviceOpts = options + + if p.logger == nil { + return fmt.Errorf("%s: logger not initialized", logMetricsPrefix) + } + + if err := p.initializeMetrics(); err != nil { + return err + } + + endpointName, err := url.JoinPath("/", p.serviceOpts.EndpointName) + if err != nil { + return err + } + + // Prometheus service handler + fastPrometheusHandler := fasthttpadaptor.NewFastHTTPHandler(promhttp.HandlerFor(p.registry, promhttp.HandlerOpts{})) + metricsHandler := func(ctx *fasthttp.RequestCtx) { + switch string(ctx.Path()) { + case endpointName: + fastPrometheusHandler(ctx) + return + default: + ctx.Error("Unsupported path", fasthttp.StatusNotFound) + } + } + + metricsAPI := fasthttp.Server{ + Handler: metricsHandler, + ReadTimeout: p.serviceOpts.ReadTimeout, + WriteTimeout: p.serviceOpts.WriteTimeout, + NoDefaultServerHeader: true, + Logger: &config.ZerologAdapter{Logger: *p.logger}, + } + + return metricsAPI.ListenAndServe(p.serviceOpts.Host) +} + +// initializeMetrics initializes prometheus registry and registers metrics +func (p *PrometheusMetrics) initializeMetrics() error { + if p.registry == nil { + p.registry = prometheus.NewRegistry() + p.registry.MustRegister(TotalErrors, ErrorTypeCounter, HttpRequestsTotal, HttpRequestDuration) + + return nil + } + + return errors.New("registry not initialized") +} + +func (p *PrometheusMetrics) IncErrorTypeCounter(err string, schemaID int) { + if !p.enabled { + return + } + + TotalErrors.Add(1) + ErrorTypeCounter.WithLabelValues(err, strconv2.Itoa(schemaID)).Inc() +} + +func (p *PrometheusMetrics) IncHTTPRequestStat(start time.Time, schemaID int, statusCode int) { + if !p.enabled { + return + } + + HttpRequestDuration.WithLabelValues(strconv2.Itoa(schemaID)).Observe(time.Since(start).Seconds()) + HttpRequestsTotal.WithLabelValues(strconv2.Itoa(schemaID), strconv2.Itoa(statusCode)).Inc() +} + +func (p *PrometheusMetrics) IncHTTPRequestTotalCountOnly(schemaID int, statusCode int) { + if !p.enabled { + return + } + + HttpRequestsTotal.WithLabelValues(strconv2.Itoa(schemaID), strconv2.Itoa(statusCode)).Inc() +} diff --git a/internal/platform/validator/api_mode_validate_request.go b/internal/platform/validator/api_mode_validate_request.go index 1876b5d8..c2c8e6bf 100644 --- a/internal/platform/validator/api_mode_validate_request.go +++ b/internal/platform/validator/api_mode_validate_request.go @@ -14,6 +14,7 @@ import ( "github.com/valyala/fasthttp" "github.com/valyala/fasthttp/fasthttpadaptor" "github.com/valyala/fastjson" + "github.com/wallarm/api-firewall/internal/platform/metrics" "github.com/wallarm/api-firewall/internal/platform/loader" "github.com/wallarm/api-firewall/internal/platform/router" @@ -75,7 +76,7 @@ var apiModeSecurityRequirementsOptions = &openapi3filter.Options{ } // APIModeValidateRequest validates request and respond with 200, 403 (with error) or 500 status code -func APIModeValidateRequest(ctx *fasthttp.RequestCtx, jsonParserPool *fastjson.ParserPool, openAPI *loader.CustomRoute, unknownParametersDetection bool) (validationErrs []*validator.ValidationError, err error) { +func APIModeValidateRequest(ctx *fasthttp.RequestCtx, metrics metrics.Metrics, schemaID int, jsonParserPool *fastjson.ParserPool, openAPI *loader.CustomRoute, unknownParametersDetection bool) (validationErrs []*validator.ValidationError, err error) { // handle panic defer func() { @@ -85,6 +86,7 @@ func APIModeValidateRequest(ctx *fasthttp.RequestCtx, jsonParserPool *fastjson.P case error: err = e default: + metrics.IncErrorTypeCounter("request processing error", schemaID) err = fmt.Errorf("panic: %v", r) } @@ -102,7 +104,8 @@ func APIModeValidateRequest(ctx *fasthttp.RequestCtx, jsonParserPool *fastjson.P // Convert fasthttp request to net/http request req := http.Request{} if err := fasthttpadaptor.ConvertRequest(ctx, &req, false); err != nil { - return nil, errors.Wrap(err, "request conversion error") + metrics.IncErrorTypeCounter("request context error", schemaID) + return nil, errors.Wrap(err, "request context error") } // Decode request body @@ -110,6 +113,7 @@ func APIModeValidateRequest(ctx *fasthttp.RequestCtx, jsonParserPool *fastjson.P if requestContentEncoding != "" { var err error if req.Body, err = web.GetDecompressedRequestBody(&ctx.Request, requestContentEncoding); err != nil { + metrics.IncErrorTypeCounter("request body decompression error", schemaID) return nil, errors.Wrap(err, "request body decompression error") } } @@ -180,7 +184,8 @@ func APIModeValidateRequest(ctx *fasthttp.RequestCtx, jsonParserPool *fastjson.P // Parse validator error and build the response parsedValErrs, unknownErr := GetErrorResponse(currentErr) if unknownErr != nil { - return nil, errors.Wrap(unknownErr, "validator response parsing error") + metrics.IncErrorTypeCounter("request body parsing error", schemaID) + return nil, errors.Wrap(unknownErr, "request body decode error: unsupported content type") } if len(parsedValErrs) > 0 { @@ -192,7 +197,8 @@ func APIModeValidateRequest(ctx *fasthttp.RequestCtx, jsonParserPool *fastjson.P // Parse validator error and build the response parsedValErrs, unknownErr := GetErrorResponse(valErr) if unknownErr != nil { - return nil, errors.Wrap(unknownErr, "validator response parsing error") + metrics.IncErrorTypeCounter("request body parsing error", schemaID) + return nil, errors.Wrap(unknownErr, "request body decode error: unsupported content type") } if parsedValErrs != nil { respErrors = append(respErrors, parsedValErrs...) @@ -207,6 +213,7 @@ func APIModeValidateRequest(ctx *fasthttp.RequestCtx, jsonParserPool *fastjson.P // If it is a parsing error then it already handled by the request validator var parseError *ParseError if !errors.As(valUPReqErrors, &parseError) { + metrics.IncErrorTypeCounter("unknown parameter detection error", schemaID) return nil, errors.Wrap(valUPReqErrors, "unknown parameter detection error") } } diff --git a/pkg/APIMode/apifw.go b/pkg/APIMode/apifw.go index 0758855e..3c4e7167 100644 --- a/pkg/APIMode/apifw.go +++ b/pkg/APIMode/apifw.go @@ -4,12 +4,20 @@ import ( "bufio" "errors" "fmt" + "os" "sync" + "time" + "github.com/ardanlabs/conf" "github.com/valyala/fasthttp" "github.com/valyala/fastjson" + + "github.com/prometheus/client_golang/prometheus" + "github.com/wallarm/api-firewall/internal/config" + "github.com/wallarm/api-firewall/internal/platform/metrics" "github.com/wallarm/api-firewall/internal/platform/router" "github.com/wallarm/api-firewall/internal/platform/storage" + "github.com/wallarm/api-firewall/internal/version" "github.com/wallarm/api-firewall/pkg/APIMode/validator" ) @@ -17,12 +25,14 @@ type APIFirewall interface { ValidateRequestFromReader(schemaIDs []int, r *bufio.Reader) (*validator.ValidationResponse, error) ValidateRequest(schemaIDs []int, uri, method, body []byte, headers map[string][]string) (*validator.ValidationResponse, error) UpdateSpecsStorage() ([]int, bool, error) + GetPrometheusCollectors() ([]prometheus.Collector, error) } type APIFWModeAPI struct { routers map[int]*router.Mux specsStorage storage.DBOpenAPILoader parserPool *fastjson.ParserPool + metrics metrics.Metrics lock *sync.RWMutex options *Configuration } @@ -35,6 +45,7 @@ type Configuration struct { UnknownParametersDetection bool PassOptionsRequests bool MaxErrorsInResponse int + MetricsEnabled bool } type Option func(*Configuration) @@ -67,6 +78,13 @@ func DisableUnknownParameters() Option { } } +// EnablePassOptionsRequests is a functional option to enable requests with method OPTIONS +func EnablePassOptionsRequests() Option { + return func(c *Configuration) { + c.PassOptionsRequests = true + } +} + // DisablePassOptionsRequests is a functional option to disable requests with method OPTIONS func DisablePassOptionsRequests() Option { return func(c *Configuration) { @@ -74,6 +92,13 @@ func DisablePassOptionsRequests() Option { } } +// EnablePrometheusMetrics is a functional option to enable prometheus metrics +func EnablePrometheusMetrics() Option { + return func(c *Configuration) { + c.MetricsEnabled = true + } +} + func NewAPIFirewall(options ...Option) (APIFirewall, error) { // db usage lock @@ -89,11 +114,28 @@ func NewAPIFirewall(options ...Option) (APIFirewall, error) { PathToSpecDB: "", DBVersion: 0, UnknownParametersDetection: true, - PassOptionsRequests: true, + PassOptionsRequests: false, MaxErrorsInResponse: 0, + MetricsEnabled: false, }, } + var cfg config.APIMode + cfg.Version.SVN = version.Version + cfg.Version.Desc = version.ProjectName + + if err := conf.Parse(os.Args[1:], version.Namespace, &cfg); err != nil { + return nil, fmt.Errorf("parsing config: %w", err) + } + + // apply env var params + apiMode.options.PassOptionsRequests = cfg.PassOptionsRequests + apiMode.options.MaxErrorsInResponse = cfg.MaxErrorsInResponse + apiMode.options.UnknownParametersDetection = cfg.UnknownParametersDetection + apiMode.options.PathToSpecDB = cfg.PathToSpecDB + apiMode.options.DBVersion = cfg.DBVersion + apiMode.options.MetricsEnabled = cfg.Metrics.Enabled + // apply all the functional options for _, opt := range options { opt(apiMode.options) @@ -110,13 +152,16 @@ func NewAPIFirewall(options ...Option) (APIFirewall, error) { apiMode.specsStorage = specsStorage // init routers - routers, errRouters := getRouters(apiMode.specsStorage, &parserPool, apiMode.options) + routers, errRouters := getRouters(apiMode.specsStorage, &parserPool, apiMode.metrics, apiMode.options) if errRouters != nil { err = errors.Join(err, fmt.Errorf("%w: %w", validator.ErrHandlersInit, errRouters)) } apiMode.routers = routers + // init metrics + apiMode.metrics = metrics.NewPrometheusMetrics(apiMode.options.MetricsEnabled) + return &apiMode, err } @@ -140,7 +185,7 @@ func (a *APIFWModeAPI) UpdateSpecsStorage() ([]int, bool, error) { a.lock.Lock() defer a.lock.Unlock() - routers, err := getRouters(newSpecDB, a.parserPool, a.options) + routers, err := getRouters(newSpecDB, a.parserPool, a.metrics, a.options) if err != nil { return a.specsStorage.SchemaIDs(), isUpdated, fmt.Errorf("%w: %w", validator.ErrHandlersInit, err) } @@ -167,6 +212,9 @@ func (a *APIFWModeAPI) ValidateRequest(schemaIDs []int, uri, method, body []byte var wg sync.WaitGroup var m sync.Mutex + // Request handling start time + start := time.Now() + for _, schemaID := range schemaIDs { // build fasthttp RequestCTX @@ -186,8 +234,9 @@ func (a *APIFWModeAPI) ValidateRequest(schemaIDs []int, uri, method, body []byte go func(ctx *fasthttp.RequestCtx, sID int) { defer wg.Done() + defer a.metrics.IncHTTPRequestStat(start, schemaID, ctx.Response.StatusCode()) - pReqResp, pReqErrs := validator.ProcessRequest(sID, ctx, a.routers, a.lock, a.options.PassOptionsRequests, a.options.MaxErrorsInResponse) + pReqResp, pReqErrs := validator.ProcessRequest(sID, ctx, a.metrics, a.routers, a.lock, a.options.PassOptionsRequests, a.options.MaxErrorsInResponse) m.Lock() defer m.Unlock() @@ -223,6 +272,9 @@ func (a *APIFWModeAPI) ValidateRequestFromReader(schemaIDs []int, r *bufio.Reade var wg sync.WaitGroup var m sync.Mutex + // Request handling start time + start := time.Now() + for _, schemaID := range schemaIDs { // build fasthttp RequestCTX @@ -236,6 +288,8 @@ func (a *APIFWModeAPI) ValidateRequestFromReader(schemaIDs []int, r *bufio.Reade respErr = fmt.Errorf("%w; %w: %w", respErr, validator.ErrRequestParsing, err) + a.metrics.IncErrorTypeCounter("request context error", schemaID) + continue } @@ -243,8 +297,9 @@ func (a *APIFWModeAPI) ValidateRequestFromReader(schemaIDs []int, r *bufio.Reade go func(sID int) { defer wg.Done() + defer a.metrics.IncHTTPRequestStat(start, schemaID, ctx.Response.StatusCode()) - pReqResp, pReqErrs := validator.ProcessRequest(sID, ctx, a.routers, a.lock, a.options.PassOptionsRequests, a.options.MaxErrorsInResponse) + pReqResp, pReqErrs := validator.ProcessRequest(sID, ctx, a.metrics, a.routers, a.lock, a.options.PassOptionsRequests, a.options.MaxErrorsInResponse) m.Lock() defer m.Unlock() @@ -269,3 +324,18 @@ func (a *APIFWModeAPI) ValidateRequestFromReader(schemaIDs []int, r *bufio.Reade return &resp, respErr } + +// GetPrometheusCollectors returns Prometheus collectors for external registration and monitoring +func (a *APIFWModeAPI) GetPrometheusCollectors() ([]prometheus.Collector, error) { + + if !a.options.MetricsEnabled { + return nil, errors.New("metrics disabled") + } + + return []prometheus.Collector{ + metrics.ErrorTypeCounter, + metrics.TotalErrors, + metrics.HttpRequestsTotal, + metrics.HttpRequestDuration, + }, nil +} diff --git a/pkg/APIMode/apifw_test.go b/pkg/APIMode/apifw_test.go index 2dca0407..9502161b 100644 --- a/pkg/APIMode/apifw_test.go +++ b/pkg/APIMode/apifw_test.go @@ -299,6 +299,7 @@ func TestAPIFWBasic(t *testing.T) { apifw, err := NewAPIFirewall( WithPathToDB("./wallarm_apifw_test.db"), + EnablePassOptionsRequests(), ) if err != nil { t.Fatal(err) @@ -356,7 +357,6 @@ func TestAPIFWBasicUpdate(t *testing.T) { apifw, err := NewAPIFirewall( WithPathToDB(tmpFileName), DisableUnknownParameters(), - DisablePassOptionsRequests(), ) if err != nil { t.Fatal(err) @@ -419,7 +419,6 @@ func TestAPIFWBasicErrors(t *testing.T) { _, err := NewAPIFirewall( WithPathToDB("./wallarm_apifw_invalid_db_schema.db"), DisableUnknownParameters(), - DisablePassOptionsRequests(), ) if !errors.Is(err, validator.ErrSpecLoading) { t.Errorf("expected ErrSpecLoading but got %v", err) @@ -429,7 +428,6 @@ func TestAPIFWBasicErrors(t *testing.T) { _, err = NewAPIFirewall( WithPathToDB("./wallarm_apifw_invalid_spec.db"), DisableUnknownParameters(), - DisablePassOptionsRequests(), ) if !errors.Is(err, validator.ErrSpecParsing) { t.Errorf("expected ErrSpecParsing but got %v", err) @@ -439,7 +437,6 @@ func TestAPIFWBasicErrors(t *testing.T) { apifw, err := NewAPIFirewall( WithPathToDB("./wallarm_apifw_test.db"), DisableUnknownParameters(), - DisablePassOptionsRequests(), ) if err != nil { t.Fatal(err) @@ -475,7 +472,6 @@ func TestAPIFWBasicErrors(t *testing.T) { _, err = NewAPIFirewall( WithPathToDB("./wallarm_apifw_spec_validation_failed.db"), DisableUnknownParameters(), - DisablePassOptionsRequests(), ) if !errors.Is(err, validator.ErrSpecValidation) { t.Errorf("expected ErrSpecValidation but got %v", err) @@ -489,7 +485,6 @@ func TestSafeCounterThreadSafety(t *testing.T) { apifw, err := NewAPIFirewall( WithPathToDB("./wallarm_apifw_test.db"), - DisablePassOptionsRequests(), ) if err != nil { t.Fatal(err) diff --git a/pkg/APIMode/handler.go b/pkg/APIMode/handler.go index cb11d36d..b242c7e9 100644 --- a/pkg/APIMode/handler.go +++ b/pkg/APIMode/handler.go @@ -6,7 +6,9 @@ import ( "github.com/valyala/fasthttp" "github.com/valyala/fastjson" + "github.com/wallarm/api-firewall/internal/platform/loader" + "github.com/wallarm/api-firewall/internal/platform/metrics" apiMode "github.com/wallarm/api-firewall/internal/platform/validator" "github.com/wallarm/api-firewall/pkg/APIMode/validator" ) @@ -15,6 +17,7 @@ type RequestValidator struct { CustomRoute *loader.CustomRoute OpenAPIRouter *loader.Router ParserPool *fastjson.ParserPool + Metrics metrics.Metrics SchemaID int Options *Configuration } @@ -47,7 +50,7 @@ func (rv *RequestValidator) APIModeHandler(ctx *fasthttp.RequestCtx) (err error) return nil } - validationErrors, err := apiMode.APIModeValidateRequest(ctx, rv.ParserPool, rv.CustomRoute, rv.Options.UnknownParametersDetection) + validationErrors, err := apiMode.APIModeValidateRequest(ctx, rv.Metrics, rv.SchemaID, rv.ParserPool, rv.CustomRoute, rv.Options.UnknownParametersDetection) if err != nil { return err } diff --git a/pkg/APIMode/helpers.go b/pkg/APIMode/helpers.go index 39ba101e..90c8b176 100644 --- a/pkg/APIMode/helpers.go +++ b/pkg/APIMode/helpers.go @@ -8,6 +8,7 @@ import ( "github.com/valyala/fastjson" "github.com/wallarm/api-firewall/internal/platform/loader" + "github.com/wallarm/api-firewall/internal/platform/metrics" "github.com/wallarm/api-firewall/internal/platform/router" "github.com/wallarm/api-firewall/internal/platform/storage" "github.com/wallarm/api-firewall/pkg/APIMode/validator" @@ -27,7 +28,7 @@ func wrapOASpecErrs(err error) error { } // getRouters function prepares router.Mux with the routes from OpenAPI specs -func getRouters(specStorage storage.DBOpenAPILoader, parserPool *fastjson.ParserPool, options *Configuration) (map[int]*router.Mux, error) { +func getRouters(specStorage storage.DBOpenAPILoader, parserPool *fastjson.ParserPool, metrics metrics.Metrics, options *Configuration) (map[int]*router.Mux, error) { // Init routers routers := make(map[int]*router.Mux) @@ -66,6 +67,7 @@ func getRouters(specStorage storage.DBOpenAPILoader, parserPool *fastjson.Parser ParserPool: parserPool, OpenAPIRouter: newSwagRouter, SchemaID: schemaID, + Metrics: metrics, Options: options, } updRoutePathEsc, err := url.JoinPath(serverURL.Path, newSwagRouter.Routes[i].Path) diff --git a/pkg/APIMode/validator/validator.go b/pkg/APIMode/validator/validator.go index 6add4c75..9a70886f 100644 --- a/pkg/APIMode/validator/validator.go +++ b/pkg/APIMode/validator/validator.go @@ -7,6 +7,8 @@ import ( "github.com/savsgio/gotils/strconv" "github.com/valyala/fasthttp" + + "github.com/wallarm/api-firewall/internal/platform/metrics" "github.com/wallarm/api-firewall/internal/platform/router" ) @@ -21,7 +23,7 @@ var ( StatusInternalServerError int = fasthttp.StatusInternalServerError ) -func ProcessRequest(schemaID int, ctx *fasthttp.RequestCtx, routers map[int]*router.Mux, lock *sync.RWMutex, passOptionsRequests bool, maxErrorsInResponse int) (resp *ValidationResponse, err error) { +func ProcessRequest(schemaID int, ctx *fasthttp.RequestCtx, metrics metrics.Metrics, routers map[int]*router.Mux, lock *sync.RWMutex, passOptionsRequests bool, maxErrorsInResponse int) (resp *ValidationResponse, err error) { // handle panic defer func() { @@ -31,6 +33,9 @@ func ProcessRequest(schemaID int, ctx *fasthttp.RequestCtx, routers map[int]*rou case error: err = e default: + + metrics.IncErrorTypeCounter("request processing error", schemaID) + err = fmt.Errorf("%w: panic: %v", ErrRequestParsing, r) } @@ -51,6 +56,9 @@ func ProcessRequest(schemaID int, ctx *fasthttp.RequestCtx, routers map[int]*rou rctx := router.NewRouteContext() handler, err := find(routers, lock, rctx, schemaID, strconv.B2S(ctx.Method()), strconv.B2S(ctx.Request.URI().Path())) if err != nil { + + metrics.IncErrorTypeCounter("schema not found", schemaID) + return &ValidationResponse{ Summary: []*ValidationResponseSummary{ { @@ -91,6 +99,7 @@ func ProcessRequest(schemaID int, ctx *fasthttp.RequestCtx, routers map[int]*rou ctx.SetUserValue(router.RouteCtxKey, rctx) if err := handler(ctx); err != nil { + return &ValidationResponse{ Summary: []*ValidationResponseSummary{ { @@ -106,6 +115,9 @@ func ProcessRequest(schemaID int, ctx *fasthttp.RequestCtx, routers map[int]*rou statusCode, ok := ctx.UserValue(strconv2.Itoa(schemaID) + APIModePostfixStatusCode).(int) if !ok { + + metrics.IncErrorTypeCounter("request processing error", schemaID) + // Didn't receive the response code. It means that the router respond to the request because it was not valid. // The API Firewall should respond by 500 status code in this case. return &ValidationResponse{ diff --git a/resources/test/docker-compose-api-mode.yml b/resources/test/docker-compose-api-mode.yml index b6e7b831..1384a6c0 100644 --- a/resources/test/docker-compose-api-mode.yml +++ b/resources/test/docker-compose-api-mode.yml @@ -2,7 +2,7 @@ version: '3.8' services: api-firewall: container_name: api-firewall - image: wallarm/api-firewall:v0.9.1 + image: wallarm/api-firewall:v0.9.2 build: context: ../../ dockerfile: Dockerfile