diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 4047684..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,2 +0,0 @@ -github: [ lukaszraczylo ] -custom: [ monzo.me/lukaszraczylo ] \ No newline at end of file diff --git a/.github/workflows/autoupdate.yaml b/.github/workflows/autoupdate.yaml deleted file mode 100644 index 100300d..0000000 --- a/.github/workflows/autoupdate.yaml +++ /dev/null @@ -1,73 +0,0 @@ -name: Autoupdate go.mod and go.sum - -on: - workflow_dispatch: - schedule: - - cron: "0 3 * * *" - -env: - GO_VERSION: ">=1.21" - -jobs: - # This job is responsible for preparation of the build - # environment variables. - prepare: - name: Preparing build context - runs-on: ubuntu-latest - - steps: - - name: Checkout repo - uses: actions/checkout@v4 - - - name: Install Go - uses: actions/setup-go@v5 - id: cache - with: - go-version: ${{env.GO_VERSION}} - cache-dependency-path: "**/*.sum" - - - name: Go get dependencies - if: steps.cache.outputs.cache-hit != 'true' - run: | - go get ./... - - # This job is responsible for running tests and linting the codebase - test: - name: "Unit testing" - runs-on: ubuntu-latest - container: golang:1 - needs: [prepare] - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Ensure full history is checked out - token: ${{ secrets.GHCR_TOKEN }} - - - name: Install Go - uses: actions/setup-go@v5 - with: - go-version: ${{env.GO_VERSION}} - cache-dependency-path: "**/*.sum" - - - name: Install dependencies - run: | - apt-get update - apt-get install ca-certificates make -y - update-ca-certificates - go mod tidy - go get -u -v ./... - go mod tidy -v - - - name: Run unit tests - run: | - CI_RUN=${CI} make test - git config --global --add safe.directory /__w/graphql-monitoring-proxy/graphql-monitoring-proxy - - - name: Commit changes - uses: stefanzweifel/git-auto-commit-action@v5 - with: - commit_message: "Update go.mod and go.sum" - commit_options: "--no-verify --signoff" - file_pattern: "go.mod go.sum" diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml deleted file mode 100644 index 41e36e6..0000000 --- a/.github/workflows/pr.yaml +++ /dev/null @@ -1,109 +0,0 @@ -name: Run tests on PR - -on: - pull_request: - branches: - - "main" - push: - paths-ignore: - - "**/**.md" - - "**/**.yaml" - - "static/**" - branches: - - "!main" - -env: - GO_VERSION: ">=1.21" - -permissions: - # deployments permission to deploy GitHub pages website - deployments: write - # contents permission to update benchmark contents in gh-pages branch - contents: write - -jobs: - # This job is responsible for preparation of the build - # environment variables. - prepare: - name: Preparing build context - runs-on: ubuntu-latest - - steps: - - name: Checkout repo - uses: actions/checkout@v4 - - - name: Install Go - uses: actions/setup-go@v5 - id: cache - with: - go-version: ${{env.GO_VERSION}} - cache-dependency-path: "**/*.sum" - - - name: Go get dependencies - if: steps.cache.outputs.cache-hit != 'true' - run: | - go get ./... - - # This job is responsible for running tests and linting the codebase - test: - name: "Unit testing" - # needs: [prepare] - runs-on: ubuntu-latest - container: golang:1 - # container: github/super-linter:v4 - needs: [prepare] - - # services: - # # Label used to access the service container - # redis: - # # Docker Hub image - # image: redis - # # Set health checks to wait until redis has started - # options: >- - # --health-cmd "redis-cli ping" - # --health-interval 10s - # --health-timeout 5s - # --health-retries 5 - # ports: - # # Maps the container port to the host machine - # - 6379:6379 - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Install Go - uses: actions/setup-go@v5 - with: - go-version: ${{env.GO_VERSION}} - cache-dependency-path: "**/*.sum" - - - name: Install dependencies - run: | - apt-get update - apt-get install ca-certificates make -y - update-ca-certificates - go mod tidy - git config --global --add safe.directory "$GITHUB_WORKSPACE" - - - name: Run unit tests - run: | - CI_RUN=${CI} make test - - - name: Run benchmark - run: | - go test -bench=. -benchmem ./... -run=^# | tee output.txt - - - name: Store benchmark result - uses: benchmark-action/github-action-benchmark@v1 - with: - tool: "go" - output-file-path: output.txt - fail-on-alert: true - github-token: ${{ secrets.GITHUB_TOKEN }} - comment-on-alert: true - summary-always: true - # auto-push only if it's on main branch - auto-push: false - gh-pages-branch: "gh-pages" - benchmark-data-dir-path: "docs" diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml deleted file mode 100644 index 8f60a9e..0000000 --- a/.github/workflows/test.yaml +++ /dev/null @@ -1,72 +0,0 @@ -name: Test and release - -on: - workflow_dispatch: - push: - paths-ignore: - - "**/**.md" - - "**/**.yaml" - - "static/**" - branches: - - "main" - -env: - GO_VERSION: ">=1.21" - -permissions: - # deployments permission to deploy GitHub pages website - deployments: write - # contents permission to update benchmark contents in gh-pages branch - contents: write - -jobs: - shared: - uses: telegram-bot-app/ci-scripts/.github/workflows/build-test-publish-inject.yaml@main - with: - enable-code-scans: false - should-deploy: false - secrets: - ghcr-token: ${{ secrets.GHCR_TOKEN }} - - test: - name: "Benchmarking the results" - needs: [shared] - runs-on: ubuntu-latest - container: golang:1 - # container: github/super-linter:v4 - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Install Go - uses: actions/setup-go@v5 - with: - go-version: ${{env.GO_VERSION}} - cache-dependency-path: "**/*.sum" - - - name: Install dependencies - run: | - apt-get update - apt-get install ca-certificates make -y - update-ca-certificates - go mod tidy - git config --global --add safe.directory "$GITHUB_WORKSPACE" - - - name: Run benchmark - run: | - go test -bench=. -benchmem ./... -run=^# | tee output.txt - - - name: Store benchmark result - uses: benchmark-action/github-action-benchmark@v1 - with: - tool: "go" - output-file-path: output.txt - fail-on-alert: true - github-token: ${{ secrets.GITHUB_TOKEN }} - comment-on-alert: true - summary-always: true - # auto-push only if it's on main branch - auto-push: true - gh-pages-branch: "gh-pages" - benchmark-data-dir-path: "docs" diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index af12b50..0000000 --- a/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM gcr.io/distroless/base-debian12:nonroot -WORKDIR /go/src/app -ARG TARGETARCH -ARG TARGETOS -# silly workaround for distroless image as no chmod is available -COPY --chmod=777 --chown=nonroot:nonroot static/app /go/src/app -ADD dist/bot-$TARGETOS-$TARGETARCH /go/src/app/graphql-proxy -ENTRYPOINT ["/go/src/app/graphql-proxy"] diff --git a/LICENSE b/LICENSE deleted file mode 100644 index edfcb87..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2023 Lukasz Raczylo - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file diff --git a/Makefile b/Makefile deleted file mode 100644 index f1e4a66..0000000 --- a/Makefile +++ /dev/null @@ -1,34 +0,0 @@ -CI_RUN?=false -# ADDITIONAL_BUILD_FLAGS="" - -# ifeq ($(CI_RUN), true) -# ADDITIONAL_BUILD_FLAGS="-test.short" -# endif - -.PHONY: help -help: ## display this help - @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n\nTargets:\n"} /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST) - -.PHONY: run -run: build ## run application - @LOG_LEVEL=debug PURGE_METRICS_ON_CRAWL=true BLOCK_SCHEMA_INTROSPECTION=false CACHE_TTL=10 JWT_ROLE_RATE_LIMIT=false JWT_ROLE_CLAIM_PATH="Hasura.x-hasura-default-role" JWT_USER_CLAIM_PATH="Hasura.x-hasura-user-id" HOST_GRAPHQL=https://hasura8.lan/ HEALTHCHECK_GRAPHQL_URL=https://hasura8.lan/v1/graphql ./graphql-proxy - -.PHONY: build -build: ## build the binary - go build -o graphql-proxy *.go - -.PHONY: test -test: ## run tests on library - @LOG_LEVEL=info go test -v -cover -race ./... - -.PHONY: test-packages -test-packages: ## run tests on packages - @go test -v -cover ./pkg/... - -.PHONY: all -all: test-packages test - -.PHONY: update -update: ## update dependencies - @go get -u -v ./... - @go mod tidy -v diff --git a/api.go b/api.go deleted file mode 100644 index bb76d1d..0000000 --- a/api.go +++ /dev/null @@ -1,259 +0,0 @@ -package main - -import ( - "fmt" - "os" - "sync" - "time" - - "github.com/goccy/go-json" - fiber "github.com/gofiber/fiber/v2" - "github.com/gofrs/flock" - libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache" - libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config" - libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" -) - -var ( - bannedUsersIDs = make(map[string]string) - bannedUsersIDsMutex sync.RWMutex -) - -func enableApi() { - if !cfg.Server.EnableApi { - return - } - - apiserver := fiber.New(fiber.Config{ - DisableStartupMessage: true, - AppName: fmt.Sprintf("GraphQL Monitoring Proxy - %s v%s", libpack_config.PKG_NAME, libpack_config.PKG_VERSION), - }) - - api := apiserver.Group("/api") - api.Post("/user-ban", apiBanUser) - api.Post("/user-unban", apiUnbanUser) - api.Post("/cache-clear", apiClearCache) - api.Get("/cache-stats", apiCacheStats) - - go periodicallyReloadBannedUsers() - - if err := apiserver.Listen(fmt.Sprintf(":%d", cfg.Server.ApiPort)); err != nil { - cfg.Logger.Critical(&libpack_logger.LogMessage{ - Message: "Can't start the service", - Pairs: map[string]interface{}{"port": cfg.Server.ApiPort}, - }) - } -} - -func periodicallyReloadBannedUsers() { - ticker := time.NewTicker(10 * time.Second) - defer ticker.Stop() - - for range ticker.C { - loadBannedUsers() - cfg.Logger.Debug(&libpack_logger.LogMessage{ - Message: "Banned users reloaded", - Pairs: map[string]interface{}{"users": bannedUsersIDs}, - }) - } -} - -func checkIfUserIsBanned(c *fiber.Ctx, userID string) bool { - bannedUsersIDsMutex.RLock() - _, found := bannedUsersIDs[userID] - bannedUsersIDsMutex.RUnlock() - - cfg.Logger.Debug(&libpack_logger.LogMessage{ - Message: "Checking if user is banned", - Pairs: map[string]interface{}{"user_id": userID, "banned": found}, - }) - - if found { - cfg.Logger.Info(&libpack_logger.LogMessage{ - Message: "User is banned", - Pairs: map[string]interface{}{"user_id": userID}, - }) - c.Status(fiber.StatusForbidden).SendString("User is banned") - } - return found -} - -func apiClearCache(c *fiber.Ctx) error { - cfg.Logger.Debug(&libpack_logger.LogMessage{ - Message: "Clearing cache via API", - }) - libpack_cache.CacheClear() - cfg.Logger.Info(&libpack_logger.LogMessage{ - Message: "Cache cleared via API", - }) - return c.SendString("OK: cache cleared") -} - -func apiCacheStats(c *fiber.Ctx) error { - return c.JSON(libpack_cache.GetCacheStats()) -} - -type apiBanUserRequest struct { - UserID string `json:"user_id"` - Reason string `json:"reason"` -} - -func apiBanUser(c *fiber.Ctx) error { - var req apiBanUserRequest - if err := c.BodyParser(&req); err != nil { - cfg.Logger.Error(&libpack_logger.LogMessage{ - Message: "Can't parse the ban user request", - Pairs: map[string]interface{}{"error": err.Error()}, - }) - return c.Status(fiber.StatusBadRequest).SendString("Invalid request payload") - } - - if req.UserID == "" || req.Reason == "" { - return c.Status(fiber.StatusBadRequest).SendString("user_id and reason are required") - } - - bannedUsersIDsMutex.Lock() - bannedUsersIDs[req.UserID] = req.Reason - bannedUsersIDsMutex.Unlock() - - cfg.Logger.Info(&libpack_logger.LogMessage{ - Message: "Banned user", - Pairs: map[string]interface{}{"user_id": req.UserID, "reason": req.Reason}, - }) - - if err := storeBannedUsers(); err != nil { - return c.Status(fiber.StatusInternalServerError).SendString("Failed to store banned users") - } - - return c.SendString("OK: user banned") -} - -func apiUnbanUser(c *fiber.Ctx) error { - var req apiBanUserRequest - if err := c.BodyParser(&req); err != nil { - cfg.Logger.Error(&libpack_logger.LogMessage{ - Message: "Can't parse the unban user request", - Pairs: map[string]interface{}{"error": err.Error()}, - }) - return c.Status(fiber.StatusBadRequest).SendString("Invalid request payload") - } - - if req.UserID == "" { - return c.Status(fiber.StatusBadRequest).SendString("user_id is required") - } - - bannedUsersIDsMutex.Lock() - delete(bannedUsersIDs, req.UserID) - bannedUsersIDsMutex.Unlock() - - cfg.Logger.Info(&libpack_logger.LogMessage{ - Message: "Unbanned user", - Pairs: map[string]interface{}{"user_id": req.UserID}, - }) - - if err := storeBannedUsers(); err != nil { - return c.Status(fiber.StatusInternalServerError).SendString("Failed to store banned users") - } - - return c.SendString("OK: user unbanned") -} - -func storeBannedUsers() error { - fileLock := flock.New(fmt.Sprintf("%s.lock", cfg.Api.BannedUsersFile)) - if err := lockFile(fileLock); err != nil { - return err - } - defer fileLock.Unlock() - - bannedUsersIDsMutex.RLock() - data, err := json.Marshal(bannedUsersIDs) - bannedUsersIDsMutex.RUnlock() - - if err != nil { - cfg.Logger.Error(&libpack_logger.LogMessage{ - Message: "Can't marshal banned users", - Pairs: map[string]interface{}{"error": err.Error()}, - }) - return err - } - - if err := os.WriteFile(cfg.Api.BannedUsersFile, data, 0644); err != nil { - cfg.Logger.Error(&libpack_logger.LogMessage{ - Message: "Can't write banned users to file", - Pairs: map[string]interface{}{"error": err.Error()}, - }) - return err - } - - return nil -} - -func loadBannedUsers() { - if _, err := os.Stat(cfg.Api.BannedUsersFile); os.IsNotExist(err) { - cfg.Logger.Info(&libpack_logger.LogMessage{ - Message: "Banned users file doesn't exist - creating it", - Pairs: map[string]interface{}{"file": cfg.Api.BannedUsersFile}, - }) - if err := os.WriteFile(cfg.Api.BannedUsersFile, []byte("{}"), 0644); err != nil { - cfg.Logger.Error(&libpack_logger.LogMessage{ - Message: "Can't create and write to the file", - Pairs: map[string]interface{}{"error": err.Error()}, - }) - return - } - } - - fileLock := flock.New(fmt.Sprintf("%s.lock", cfg.Api.BannedUsersFile)) - if err := lockFileRead(fileLock); err != nil { - cfg.Logger.Error(&libpack_logger.LogMessage{ - Message: "Can't lock the file [load]", - Pairs: map[string]interface{}{"error": err.Error()}, - }) - return - } - defer fileLock.Unlock() - - data, err := os.ReadFile(cfg.Api.BannedUsersFile) - if err != nil { - cfg.Logger.Error(&libpack_logger.LogMessage{ - Message: "Can't read banned users from file", - Pairs: map[string]interface{}{"error": err.Error()}, - }) - return - } - - var newBannedUsers map[string]string - if err := json.Unmarshal(data, &newBannedUsers); err != nil { - cfg.Logger.Error(&libpack_logger.LogMessage{ - Message: "Can't unmarshal banned users", - Pairs: map[string]interface{}{"error": err.Error()}, - }) - return - } - - bannedUsersIDsMutex.Lock() - bannedUsersIDs = newBannedUsers - bannedUsersIDsMutex.Unlock() -} - -func lockFile(fileLock *flock.Flock) error { - if err := fileLock.Lock(); err != nil { - cfg.Logger.Error(&libpack_logger.LogMessage{ - Message: "Can't lock the file", - Pairs: map[string]interface{}{"error": err.Error()}, - }) - return err - } - return nil -} - -func lockFileRead(fileLock *flock.Flock) error { - if err := fileLock.RLock(); err != nil { - cfg.Logger.Error(&libpack_logger.LogMessage{ - Message: "Can't lock the file for reading", - Pairs: map[string]interface{}{"error": err.Error()}, - }) - return err - } - return nil -} diff --git a/cache/cache.go b/cache/cache.go deleted file mode 100644 index 443bbf6..0000000 --- a/cache/cache.go +++ /dev/null @@ -1,134 +0,0 @@ -package libpack_cache - -import ( - "sync/atomic" - "time" - - fiber "github.com/gofiber/fiber/v2" - "github.com/gookit/goutil/strutil" - libpack_cache_memory "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/memory" - libpack_cache_redis "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/redis" - libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" -) - -type CacheConfig struct { - Logger *libpack_logger.Logger - Client CacheClient - Redis struct { - URL string `json:"url"` - Password string `json:"password"` - DB int `json:"db"` - Enable bool `json:"enable"` - } - TTL int `json:"ttl"` -} - -type CacheStats struct { - CachedQueries int64 `json:"cached_queries"` - CacheHits int64 `json:"cache_hits"` - CacheMisses int64 `json:"cache_misses"` -} - -type CacheClient interface { - Set(key string, value []byte, ttl time.Duration) - Get(key string) ([]byte, bool) - Delete(key string) - Clear() - CountQueries() int64 -} - -var ( - cacheStats *CacheStats - config *CacheConfig -) - -func CalculateHash(c *fiber.Ctx) string { - return strutil.Md5(c.Body()) -} - -func EnableCache(cfg *CacheConfig) { - if cfg.Logger == nil { - cfg.Logger = libpack_logger.New() - cfg.Logger.Info(&libpack_logger.LogMessage{ - Message: "Initializing in-module logger", - }) - } - cacheStats = &CacheStats{} - if ShouldUseRedisCache(cfg) { - cfg.Logger.Info(&libpack_logger.LogMessage{ - Message: "Using Redis cache", - }) - cfg.Client = libpack_cache_redis.New(&libpack_cache_redis.RedisClientConfig{ - RedisDB: cfg.Redis.DB, - RedisServer: cfg.Redis.URL, - RedisPassword: cfg.Redis.Password, - }) - } else { - cfg.Logger.Info(&libpack_logger.LogMessage{ - Message: "Using in-memory cache", - }) - cfg.Client = libpack_cache_memory.New(time.Duration(cfg.TTL) * time.Second) - } - config = cfg -} - -func CacheLookup(hash string) []byte { - obj, found := config.Client.Get(hash) - if found { - atomic.AddInt64(&cacheStats.CacheHits, 1) - return obj - } - atomic.AddInt64(&cacheStats.CacheMisses, 1) - return nil -} - -func CacheDelete(hash string) { - config.Logger.Debug(&libpack_logger.LogMessage{ - Message: "Deleting data from cache", - Pairs: map[string]interface{}{"hash": hash}, - }) - atomic.AddInt64(&cacheStats.CachedQueries, -1) - config.Client.Delete(hash) -} - -func CacheStore(hash string, data []byte) { - config.Logger.Debug(&libpack_logger.LogMessage{ - Message: "Storing data in cache", - Pairs: map[string]interface{}{"hash": hash}, - }) - atomic.AddInt64(&cacheStats.CachedQueries, 1) - config.Client.Set(hash, data, time.Duration(config.TTL)*time.Second) -} - -func CacheStoreWithTTL(hash string, data []byte, ttl time.Duration) { - config.Logger.Debug(&libpack_logger.LogMessage{ - Message: "Storing data in cache with TTL", - Pairs: map[string]interface{}{"hash": hash, "ttl": ttl}, - }) - atomic.AddInt64(&cacheStats.CachedQueries, 1) - config.Client.Set(hash, data, ttl) -} - -func CacheGetQueries() int64 { - config.Logger.Debug(&libpack_logger.LogMessage{ - Message: "Counting cache queries", - }) - return config.Client.CountQueries() -} - -func CacheClear() { - config.Client.Clear() - cacheStats = &CacheStats{} -} - -func GetCacheStats() *CacheStats { - config.Logger.Debug(&libpack_logger.LogMessage{ - Message: "Getting cache stats", - }) - cacheStats.CachedQueries = CacheGetQueries() - return cacheStats -} - -func ShouldUseRedisCache(cfg *CacheConfig) bool { - return cfg.Redis.Enable -} diff --git a/cache/cache_bench_test.go b/cache/cache_bench_test.go deleted file mode 100644 index 271fdd5..0000000 --- a/cache/cache_bench_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package libpack_cache - -import ( - "testing" - "time" - - "github.com/alicebob/miniredis/v2" - libpack_cache_memory "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/memory" - libpack_cache_redis "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/redis" - libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" -) - -const ( - Parallelism = 4 - RequestPerSec = 10000 -) - -func BenchmarkCacheLookupInMemory(b *testing.B) { - config = &CacheConfig{ - Logger: libpack_logger.New(), - Client: libpack_cache_memory.New(5 * time.Minute), - TTL: 5, - } - EnableCache(config) - - hash := "00000000000000000000000000000000001337" - data := []byte("it's fine.") - CacheStore(hash, data) - - b.SetParallelism(Parallelism) - b.ResetTimer() - - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - CacheLookup(hash) - } - }) -} - -func BenchmarkCacheLookupRedis(b *testing.B) { - redis_server, _ := miniredis.Run() - mockedCache := libpack_cache_redis.New(&libpack_cache_redis.RedisClientConfig{ - RedisServer: redis_server.Addr(), - RedisDB: 0, - }) - - config = &CacheConfig{ - Logger: libpack_logger.New(), - Client: mockedCache, - TTL: 5, - } - config.Redis.Enable = true - EnableCache(config) - - hash := "00000000000000000000000000000000001337" - data := []byte("it's fine.") - CacheStore(hash, data) - - b.SetParallelism(Parallelism) - b.ResetTimer() - - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - CacheLookup(hash) - } - }) -} - -func BenchmarkCacheStoreInMemory(b *testing.B) { - config = &CacheConfig{ - Logger: libpack_logger.New(), - Client: libpack_cache_memory.New(5 * time.Minute), - TTL: 5, - } - EnableCache(config) - - hash := "00000000000000000000000000000000001337" - data := []byte("it's fine.") - - b.SetParallelism(Parallelism) - b.ResetTimer() - - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - CacheStore(hash, data) - } - }) -} - -func BenchmarkCacheStoreRedis(b *testing.B) { - redis_server, _ := miniredis.Run() - mockedCache := libpack_cache_redis.New(&libpack_cache_redis.RedisClientConfig{ - RedisServer: redis_server.Addr(), - RedisDB: 0, - }) - - config = &CacheConfig{ - Logger: libpack_logger.New(), - Client: mockedCache, - TTL: 5, - } - config.Redis.Enable = true - EnableCache(config) - - hash := "00000000000000000000000000000000001337" - data := []byte("it's fine.") - - b.SetParallelism(Parallelism) - b.ResetTimer() - - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - CacheStore(hash, data) - } - }) -} diff --git a/cache/cache_suite_test.go b/cache/cache_suite_test.go deleted file mode 100644 index dda51d6..0000000 --- a/cache/cache_suite_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package libpack_cache - -import ( - "testing" - - "github.com/alicebob/miniredis/v2" - assertions "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -type Tests struct { - suite.Suite -} - -var ( - assert *assertions.Assertions - redisMockServer, _ = miniredis.Run() -) - -func (suite *Tests) BeforeTest(suiteName, testName string) { -} - -func (suite *Tests) SetupTest() { - cacheStats = &CacheStats{} - assert = assertions.New(suite.T()) -} - -// TearDownTest is run after each test to clean up -func (suite *Tests) TearDownTest() { -} - -func TestSuite(t *testing.T) { - suite.Run(t, new(Tests)) -} diff --git a/cache/cache_test.go b/cache/cache_test.go deleted file mode 100644 index b6851d2..0000000 --- a/cache/cache_test.go +++ /dev/null @@ -1,215 +0,0 @@ -package libpack_cache - -import ( - "fmt" - "sync" - "time" - - "github.com/alicebob/miniredis/v2" - libpack_cache_memory "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/memory" - libpack_cache_redis "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/redis" - libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" -) - -func (suite *Tests) Test_cacheLookupInmemory() { - config = &CacheConfig{ - Logger: libpack_logger.New(), - Client: libpack_cache_memory.New(5 * time.Minute), - TTL: 5, - } - - type args struct { - hash string - } - tests := []struct { - name string - args args - want []byte - addCache struct { - data []byte - } - }{ - { - name: "test_non_existent", - args: args{ - hash: "00000000000000000000000000000000000000", - }, - want: nil, - }, - { - name: "test_existent", - args: args{ - hash: "00000000000000000000000000000000001337", - }, - want: []byte("it's fine."), - addCache: struct { - data []byte - }{ - data: []byte("it's fine."), - }, - }, - } - for _, tt := range tests { - suite.Run(tt.name, func() { - if tt.addCache.data != nil { - CacheStore(tt.args.hash, tt.addCache.data) - } - got := CacheLookup(tt.args.hash) - assert.Equal(tt.want, got, "Unexpected cache lookup result") - }) - } -} - -func (suite *Tests) Test_cacheLookupRedis() { - // redis_server := envutil.Getenv("REDIS_SERVER", "localhost:6379") - // config.Client = libpack_cache_redis.NewClient(&libpack_cache_redis.RedisClientConfig{ - // RedisServer: redis_server, - // RedisPassword: "", - // RedisDB: 0, - // }) - - mockedCache := libpack_cache_redis.New(&libpack_cache_redis.RedisClientConfig{ - RedisServer: redisMockServer.Addr(), - RedisDB: 0, - }) - - config = &CacheConfig{ - Logger: libpack_logger.New(), - Client: mockedCache, - TTL: 5, - } - - type args struct { - hash string - } - tests := []struct { - name string - args args - want []byte - addCache struct { - data []byte - } - }{ - { - name: "test_non_existent", - args: args{ - hash: "00000000000000000000000000000000000000", - }, - want: nil, - }, - { - name: "test_existent", - args: args{ - hash: "00000000000000000000000000000000001337", - }, - want: []byte("it's fine."), - addCache: struct { - data []byte - }{ - data: []byte("it's fine."), - }, - }, - } - for _, tt := range tests { - suite.Run(tt.name, func() { - if tt.addCache.data != nil { - CacheStore(tt.args.hash, tt.addCache.data) - } - got := CacheLookup(tt.args.hash) - assert.Equal(tt.want, got, "Unexpected cache lookup result") - }) - } -} - -func (suite *Tests) Test_cacheConcurrency() { - config = &CacheConfig{ - Logger: libpack_logger.New(), - Client: libpack_cache_memory.New(5 * time.Second), - TTL: 5, - } - - const numGoroutines = 10 - const numOperations = 1000 - - var wg sync.WaitGroup - wg.Add(numGoroutines) - - for i := 0; i < numGoroutines; i++ { - go func(id int) { - defer wg.Done() - for j := 0; j < numOperations; j++ { - key := fmt.Sprintf("key-%d-%d", id, j) - value := []byte(fmt.Sprintf("value-%d-%d", id, j)) - CacheStore(key, value) - retrieved := CacheLookup(key) - assert.Equal(string(value), string(retrieved), "Concurrent cache operation failed") - } - }(i) - } - - wg.Wait() -} - -// func (suite *Tests) Test_cacheEviction() { -// config = &CacheConfig{ -// Logger: libpack_logger.New(), -// Client: libpack_cache_memory.New(3 * time.Second), // 3 seconds TTL -// TTL: 3, -// } - -// // Fill the cache -// for i := 0; i < 20; i++ { -// key := fmt.Sprintf("key-%d", i) -// value := []byte(fmt.Sprintf("value-%d", i)) -// CacheStore(key, value) -// time.Sleep(100 * time.Millisecond) // Ensure different creation times -// } - -// // Wait for the TTL to expire for the first half of the items -// time.Sleep(3100 * time.Millisecond) - -// // Check that the oldest items have been evicted -// for i := 0; i < 10; i++ { -// key := fmt.Sprintf("key-%d", i) -// retrieved := CacheLookup(key) -// assert.Nil(retrieved, fmt.Sprintf("Old item %s should have been evicted", key)) -// } - -// // Check that the newer items are still in the cache -// for i := 10; i < 20; i++ { -// key := fmt.Sprintf("key-%d", i) -// expected := []byte(fmt.Sprintf("value-%d", i)) -// retrieved := CacheLookup(key) -// assert.Equal(expected, retrieved, fmt.Sprintf("Recent item %s should be in cache", key)) -// } -// } - -func (suite *Tests) Test_cacheRedisFailure() { - mr, err := miniredis.Run() - if err != nil { - suite.T().Fatal(err) - } - defer mr.Close() - - config = &CacheConfig{ - Logger: libpack_logger.New(), - Client: libpack_cache_redis.New(&libpack_cache_redis.RedisClientConfig{ - RedisServer: mr.Addr(), - RedisDB: 0, - }), - TTL: 5, - } - - // Test normal operation - CacheStore("test-key", []byte("test-value")) - retrieved := CacheLookup("test-key") - assert.Equal([]byte("test-value"), retrieved) - - // Simulate Redis failure - mr.Close() - - // Operations should not panic, but should return errors or nil values - CacheStore("another-key", []byte("another-value")) - retrieved = CacheLookup("another-key") - assert.Nil(retrieved, "Lookup should return nil when Redis is down") -} diff --git a/cache/memory/memory.go b/cache/memory/memory.go deleted file mode 100644 index fafaa68..0000000 --- a/cache/memory/memory.go +++ /dev/null @@ -1,157 +0,0 @@ -package libpack_cache_memory - -import ( - "bytes" - "compress/gzip" - "io" - "log" - "sync" - "time" -) - -type CacheEntry struct { - ExpiresAt time.Time - Value []byte -} - -type Cache struct { - compressPool sync.Pool - decompressPool sync.Pool - entries sync.Map - globalTTL time.Duration - sync.RWMutex -} - -func New(globalTTL time.Duration) *Cache { - cache := &Cache{ - globalTTL: globalTTL, - compressPool: sync.Pool{ - New: func() interface{} { - return gzip.NewWriter(nil) - }, - }, - decompressPool: sync.Pool{ - New: func() interface{} { - r, _ := gzip.NewReader(bytes.NewReader([]byte{})) - return r - }, - }, - } - - go cache.cleanupRoutine(globalTTL) - return cache -} - -func (c *Cache) cleanupRoutine(globalTTL time.Duration) { - ticker := time.NewTicker(globalTTL / 2) - defer ticker.Stop() - - for range ticker.C { - c.CleanExpiredEntries() - } -} - -func (c *Cache) Set(key string, value []byte, ttl time.Duration) { - expiresAt := time.Now().Add(ttl) - - compressedValue, err := c.compress(value) - if err != nil { - log.Printf("Error compressing value for key %s: %v", key, err) - return - } - - entry := CacheEntry{ - Value: compressedValue, - ExpiresAt: expiresAt, - } - c.entries.Store(key, entry) -} - -func (c *Cache) Get(key string) ([]byte, bool) { - entry, ok := c.entries.Load(key) - if !ok { - return nil, false - } - - cacheEntry := entry.(CacheEntry) - if cacheEntry.ExpiresAt.Before(time.Now()) { - c.entries.Delete(key) - return nil, false - } - - value, err := c.decompress(cacheEntry.Value) - if err != nil { - log.Printf("Error decompressing value for key %s: %v", key, err) - return nil, false - } - return value, true -} - -func (c *Cache) Delete(key string) { - c.entries.Delete(key) -} - -func (c *Cache) Clear() { - c.entries.Range(func(key, value interface{}) bool { - c.entries.Delete(key) - return true - }) -} - -func (c *Cache) CountQueries() int64 { - var count int - c.entries.Range(func(_, _ interface{}) bool { - count++ - return true - }) - return int64(count) -} - -func (c *Cache) compress(data []byte) ([]byte, error) { - var buf bytes.Buffer - w := c.compressPool.Get().(*gzip.Writer) - defer func() { - w.Close() - c.compressPool.Put(w) - }() - w.Reset(&buf) - if _, err := w.Write(data); err != nil { - return nil, err - } - if err := w.Close(); err != nil { - return nil, err - } - return buf.Bytes(), nil -} - -func (c *Cache) decompress(data []byte) ([]byte, error) { - r, ok := c.decompressPool.Get().(*gzip.Reader) - if !ok || r == nil { - var err error - r, err = gzip.NewReader(bytes.NewReader(data)) - if err != nil { - return nil, err - } - } else { - if err := r.Reset(bytes.NewReader(data)); err != nil { - return nil, err - } - } - defer func() { - r.Close() - c.decompressPool.Put(r) - }() - - return io.ReadAll(r) -} - -func (c *Cache) CleanExpiredEntries() { - now := time.Now() - c.entries.Range(func(key, value interface{}) bool { - entry := value.(CacheEntry) - if entry.ExpiresAt.Before(now) { - c.entries.Delete(key) - } - return true - }) -} diff --git a/cache/memory/memory_bench_test.go b/cache/memory/memory_bench_test.go deleted file mode 100644 index 054cbfb..0000000 --- a/cache/memory/memory_bench_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package libpack_cache_memory - -import ( - "fmt" - "testing" - "time" -) - -// Assume that New function initializes the cache and it is defined somewhere in the libpack_cache package. - -func BenchmarkMemCacheSet(b *testing.B) { - cache := New(30 * time.Second) // Initializing the cache with a TTL of 30 seconds - key := "benchmark-key" - value := []byte("benchmark-value") - - b.ResetTimer() // Reset the timer to exclude the setup time from the benchmark - - for i := 0; i < b.N; i++ { - cache.Set(key, value, 5*time.Second) - } -} - -func BenchmarkMemCacheGet(b *testing.B) { - cache := New(30 * time.Second) // Initializing the cache - key := "benchmark-key" - value := []byte("benchmark-value") - cache.Set(key, value, 5*time.Second) // Pre-set a value to retrieve - - b.ResetTimer() // Start timing - - for i := 0; i < b.N; i++ { - _, _ = cache.Get(key) - } -} - -func BenchmarkMemCacheExpire(b *testing.B) { - key := "benchmark-expire-key" - value := []byte("benchmark-value") - ttl := 5 * time.Millisecond // Setting a short TTL for quick expiration - - for i := 0; i < b.N; i++ { - cache := New(30 * time.Second) - cache.Set(key, value, ttl) - time.Sleep(ttl) // Wait for the key to expire - _, _ = cache.Get(key) - } -} - -func BenchmarkMemCacheStats(b *testing.B) { - cache := New(30 * time.Second) // Initializing the cache - key := "benchmark-key" - value := []byte("benchmark-value") - cache.Set(key, value, 5*time.Second) // Pre-set a value to retrieve - cache.Get(key) -} - -func BenchmarkCacheSet(b *testing.B) { - cache := New(5 * time.Second) - b.ResetTimer() - for i := 0; i < b.N; i++ { - cache.Set(fmt.Sprintf("key-%d", i), []byte("value"), 5*time.Second) - } -} - -func BenchmarkCacheGet(b *testing.B) { - cache := New(5 * time.Second) - cache.Set("test-key", []byte("test-value"), 5*time.Second) - b.ResetTimer() - for i := 0; i < b.N; i++ { - cache.Get("test-key") - } -} - -func BenchmarkCacheDelete(b *testing.B) { - cache := New(5 * time.Second) - b.ResetTimer() - for i := 0; i < b.N; i++ { - key := fmt.Sprintf("key-%d", i) - cache.Set(key, []byte("value"), 5*time.Second) - cache.Delete(key) - } -} diff --git a/cache/memory/memory_test.go b/cache/memory/memory_test.go deleted file mode 100644 index 948091c..0000000 --- a/cache/memory/memory_test.go +++ /dev/null @@ -1,168 +0,0 @@ -package libpack_cache_memory - -import ( - "fmt" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/suite" -) - -type MemoryTestSuite struct { - suite.Suite -} - -func (suite *MemoryTestSuite) SetupTest() { -} - -func TestCachingTestSuite(t *testing.T) { - suite.Run(t, new(MemoryTestSuite)) -} - -func (suite *MemoryTestSuite) Test_New() { - suite.T().Run("should return a new cache", func(t *testing.T) { - cache := New(2 * time.Second) - suite.NotNil(cache) - }) -} - -func (suite *MemoryTestSuite) Test_CacheUse() { - cache := New(30 * time.Second) - tests := []struct { - name string - cache_value string - }{ - { - name: "test1", - cache_value: "test1-123", - }, - { - name: "test2", - cache_value: "test2-123", - }, - } - for _, tt := range tests { - suite.T().Run(tt.name, func(t *testing.T) { - cache.Set(tt.name, []byte(tt.name), 5*time.Second) - c, ok := cache.Get(tt.name) - suite.Equal(true, ok) - suite.Equal(tt.name, string(c)) - }) - } -} - -func (suite *MemoryTestSuite) Test_CacheDelete() { - cache := New(30 * time.Second) - tests := []struct { - name string - cache_value string - }{ - { - name: "test1", - cache_value: "test1-123", - }, - { - name: "test2", - cache_value: "test2-123", - }, - } - for _, tt := range tests { - suite.T().Run(tt.name, func(t *testing.T) { - cache.Set(tt.name, []byte(tt.name), 5*time.Second) - c, ok := cache.Get(tt.name) - suite.Equal(true, ok) - suite.Equal(tt.name, string(c)) - cache.Delete(tt.name) - c, ok = cache.Get(tt.name) - suite.Equal(false, ok) - suite.Equal("", string(c)) - }) - } -} - -func (suite *MemoryTestSuite) Test_CacheExpire() { - cache := New(30 * time.Second) - tests := []struct { - name string - cache_value string - ttl time.Duration - }{ - { - name: "test1", - cache_value: "test1-123", - ttl: 2 * time.Second, - }, - { - name: "test2", - cache_value: "test2-123", - ttl: 5 * time.Second, - }, - } - for _, tt := range tests { - suite.T().Run(tt.name, func(t *testing.T) { - cache.Set(tt.name, []byte(tt.name), tt.ttl) - c, ok := cache.Get(tt.name) - suite.Equal(true, ok) - suite.Equal(tt.name, string(c)) - time.Sleep(tt.ttl) - c, ok = cache.Get(tt.name) - suite.Equal(false, ok) - suite.Equal("", string(c)) - }) - } -} - -func (suite *MemoryTestSuite) Test_ConcurrentReadWrite() { - cache := New(5 * time.Second) - const numGoroutines = 100 - const numOperations = 1000 - - var wg sync.WaitGroup - wg.Add(numGoroutines) - - for i := 0; i < numGoroutines; i++ { - go func(id int) { - defer wg.Done() - for j := 0; j < numOperations; j++ { - key := fmt.Sprintf("key-%d-%d", id, j) - value := []byte(fmt.Sprintf("value-%d-%d", id, j)) - - if j%2 == 0 { - cache.Set(key, value, 5*time.Second) - } else { - _, _ = cache.Get(key) - } - } - }(i) - } - - wg.Wait() -} - -func (suite *MemoryTestSuite) Test_LargeItems() { - cache := New(5 * time.Second) - largeValue := make([]byte, 10*1024*1024) // 10MB - cache.Set("large-key", largeValue, 5*time.Second) - - retrieved, found := cache.Get("large-key") - suite.Assert().True(found) - suite.Assert().Equal(largeValue, retrieved) -} - -func (suite *MemoryTestSuite) Test_ZeroTTL() { - cache := New(5 * time.Second) - cache.Set("zero-ttl", []byte("value"), 0) - - _, found := cache.Get("zero-ttl") - suite.Assert().False(found, "Item with zero TTL should not be stored") -} - -func (suite *MemoryTestSuite) Test_LongTTL() { - cache := New(5 * time.Second) - cache.Set("long-ttl", []byte("value"), 24*365*time.Hour) // 1 year - - retrieved, found := cache.Get("long-ttl") - suite.Assert().True(found) - suite.Assert().Equal([]byte("value"), retrieved) -} diff --git a/cache/redis/redis.go b/cache/redis/redis.go deleted file mode 100644 index 01edcbb..0000000 --- a/cache/redis/redis.go +++ /dev/null @@ -1,96 +0,0 @@ -package libpack_cache_redis - -import ( - "context" - "strings" - "time" - - "sync" - - redis "github.com/redis/go-redis/v9" -) - -type RedisConfig struct { - ctx context.Context - client *redis.Client - builderPool *sync.Pool - prefix string -} - -func (c *RedisConfig) prependKeyName(key string) string { - builder := c.builderPool.Get().(*strings.Builder) - defer c.builderPool.Put(builder) - builder.Reset() - builder.WriteString(c.prefix) - builder.WriteString(key) - return builder.String() -} - -type RedisClientConfig struct { - RedisServer string - RedisPassword string - Prefix string - RedisDB int -} - -func New(redisClientConfig *RedisClientConfig) *RedisConfig { - c := &RedisConfig{ - client: redis.NewClient(&redis.Options{ - Addr: redisClientConfig.RedisServer, - Password: redisClientConfig.RedisPassword, - DB: redisClientConfig.RedisDB, - }), - ctx: context.Background(), - prefix: redisClientConfig.Prefix, - builderPool: &sync.Pool{ - New: func() interface{} { - return &strings.Builder{} - }, - }, - } - - _, err := c.client.Ping(c.ctx).Result() - if err != nil { - panic(err) - } - return c -} - -func (c *RedisConfig) Set(key string, value []byte, ttl time.Duration) { - c.client.Set(c.ctx, c.prependKeyName(key), value, ttl) -} - -func (c *RedisConfig) Get(key string) ([]byte, bool) { - val, err := c.client.Get(c.ctx, c.prependKeyName(key)).Result() - if err == redis.Nil { - return nil, false - } - if err != nil { - return nil, false - } - return []byte(val), true -} - -func (c *RedisConfig) Delete(key string) { - c.client.Del(c.ctx, c.prependKeyName(key)) -} - -func (c *RedisConfig) Clear() { - c.client.FlushDB(c.ctx) -} - -func (c *RedisConfig) CountQueries() int64 { - keys, err := c.client.Keys(c.ctx, c.prependKeyName("*")).Result() - if err != nil { - return 0 - } - return int64(len(keys)) -} - -func (c *RedisConfig) CountQueriesWithPattern(pattern string) int { - keys, err := c.client.Keys(c.ctx, c.prependKeyName(pattern)).Result() - if err != nil { - return 0 - } - return len(keys) -} diff --git a/cache/redis/redis_test.go b/cache/redis/redis_test.go deleted file mode 100644 index c9ed41b..0000000 --- a/cache/redis/redis_test.go +++ /dev/null @@ -1,130 +0,0 @@ -package libpack_cache_redis - -import ( - "testing" - "time" - - "github.com/alicebob/miniredis/v2" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -type RedisConfigSuite struct { - suite.Suite - redisConfig *RedisConfig - redis_server *miniredis.Miniredis -} - -func (suite *RedisConfigSuite) SetupTest() { - suite.redis_server, _ = miniredis.Run() - suite.redisConfig = New(&RedisClientConfig{ - RedisServer: suite.redis_server.Addr(), - RedisPassword: "", - RedisDB: 0, - }) - suite.redisConfig.Delete("testkey") -} - -func TestRedisConfigSuite(t *testing.T) { - suite.Run(t, new(RedisConfigSuite)) -} - -func (suite *RedisConfigSuite) TestSet() { - key := "testkeyset" - value := []byte("testvalue") - suite.redisConfig.Delete(key) // Ensure the key is deleted before the test - - // Test writing a new key-value pair - suite.redisConfig.Set(key, value, 0) - storedValue, found := suite.redisConfig.Get(key) - assert.True(suite.T(), found) - assert.Equal(suite.T(), value, storedValue) - - // Test overwriting an existing key-value pair - newValue := []byte("newvalue") - suite.redisConfig.Set(key, newValue, 0) - storedValue, found = suite.redisConfig.Get(key) - assert.True(suite.T(), found) - assert.Equal(suite.T(), newValue, storedValue) - - suite.redisConfig.Delete(key) // Clean up after the test -} - -func (suite *RedisConfigSuite) TestSetWithExpiry() { - key := "testkey_with_expiry" - value := []byte("testvaluewithexpiry") - expiry := 2 * time.Second - suite.redisConfig.Delete(key) // Ensure the key is deleted before the test - - // Test writing a new key-value pair - suite.redisConfig.Set(key, value, expiry) - storedValue, found := suite.redisConfig.Get(key) - assert.True(suite.T(), found) - assert.Equal(suite.T(), value, storedValue) - _, found = suite.redisConfig.Get(key) - assert.True(suite.T(), found, "Key should exist") - - // Test that key expires after the specified time - suite.redis_server.FastForward(3 * time.Second) - _, found = suite.redisConfig.Get(key) - assert.False(suite.T(), found, "Key should have expired after 2 seconds") - - suite.redisConfig.Delete(key) // Clean up after the test -} - -func (suite *RedisConfigSuite) TestGet() { - key := "testkeyget" - value := []byte("testvalue") - suite.redisConfig.Set(key, value, 0) // Set the key-value pair - storedValue, found := suite.redisConfig.Get(key) - assert.True(suite.T(), found) - assert.Equal(suite.T(), value, storedValue) -} - -func (suite *RedisConfigSuite) TestDeleteKey() { - key := "testkeydelete" - value := []byte("testvalue") - suite.redisConfig.Set(key, value, 0) // Set the key-value pair - suite.redisConfig.Delete(key) - _, found := suite.redisConfig.Get(key) - assert.False(suite.T(), found) -} - -func (suite *RedisConfigSuite) TestCheckIfKeyExists() { - ttl := time.Duration(10) * time.Second - key := "testkeyifexists" - value := []byte("testvalue") - suite.redisConfig.Set(key, value, ttl) // Set the key-value pair - _, found := suite.redisConfig.Get(key) - assert.True(suite.T(), found) - - suite.redisConfig.Delete(key) - _, found = suite.redisConfig.Get(key) - assert.False(suite.T(), found) -} - -func (suite *RedisConfigSuite) TestGetKeys() { - ttl := time.Duration(10) * time.Second - suite.redisConfig.Set("testkey1", []byte("testvalue1"), ttl) - suite.redisConfig.Set("testkey2", []byte("testvalue2"), ttl) - suite.redisConfig.Set("otherkey", []byte("othervalue"), ttl) - - keys, _ := suite.redisConfig.client.Keys(suite.redisConfig.ctx, "testkey*").Result() - expectedKeys := []string{"testkey1", "testkey2"} - assert.ElementsMatch(suite.T(), expectedKeys, keys) - - suite.redisConfig.client.Del(suite.redisConfig.ctx, "testkey1", "testkey2", "otherkey") -} - -func (suite *RedisConfigSuite) TestGetKeysCount() { - ttl := time.Duration(10) * time.Second - suite.redisConfig.Set("testkey1", []byte("testvalue1"), ttl) - suite.redisConfig.Set("testkey2", []byte("testvalue2"), ttl) - suite.redisConfig.Set("otherkey", []byte("othervalue"), ttl) - - assert.Equal(suite.T(), 2, suite.redisConfig.CountQueriesWithPattern("testkey*")) - assert.Equal(suite.T(), 1, suite.redisConfig.CountQueriesWithPattern("otherkey*")) - assert.Equal(suite.T(), int64(3), suite.redisConfig.CountQueries()) - - suite.redisConfig.client.Del(suite.redisConfig.ctx, "testkey1", "testkey2", "otherkey") -} diff --git a/config/config.go b/config/config.go deleted file mode 100644 index 82998d0..0000000 --- a/config/config.go +++ /dev/null @@ -1,6 +0,0 @@ -package libpack_config - -var ( - PKG_NAME string = "not-specified" - PKG_VERSION string = "0.0.0-dev" -) diff --git a/details.go b/details.go deleted file mode 100644 index dc35a1a..0000000 --- a/details.go +++ /dev/null @@ -1,65 +0,0 @@ -package main - -import ( - "encoding/base64" - "fmt" - "strings" - - "github.com/goccy/go-json" - "github.com/lukaszraczylo/ask" - libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" - libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring" -) - -const defaultValue = "-" - -var emptyMetrics = map[string]string{} - -func extractClaimsFromJWTHeader(authorization string) (usr, role string) { - usr, role = defaultValue, defaultValue - - tokenParts := strings.SplitN(authorization, ".", 3) - if len(tokenParts) != 3 { - handleError("Can't split the token", map[string]interface{}{"token": authorization}) - return - } - - claim, err := base64.RawURLEncoding.DecodeString(tokenParts[1]) - if err != nil { - handleError("Can't decode the token", map[string]interface{}{"token": authorization}) - return - } - - var claimMap map[string]interface{} - if err = json.Unmarshal(claim, &claimMap); err != nil { - handleError("Can't unmarshal the claim", map[string]interface{}{"token": authorization}) - return - } - - usr = extractClaim(claimMap, cfg.Client.JWTUserClaimPath, "user id") - role = extractClaim(claimMap, cfg.Client.JWTRoleClaimPath, "role") - - return -} - -func extractClaim(claimMap map[string]interface{}, claimPath, name string) string { - if claimPath == "" { - return defaultValue - } - - value, ok := ask.For(claimMap, claimPath).String(defaultValue) - if !ok { - handleError(fmt.Sprintf("Can't find the %s", name), map[string]interface{}{"claim_map": claimMap, "path": claimPath}) - return defaultValue - } - - return value -} - -func handleError(msg string, details map[string]interface{}) { - cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, emptyMetrics) - cfg.Logger.Error(&libpack_logger.LogMessage{ - Message: msg, - Pairs: details, - }) -} diff --git a/details_test.go b/details_test.go deleted file mode 100644 index f5e5a70..0000000 --- a/details_test.go +++ /dev/null @@ -1,81 +0,0 @@ -package main - -func (suite *Tests) Test_extractClaimsFromJWTHeader() { - jwt_token_for_tests := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiSGFzdXJhIjp7IngtaGFzdXJhLWFsbG93ZWQtcm9sZXMiOlsiZ3Vlc3QiLCJ1c2VyIiwiZ3JvdXBhZG1pbiIsInBheWFkbWluIl0sIngtaGFzdXJhLWRlZmF1bHQtcm9sZSI6Imd1ZXN0IiwieC1oYXN1cmEtdXNlci1pZCI6IjE2NyIsIngtaGFzdXJhLXVzZXItdXVpZCI6ImRkM2U2ZTM1LTA0MDktNDNiMC1iZmYxLWNlZjNjNmVkNWYxMCJ9LCJpc3MiOiJBdXRoU2VydmljZSIsImV4cCI6MTY5NjgwMTcyNiwibmJmIjoxNjk2NTg1NzI2LCJpYXQiOjE2OTY1ODU3MjZ9.dsJ5JKzG5tXOlqeZ_Gfe2XC-vyrcwtYwOGfhvt8q9UY" - - type args struct { - authorization string - } - - tests := []struct { - name string - args args - wantUsr string - wantRole string - jwt_token_path string - jwt_role_path string - }{ - { - name: "test_empty", - wantUsr: "-", - wantRole: "-", - }, - { - name: "test_invalid_path", - args: args{ - authorization: jwt_token_for_tests, - }, - wantUsr: "-", - wantRole: "-", - jwt_token_path: "invalid", - }, - { - name: "test_invalid_role_path", - args: args{ - authorization: jwt_token_for_tests, - }, - wantUsr: "-", - wantRole: "-", - jwt_role_path: "invalid", - }, - { - name: "test_valid", - args: args{ - authorization: jwt_token_for_tests, - }, - wantUsr: "167", - wantRole: "guest", - jwt_token_path: "Hasura.x-hasura-user-id", - jwt_role_path: "Hasura.x-hasura-default-role", - }, - { - name: "test_invalid_token", - args: args{ - authorization: "invalid", - }, - wantUsr: "-", - wantRole: "-", - }, - { - name: "test_invalid_three_part_token", - args: args{ - authorization: "invalid.threepart.token", - }, - wantUsr: "-", - wantRole: "-", - }, - } - for _, tt := range tests { - suite.Run(tt.name, func() { - if len(tt.jwt_token_path) > 0 { - cfg.Client.JWTUserClaimPath = tt.jwt_token_path - } - if len(tt.jwt_role_path) > 0 { - cfg.Client.JWTRoleClaimPath = tt.jwt_role_path - } - gotUsr, gotRole := extractClaimsFromJWTHeader(tt.args.authorization) - assert.Equal(tt.wantUsr, gotUsr, "Unexpected user ID") - assert.Equal(tt.wantRole, gotRole, "Unexpected role") - }) - } -} diff --git a/events.go b/events.go deleted file mode 100644 index 155dd41..0000000 --- a/events.go +++ /dev/null @@ -1,88 +0,0 @@ -package main - -import ( - "context" - "fmt" - "time" - - "github.com/jackc/pgx/v5/pgxpool" - libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" -) - -const ( - initialDelay = 60 * time.Second - cleanupInterval = 1 * time.Hour -) - -var delQueries = [...]string{ - "DELETE FROM hdb_catalog.event_invocation_logs WHERE created_at < NOW() - interval '%d days';", - "DELETE FROM hdb_catalog.event_log WHERE created_at < NOW() - interval '%d days';", - "DELETE FROM hdb_catalog.hdb_action_log WHERE created_at < NOW() - INTERVAL '%d days';", - "DELETE FROM hdb_catalog.hdb_cron_event_invocation_logs WHERE created_at < NOW() - INTERVAL '%d days';", - "DELETE FROM hdb_catalog.hdb_scheduled_event_invocation_logs WHERE created_at < NOW() - INTERVAL '%d days';", -} - -func enableHasuraEventCleaner() { - if !cfg.HasuraEventCleaner.Enable { - return - } - - if cfg.HasuraEventCleaner.EventMetadataDb == "" { - cfg.Logger.Warning(&libpack_logger.LogMessage{ - Message: "Event metadata db URL not specified, event cleaner not active", - }) - return - } - - cfg.Logger.Info(&libpack_logger.LogMessage{ - Message: "Event cleaner enabled", - Pairs: map[string]interface{}{"interval_in_days": cfg.HasuraEventCleaner.ClearOlderThan}, - }) - - go func() { - pool, err := pgxpool.New(context.Background(), cfg.HasuraEventCleaner.EventMetadataDb) - if err != nil { - cfg.Logger.Error(&libpack_logger.LogMessage{ - Message: "Failed to create connection pool", - Pairs: map[string]interface{}{"error": err.Error()}, - }) - return - } - defer pool.Close() - - time.Sleep(initialDelay) - - cfg.Logger.Info(&libpack_logger.LogMessage{ - Message: "Initial cleanup of old events", - }) - cleanEvents(pool) - - ticker := time.NewTicker(cleanupInterval) - defer ticker.Stop() - - for range ticker.C { - cfg.Logger.Info(&libpack_logger.LogMessage{ - Message: "Cleaning up old events", - }) - cleanEvents(pool) - } - }() -} - -func cleanEvents(pool *pgxpool.Pool) { - ctx := context.Background() - for _, query := range delQueries { - _, err := pool.Exec(ctx, fmt.Sprintf(query, cfg.HasuraEventCleaner.ClearOlderThan)) - if err != nil { - cfg.Logger.Error(&libpack_logger.LogMessage{ - Message: "Failed to execute query", - Pairs: map[string]interface{}{"query": query, "error": err.Error()}, - }) - } else { - cfg.Logger.Debug(&libpack_logger.LogMessage{ - Message: "Successfully executed query", - Pairs: map[string]interface{}{"query": query}, - }) - } - } -} diff --git a/go.mod b/go.mod deleted file mode 100644 index 11c9d67..0000000 --- a/go.mod +++ /dev/null @@ -1,54 +0,0 @@ -module github.com/lukaszraczylo/graphql-monitoring-proxy - -go 1.22.4 - -require ( - github.com/VictoriaMetrics/metrics v1.34.0 - github.com/alicebob/miniredis/v2 v2.33.0 - github.com/avast/retry-go/v4 v4.6.0 - github.com/goccy/go-json v0.10.3 - github.com/gofiber/fiber/v2 v2.52.4 - github.com/gofrs/flock v0.9.0 - github.com/google/uuid v1.6.0 - github.com/gookit/goutil v0.6.15 - github.com/graphql-go/graphql v0.8.1 - github.com/jackc/pgx/v5 v5.6.0 - github.com/lukaszraczylo/ask v0.0.0-20230927103145-2ff1123b4415 - github.com/lukaszraczylo/go-ratecounter v0.1.12 - github.com/lukaszraczylo/go-simple-graphql v1.2.17 - github.com/redis/go-redis/v9 v9.5.3 - github.com/stretchr/testify v1.9.0 - github.com/valyala/fasthttp v1.55.0 -) - -require ( - github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect - github.com/andybalholm/brotli v1.1.0 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/gookit/color v1.5.4 // indirect - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/puddle/v2 v2.2.1 // indirect - github.com/klauspost/compress v1.17.9 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rivo/uniseg v0.4.7 // indirect - github.com/rs/zerolog v1.33.0 // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fastrand v1.1.0 // indirect - github.com/valyala/histogram v1.2.0 // indirect - github.com/valyala/tcplisten v1.0.0 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - github.com/yuin/gopher-lua v1.1.1 // indirect - golang.org/x/crypto v0.24.0 // indirect - golang.org/x/net v0.26.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/term v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/go.sum b/go.sum deleted file mode 100644 index 0e7902c..0000000 --- a/go.sum +++ /dev/null @@ -1,120 +0,0 @@ -github.com/VictoriaMetrics/metrics v1.34.0 h1:0i8k/gdOJdSoZB4Z9pikVnVQXfhcIvnG7M7h2WaQW2w= -github.com/VictoriaMetrics/metrics v1.34.0/go.mod h1:r7hveu6xMdUACXvB8TYdAj8WEsKzWB0EkpJN+RDtOf8= -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.33.0 h1:uvTF0EDeu9RLnUEG27Db5I68ESoIxTiXbNUiji6lZrA= -github.com/alicebob/miniredis/v2 v2.33.0/go.mod h1:MhP4a3EU7aENRi9aO+tHfTBZicLqQevyi/DJpoj6mi0= -github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= -github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= -github.com/avast/retry-go/v4 v4.6.0 h1:K9xNA+KeB8HHc2aWFuLb25Offp+0iVRXEvFx8IinRJA= -github.com/avast/retry-go/v4 v4.6.0/go.mod h1:gvWlPhBVsvBbLkVGDg/KwvBv0bEkCOLRRSHKIr2PyOE= -github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= -github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= -github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= -github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -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= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= -github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gofiber/fiber/v2 v2.52.4 h1:P+T+4iK7VaqUsq2PALYEfBBo6bJZ4q3FP8cZ84EggTM= -github.com/gofiber/fiber/v2 v2.52.4/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= -github.com/gofrs/flock v0.9.0 h1:QqEH0zKHPdEyY4YbJLleD9Il4ft7h6hn3gECO6Ss4rQ= -github.com/gofrs/flock v0.9.0/go.mod h1:O+L78Axre/Bc0Ya3RlNiGP+Rt0tFHWjtHTQ+B2uPZw8= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= -github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= -github.com/gookit/goutil v0.6.15 h1:mMQ0ElojNZoyPD0eVROk5QXJPh2uKR4g06slgPDF5Jo= -github.com/gookit/goutil v0.6.15/go.mod h1:qdKdYEHQdEtyH+4fNdQNZfJHhI0jUZzHxQVAV3DaMDY= -github.com/graphql-go/graphql v0.8.1 h1:p7/Ou/WpmulocJeEx7wjQy611rtXGQaAcXGqanuMMgc= -github.com/graphql-go/graphql v0.8.1/go.mod h1:nKiHzRM0qopJEwCITUuIsxk9PlVlwIiiI8pnJEhordQ= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= -github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= -github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= -github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= -github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lukaszraczylo/ask v0.0.0-20230927103145-2ff1123b4415 h1:lvI8Wlbg4PxkRcg2f10wgoaRpfN19v+YdRek3+dLtlM= -github.com/lukaszraczylo/ask v0.0.0-20230927103145-2ff1123b4415/go.mod h1:M+UVdyqZs++xtEPrascaVmZdOMhCnxjZ2SgH+xHpR0c= -github.com/lukaszraczylo/go-ratecounter v0.1.12 h1:VO6hHYGw/Jy9JUizXf/bS0AI2QX1ueWWAWckMFVJ/w4= -github.com/lukaszraczylo/go-ratecounter v0.1.12/go.mod h1:TqXEOCtFJStk1i0tkipprv1kiDHGon1MVUisjSTBSKM= -github.com/lukaszraczylo/go-simple-graphql v1.2.17 h1:XxUUgxcCIZSVLzI4UfhBDXoFoMlygcXHfAJwXxawr1s= -github.com/lukaszraczylo/go-simple-graphql v1.2.17/go.mod h1:pSKmm9OLGoS9pjmIvhBB/fo0+LganRrL29CN3fdkRPw= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -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/redis/go-redis/v9 v9.5.3 h1:fOAp1/uJG+ZtcITgZOfYFmTKPE7n4Vclj1wZFgRciUU= -github.com/redis/go-redis/v9 v9.5.3/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= -github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -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.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8= -github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM= -github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8= -github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ= -github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ= -github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY= -github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= -github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= -github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= -golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= -golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/graphql.go b/graphql.go deleted file mode 100644 index 2072427..0000000 --- a/graphql.go +++ /dev/null @@ -1,212 +0,0 @@ -package main - -import ( - "strconv" - "strings" - "sync" - "unsafe" - - "github.com/goccy/go-json" - fiber "github.com/gofiber/fiber/v2" - "github.com/graphql-go/graphql/language/ast" - "github.com/graphql-go/graphql/language/parser" - libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" - libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring" -) - -var ( - introspectionQueries = map[string]struct{}{ - "__schema": {}, "__type": {}, "__typename": {}, "__directive": {}, - "__directivelocation": {}, "__field": {}, "__inputvalue": {}, - "__enumvalue": {}, "__typekind": {}, "__fieldtype": {}, - "__inputobjecttype": {}, "__enumtype": {}, "__uniontype": {}, - "__scalars": {}, "__objects": {}, "__interfaces": {}, - "__unions": {}, "__enums": {}, "__inputobjects": {}, "__directives": {}, - } - introspectionAllowedQueries = make(map[string]struct{}) - allowedUrls = make(map[string]struct{}) - mu sync.RWMutex -) - -func prepareQueriesAndExemptions() { - mu.Lock() - defer mu.Unlock() - for _, q := range cfg.Security.IntrospectionAllowed { - introspectionAllowedQueries[strings.ToLower(q)] = struct{}{} - } - for _, u := range cfg.Server.AllowURLs { - allowedUrls[u] = struct{}{} - } -} - -type parseGraphQLQueryResult struct { - operationType string - operationName string - activeEndpoint string - cacheTime int - cacheRequest bool - cacheRefresh bool - shouldBlock bool - shouldIgnore bool -} - -var ( - queryPool = sync.Pool{ - New: func() interface{} { - return make(map[string]interface{}, 4) - }, - } - resultPool = sync.Pool{ - New: func() interface{} { - return &parseGraphQLQueryResult{} - }, - } -) - -func parseGraphQLQuery(c *fiber.Ctx) *parseGraphQLQueryResult { - res := resultPool.Get().(*parseGraphQLQueryResult) - defer resultPool.Put(res) - *res = parseGraphQLQueryResult{shouldIgnore: true} - - m := queryPool.Get().(map[string]interface{}) - defer queryPool.Put(m) - for k := range m { - delete(m, k) - } - - if err := json.Unmarshal(c.Body(), &m); err != nil { - cfg.Logger.Error(&libpack_logger.LogMessage{ - Message: "Can't unmarshal the request", - Pairs: map[string]interface{}{"error": err.Error(), "body": unsafeString(c.Body())}, - }) - if ifNotInTest() { - cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil) - } - return res - } - - query, ok := m["query"].(string) - if !ok { - cfg.Logger.Error(&libpack_logger.LogMessage{ - Message: "Can't find the query", - Pairs: map[string]interface{}{"m_val": m}, - }) - if ifNotInTest() { - cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil) - } - return res - } - - p, err := parser.Parse(parser.ParseParams{Source: query}) - if err != nil { - cfg.Logger.Error(&libpack_logger.LogMessage{ - Message: "Can't parse the query", - Pairs: map[string]interface{}{"query": query, "m_val": m}, - }) - if ifNotInTest() { - cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil) - } - return res - } - - res.shouldIgnore = false - res.operationName = "undefined" - res.activeEndpoint = cfg.Server.HostGraphQL - - for _, d := range p.Definitions { - if oper, ok := d.(*ast.OperationDefinition); ok { - // If we haven't set an operation type yet, use this one - if res.operationType == "" { - res.operationType = strings.ToLower(oper.Operation) - if oper.Name != nil { - res.operationName = oper.Name.Value - } - } - - if cfg.Server.HostGraphQLReadOnly != "" && res.operationType != "mutation" { - res.activeEndpoint = cfg.Server.HostGraphQLReadOnly - } - - if res.operationType == "mutation" && cfg.Server.ReadOnlyMode { - cfg.Logger.Warning(&libpack_logger.LogMessage{ - Message: "Mutation blocked", - Pairs: map[string]interface{}{"query": query}, - }) - if ifNotInTest() { - cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil) - } - _ = c.Status(403).SendString("The server is in read-only mode") - res.shouldBlock = true - return res - } - - for _, dir := range oper.Directives { - if dir.Name.Value == "cached" { - res.cacheRequest = true - for _, arg := range dir.Arguments { - switch arg.Name.Value { - case "ttl": - if v, ok := arg.Value.GetValue().(string); ok { - res.cacheTime, _ = strconv.Atoi(v) - } - case "refresh": - if v, ok := arg.Value.GetValue().(bool); ok { - res.cacheRefresh = v - } - } - } - } - } - - if cfg.Security.BlockIntrospection { - res.shouldBlock = checkSelections(c, oper.GetSelectionSet().Selections) - if res.shouldBlock { - return res - } - } - } - } - return res -} - -func unsafeString(b []byte) string { - return *(*string)(unsafe.Pointer(&b)) -} - -func checkSelections(c *fiber.Ctx, selections []ast.Selection) bool { - for _, s := range selections { - if field, ok := s.(*ast.Field); ok { - if checkIfContainsIntrospection(c, field.Name.Value) { - return true - } - if field.SelectionSet != nil && checkSelections(c, field.GetSelectionSet().Selections) { - return true - } - } - } - return false -} - -func checkIfContainsIntrospection(c *fiber.Ctx, whatever string) bool { - whateverLower := strings.ToLower(whatever) - mu.RLock() - defer mu.RUnlock() - - if _, exists := introspectionQueries[whateverLower]; exists { - if len(cfg.Security.IntrospectionAllowed) > 0 { - if _, allowed := introspectionAllowedQueries[whateverLower]; allowed { - cfg.Logger.Debug(&libpack_logger.LogMessage{ - Message: "Introspection query allowed, passing through", - Pairs: map[string]interface{}{"query": whatever}, - }) - return false - } - } - if ifNotInTest() { - cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil) - } - _ = c.Status(403).SendString("Introspection queries are not allowed") - return true - } - return false -} diff --git a/graphql_test.go b/graphql_test.go deleted file mode 100644 index f49924a..0000000 --- a/graphql_test.go +++ /dev/null @@ -1,432 +0,0 @@ -package main - -import ( - "fmt" - "strings" - - fiber "github.com/gofiber/fiber/v2" - "github.com/valyala/fasthttp" -) - -func (suite *Tests) Test_parseGraphQLQuery() { - - type results struct { - op_name string - op_type string - cached_ttl int - returnCode int - is_cached bool - shouldBlock bool - shouldIgnore bool - } - - type queries struct { - headers map[string]string - body string - } - - tests := []struct { - name string - suppliedSettings *config - suppliedQuery queries - wantResults results - }{ - { - name: "test empty body", - suppliedQuery: queries{ - body: "", - headers: map[string]string{}, - }, - wantResults: results{ - is_cached: false, - shouldBlock: false, - shouldIgnore: true, - op_name: "", - op_type: "", - }, - }, - - { - name: "test empty json", - suppliedQuery: queries{ - body: "{}", - headers: map[string]string{}, - }, - wantResults: results{ - is_cached: false, - shouldBlock: false, - shouldIgnore: true, - op_name: "", - op_type: "", - }, - }, - - { - name: "test empty with some random garbage", - suppliedQuery: queries{ - body: "{\"variables\": {\"id\": \"1\"}}", - headers: map[string]string{}, - }, - wantResults: results{ - is_cached: false, - shouldBlock: false, - shouldIgnore: true, - op_name: "", - op_type: "", - }, - }, - - { - name: "test valid query with op name", - suppliedQuery: queries{ - body: "{\"query\":\"query MyQuery { tg_users(where: {handle: {_eq: \\\"tozuo\\\"}}) { id __typename } }\"}", - }, - wantResults: results{ - is_cached: false, - shouldBlock: false, - shouldIgnore: false, - op_name: "MyQuery", - op_type: "query", - }, - }, - - { - name: "test valid query with op name, variables and cache", - suppliedQuery: queries{ - body: "{\"query\":\"query MyQuery @cached { tg_users(where: {handle: {_eq: \\\"tozuo\\\"}}) { id __typename } }\", \"variables\": {\"id\": \"1\"}}", - }, - wantResults: results{ - is_cached: true, - shouldBlock: false, - shouldIgnore: false, - op_name: "MyQuery", - op_type: "query", - }, - }, - - { - name: "test valid query with op name, cache and ttl", - suppliedQuery: queries{ - body: "{\"query\":\"query MyQuery @cached(ttl: 60) { tg_users(where: {handle: {_eq: \\\"tozuo\\\"}}) { id __typename } }\", \"variables\": {\"id\": \"1\"}}", - }, - wantResults: results{ - is_cached: true, - cached_ttl: 60, - shouldBlock: false, - shouldIgnore: false, - op_name: "MyQuery", - op_type: "query", - }, - }, - - { - name: "test valid query with op name, force refreshed cache", - suppliedQuery: queries{ - body: "{\"query\":\"query MyQuery @cached(refresh: true) { tg_users(where: {handle: {_eq: \\\"tozuo\\\"}}) { id __typename } }\", \"variables\": {\"id\": \"1\"}}", - }, - wantResults: results{ - is_cached: true, - cached_ttl: 0, - shouldBlock: false, - shouldIgnore: false, - op_name: "MyQuery", - op_type: "query", - }, - }, - - { - name: "test valid query with op name, cache and INVALID ttl", - suppliedQuery: queries{ - body: "{\"query\":\"query MyQuery @cached(ttl: nope) { tg_users(where: {handle: {_eq: \\\"tozuo\\\"}}) { id __typename } }\", \"variables\": {\"id\": \"1\"}}", - }, - wantResults: results{ - is_cached: true, - cached_ttl: 0, - shouldBlock: false, - shouldIgnore: false, - op_name: "MyQuery", - op_type: "query", - }, - }, - - { - name: "test mutation query with op name", - suppliedQuery: queries{ - body: "{\"query\":\"mutation MyMutation { tg_users(where: {handle: {_eq: \\\"tozuo\\\"}}) { id __typename } }\"}", - }, - wantResults: results{ - is_cached: false, - shouldBlock: false, - shouldIgnore: false, - op_name: "MyMutation", - op_type: "mutation", - }, - }, - - { - name: "test mutation query with config: read only", - suppliedSettings: func() *config { - parseConfig() - cfg.Server.ReadOnlyMode = true - return cfg - }(), - suppliedQuery: queries{ - body: "{\"query\":\"mutation MyMutation { tg_users(where: {handle: {_eq: \\\"tozuo\\\"}}) { id __typename } }\"}", - }, - wantResults: results{ - is_cached: false, - shouldBlock: true, - shouldIgnore: false, - op_name: "MyMutation", - op_type: "mutation", - returnCode: 403, - }, - }, - - { - name: "test simple query with introspection __schema", - suppliedQuery: queries{ - body: "{\"query\":\"mutation MyMutation { tg_users(where: {handle: {_eq: \\\"tozuo\\\"}}) { id __schema } }\"}", - }, - wantResults: results{ - is_cached: false, - shouldBlock: false, - shouldIgnore: false, - op_name: "MyMutation", - op_type: "mutation", - }, - }, - - { - name: "test simple query with introspection __schema config: block introspection", - suppliedSettings: func() *config { - parseConfig() - cfg.Security.BlockIntrospection = true - return cfg - }(), - suppliedQuery: queries{ - body: "{\"query\":\"query MyIntroQuery { tg_users(where: {handle: {_eq: \\\"tozuo\\\"}}) { id __schema } }\"}", - }, - wantResults: results{ - is_cached: false, - shouldBlock: true, - shouldIgnore: false, - op_name: "MyIntroQuery", - op_type: "query", - returnCode: 403, - }, - }, - - { - name: "test user supplied query with introspection #1 - config: block", - suppliedSettings: func() *config { - parseConfig() - cfg.Security.BlockIntrospection = true - cfg.Security.IntrospectionAllowed = []string{} - return cfg - }(), - suppliedQuery: queries{ - body: "{\"query\":\"{__schema {queryType {fields {name description}}}}\"}", - }, - wantResults: results{ - is_cached: false, - shouldBlock: true, - shouldIgnore: false, - op_name: "undefined", - op_type: "query", - returnCode: 403, - }, - }, - - { - name: "test user supplied query with introspection #1 - config: block & allow __schema", - suppliedSettings: func() *config { - parseConfig() - cfg.Security.BlockIntrospection = true - cfg.Security.IntrospectionAllowed = []string{"__schema"} - return cfg - }(), - suppliedQuery: queries{ - body: "{\"query\":\"{__schema {queryType {fields {name description}}}}\"}", - }, - wantResults: results{ - is_cached: false, - shouldBlock: false, - shouldIgnore: false, - op_name: "undefined", - op_type: "query", - returnCode: 200, - }, - }, - - { - name: "test invalid query", - suppliedQuery: queries{ - body: "{\"query\":\"query MyQuery tg_users(where: {handle: {_eq: \\\"tozuo\\\"}}) { id __typename } \"}", - }, - wantResults: results{ - is_cached: false, - shouldBlock: false, - shouldIgnore: true, - op_name: "", - op_type: "", - }, - }, - } - - for _, tt := range tests { - suite.Run(tt.name, func() { - cfg = &config{} - parseConfig() - ctx_headers := func() *fasthttp.RequestHeader { - h := fasthttp.RequestHeader{} - for k, v := range tt.suppliedQuery.headers { - h.Add(k, v) - } - return &h - }() - - ctx_request := fasthttp.Request{ - Header: *ctx_headers, - } - - ctx_request.AppendBody([]byte(tt.suppliedQuery.body)) - - ctx := suite.app.AcquireCtx(&fasthttp.RequestCtx{ - Request: ctx_request, - }) - - // defer func() { - // cfg = &config{} - // parseConfig() - // suite.app.ReleaseCtx(ctx) - // }() - - assert.NotNil(ctx, "Fiber context is nil") - - if tt.suppliedSettings != nil { - cfg = tt.suppliedSettings - } - prepareQueriesAndExemptions() - parseResult := parseGraphQLQuery(ctx) - assert.Equal(tt.wantResults.op_type, parseResult.operationType, "Unexpected operation type "+tt.name) - assert.Equal(tt.wantResults.op_name, parseResult.operationName, "Unexpected operation name "+tt.name) - assert.Equal(tt.wantResults.is_cached, parseResult.cacheRequest, "Unexpected cache value "+tt.name) - assert.Equal(tt.wantResults.cached_ttl, parseResult.cacheTime, "Unexpected cache TTL value "+tt.name) - assert.Equal(tt.wantResults.shouldBlock, parseResult.shouldBlock, "Unexpected block value "+tt.name) - assert.Equal(tt.wantResults.shouldIgnore, parseResult.shouldIgnore, "Unexpected ignore value "+tt.name) - - if tt.wantResults.returnCode > 0 { - assert.Equal(tt.wantResults.returnCode, ctx.Response().StatusCode(), "Unexpected return code", tt.name) - } - }) - } -} - -func (suite *Tests) Test_parseGraphQLQuery_complex() { - // ... existing tests ... - - // Add these new test cases - suite.Run("test complex query with multiple operations", func() { - query := ` - query GetUser($id: ID!) { - user(id: $id) { - name - email - } - } - mutation UpdateUser($id: ID!, $name: String!) { - updateUser(id: $id, name: $name) { - id - name - } - } - ` - body := fmt.Sprintf(`{"query": %q}`, query) - ctx := createTestContext(body) - result := parseGraphQLQuery(ctx) - assert.Equal("query", result.operationType) - assert.Equal("GetUser", result.operationName) - assert.False(result.shouldBlock) - }) - - suite.Run("test query with custom directives", func() { - query := ` - query GetUser($id: ID!) @custom(directive: "value") { - user(id: $id) { - name - email - } - } - ` - body := fmt.Sprintf(`{"query": %q}`, query) - ctx := createTestContext(body) - result := parseGraphQLQuery(ctx) - assert.Equal("query", result.operationType) - assert.Equal("GetUser", result.operationName) - assert.False(result.shouldBlock) - assert.False(result.shouldBlock) - }) -} - -func (suite *Tests) Test_checkAllowedURLs() { - tests := []struct { - name string - path string - allowed []string - expected bool - }{ - {"allowed path", "/v1/graphql", []string{"/v1/graphql"}, true}, - {"disallowed path", "/v2/graphql", []string{"/v1/graphql"}, false}, - {"empty allowed list", "/v1/graphql", []string{}, true}, - {"multiple allowed paths", "/v2/graphql", []string{"/v1/graphql", "/v2/graphql"}, true}, - } - - for _, tt := range tests { - suite.Run(tt.name, func() { - allowedUrls = make(map[string]struct{}) - for _, url := range tt.allowed { - allowedUrls[url] = struct{}{} - } - app := fiber.New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - ctx.Request().SetRequestURI(tt.path) - ctx.Request().URI().SetPath(tt.path) - result := checkAllowedURLs(ctx) - assert.Equal(tt.expected, result) - }) - } -} - -func (suite *Tests) Test_checkIfContainsIntrospection() { - tests := []struct { - name string - query string - allowed []string - expected bool - }{ - {"allowed introspection", "__schema", []string{"__schema"}, false}, - {"disallowed introspection", "__type", []string{"__schema"}, true}, - {"non-introspection query", "normalQuery", []string{}, false}, - } - - for _, tt := range tests { - suite.Run(tt.name, func() { - cfg.Security.IntrospectionAllowed = tt.allowed - introspectionAllowedQueries = make(map[string]struct{}) - for _, q := range tt.allowed { - introspectionAllowedQueries[strings.ToLower(q)] = struct{}{} - } - ctx := createTestContext("") - result := checkIfContainsIntrospection(ctx, tt.query) - assert.Equal(tt.expected, result) - }) - } -} - -func createTestContext(body string) *fiber.Ctx { - app := fiber.New() - ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) - ctx.Request().SetBody([]byte(body)) - return ctx -} diff --git a/logging/logger.go b/logging/logger.go deleted file mode 100644 index 7b1a964..0000000 --- a/logging/logger.go +++ /dev/null @@ -1,204 +0,0 @@ -package libpack_logger - -import ( - "bytes" - "flag" - "fmt" - "io" - "os" - "path/filepath" - "runtime" - "strings" - "sync" - "time" - - "github.com/goccy/go-json" -) - -const ( - _ = iota - LEVEL_DEBUG - LEVEL_INFO - LEVEL_WARN - LEVEL_ERROR - LEVEL_FATAL -) - -var LevelNames = [...]string{ - "none", - "debug", - "info", - "warn", - "error", - "fatal", -} - -const ( - defaultFormat = time.RFC3339 - defaultMinLevel = LEVEL_INFO - defaultShowCaller = false -) - -var defaultOutput = os.Stdout - -type Logger struct { - output io.Writer - format string - minLogLevel int - showCaller bool -} - -type LogMessage struct { - output io.Writer - Pairs map[string]any - Message string -} - -func (m *LogMessage) String() string { - return m.Message -} - -var fieldNames = map[string]string{ - "timestamp": "timestamp", - "level": "level", - "message": "message", -} - -func New() *Logger { - return &Logger{ - format: defaultFormat, - minLogLevel: defaultMinLevel, - output: defaultOutput, - showCaller: defaultShowCaller, - } -} - -func (l *Logger) SetOutput(output io.Writer) *Logger { - l.output = output - return l -} - -var bufferPool = sync.Pool{ - New: func() any { - return new(bytes.Buffer) - }, -} - -var defaultPairs = make(map[string]any) - -func GetLogLevel(level string) int { - for i, name := range LevelNames { - if name == strings.ToLower(level) { - return i - } - } - return defaultMinLevel -} - -func (l *Logger) log(level int, m *LogMessage) { - if m.Pairs == nil { - m.Pairs = defaultPairs - } - - m.Pairs[fieldNames["timestamp"]] = time.Now().Format(l.format) - m.Pairs[fieldNames["level"]] = LevelNames[level] - m.Pairs[fieldNames["message"]] = m.Message - - if l.showCaller { - m.Pairs["caller"] = getCaller() - } - - buffer := bufferPool.Get().(*bytes.Buffer) - defer bufferPool.Put(buffer) - buffer.Reset() - - var encoder = json.NewEncoder(buffer) - err := encoder.Encode(m.Pairs) - if err != nil { - fmt.Println("Error marshalling log message:", err) - return - } - - // if not running in test - use stderr and stdout, otherwise - use logger's output setting - if flag.Lookup("test.v") != nil { - m.output = os.Stdout - if level >= LEVEL_ERROR { - m.output = os.Stderr - } - } - - // Use logger's output setting instead of os.Stdout or os.Stderr - l.output.Write(buffer.Bytes()) -} - -func (l *Logger) Debug(m *LogMessage) { - if l.shouldLog(LEVEL_DEBUG) { - l.log(LEVEL_DEBUG, m) - } -} - -func (l *Logger) Info(m *LogMessage) { - if l.shouldLog(LEVEL_INFO) { - l.log(LEVEL_INFO, m) - } -} - -func (l *Logger) Warn(m *LogMessage) { - if l.shouldLog(LEVEL_WARN) { - l.log(LEVEL_WARN, m) - } -} - -func (l *Logger) Warning(m *LogMessage) { - l.Warn(m) -} - -func (l *Logger) Error(m *LogMessage) { - if l.shouldLog(LEVEL_ERROR) { - l.log(LEVEL_ERROR, m) - } -} - -func (l *Logger) Fatal(m *LogMessage) { - if l.shouldLog(LEVEL_FATAL) { - l.log(LEVEL_FATAL, m) - } -} - -func (l *Logger) Critical(m *LogMessage) { - l.Fatal(m) - os.Exit(1) -} - -func (l *Logger) shouldLog(level int) bool { - return level >= l.minLogLevel -} - -func (l *Logger) SetFormat(format string) *Logger { - l.format = format - return l -} - -func (l *Logger) SetMinLogLevel(level int) *Logger { - l.minLogLevel = level - return l -} - -func (l *Logger) SetFieldName(field, name string) *Logger { - fieldNames[field] = name - return l -} - -func (l *Logger) SetShowCaller(show bool) *Logger { - l.showCaller = show - return l -} - -func getCaller() string { - _, file, line, ok := runtime.Caller(3) - if !ok { - return "unknown:0" - } - file = filepath.Base(file) - return fmt.Sprintf("%s:%d", file, line) -} diff --git a/logging/logger_bench_test.go b/logging/logger_bench_test.go deleted file mode 100644 index 9d92425..0000000 --- a/logging/logger_bench_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package libpack_logger - -import ( - "bytes" - "testing" - "time" -) - -func Benchmark_NewLogger(b *testing.B) { - type triggers struct { - ModFormat struct { - Format string - } - ModLevel struct { - Level int - } - } - - tests := []struct { - name string - triggers triggers - }{ - { - name: "BenchmarkNew", - }, - { - name: "BenchmarkNewChangeTimeFormat", - triggers: triggers{ - ModFormat: struct{ Format string }{ - Format: time.RFC3339Nano, - }, - }, - }, - { - name: "BenchmarkNewChangeLogLevel", - triggers: triggers{ - ModLevel: struct{ Level int }{ - Level: LEVEL_DEBUG, - }, - }, - }, - { - name: "BenchmarkNewChangeTimeFormatAndLogLevel", - triggers: triggers{ - ModFormat: struct{ Format string }{ - Format: time.RFC3339Nano, - }, - ModLevel: struct{ Level int }{ - Level: LEVEL_DEBUG, - }, - }, - }, - } - - for _, tt := range tests { - b.Run(tt.name, func(b *testing.B) { - for i := 0; i < b.N; i++ { - got := New() - - if tt.triggers.ModFormat.Format != "" { - got = got.SetFormat(tt.triggers.ModFormat.Format) - } - - if tt.triggers.ModLevel.Level != 0 { - got = got.SetMinLogLevel(tt.triggers.ModLevel.Level) - } - } - }) - } -} - -func Benchmark_Log_Debug(b *testing.B) { - output := &bytes.Buffer{} - logger := New().SetMinLogLevel(LEVEL_DEBUG).SetOutput(output) - msg := &LogMessage{ - Message: "debug message", - Pairs: make(map[string]any), - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - logger.Debug(msg) - } -} - -func Benchmark_Log_Info(b *testing.B) { - output := &bytes.Buffer{} - logger := New().SetMinLogLevel(LEVEL_INFO).SetOutput(output) - msg := &LogMessage{ - Message: "info message", - Pairs: make(map[string]any), - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - logger.Info(msg) - } -} - -func Benchmark_Log_Warn(b *testing.B) { - output := &bytes.Buffer{} - logger := New().SetMinLogLevel(LEVEL_WARN).SetOutput(output) - msg := &LogMessage{ - Message: "warn message", - Pairs: make(map[string]any), - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - logger.Warn(msg) - } -} - -func Benchmark_Log_Error(b *testing.B) { - output := &bytes.Buffer{} - logger := New().SetMinLogLevel(LEVEL_ERROR).SetOutput(output) - msg := &LogMessage{ - Message: "error message", - Pairs: map[string]any{"key": "value"}, - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - logger.Error(msg) - } -} - -func Benchmark_Log_Fatal(b *testing.B) { - output := &bytes.Buffer{} - logger := New().SetMinLogLevel(LEVEL_FATAL).SetOutput(output) - msg := &LogMessage{ - Message: "fatal message", - Pairs: make(map[string]any), - } - - b.ResetTimer() - for i := 0; i < b.N; i++ { - logger.Fatal(msg) - } -} diff --git a/logging/logger_suite_test.go b/logging/logger_suite_test.go deleted file mode 100644 index 965e7f4..0000000 --- a/logging/logger_suite_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package libpack_logger - -import ( - "testing" - - assertions "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -type LoggerTestSuite struct { - suite.Suite -} - -var ( - assert *assertions.Assertions -) - -func (suite *LoggerTestSuite) BeforeTest(suiteName, testName string) { -} - -func (suite *LoggerTestSuite) SetupTest() { - assert = assertions.New(suite.T()) -} - -// TearDownTest is run after each test to clean up -func (suite *LoggerTestSuite) TearDownTest() { -} - -func TestSuite(t *testing.T) { - suite.Run(t, new(LoggerTestSuite)) -} diff --git a/logging/logger_test.go b/logging/logger_test.go deleted file mode 100644 index 92d04a9..0000000 --- a/logging/logger_test.go +++ /dev/null @@ -1,182 +0,0 @@ -package libpack_logger - -import ( - "bytes" - "fmt" - "os" - "reflect" - "testing" - "time" - - "github.com/goccy/go-json" -) - -func captureStderr(f func()) string { - originalStderr := os.Stderr - r, w, _ := os.Pipe() - os.Stderr = w - f() - w.Close() - var buf bytes.Buffer - buf.ReadFrom(r) - os.Stderr = originalStderr - return buf.String() -} - -func captureStdOut(f func()) string { - originalStdout := os.Stdout - r, w, _ := os.Pipe() - os.Stdout = w - f() - w.Close() - var buf bytes.Buffer - buf.ReadFrom(r) - os.Stdout = originalStdout - return buf.String() -} - -func (suite *LoggerTestSuite) Test_LogMessageString() { - msg := &LogMessage{ - Message: "test message", - } - - assert.Equal("test message", msg.String()) -} - -func callLoggerMethod(logger *Logger, methodName string, message *LogMessage) { - // Get the method by name using reflection - method := reflect.ValueOf(logger).MethodByName(methodName) - if method.IsValid() { - // Call the method with the message as an argument - method.Call([]reflect.Value{reflect.ValueOf(message)}) - } else { - fmt.Printf("Method %s does not exist on Logger\n", methodName) - } -} - -func (suite *LoggerTestSuite) Test_LogsLevelsPrint() { - output := &bytes.Buffer{} - logger := New().SetOutput(output) - - tests := []struct { - pairs map[string]any - name string - method string - message string - loggerMinLevel int - messageLogLevel int - wantOutput bool - }{ - { - name: "Log: Debug, Level: Debug - no pairs", - method: "Debug", - loggerMinLevel: LEVEL_DEBUG, - messageLogLevel: LEVEL_DEBUG, - message: "debug message", - wantOutput: true, - }, - { - name: "Log: Info, Level: Info - one pair", - method: "Info", - loggerMinLevel: LEVEL_INFO, - messageLogLevel: LEVEL_INFO, - message: "info message", - pairs: map[string]any{ - "key": "value", - }, - wantOutput: true, - }, - { - name: "Log: Info, Level: Warn - with pairs", - method: "Info", - loggerMinLevel: LEVEL_WARN, - messageLogLevel: LEVEL_INFO, - message: "warn message", - pairs: map[string]any{ - "key1": "value1", - "key2": "value2", - }, - wantOutput: false, - }, - { - name: "Log: Warn, Level: Info - with 500 pairs", - method: "Warn", - loggerMinLevel: LEVEL_INFO, - messageLogLevel: LEVEL_WARN, - message: "warn message with 500 pairs", - pairs: func() map[string]any { - pairs := make(map[string]any) - for i := 0; i < 500; i++ { - pairs[fmt.Sprintf("key%d", i)] = fmt.Sprintf("value%d", i) - } - return pairs - }(), - wantOutput: true, - }, - } - - for _, tt := range tests { - suite.T().Run(tt.name, func(t *testing.T) { - msg := &LogMessage{ - Message: tt.message, - Pairs: tt.pairs, - } - output.Reset() - - // Set logger's minimum log level - logger.SetMinLogLevel(tt.loggerMinLevel) - fmt.Println("Logger min log level:", LevelNames[logger.minLogLevel]) - - // Call the logging method - callLoggerMethod(logger, tt.method, msg) - - logOutput := output.String() - fmt.Println("Output:", logOutput) - - if tt.wantOutput { - var loggedMessage map[string]any - err := json.Unmarshal([]byte(logOutput), &loggedMessage) - if err != nil { - t.Fatalf("Error unmarshalling log message: %v\nLog output: %s", err, logOutput) - } - - if !containsLogMessage(logOutput, tt.message) { - t.Errorf("Expected log message %q, but got %q", tt.message, logOutput) - } - assert.Equal(LevelNames[tt.messageLogLevel], loggedMessage["level"]) - if tt.pairs != nil { - for k, v := range tt.pairs { - assert.Equal(v, loggedMessage[k]) - } - } - } else { - assert.Equal("", logOutput) - } - }) - } -} - -func containsLogMessage(logOutput, expectedMessage string) bool { - return bytes.Contains([]byte(logOutput), []byte(expectedMessage)) -} - -func (suite *LoggerTestSuite) Test_SetFormat() { - logger := New().SetFormat(time.RFC3339Nano) - - assert.Equal(time.RFC3339Nano, logger.format) -} - -func (suite *LoggerTestSuite) Test_SetMinLogLevel() { - logger := New().SetMinLogLevel(LEVEL_DEBUG) - - assert.Equal(LEVEL_DEBUG, logger.minLogLevel) -} - -func (suite *LoggerTestSuite) Test_ShouldLog() { - logger := New().SetMinLogLevel(LEVEL_WARN) - - assert.True(logger.shouldLog(LEVEL_WARN)) - assert.True(logger.shouldLog(LEVEL_ERROR)) - assert.False(logger.shouldLog(LEVEL_INFO)) - assert.False(logger.shouldLog(LEVEL_DEBUG)) -} diff --git a/main.go b/main.go deleted file mode 100644 index 66bf629..0000000 --- a/main.go +++ /dev/null @@ -1,122 +0,0 @@ -package main - -import ( - "flag" - "os" - "strings" - "sync" - - "github.com/gofiber/fiber/v2/middleware/proxy" - "github.com/gookit/goutil/envutil" - graphql "github.com/lukaszraczylo/go-simple-graphql" - libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache" - libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config" - libpack_logging "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" -) - -var cfg *config -var once sync.Once - -// function get value from the env where the value can be anything -func getDetailsFromEnv[T any](key string, defaultValue T) T { - var result any - if _, ok := os.LookupEnv("GMP_" + key); ok { - key = "GMP_" + key - } - switch v := any(defaultValue).(type) { - case string: - result = envutil.Getenv(key, v) - case int: - result = envutil.GetInt(key, v) - case bool: - result = envutil.GetBool(key, v) - default: - result = defaultValue - } - return result.(T) -} - -func parseConfig() { - libpack_config.PKG_NAME = "graphql_proxy" - c := config{} - c.Server.PortGraphQL = getDetailsFromEnv("PORT_GRAPHQL", 8080) - c.Server.PortMonitoring = getDetailsFromEnv("MONITORING_PORT", 9393) - c.Server.HostGraphQL = getDetailsFromEnv("HOST_GRAPHQL", "http://localhost/") - c.Server.HostGraphQLReadOnly = getDetailsFromEnv("HOST_GRAPHQL_READONLY", "") - c.Client.JWTUserClaimPath = getDetailsFromEnv("JWT_USER_CLAIM_PATH", "") - c.Client.JWTRoleClaimPath = getDetailsFromEnv("JWT_ROLE_CLAIM_PATH", "") - c.Client.RoleFromHeader = getDetailsFromEnv("ROLE_FROM_HEADER", "") - c.Client.RoleRateLimit = getDetailsFromEnv("ROLE_RATE_LIMIT", false) - /* in-memory cache */ - c.Cache.CacheEnable = getDetailsFromEnv("ENABLE_GLOBAL_CACHE", false) - c.Cache.CacheTTL = getDetailsFromEnv("CACHE_TTL", 60) - /* redis cache */ - c.Cache.CacheRedisEnable = getDetailsFromEnv("ENABLE_REDIS_CACHE", false) - c.Cache.CacheRedisURL = getDetailsFromEnv("CACHE_REDIS_URL", "localhost:6379") - c.Cache.CacheRedisPassword = getDetailsFromEnv("CACHE_REDIS_PASSWORD", "") - c.Cache.CacheRedisDB = getDetailsFromEnv("CACHE_REDIS_DB", 0) - c.Security.BlockIntrospection = getDetailsFromEnv("BLOCK_SCHEMA_INTROSPECTION", false) - c.Security.IntrospectionAllowed = func() []string { - urls := getDetailsFromEnv("ALLOWED_INTROSPECTION", "") - if urls == "" { - return nil - } - return strings.Split(urls, ",") - }() - c.LogLevel = strings.ToUpper(getDetailsFromEnv("LOG_LEVEL", "info")) - c.Logger = libpack_logging.New().SetMinLogLevel(libpack_logging.GetLogLevel(c.LogLevel)).SetFieldName("timestamp", "ts").SetFieldName("message", "msg").SetShowCaller(false) - c.Server.HealthcheckGraphQL = getDetailsFromEnv("HEALTHCHECK_GRAPHQL_URL", "") - c.Client.GQLClient = graphql.NewConnection() - c.Client.GQLClient.SetEndpoint(c.Server.HealthcheckGraphQL) - c.Server.AccessLog = getDetailsFromEnv("ENABLE_ACCESS_LOG", false) - c.Server.ReadOnlyMode = getDetailsFromEnv("READ_ONLY_MODE", false) - c.Server.AllowURLs = func() []string { - urls := getDetailsFromEnv("ALLOWED_URLS", "") - if urls == "" { - return nil - } - return strings.Split(urls, ",") - }() - c.Client.ClientTimeout = getDetailsFromEnv("PROXIED_CLIENT_TIMEOUT", 120) - c.Client.FastProxyClient = createFasthttpClient(c.Client.ClientTimeout) - proxy.WithClient(c.Client.FastProxyClient) // setting the global proxy client here instead of per request - c.Server.EnableApi = getDetailsFromEnv("ENABLE_API", false) - c.Server.ApiPort = getDetailsFromEnv("API_PORT", 9090) - c.Api.BannedUsersFile = getDetailsFromEnv("BANNED_USERS_FILE", "/go/src/app/banned_users.json") - c.Server.PurgeOnCrawl = getDetailsFromEnv("PURGE_METRICS_ON_CRAWL", false) - c.Server.PurgeEvery = getDetailsFromEnv("PURGE_METRICS_ON_TIMER", 0) - c.HasuraEventCleaner.Enable = getDetailsFromEnv("HASURA_EVENT_CLEANER", false) - c.HasuraEventCleaner.ClearOlderThan = getDetailsFromEnv("HASURA_EVENT_CLEANER_OLDER_THAN", 1) - c.HasuraEventCleaner.EventMetadataDb = getDetailsFromEnv("HASURA_EVENT_METADATA_DB", "") - cfg = &c - - if cfg.Cache.CacheEnable || cfg.Cache.CacheRedisEnable { - cacheConfig := &libpack_cache.CacheConfig{ - Logger: cfg.Logger, - TTL: cfg.Cache.CacheTTL, - } - if cfg.Cache.CacheRedisEnable { - cacheConfig.Redis.Enable = true - cacheConfig.Redis.URL = cfg.Cache.CacheRedisURL - cacheConfig.Redis.Password = cfg.Cache.CacheRedisPassword - cacheConfig.Redis.DB = cfg.Cache.CacheRedisDB - } - libpack_cache.EnableCache(cacheConfig) - } - loadRatelimitConfig() - once.Do(func() { - go enableApi() - go enableHasuraEventCleaner() - }) - prepareQueriesAndExemptions() -} - -func main() { - parseConfig() - StartMonitoringServer() - StartHTTPProxy() -} - -func ifNotInTest() bool { - return flag.Lookup("test.v") == nil -} diff --git a/main_test.go b/main_test.go deleted file mode 100644 index e2ea74c..0000000 --- a/main_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package main - -import ( - "os" - "testing" - "time" - - "github.com/goccy/go-json" - "github.com/gofiber/fiber/v2" - libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/memory" - libpack_logging "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" - assertions "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -type Tests struct { - suite.Suite - app *fiber.App -} - -var ( - assert *assertions.Assertions -) - -func (suite *Tests) BeforeTest(suiteName, testName string) { -} - -func (suite *Tests) SetupTest() { - assert = assertions.New(suite.T()) - suite.app = fiber.New( - fiber.Config{ - DisableStartupMessage: true, - JSONEncoder: json.Marshal, - JSONDecoder: json.Unmarshal, - }, - ) - - // Initialize a simple in-memory cache client for testing purposes - libpack_cache.New(5 * time.Minute) - parseConfig() - enableApi() - StartMonitoringServer() - cfg.Logger = libpack_logging.New().SetMinLogLevel(libpack_logging.GetLogLevel(getDetailsFromEnv("LOG_LEVEL", "info"))) - // Setup environment variables here if needed - os.Setenv("GMP_TEST_STRING", "testValue") - os.Setenv("GMP_TEST_INT", "123") - os.Setenv("GMP_TEST_BOOL", "true") - os.Setenv("NON_GMP_TEST_INT", "31337") -} - -// TearDownTest is run after each test to clean up -func (suite *Tests) TearDownTest() { - // Clean up environment variables here if needed - os.Unsetenv("GMP_TEST_STRING") - os.Unsetenv("GMP_TEST_INT") - os.Unsetenv("GMP_TEST_BOOL") - os.Unsetenv("NON_GMP_TEST_INT") -} - -// func (suite *Tests) AfterTest(suiteName, testName string) {) - -func TestSuite(t *testing.T) { - cfg = &config{} - parseConfig() - StartMonitoringServer() - suite.Run(t, new(Tests)) -} - -func (suite *Tests) Test_envVariableSetting() { - tests := []struct { - defaultValue any - expected any - name string - envKey string - }{ - { - name: "test_string", - envKey: "TEST_STRING", - defaultValue: "default", - expected: "testValue", - }, - { - name: "test_int", - envKey: "TEST_INT", - defaultValue: 0, - expected: 123, - }, - { - name: "test_bool", - envKey: "TEST_BOOL", - defaultValue: false, - expected: true, - }, - { - name: "test_non_prefixed", - envKey: "NON_GMP_TEST_INT", - defaultValue: 0, - expected: 31337, - }, - { - name: "test_non_existing", - envKey: "NON_EXISTING", - defaultValue: "default_val", - expected: "default_val", - }, - } - - for _, tt := range tests { - suite.Run(tt.name, func() { - result := getDetailsFromEnv(tt.envKey, tt.defaultValue) - assert.Equal(tt.expected, result) - }) - } -} - -func (suite *Tests) Test_getDetailsFromEnv() { - tests := []struct { - name string - key string - defaultValue interface{} - envValue string - expected interface{} - }{ - {"string value", "TEST_STRING", "default", "envValue", "envValue"}, - {"int value", "TEST_INT", 0, "123", 123}, - {"bool value", "TEST_BOOL", false, "true", true}, - {"default value", "NON_EXISTENT", "default", "", "default"}, - } - - for _, tt := range tests { - suite.Run(tt.name, func() { - if tt.envValue != "" { - os.Setenv("GMP_"+tt.key, tt.envValue) - defer os.Unsetenv("GMP_" + tt.key) - } - result := getDetailsFromEnv(tt.key, tt.defaultValue) - assert.Equal(tt.expected, result) - }) - } -} diff --git a/monitoring.go b/monitoring.go deleted file mode 100644 index bad9af8..0000000 --- a/monitoring.go +++ /dev/null @@ -1,11 +0,0 @@ -package main - -import ( - libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring" -) - -func StartMonitoringServer() { - cfg.Monitoring = libpack_monitoring.NewMonitoring(&libpack_monitoring.InitConfig{PurgeOnCrawl: cfg.Server.PurgeOnCrawl, PurgeEvery: cfg.Server.PurgeEvery}) - cfg.Monitoring.AddMetricsPrefix("graphql_proxy") - cfg.Monitoring.RegisterDefaultMetrics() -} diff --git a/monitoring/defaults.go b/monitoring/defaults.go deleted file mode 100644 index f0e7ee7..0000000 --- a/monitoring/defaults.go +++ /dev/null @@ -1,15 +0,0 @@ -package libpack_monitoring - -func (ms *MetricsSetup) RegisterDefaultMetrics() { - ms.RegisterMetricsCounter(MetricsSucceeded, nil) - ms.RegisterMetricsCounter(MetricsFailed, nil) - ms.RegisterMetricsCounter(MetricsSkipped, nil) - ms.RegisterMetricsHistogram(MetricsDuration, nil) - ms.RegisterMetricsCounter(MetricsCacheHit, nil) - ms.RegisterMetricsCounter(MetricsCacheMiss, nil) - ms.RegisterMetricsCounter(MetricsQueriesCached, nil) -} - -func (ms *MetricsSetup) RegisterGoMetrics() { - // TODO: metrics.WriteProcessMetrics(ms.metrics_set) -} diff --git a/monitoring/helpers.go b/monitoring/helpers.go deleted file mode 100644 index 4773866..0000000 --- a/monitoring/helpers.go +++ /dev/null @@ -1,173 +0,0 @@ -package libpack_monitoring - -import ( - "bytes" - "fmt" - "os" - "sort" - "strings" - "sync" - "unicode" - - libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config" -) - -var sortedLabelKeysCache = struct { - m sync.Map -}{} - -func (ms *MetricsSetup) get_metrics_name(name string, labels map[string]string) string { - const unknownPodName = "unknown" - var buf bytes.Buffer - - podName := getPodName() - if labels == nil { - labels = defaultLabels(podName) - } else { - ensureDefaultLabels(&labels, podName) - } - - if ms.metrics_prefix != "" { - buf.WriteString(ms.metrics_prefix) - buf.WriteByte('_') - } - buf.WriteString(name) - - if len(labels) > 0 { - buf.WriteByte('{') - appendSortedLabels(&buf, labels) - buf.WriteByte('}') - } - - return buf.String() -} - -func getPodName() string { - const unknownPodName = "unknown" - if hn, err := os.Hostname(); err == nil { - return hn - } - return unknownPodName -} - -func defaultLabels(podName string) map[string]string { - return map[string]string{ - "microservice": libpack_config.PKG_NAME, - "pod": podName, - } -} - -func ensureDefaultLabels(labels *map[string]string, podName string) { - if *labels == nil { - *labels = make(map[string]string) - } - if _, exists := (*labels)["microservice"]; !exists { - (*labels)["microservice"] = libpack_config.PKG_NAME - } - if _, exists := (*labels)["pod"]; !exists { - (*labels)["pod"] = podName - } -} - -func appendSortedLabels(buf *bytes.Buffer, labels map[string]string) { - keys := getSortedKeys(labels) - for i, k := range keys { - if i > 0 { - buf.WriteByte(',') - } - buf.WriteString(k) - buf.WriteString(`="`) - buf.WriteString(labels[k]) - buf.WriteByte('"') - } -} - -func getSortedKeys(labels map[string]string) []string { - labelsKey := labelsToString(labels) - - if keys, ok := sortedLabelKeysCache.m.Load(labelsKey); ok { - return keys.([]string) - } - - keys := make([]string, 0, len(labels)) - for k := range labels { - keys = append(keys, k) - } - sort.Strings(keys) - - sortedLabelKeysCache.m.Store(labelsKey, keys) - - return keys -} - -func labelsToString(labels map[string]string) string { - var sb strings.Builder - for k, v := range labels { - sb.WriteString(k) - sb.WriteByte('=') - sb.WriteString(v) - sb.WriteByte(';') - } - return sb.String() -} - -func validate_metrics_name(name string) error { - cleanedName := clean_metric_name(name) - - finalName := strings.Trim(cleanedName, "_") - - if finalName != name { - return fmt.Errorf("invalid metric name: %s, expected %s", name, finalName) - } - return nil -} - -func clean_metric_name(name string) string { - var buf bytes.Buffer - lastWasUnderscore := false - - for _, r := range name { - if is_allowed_rune(r) { - if is_special_rune(r) { - if lastWasUnderscore { - continue - } - r = '_' - lastWasUnderscore = true - } else { - lastWasUnderscore = false - } - buf.WriteRune(r) - } else if !lastWasUnderscore { - buf.WriteByte('_') - lastWasUnderscore = true - } - } - - return strings.Trim(buf.String(), "_") -} - -func is_allowed_rune(r rune) bool { - return unicode.IsLetter(r) || unicode.IsDigit(r) || r == ' ' || r == '_' -} - -func is_special_rune(r rune) bool { - return r == ' ' || r == '_' -} - -func compile_metrics_with_labels(name string, labels map[string]string) string { - var buf bytes.Buffer - - buf.WriteString(name) - - keys := getSortedKeys(labels) - - for _, k := range keys { - buf.WriteByte('_') - buf.WriteString(k) - buf.WriteByte('_') - buf.WriteString(labels[k]) - } - - return buf.String() -} diff --git a/monitoring/helpers_bench_test.go b/monitoring/helpers_bench_test.go deleted file mode 100644 index 84791f5..0000000 --- a/monitoring/helpers_bench_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package libpack_monitoring - -import ( - "testing" - - libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config" -) - -func BenchmarkGetMetricsName(b *testing.B) { - // Setup environment - libpack_config.PKG_NAME = "test_service" - - ms := &MetricsSetup{metrics_prefix: "test_prefix"} - - labels := map[string]string{ - "env": "production", - "region": "us-west-2", - } - - // Run the benchmark - for n := 0; n < b.N; n++ { - ms.get_metrics_name("request_count", labels) - } -} - -func BenchmarkCompileMetricsWithLabels(b *testing.B) { - labels := map[string]string{ - "env": "production", - "region": "us-west-2", - "app": "api-server", - } - - for n := 0; n < b.N; n++ { - compile_metrics_with_labels("request_count", labels) - } -} - -func BenchmarkValidateMetricsName(b *testing.B) { - input := "valid metric name with special chars @#! and underscores__" - - for n := 0; n < b.N; n++ { - validate_metrics_name(input) - } -} diff --git a/monitoring/helpers_test.go b/monitoring/helpers_test.go deleted file mode 100644 index f3f0981..0000000 --- a/monitoring/helpers_test.go +++ /dev/null @@ -1,228 +0,0 @@ -package libpack_monitoring - -import ( - "testing" - - libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config" - "github.com/stretchr/testify/assert" -) - -func TestGetMetricsName(t *testing.T) { - ms := &MetricsSetup{metrics_prefix: "prefix"} - libpack_config.PKG_NAME = "example_microservice" - - tests := []struct { - name string - metricName string - labels map[string]string - expectedOutput string - }{ - { - name: "No labels", - metricName: "test_metric", - labels: nil, - expectedOutput: "prefix_test_metric{microservice=\"example_microservice\",pod=\"" + getPodName() + "\"}", - }, - { - name: "With labels", - metricName: "test_metric", - labels: map[string]string{ - "label1": "value1", - "label2": "value2", - }, - expectedOutput: "prefix_test_metric{label1=\"value1\",label2=\"value2\",microservice=\"example_microservice\",pod=\"" + getPodName() + "\"}", - }, - { - name: "Alphabetical order labels", - metricName: "test_metric", - labels: map[string]string{ - "label2": "value2", - "label1": "value1", - }, - expectedOutput: "prefix_test_metric{label1=\"value1\",label2=\"value2\",microservice=\"example_microservice\",pod=\"" + getPodName() + "\"}", - }, - { - name: "Empty metric name", - metricName: "", - labels: nil, - expectedOutput: "prefix_{microservice=\"example_microservice\",pod=\"" + getPodName() + "\"}", - }, - { - name: "Empty labels map", - metricName: "test_metric", - labels: map[string]string{}, - expectedOutput: "prefix_test_metric{microservice=\"example_microservice\",pod=\"" + getPodName() + "\"}", - }, - { - name: "Single label", - metricName: "test_metric", - labels: map[string]string{ - "label1": "value1", - }, - expectedOutput: "prefix_test_metric{label1=\"value1\",microservice=\"example_microservice\",pod=\"" + getPodName() + "\"}", - }, - { - name: "Multiple labels with special characters", - metricName: "test_metric", - labels: map[string]string{ - "label-2": "value-2", - "label_1": "value_1", - }, - expectedOutput: "prefix_test_metric{label-2=\"value-2\",label_1=\"value_1\",microservice=\"example_microservice\",pod=\"" + getPodName() + "\"}", - }, - { - name: "Prefix only", - metricName: "", - labels: map[string]string{ - "label1": "value1", - }, - expectedOutput: "prefix_{label1=\"value1\",microservice=\"example_microservice\",pod=\"" + getPodName() + "\"}", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := ms.get_metrics_name(tt.metricName, tt.labels) - assert.Equal(t, tt.expectedOutput, result) - }) - } -} - -func TestCompileMetricsWithLabels(t *testing.T) { - tests := []struct { - name string - labels map[string]string - want string - }{ - {"request_count", map[string]string{"env": "production", "region": "us-west-2"}, "request_count_env_production_region_us-west-2"}, - {"metric_name", map[string]string{}, "metric_name"}, - {"metric_name", nil, "metric_name"}, - {"metric_name", map[string]string{"key1": "value1"}, "metric_name_key1_value1"}, - {"metric_name", map[string]string{"k": "v", "key2": "value2"}, "metric_name_k_v_key2_value2"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := compile_metrics_with_labels(tt.name, tt.labels); got != tt.want { - t.Errorf("compile_metrics_with_labels() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestValidateMetricsName(t *testing.T) { - tests := []struct { - name string - input string - wantErr bool - }{ - {"Valid name", "valid_metric_name", false}, - {"Name with spaces", "valid metric name", true}, - {"Name with special chars", "valid@metric#name!", true}, - {"Name with leading underscore", "_valid_metric_name", true}, - {"Name with trailing underscore", "valid_metric_name_", true}, - {"Name with consecutive underscores", "valid__metric__name", true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := validate_metrics_name(tt.input); (err != nil) != tt.wantErr { - t.Errorf("validate_metrics_name() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func TestCleanMetricName(t *testing.T) { - tests := []struct { - input string - expected string - }{ - {"valid metric name", "valid_metric_name"}, - {"valid@metric#name!", "valid_metric_name"}, - {"__valid__metric__name__", "valid_metric_name"}, - {" valid metric name ", "valid_metric_name"}, - } - - for _, tt := range tests { - t.Run(tt.input, func(t *testing.T) { - assert.Equal(t, tt.expected, clean_metric_name(tt.input)) - }) - } -} - -func TestDefaultLabels(t *testing.T) { - podName := "test-pod" - libpack_config.PKG_NAME = "example_microservice" - expected := map[string]string{ - "microservice": "example_microservice", - "pod": podName, - } - - assert.Equal(t, expected, defaultLabels(podName)) -} - -func TestEnsureDefaultLabels(t *testing.T) { - podName := "test-pod" - libpack_config.PKG_NAME = "example_microservice" - - tests := []struct { - inputLabels map[string]string - expectedLabels map[string]string - name string - }{ - { - name: "Nil labels", - inputLabels: nil, - expectedLabels: map[string]string{"microservice": "example_microservice", "pod": podName}, - }, - { - name: "Empty labels", - inputLabels: map[string]string{}, - expectedLabels: map[string]string{"microservice": "example_microservice", "pod": podName}, - }, - { - name: "Partial labels", - inputLabels: map[string]string{"microservice": "test_service"}, - expectedLabels: map[string]string{"microservice": "test_service", "pod": podName}, - }, - { - name: "Complete labels", - inputLabels: map[string]string{"microservice": "test_service", "pod": "custom_pod"}, - expectedLabels: map[string]string{"microservice": "test_service", "pod": "custom_pod"}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ensureDefaultLabels(&tt.inputLabels, podName) - assert.Equal(t, tt.expectedLabels, tt.inputLabels) - }) - } -} - -func TestLabelsToString(t *testing.T) { - tests := []struct { - labels map[string]string - expected string - }{ - { - labels: map[string]string{"key1": "value1", "key2": "value2"}, - expected: "key1=value1;key2=value2;", - }, - { - labels: map[string]string{"a": "1", "b": "2"}, - expected: "a=1;b=2;", - }, - { - labels: map[string]string{}, - expected: "", - }, - } - - for _, tt := range tests { - t.Run(tt.expected, func(t *testing.T) { - assert.Equal(t, tt.expected, labelsToString(tt.labels)) - }) - } -} diff --git a/monitoring/monitoring.go b/monitoring/monitoring.go deleted file mode 100644 index 165a0df..0000000 --- a/monitoring/monitoring.go +++ /dev/null @@ -1,174 +0,0 @@ -package libpack_monitoring - -import ( - "flag" - "fmt" - "time" - - "github.com/VictoriaMetrics/metrics" - "github.com/gofiber/fiber/v2" - "github.com/gookit/goutil/envutil" - libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config" - libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" -) - -type MetricsSetup struct { - metrics_set *metrics.Set - metrics_set_custom *metrics.Set - ic *InitConfig - metrics_prefix string -} - -var log = libpack_logger.New().SetMinLogLevel(libpack_logger.LEVEL_INFO) - -type InitConfig struct { - PurgeOnCrawl bool - PurgeEvery int -} - -func NewMonitoring(ic *InitConfig) *MetricsSetup { - ms := &MetricsSetup{ - ic: ic, - metrics_set: metrics.NewSet(), - metrics_set_custom: metrics.NewSet(), - } - - if flag.Lookup("test.v") == nil { - go ms.startPrometheusEndpoint() - - if ic.PurgeEvery > 0 { - ticker := time.NewTicker(time.Duration(ic.PurgeEvery) * time.Second) - go func() { - for range ticker.C { - ms.PurgeMetrics() - } - }() - } - } - - return ms -} - -func (ms *MetricsSetup) startPrometheusEndpoint() { - app := fiber.New(fiber.Config{ - DisableStartupMessage: true, - AppName: fmt.Sprintf("GraphQL Monitoring Proxy - %s v%s", libpack_config.PKG_NAME, libpack_config.PKG_VERSION), - }) - app.Get("/metrics", ms.metricsEndpoint) - if err := app.Listen(fmt.Sprintf(":%d", envutil.GetInt("MONITORING_PORT", 9393))); err != nil { - log.Critical(&libpack_logger.LogMessage{ - Message: "Can't start the service", - Pairs: map[string]interface{}{"error": err}, - }) - } -} - -func (ms *MetricsSetup) metricsEndpoint(c *fiber.Ctx) error { - ms.metrics_set.WritePrometheus(c.Response().BodyWriter()) - ms.metrics_set_custom.WritePrometheus(c.Response().BodyWriter()) - - if ms.ic.PurgeOnCrawl && ms.ic.PurgeEvery == 0 { - ms.PurgeMetrics() - } - return nil -} - -func (ms *MetricsSetup) AddMetricsPrefix(prefix string) { - ms.metrics_prefix = prefix -} - -func (ms *MetricsSetup) ListActiveMetrics() []string { - return ms.metrics_set.ListMetricNames() -} - -func (ms *MetricsSetup) RegisterMetricsGauge(metric_name string, labels map[string]string, val float64) *metrics.Gauge { - if err := validate_metrics_name(metric_name); err != nil { - log.Critical(&libpack_logger.LogMessage{ - Message: "RegisterMetricsGauge() error", - Pairs: map[string]interface{}{"_error": "Invalid metric name", "_metric_name": metric_name}, - }) - return nil - } - return ms.metrics_set_custom.GetOrCreateGauge(ms.get_metrics_name(metric_name, labels), func() float64 { - return val - }) -} - -func (ms *MetricsSetup) RegisterMetricsCounter(metric_name string, labels map[string]string) *metrics.Counter { - if err := validate_metrics_name(metric_name); err != nil { - log.Critical(&libpack_logger.LogMessage{ - Message: "RegisterMetricsCounter() error", - Pairs: map[string]interface{}{"_error": "Invalid metric name", "_metric_name": metric_name}, - }) - return nil - } - if metric_name == MetricsSucceeded || metric_name == MetricsFailed || metric_name == MetricsSkipped { - return ms.metrics_set.GetOrCreateCounter(ms.get_metrics_name(metric_name, labels)) - } - return ms.metrics_set_custom.GetOrCreateCounter(ms.get_metrics_name(metric_name, labels)) -} - -func (ms *MetricsSetup) RegisterFloatCounter(metric_name string, labels map[string]string) *metrics.FloatCounter { - if err := validate_metrics_name(metric_name); err != nil { - log.Critical(&libpack_logger.LogMessage{ - Message: "RegisterFloatCounter() error", - Pairs: map[string]interface{}{"_error": "Invalid metric name", "_metric_name": metric_name}, - }) - return nil - } - return ms.metrics_set_custom.GetOrCreateFloatCounter(ms.get_metrics_name(metric_name, labels)) -} - -func (ms *MetricsSetup) RegisterMetricsSummary(metric_name string, labels map[string]string) *metrics.Summary { - if err := validate_metrics_name(metric_name); err != nil { - log.Critical(&libpack_logger.LogMessage{ - Message: "RegisterMetricsSummary() error", - Pairs: map[string]interface{}{"_error": "Invalid metric name", "_metric_name": metric_name}, - }) - return nil - } - return ms.metrics_set_custom.GetOrCreateSummary(ms.get_metrics_name(metric_name, labels)) -} - -func (ms *MetricsSetup) RegisterMetricsHistogram(metric_name string, labels map[string]string) *metrics.Histogram { - if err := validate_metrics_name(metric_name); err != nil { - log.Critical(&libpack_logger.LogMessage{ - Message: "RegisterMetricsHistogram() error", - Pairs: map[string]interface{}{"_error": "Invalid metric name", "_metric_name": metric_name}, - }) - return nil - } - return ms.metrics_set_custom.GetOrCreateHistogram(ms.get_metrics_name(metric_name, labels)) -} - -func (ms *MetricsSetup) Increment(metric_name string, labels map[string]string) { - ms.RegisterMetricsCounter(metric_name, labels).Inc() -} - -func (ms *MetricsSetup) IncrementFloat(metric_name string, labels map[string]string, value float64) { - ms.RegisterFloatCounter(metric_name, labels).Add(value) -} - -func (ms *MetricsSetup) Set(metric_name string, labels map[string]string, value uint64) { - ms.RegisterMetricsCounter(metric_name, labels).Set(value) -} - -func (ms *MetricsSetup) Update(metric_name string, labels map[string]string, value float64) { - ms.RegisterMetricsHistogram(metric_name, labels).Update(value) -} - -func (ms *MetricsSetup) UpdateDuration(metric_name string, labels map[string]string, value time.Time) { - ms.RegisterMetricsHistogram(metric_name, labels).UpdateDuration(value) -} - -func (ms *MetricsSetup) UpdateSummary(metric_name string, labels map[string]string, value float64) { - ms.RegisterMetricsSummary(metric_name, labels).Update(value) -} - -func (ms *MetricsSetup) RemoveMetrics(metric_name string, labels map[string]string) { - ms.metrics_set_custom.UnregisterMetric(ms.get_metrics_name(metric_name, labels)) -} - -func (ms *MetricsSetup) PurgeMetrics() { - ms.metrics_set_custom.UnregisterAllMetrics() -} diff --git a/monitoring/structs.go b/monitoring/structs.go deleted file mode 100644 index 1f99194..0000000 --- a/monitoring/structs.go +++ /dev/null @@ -1,14 +0,0 @@ -package libpack_monitoring - -const ( - MetricsSucceeded = "requests_succesful" - MetricsFailed = "requests_failed" - MetricsDuration = "requests_duration" - MetricsSkipped = "requests_skipped" - MetricsExecutedQuery = "executed_query" - MetricsTimedQuery = "timed_query" - - MetricsCacheHit = "cache_hit" - MetricsCacheMiss = "cache_miss" - MetricsQueriesCached = "cached_queries" -) diff --git a/proxy.go b/proxy.go deleted file mode 100644 index 3ff07ff..0000000 --- a/proxy.go +++ /dev/null @@ -1,130 +0,0 @@ -package main - -import ( - "crypto/tls" - "fmt" - "net/url" - "time" - - "github.com/avast/retry-go/v4" - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/proxy" - libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" - libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring" - "github.com/valyala/fasthttp" -) - -var ( - httpClient *fasthttp.Client -) - -func init() { - httpClient = createFasthttpClient(30) // Assuming a default timeout of 30 seconds -} - -func createFasthttpClient(timeout int) *fasthttp.Client { - return &fasthttp.Client{ - Name: "graphql_proxy", - NoDefaultUserAgentHeader: true, - TLSConfig: &tls.Config{ - InsecureSkipVerify: true, - }, - MaxConnsPerHost: 2048, - ReadTimeout: time.Duration(timeout) * time.Second, - WriteTimeout: time.Duration(timeout) * time.Second, - MaxIdleConnDuration: time.Duration(timeout) * time.Second, - MaxConnDuration: time.Duration(timeout) * time.Second, - DisableHeaderNamesNormalizing: true, - } -} -func proxyTheRequest(c *fiber.Ctx, currentEndpoint string) error { - if !checkAllowedURLs(c) { - cfg.Logger.Error(&libpack_logger.LogMessage{ - Message: "Request blocked", - Pairs: map[string]interface{}{"path": c.Path()}, - }) - if ifNotInTest() { - cfg.Monitoring.Increment(libpack_monitoring.MetricsSkipped, nil) - } - return fmt.Errorf("request blocked - not allowed URL: %s", c.Path()) - } - - proxyURL := currentEndpoint + c.Path() - _, err := url.Parse(proxyURL) - if err != nil { - return fmt.Errorf("invalid URL: %v", err) - } - - if cfg.LogLevel == "debug" { - logDebugRequest(c) - } - - err = retry.Do( - func() error { - return proxy.DoRedirects(c, proxyURL, 3, httpClient) - }, - retry.OnRetry(func(n uint, err error) { - cfg.Logger.Warning(&libpack_logger.LogMessage{ - Message: "Retrying the request", - Pairs: map[string]interface{}{ - "path": c.Path(), - "error": err.Error(), - }, - }) - }), - retry.Attempts(3), - retry.DelayType(retry.BackOffDelay), - retry.Delay(250*time.Millisecond), - retry.LastErrorOnly(true), - ) - - if err != nil { - cfg.Logger.Warning(&libpack_logger.LogMessage{ - Message: "Can't proxy the request", - Pairs: map[string]interface{}{"error": err.Error()}, - }) - if ifNotInTest() { - cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil) - } - return fmt.Errorf("failed to proxy request: %v", err) - } - - if cfg.LogLevel == "debug" { - logDebugResponse(c) - } - - if c.Response().StatusCode() != 200 { - if ifNotInTest() { - cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil) - } - return fmt.Errorf("received non-200 response from the GraphQL server: %d", c.Response().StatusCode()) - } - - c.Response().Header.Del(fiber.HeaderServer) - return nil -} - -func logDebugRequest(c *fiber.Ctx) { - cfg.Logger.Debug(&libpack_logger.LogMessage{ - Message: "Proxying the request", - Pairs: map[string]interface{}{ - "path": c.Path(), - "body": string(c.Body()), - "headers": c.GetReqHeaders(), - "request_uuid": c.Locals("request_uuid"), - }, - }) -} - -func logDebugResponse(c *fiber.Ctx) { - cfg.Logger.Debug(&libpack_logger.LogMessage{ - Message: "Received proxied response", - Pairs: map[string]interface{}{ - "path": c.Path(), - "response_body": string(c.Response().Body()), - "response_code": c.Response().StatusCode(), - "headers": c.GetRespHeaders(), - "request_uuid": c.Locals("request_uuid"), - }, - }) -} diff --git a/proxy_test.go b/proxy_test.go deleted file mode 100644 index afead6b..0000000 --- a/proxy_test.go +++ /dev/null @@ -1,127 +0,0 @@ -package main - -import ( - "strings" - - "github.com/valyala/fasthttp" -) - -func (suite *Tests) Test_proxyTheRequest() { - - supplied_headers := map[string]string{ - "X-Forwarded-For": "127.0.0.1", - "Content-Type": "application/json", - } - - tests := []struct { - headers map[string]string - name string - body string - host string - hostRO string - path string - wantErr bool - }{ - { - name: "test_empty", - body: `{"query":"query {\n __type(name: \"Query\") {\n name\n }\n }"}`, - host: "https://telegram-bot.app/", - path: "/v1/graphql", - headers: supplied_headers, - wantErr: false, - }, - { - name: "test_wrong_url", - body: `{"query":"query {\n __type(name: \"Query\") {\n name\n }\n }"}`, - host: "https://google.com/", - path: "/v1/wrongURL", - headers: supplied_headers, - wantErr: true, - }, - { - name: "Test read only mode", - body: `{"query":"query {\n __type(name: \"Query\") {\n name\n }\n }"}`, - host: "https://google.com/", - hostRO: "https://telegram-bot.app/", - path: "/v1/graphql", - headers: supplied_headers, - wantErr: false, - }, - { - name: "Test read only mode wrong host", - body: `{"query":"query {\n __type(name: \"Query\") {\n name\n }\n }"}`, - host: "https://telegram-bot.app/", - hostRO: "https://google.com/", - path: "/v1/graphql", - headers: supplied_headers, - wantErr: true, - }, - } - - for _, tt := range tests { - suite.Run(tt.name, func() { - - cfg = &config{} - parseConfig() - cfg.Server.HostGraphQL = tt.host - - if tt.hostRO != "" { - cfg.Server.HostGraphQLReadOnly = tt.hostRO - } - - ctx_headers := func() *fasthttp.RequestHeader { - h := fasthttp.RequestHeader{} - for k, v := range tt.headers { - h.Add(k, v) - } - return &h - }() - - ctx_request := fasthttp.Request{ - Header: *ctx_headers, - } - ctx_request.SetBody([]byte(tt.body)) - ctx_request.SetRequestURI(tt.path) - ctx_request.Header.SetMethod("POST") - ctx := suite.app.AcquireCtx(&fasthttp.RequestCtx{ - Request: ctx_request, - }) - res := parseGraphQLQuery(ctx) - assert.NotNil(ctx, "Fiber context is nil", tt.name) - err := proxyTheRequest(ctx, res.activeEndpoint) - if tt.wantErr { - assert.NotNil(err, "Error is nil", tt.name) - } else { - assert.Nil(err, "Error is not nil", tt.name) - } - }) - } -} - -func (suite *Tests) Test_proxyTheRequestWithPayloads() { - allowedUrls = make(map[string]struct{}) - allowedUrls["/"] = struct{}{} - - suite.Run("Test with invalid URL", func() { - cfg.Server.HostGraphQL = "://invalid-url" - ctx := suite.app.AcquireCtx(&fasthttp.RequestCtx{}) - err := proxyTheRequest(ctx, cfg.Server.HostGraphQL) - assert.NotNil(err) - }) - - suite.Run("Test with network error", func() { - cfg.Server.HostGraphQL = "http://non-existent-host.invalid" - ctx := suite.app.AcquireCtx(&fasthttp.RequestCtx{}) - err := proxyTheRequest(ctx, cfg.Server.HostGraphQL) - assert.NotNil(err) - }) - - suite.Run("Test with large payload", func() { - cfg.Server.HostGraphQL = "https://telegram-bot.app/" - ctx := suite.app.AcquireCtx(&fasthttp.RequestCtx{}) - largePayload := strings.Repeat("a", 10*1024*1024) // 10MB payload - ctx.Context().Request.SetBody([]byte(largePayload)) - err := proxyTheRequest(ctx, cfg.Server.HostGraphQL) - assert.Nil(err) - }) -} diff --git a/ratelimit.go b/ratelimit.go deleted file mode 100644 index 18e9f85..0000000 --- a/ratelimit.go +++ /dev/null @@ -1,124 +0,0 @@ -package main - -import ( - "os" - "sync" - "time" - - "github.com/goccy/go-json" - goratecounter "github.com/lukaszraczylo/go-ratecounter" - libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" -) - -type RateLimitConfig struct { - RateCounterTicker *goratecounter.RateCounter - Interval time.Duration `json:"interval"` - Req int `json:"req"` -} - -var ( - rateLimits = make(map[string]RateLimitConfig) - rateLimitMu sync.RWMutex -) - -func loadRatelimitConfig() error { - paths := []string{"/go/src/app/ratelimit.json", "./ratelimit.json", "./static/app/default-ratelimit.json"} - for _, path := range paths { - if err := loadConfigFromPath(path); err == nil { - return nil - } - } - cfg.Logger.Error(&libpack_logger.LogMessage{ - Message: "Rate limit config not found", - Pairs: map[string]interface{}{"paths": paths}, - }) - return os.ErrNotExist -} - -func loadConfigFromPath(path string) error { - file, err := os.ReadFile(path) - if err != nil { - cfg.Logger.Debug(&libpack_logger.LogMessage{ - Message: "Failed to load config", - Pairs: map[string]interface{}{"path": path, "error": err}, - }) - return err - } - - var config struct { - RateLimit map[string]RateLimitConfig `json:"ratelimit"` - } - - if err := json.Unmarshal(file, &config); err != nil { - return err - } - - newRateLimits := make(map[string]RateLimitConfig, len(config.RateLimit)) - for key, value := range config.RateLimit { - value.RateCounterTicker = goratecounter.NewRateCounter().WithConfig(goratecounter.RateCounterConfig{ - Interval: value.Interval, - }) - - if cfg.LogLevel == "debug" { - cfg.Logger.Debug(&libpack_logger.LogMessage{ - Message: "Setting ratelimit config for role", - Pairs: map[string]interface{}{ - "role": key, - "interval_used": value.Interval, - "ratelimit": value.Req, - }, - }) - } - newRateLimits[key] = value - } - - rateLimitMu.Lock() - rateLimits = newRateLimits - rateLimitMu.Unlock() - - cfg.Logger.Debug(&libpack_logger.LogMessage{ - Message: "Rate limit config loaded", - Pairs: map[string]interface{}{"ratelimit": rateLimits}, - }) - return nil -} - -func rateLimitedRequest(userID, userRole string) bool { - rateLimitMu.RLock() - roleConfig, ok := rateLimits[userRole] - rateLimitMu.RUnlock() - - if !ok || roleConfig.RateCounterTicker == nil { - cfg.Logger.Debug(&libpack_logger.LogMessage{ - Message: "Rate limit role not found or ticker not initialized", - Pairs: map[string]interface{}{"user_role": userRole}, - }) - return true - } - - roleConfig.RateCounterTicker.Incr(1) - tickerRate := roleConfig.RateCounterTicker.GetRate() - - logDetails := map[string]interface{}{ - "user_role": userRole, - "user_id": userID, - "rate": tickerRate, - "config_rate": roleConfig.Req, - "interval": roleConfig.Interval, - } - - cfg.Logger.Debug(&libpack_logger.LogMessage{ - Message: "Rate limit ticker", - Pairs: map[string]interface{}{"log_details": logDetails}, - }) - - if tickerRate > float64(roleConfig.Req) { - cfg.Logger.Debug(&libpack_logger.LogMessage{ - Message: "Rate limit exceeded", - Pairs: map[string]interface{}{"log_details": logDetails}, - }) - return false - } - - return true -} diff --git a/semver.yaml b/semver.yaml deleted file mode 100644 index be80efe..0000000 --- a/semver.yaml +++ /dev/null @@ -1,15 +0,0 @@ -version: 1 -force: - existing: true - strict: false - minor: 1 -wording: - patch: - - update - - initial - - fix - minor: - - improve - - release - major: - - breaking diff --git a/server.go b/server.go deleted file mode 100644 index 5b02d60..0000000 --- a/server.go +++ /dev/null @@ -1,265 +0,0 @@ -package main - -import ( - "fmt" - "strconv" - "sync" - "time" - - "github.com/goccy/go-json" - fiber "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/cors" - "github.com/google/uuid" - - libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache" - libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config" - libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" - libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring" -) - -const ( - healthCheckQueryStr = `{ __typename }` -) - -var ( - ctxPool = sync.Pool{ - New: func() interface{} { - return new(fiber.Ctx) - }, - } -) - -func StartHTTPProxy() { - cfg.Logger.Debug(&libpack_logger.LogMessage{ - Message: "Starting the HTTP proxy", - }) - - serverConfig := fiber.Config{ - DisableStartupMessage: true, - AppName: fmt.Sprintf("GraphQL Monitoring Proxy - %s v%s", libpack_config.PKG_NAME, libpack_config.PKG_VERSION), - IdleTimeout: time.Duration(cfg.Client.ClientTimeout) * time.Second * 2, - ReadTimeout: time.Duration(cfg.Client.ClientTimeout) * time.Second * 2, - WriteTimeout: time.Duration(cfg.Client.ClientTimeout) * time.Second * 2, - JSONEncoder: json.Marshal, - JSONDecoder: json.Unmarshal, - } - - server := fiber.New(serverConfig) - - server.Use(cors.New(cors.Config{ - AllowOrigins: "*", - })) - - server.Use(AddRequestUUID) - - server.Get("/healthz", healthCheck) - server.Get("/livez", healthCheck) - - server.Post("/*", processGraphQLRequest) - server.Get("/*", proxyTheRequestToDefault) - - cfg.Logger.Info(&libpack_logger.LogMessage{ - Message: "GraphQL proxy started", - Pairs: map[string]interface{}{"port": cfg.Server.PortGraphQL}, - }) - - if err := server.Listen(fmt.Sprintf(":%d", cfg.Server.PortGraphQL)); err != nil { - cfg.Logger.Critical(&libpack_logger.LogMessage{ - Message: "Can't start the service", - Pairs: map[string]interface{}{"port": cfg.Server.PortGraphQL, "error": err.Error()}, - }) - } -} - -func proxyTheRequestToDefault(c *fiber.Ctx) error { - return proxyTheRequest(c, cfg.Server.HostGraphQL) -} - -func AddRequestUUID(c *fiber.Ctx) error { - c.Locals("request_uuid", uuid.NewString()) - return c.Next() -} - -func checkAllowedURLs(c *fiber.Ctx) bool { - if len(allowedUrls) == 0 { - return true - } - path := c.OriginalURL() - _, ok := allowedUrls[path] - return ok -} - -func healthCheck(c *fiber.Ctx) error { - if len(cfg.Server.HealthcheckGraphQL) > 0 { - cfg.Logger.Debug(&libpack_logger.LogMessage{ - Message: "Health check enabled", - Pairs: map[string]interface{}{"url": cfg.Server.HealthcheckGraphQL}, - }) - - _, err := cfg.Client.GQLClient.Query(healthCheckQueryStr, nil, nil) - if err != nil { - cfg.Logger.Error(&libpack_logger.LogMessage{ - Message: "Can't reach the GraphQL server", - Pairs: map[string]interface{}{"error": err.Error()}, - }) - cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil) - return c.Status(500).SendString("Can't reach the GraphQL server with {__typename} query") - } - } - - cfg.Logger.Debug(&libpack_logger.LogMessage{ - Message: "Health check returning OK", - }) - return c.Status(200).SendString("Health check OK") -} - -func processGraphQLRequest(c *fiber.Ctx) error { - startTime := time.Now() - - extractedUserID := "-" - extractedRoleName := "-" - - if authorization := c.Get("Authorization"); authorization != "" && (len(cfg.Client.JWTUserClaimPath) > 0 || len(cfg.Client.JWTRoleClaimPath) > 0) { - extractedUserID, extractedRoleName = extractClaimsFromJWTHeader(authorization) - } - - if checkIfUserIsBanned(c, extractedUserID) { - return c.Status(403).SendString("User is banned") - } - - if cfg.Client.RoleFromHeader != "" { - if role := c.Get(cfg.Client.RoleFromHeader); role != "" { - extractedRoleName = role - } - } - - if cfg.Client.RoleRateLimit { - cfg.Logger.Debug(&libpack_logger.LogMessage{ - Message: "Rate limiting enabled", - Pairs: map[string]interface{}{"user_id": extractedUserID, "role_name": extractedRoleName}, - }) - if !rateLimitedRequest(extractedUserID, extractedRoleName) { - return c.Status(429).SendString("Rate limit exceeded, try again later") - } - } - - parsedResult := parseGraphQLQuery(c) - if parsedResult.shouldBlock { - return c.Status(403).SendString("Request blocked") - } - - if parsedResult.shouldIgnore { - cfg.Logger.Debug(&libpack_logger.LogMessage{ - Message: "Request passed as-is - probably not a GraphQL", - }) - return proxyTheRequest(c, parsedResult.activeEndpoint) - } - - calculatedQueryHash := libpack_cache.CalculateHash(c) - - if parsedResult.cacheTime == 0 { - if cacheQuery := c.Get("X-Cache-Graphql-Query"); cacheQuery != "" { - parsedResult.cacheTime, _ = strconv.Atoi(cacheQuery) - } else { - parsedResult.cacheTime = cfg.Cache.CacheTTL - } - } - - wasCached := false - - if parsedResult.cacheRefresh { - cfg.Logger.Debug(&libpack_logger.LogMessage{ - Message: "Cache refresh requested via query", - Pairs: map[string]interface{}{"user_id": extractedUserID, "request_uuid": c.Locals("request_uuid")}, - }) - libpack_cache.CacheDelete(calculatedQueryHash) - } - - if parsedResult.cacheRequest || cfg.Cache.CacheEnable || cfg.Cache.CacheRedisEnable { - cfg.Logger.Debug(&libpack_logger.LogMessage{ - Message: "Cache enabled", - Pairs: map[string]interface{}{"via_query": parsedResult.cacheRequest, "via_env": cfg.Cache.CacheEnable}, - }) - - if cachedResponse := libpack_cache.CacheLookup(calculatedQueryHash); cachedResponse != nil { - cfg.Monitoring.Increment(libpack_monitoring.MetricsCacheHit, nil) - cfg.Logger.Debug(&libpack_logger.LogMessage{ - Message: "Cache hit", - Pairs: map[string]interface{}{"hash": calculatedQueryHash, "user_id": extractedUserID, "request_uuid": c.Locals("request_uuid")}, - }) - c.Set("X-Cache-Hit", "true") - wasCached = true - return c.Send(cachedResponse) - } - - cfg.Monitoring.Increment(libpack_monitoring.MetricsCacheMiss, nil) - cfg.Logger.Debug(&libpack_logger.LogMessage{ - Message: "Cache miss", - Pairs: map[string]interface{}{"hash": calculatedQueryHash, "user_id": extractedUserID, "request_uuid": c.Locals("request_uuid")}, - }) - if err := proxyAndCacheTheRequest(c, calculatedQueryHash, parsedResult.cacheTime, parsedResult.activeEndpoint); err != nil { - return err - } - } else { - if err := proxyTheRequest(c, parsedResult.activeEndpoint); err != nil { - cfg.Logger.Error(&libpack_logger.LogMessage{ - Message: "Can't proxy the request", - Pairs: map[string]interface{}{"error": err.Error()}, - }) - cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil) - return c.Status(500).SendString("Can't proxy the request - try again later") - } - } - - logAndMonitorRequest(c, extractedUserID, parsedResult.operationType, parsedResult.operationName, wasCached, time.Since(startTime), startTime) - - return nil -} - -func proxyAndCacheTheRequest(c *fiber.Ctx, queryCacheHash string, cacheTime int, currentEndpoint string) error { - if err := proxyTheRequest(c, currentEndpoint); err != nil { - cfg.Logger.Error(&libpack_logger.LogMessage{ - Message: "Can't proxy the request", - Pairs: map[string]interface{}{"error": err.Error()}, - }) - cfg.Monitoring.Increment(libpack_monitoring.MetricsFailed, nil) - return c.Status(500).SendString("Can't proxy the request - try again later") - } - - libpack_cache.CacheStoreWithTTL(queryCacheHash, c.Response().Body(), time.Duration(cacheTime)*time.Second) - cfg.Monitoring.Increment(libpack_monitoring.MetricsQueriesCached, nil) - return c.Send(c.Response().Body()) -} - -func logAndMonitorRequest(c *fiber.Ctx, userID, opType, opName string, wasCached bool, duration time.Duration, startTime time.Time) { - labels := map[string]string{ - "op_type": opType, - "op_name": opName, - "cached": strconv.FormatBool(wasCached), - "user_id": userID, - } - - if cfg.Server.AccessLog { - cfg.Logger.Info(&libpack_logger.LogMessage{ - Message: "Request processed", - Pairs: map[string]interface{}{ - "ip": c.IP(), - "fwd-ip": c.Get("X-Forwarded-For"), - "user_id": userID, - "op_type": opType, - "op_name": opName, - "time": duration, - "cache": wasCached, - "request_uuid": c.Locals("request_uuid"), - }, - }) - } - - cfg.Monitoring.Increment(libpack_monitoring.MetricsSucceeded, nil) - cfg.Monitoring.Increment(libpack_monitoring.MetricsExecutedQuery, labels) - - if !wasCached { - cfg.Monitoring.UpdateDuration(libpack_monitoring.MetricsTimedQuery, labels, startTime) - cfg.Monitoring.Update(libpack_monitoring.MetricsTimedQuery, labels, float64(duration.Milliseconds())) - } -} diff --git a/static/app/.keep b/static/app/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/static/app/banned_users.json b/static/app/banned_users.json deleted file mode 100644 index 0967ef4..0000000 --- a/static/app/banned_users.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/static/app/banned_users.json.lock b/static/app/banned_users.json.lock deleted file mode 100644 index e69de29..0000000 diff --git a/static/app/ratelimit.json b/static/app/ratelimit.json deleted file mode 100644 index dada06d..0000000 --- a/static/app/ratelimit.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "ratelimit": { - "admin": { - "req": 100, - "interval": "second" - }, - "guest": { - "req": 3, - "interval": "second" - }, - "-": { - "req": 100, - "interval": "hour" - } - } -} \ No newline at end of file diff --git a/static/kubernetes-deployment.yaml b/static/kubernetes-deployment.yaml deleted file mode 100644 index ceac15d..0000000 --- a/static/kubernetes-deployment.yaml +++ /dev/null @@ -1,92 +0,0 @@ ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: hasura-proxy-internal - labels: - app: hasura-proxy-internal - type: support -spec: - replicas: 2 - selector: - matchLabels: - app: hasura-proxy-internal - type: support - template: - metadata: - labels: - app: hasura-proxy-internal - type: support - annotations: - prometheus.io/scrape: "true" - prometheus.io/port: "9393" - prometheus.io/path: "/metrics" - spec: - securityContext: - runAsUser: 65534 # nobody - affinity: - nodeAffinity: - requiredDuringSchedulingIgnoredDuringExecution: - nodeSelectorTerms: - - matchExpressions: - - key: node-role.kubernetes.io/worker - operator: Exists - containers: - - name: graphql-proxy - image: ghcr.io/lukaszraczylo/graphql-monitoring-proxy:latest - imagePullPolicy: Always - resources: - limits: - cpu: "1" - memory: "640Mi" - requests: - cpu: "0.75" - memory: "512Mi" - livenessProbe: - httpGet: - path: /healthz - port: 8080 - initialDelaySeconds: 5 - ports: - - name: web - containerPort: 8080 - - name: monitoring - containerPort: 9393 - env: - - name: PORT_GRAPHQL - value: "8080" - - name: MONITORING_PORT - value: "9393" - - name: HOST_GRAPHQL - value: http://hasura-internal:8080/v1/graphql - - name: ENABLE_GLOBAL_CACHE - value: "true" - - name: CACHE_TTL - value: "10" - - name: BLOCK_SCHEMA_INTROSPECTION - value: "true" - ---- -apiVersion: v1 -kind: Service -metadata: - name: hasura-proxy-internal - labels: - app: hasura-proxy-internal - type: support - annotations: - prometheus.io/scrape: "true" - prometheus.io/port: "9393" - prometheus.io/path: "/metrics" -spec: - ports: - - name: web - port: 8080 - targetPort: 8080 - - name: monitoring - port: 9393 - targetPort: 9393 - selector: - app: hasura-proxy-internal - type: support - type: ClusterIP \ No newline at end of file diff --git a/static/kubernetes-single-deployment-with-ro.yaml b/static/kubernetes-single-deployment-with-ro.yaml deleted file mode 100644 index 6e634c0..0000000 --- a/static/kubernetes-single-deployment-with-ro.yaml +++ /dev/null @@ -1,165 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: hasura-w-proxy-internal - labels: - app: hasura-w-proxy-internal - type: support -spec: - replicas: 2 - selector: - matchLabels: - app: hasura-w-proxy-internal - type: support - template: - metadata: - labels: - app: hasura-w-proxy-internal - type: support - spec: - securityContext: - runAsUser: 65534 # nobody - affinity: - nodeAffinity: - requiredDuringSchedulingIgnoredDuringExecution: - nodeSelectorTerms: - - matchExpressions: - - key: node-role.kubernetes.io/worker - operator: Exists - containers: - - name: hasura - image: hasura/graphql-engine:v2.33.1-ce - ports: - - name: hasura-internal - containerPort: 8080 - livenessProbe: - httpGet: - path: /healthz - port: 8080 - initialDelaySeconds: 30 - resources: - limits: - cpu: "1" - memory: "640Mi" - requests: - cpu: "0.75" - memory: "512Mi" - env: - - name: HASURA_GRAPHQL_DATABASE_URL - value: postgres://postgres:xxx@yyy:5432/postgres - - name: HASURA_GRAPHQL_ENABLE_CONSOLE - value: "true" - - name: HASURA_GRAPHQL_DEV_MODE - value: "true" - - name: HASURA_GRAPHQL_ENABLE_TELEMETRY - value: "false" - - name: HASURA_GRAPHQL_EXPERIMENTAL_FEATURES - value: "inherited_roles" - - name: HASURA_GRAPHQL_PG_CONNECTIONS - value: "20" - - name: HASURA_GRAPHQL_LOG_LEVEL - value: "error" - - - name: hasura-ro - image: hasura/graphql-engine:v2.33.1-ce - ports: - - name: hasura-internal-ro - containerPort: 8088 - livenessProbe: - httpGet: - path: /healthz - port: 8088 - initialDelaySeconds: 30 - resources: - limits: - cpu: "1" - memory: "640Mi" - requests: - cpu: "0.75" - memory: "512Mi" - env: - - name: HASURA_GRAPHQL_DATABASE_URL - value: postgres://postgres:xxx@yyy.read-only:5432/postgres - # POINT METADATA TO THE RW database (!!!) - - name: HASURA_GRAPHQL_METADATA_DATABASE_URL - value: postgres://postgres:xxx@yyy:5432/postgres - - name: HASURA_GRAPHQL_ENABLE_CONSOLE - value: "true" - - name: HASURA_GRAPHQL_DEV_MODE - value: "true" - - name: HASURA_GRAPHQL_ENABLE_TELEMETRY - value: "false" - - name: HASURA_GRAPHQL_EXPERIMENTAL_FEATURES - value: "inherited_roles" - - name: HASURA_GRAPHQL_PG_CONNECTIONS - value: "20" - - name: HASURA_GRAPHQL_LOG_LEVEL - value: "error" - - name: HASURA_GRAPHQL_SERVER_PORT - value: "8088" - - - name: graphql-proxy - image: ghcr.io/lukaszraczylo/graphql-monitoring-proxy:latest - imagePullPolicy: Always - resources: - limits: - cpu: "1" - memory: "640Mi" - requests: - cpu: "0.75" - memory: "128Mi" - livenessProbe: - httpGet: - path: /healthz - port: 8080 - initialDelaySeconds: 5 - timeoutSeconds: 5 - ports: - - name: web - containerPort: 8181 - - name: monitoring - containerPort: 9393 - env: - - name: PORT_GRAPHQL - value: "8181" - - name: MONITORING_PORT - value: "9393" - - name: HOST_GRAPHQL - value: http://localhost:8080/ - - name: HOST_GRAPHQL_READONLY - value: http://localhost:8088/ - - name: ENABLE_GLOBAL_CACHE - value: "true" - - name: CACHE_TTL - value: "10" - ---- -apiVersion: v1 -kind: Service -metadata: - name: hasura-w-proxy-internal - labels: - app: hasura-w-proxy-internal - type: support - annotations: - prometheus.io/scrape: "true" - prometheus.io/port: "9393" - prometheus.io/path: "/metrics" -spec: - ports: - - name: hasura - port: 8080 - targetPort: 8080 - - name: hasura-ro - port: 8088 - targetPort: 8088 - - name: proxy - port: 8181 - targetPort: 8181 - - name: monitoring - port: 9393 - targetPort: 9393 - selector: - app: hasura-w-proxy-internal - type: support - type: ClusterIP diff --git a/static/kubernetes-single-deployment.yaml b/static/kubernetes-single-deployment.yaml deleted file mode 100644 index 82125cc..0000000 --- a/static/kubernetes-single-deployment.yaml +++ /dev/null @@ -1,121 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: hasura-w-proxy-internal - labels: - app: hasura-w-proxy-internal - type: support -spec: - replicas: 2 - selector: - matchLabels: - app: hasura-w-proxy-internal - type: support - template: - metadata: - labels: - app: hasura-w-proxy-internal - type: support - spec: - securityContext: - runAsUser: 65534 # nobody - affinity: - nodeAffinity: - requiredDuringSchedulingIgnoredDuringExecution: - nodeSelectorTerms: - - matchExpressions: - - key: node-role.kubernetes.io/worker - operator: Exists - containers: - - name: hasura - image: hasura/graphql-engine:v2.33.1-ce - ports: - - name: hasura-internal - containerPort: 8080 - livenessProbe: - httpGet: - path: /healthz - port: 8080 - initialDelaySeconds: 30 - resources: - limits: - cpu: "1" - memory: "640Mi" - requests: - cpu: "0.75" - memory: "512Mi" - env: - - name: HASURA_GRAPHQL_DATABASE_URL - value: postgres://postgres:xxx@yyy:5432/postgres - - name: HASURA_GRAPHQL_ENABLE_CONSOLE - value: "true" - - name: HASURA_GRAPHQL_DEV_MODE - value: "true" - - name: HASURA_GRAPHQL_ENABLE_TELEMETRY - value: "false" - - name: HASURA_GRAPHQL_EXPERIMENTAL_FEATURES - value: "inherited_roles" - - name: HASURA_GRAPHQL_PG_CONNECTIONS - value: "20" - - name: HASURA_GRAPHQL_LOG_LEVEL - value: "error" - - name: graphql-proxy - image: ghcr.io/lukaszraczylo/graphql-monitoring-proxy:latest - imagePullPolicy: Always - resources: - limits: - cpu: "1" - memory: "640Mi" - requests: - cpu: "0.75" - memory: "128Mi" - livenessProbe: - httpGet: - path: /healthz - port: 8080 - initialDelaySeconds: 5 - timeoutSeconds: 5 - ports: - - name: web - containerPort: 8181 - - name: monitoring - containerPort: 9393 - env: - - name: PORT_GRAPHQL - value: "8181" - - name: MONITORING_PORT - value: "9393" - - name: HOST_GRAPHQL - value: http://localhost:8080/ - - name: ENABLE_GLOBAL_CACHE - value: "true" - - name: CACHE_TTL - value: "10" - ---- -apiVersion: v1 -kind: Service -metadata: - name: hasura-w-proxy-internal - labels: - app: hasura-w-proxy-internal - type: support - annotations: - prometheus.io/scrape: "true" - prometheus.io/port: "9393" - prometheus.io/path: "/metrics" -spec: - ports: - - name: hasura - port: 8080 - targetPort: 8080 - - name: proxy - port: 8181 - targetPort: 8181 - - name: monitoring - port: 9393 - targetPort: 9393 - selector: - app: hasura-w-proxy-internal - type: support - type: ClusterIP \ No newline at end of file diff --git a/static/monitoring-at-glance.png b/static/monitoring-at-glance.png deleted file mode 100644 index ef06b69..0000000 Binary files a/static/monitoring-at-glance.png and /dev/null differ diff --git a/struct_config.go b/struct_config.go deleted file mode 100644 index 0b0d65e..0000000 --- a/struct_config.go +++ /dev/null @@ -1,57 +0,0 @@ -package main - -import ( - graphql "github.com/lukaszraczylo/go-simple-graphql" - libpack_logging "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" - libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring" - "github.com/valyala/fasthttp" -) - -// config is a struct that holds the configuration of the application. -type config struct { - Logger *libpack_logging.Logger - LogLevel string - Monitoring *libpack_monitoring.MetricsSetup - Api struct{ BannedUsersFile string } - Client struct { - GQLClient *graphql.BaseClient - FastProxyClient *fasthttp.Client - JWTUserClaimPath string - JWTRoleClaimPath string - RoleFromHeader string - proxy string - ClientTimeout int - RoleRateLimit bool - } - Security struct { - IntrospectionAllowed []string - BlockIntrospection bool - } - HasuraEventCleaner struct { - EventMetadataDb string - ClearOlderThan int - Enable bool - } - Cache struct { - CacheRedisURL string - CacheRedisPassword string - CacheTTL int - CacheRedisDB int - CacheEnable bool - CacheRedisEnable bool - } - Server struct { - HostGraphQL string - HostGraphQLReadOnly string - HealthcheckGraphQL string - AllowURLs []string - PortGraphQL int - PortMonitoring int - ApiPort int - PurgeEvery int - AccessLog bool - ReadOnlyMode bool - EnableApi bool - PurgeOnCrawl bool - } -}