mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-10-21 06:37:21 +00:00
fixup! fixup! Initial commit on gh-pages
This commit is contained in:
@@ -1,2 +0,0 @@
|
|||||||
github: [ lukaszraczylo ]
|
|
||||||
custom: [ monzo.me/lukaszraczylo ]
|
|
||||||
@@ -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"
|
|
||||||
@@ -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"
|
|
||||||
@@ -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"
|
|
||||||
@@ -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"]
|
|
||||||
@@ -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.
|
|
||||||
@@ -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<target>\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
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
Vendored
-134
@@ -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
|
|
||||||
}
|
|
||||||
Vendored
-116
@@ -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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Vendored
-34
@@ -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))
|
|
||||||
}
|
|
||||||
Vendored
-215
@@ -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")
|
|
||||||
}
|
|
||||||
Vendored
-157
@@ -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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Vendored
-82
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Vendored
-168
@@ -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)
|
|
||||||
}
|
|
||||||
Vendored
-96
@@ -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)
|
|
||||||
}
|
|
||||||
Vendored
-130
@@ -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")
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package libpack_config
|
|
||||||
|
|
||||||
var (
|
|
||||||
PKG_NAME string = "not-specified"
|
|
||||||
PKG_VERSION string = "0.0.0-dev"
|
|
||||||
)
|
|
||||||
-65
@@ -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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -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")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
@@ -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=
|
|
||||||
-212
@@ -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
|
|
||||||
}
|
|
||||||
-432
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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))
|
|
||||||
}
|
|
||||||
@@ -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))
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
-140
@@ -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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
)
|
|
||||||
@@ -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"),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
-127
@@ -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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
-124
@@ -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
|
|
||||||
}
|
|
||||||
-15
@@ -1,15 +0,0 @@
|
|||||||
version: 1
|
|
||||||
force:
|
|
||||||
existing: true
|
|
||||||
strict: false
|
|
||||||
minor: 1
|
|
||||||
wording:
|
|
||||||
patch:
|
|
||||||
- update
|
|
||||||
- initial
|
|
||||||
- fix
|
|
||||||
minor:
|
|
||||||
- improve
|
|
||||||
- release
|
|
||||||
major:
|
|
||||||
- breaking
|
|
||||||
@@ -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()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"ratelimit": {
|
|
||||||
"admin": {
|
|
||||||
"req": 100,
|
|
||||||
"interval": "second"
|
|
||||||
},
|
|
||||||
"guest": {
|
|
||||||
"req": 3,
|
|
||||||
"interval": "second"
|
|
||||||
},
|
|
||||||
"-": {
|
|
||||||
"req": 100,
|
|
||||||
"interval": "hour"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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:[email protected]: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
|
|
||||||
@@ -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
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 336 KiB |
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user